# Feature Research

**Domain:** 2D top-down multiplayer online game (BN Online rebuild) — small-scale (<50 CCU), web-delivered, persistent accounts, casual movement + chat focus
**Researched:** 2026-05-01
**Confidence:** HIGH for genre table-stakes and movement-sync recommendation; MEDIUM for original-game parity feature inventory (definitive list comes from Stage 2/3 GML extraction); HIGH for anti-feature scope guardrails (already locked in `PROJECT.md` Out of Scope).

## Scope Note

The feature landscape is dual-sourced:

1. **Genre table stakes** — what any 2D persistent multiplayer game needs to feel complete (researched here from current ecosystem patterns).
2. **Original-game parity** — what BNO specifically had. This research treats parity inferentially from `decomp/wiki/`, `legacy/servers/*` artifacts, `,ServerCommands.txt`, `Ctrl+O Codes.txt`, and the documented save formats. The authoritative parity inventory will land in Stage 2 (client features) and Stage 3 (server features). Until then, "parity" features below are flagged with `[parity-inferred]` and may shift.

The MVP gate per `PROJECT.md` Stage 6 is **two players in the same room, see each other move, exchange chat messages**. Everything in this document is sized against that anchor.

---

## Feature Landscape

### Table Stakes (Users Expect These)

Features missing from this list = product feels broken to a returning BNO player or any modern 2D MMO player. These split into **MVP-critical** (block the Stage 6 gate) and **parity-critical** (block Stage 7 ship).

#### MVP-Critical (Stage 6 gate)

| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Account creation (new) | Players need an identity that survives reconnect; anonymous-only is for arcade browser games, not persistent ones | MEDIUM (S) | bcrypt/argon2 hash, email optional for v1 (returning-player UX is username-first per `PROJECT.md` Auth decision), no email verification for MVP. Username uniqueness check. |
| Login (existing) | Stage 7 must accept migrated original-BNO usernames | MEDIUM (S) | Two paths: fresh accounts (bcrypt verify) and migrated accounts (transparent re-hash on first successful auth, OR forced reset — see "Account migration" decision section below). |
| Session token / connection auth | WebSocket connection must be tied to authenticated identity, not just an open socket | SMALL | Short-lived JWT or opaque token issued at login, presented on WS upgrade or first WS message. Server rejects unauthenticated frames. |
| Persistent character state | A player who logs out, then back in, expects to be where they left off (or at a sane spawn) with the same identity | MEDIUM | At minimum: position room + spawn override on login, plus character name. `[parity-inferred]` BNO `.bnu` files persist much more (see Differentiators / parity restoration below). MVP can persist only `{username, last_room, x, y}`. |
| Movement sync (other players visible & moving smoothly) | The whole point of multiplayer | MEDIUM | See "Movement-sync recommendation" section below. Recommendation: client-side prediction for local player + interpolation for remote players. |
| Server-side movement validation | Without it, anyone with browser devtools teleports / speedhacks / walks through walls | MEDIUM | Authoritative server holds ground-truth position; validates `delta_t * max_speed` per intent message; clamps to walkable tiles. Already locked as architectural decision (`PROJECT.md` Stage 4). |
| Basic chat (room/local channel) | Cannot have "multiplayer + chat" MVP without it | SMALL | Server broadcasts chat to all sockets in the same room. No history persistence at MVP. Plain text, length-capped (e.g., 200 chars). |
| Room/zone model | BNO had distinct rooms (the GameMaker `room_*` model); modern players expect zones | MEDIUM | Server tracks `players[room_id] -> Set<sessionId>`. Movement and chat scoped to room. Room transitions are explicit messages. Single-process, multi-room. See "Room/zone management" section below. |
| Disconnect / reconnect handling | TCP/WS drops happen constantly on real networks; players expect to drop back in without losing 30 minutes | MEDIUM | Server keeps player state in memory for N seconds (e.g., 60s grace) after socket close; reconnect within window resumes session. After grace window, force re-login but state is on disk. Broadcast leave/join only after grace expires for a clean drop. |
| Player nameplate / visible identity | Players can't socialize if they can't tell who is who | SMALL | Render username above sprite. Trivial once movement sync works. |
| Sane error UX (login failed, server down, kicked) | "Connection lost" with no message = uninstall | SMALL | Client surfaces server-emitted reason codes as user-readable strings. |

#### Parity-Critical (Stage 7) — Beyond MVP, Required For Ship

| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Full character persistence | `[parity-inferred]` Original `.bnu` per-user state is rich (HXB, Inv, MB_News are documented dirs in `legacy/servers/*/UserData/`) — inventory, message-board reads, and presumably more | LARGE | Schema definition is **blocked on Stage 3 GML extraction** of `Master 5-4.gmd`. Until then, treat `.bnu` as opaque. Once schema known, define migration script from `.bnu` → SQL/JSON. |
| All original rooms / world map | Returning players expect to recognize the world | LARGE | Room layouts extracted in Stage 1, ported to client tilemaps in Stage 6/7. Not a server concern beyond room-id registry. |
| All original sprites + animations | Same — recognizability is the parity bar | LARGE | Asset pipeline. Client-side. |
| Server-side admin commands (kick, mute, ban) | `[parity-inferred]` `,ServerCommands.txt` documents `Ctrl+E` clipboard exec, `Ctrl+Q` variable inspect, `Ctrl+???` client-side exec | MEDIUM | Modernized: replace clipboard-exec-as-superuser with a vetted command set callable from an admin auth scope. **Do not port the original RCE-by-design model** (flagged in `CONCERNS.md` as wide-open RCE). |
| Message board / persistent posting | `[parity-inferred]` `MB_News`, `MB_Log.bnb` exist in every server snapshot — the game had a persistent message board | MEDIUM | Schema TBD pending Stage 3. Database-backed in rebuild (not flat-file). |
| Per-user settings persisted | `[parity-inferred]` `Settings.bno` exists; original game had server-side user settings | SMALL once schema known | Same blocker as `.bnu` — needs Stage 3 extraction. |
| Server-authoritative game-state events | Whatever the original game did beyond "walk + chat" (combat? trades? NPCs?) belongs server-side | LARGE | Scope unknown until Stage 3. The `Crasher.exe` and `Server Saver.exe` artifacts in `legacy/servers/` hint at server lifecycle features (state snapshot, recovery) that need cataloguing. |

### Differentiators (Quality / Competitive Advantage)

These are the "feels modern, not just faithful" upgrades. They map to "modernized UX" in `PROJECT.md` Stage 6.

| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Friends list | Persistent social graph; lets players find each other across sessions instead of yelling in chat | MEDIUM | Add table `friendships(user_a, user_b, status, created_at)`. Symmetric. UI surface: list with online/offline status. **Depends on**: account system (table-stakes). |
| Online/offline presence | Without it, the friends list is just a list of strings | SMALL | Server tracks `online_users: Set<userId>`. Friends list query joins against it. Push presence change events to friends. |
| Private messages (whisper / DM) | Lets friends talk across rooms; current chat is room-scoped | SMALL once chat works | Same chat-message infra, addressed by recipient userId instead of broadcast. Persist to DB if offline-message support is wanted (next row). |
| Offline message delivery | Send a DM to a friend who's offline; they see it on next login | MEDIUM | `messages(from, to, body, sent_at, read_at)` table. Needs a TTL/cap policy (e.g., 100 messages or 30 days) to bound storage. |
| Ignore / block list | Lets users self-moderate; reduces moderator load | SMALL | Per-user `blocks(blocker, blocked)` table. Server filters chat/whispers/visibility based on blocker's list. Cheaper than profanity filtering for most user-vs-user friction. |
| Player report mechanism | Lets the community surface bad actors without admin attention required for every incident | MEDIUM | Reports persist with a snippet of recent chat / context. Admin queue in admin tool. **Depends on**: chat history retention (even if just a rolling 100-line buffer). |
| Moderation tools (mute / kick / ban) for non-server-console admins | The original game required physical/RDP server console access (huge security/scaling problem) | MEDIUM | Web-based admin UI gated on admin role flag. Granularity: per-user mute (chat only), kick (forces disconnect), ban (refuses login by username + optional IP). |
| Chat history per room (recent N lines) | Players who join mid-conversation can see context | SMALL | In-memory rolling buffer per room, e.g., last 50 lines, broadcast as a single payload on room enter. Optional DB persistence for moderation review. |
| Account recovery (email-based reset) | Without it, every forgotten password is a manual admin task — does not scale even at 50 CCU if churn is high | MEDIUM | Requires email-on-account (optional at signup, but pushed for migrated accounts). SMTP via a transactional provider (Resend/Postmark/SES). One-time signed reset token. |
| Account-level settings (audio, keybinds, accessibility) | Modern players expect to rebind keys, mute SFX, adjust UI scale | MEDIUM | Stored per-user; client fetches on login. Most settings are client-only and don't need server validation. |
| In-game UI polish (HiDPI, responsive layout, decent typography) | Already a `PROJECT.md` constraint ("Modernized UX (HiDPI, responsive, keyboard + gamepad input)") | MEDIUM | Phaser 3 / Pixi handle DPR; the work is mostly choosing to do it from day one rather than retrofitting. |
| Emotes / canned animations | Cheap social signaling; classic 2D MMO staple | SMALL | New packet type `EMOTE`, server broadcasts to room. Client plays sprite animation. **Depends on**: per-character animation system. |
| Party / grouping (named groups across rooms) | Lets friends play together when split across rooms | LARGE | Group state, group chat channel, group-leave/join semantics, leadership transfer, persistence across disconnect. **Defer past v1** — friends list + whispers covers the social need at <50 CCU scale. |
| Web-native delivery (no install, share-a-URL) | The biggest single UX win over the original; original required Win32 binary + 39dll + manual server connect | SMALL (it's a consequence of the stack choice, not a feature to build) | Already locked in `PROJECT.md` (Chrome desktop, Vite client). Worth calling out because it's a major differentiator vs. the original. |

### Anti-Features (Out of Scope For v1)

Most of these are already locked in `PROJECT.md` "Out of Scope". Documented here to surface the *why* and the alternative.

| Feature | Why Requested | Why Problematic | Alternative |
|---------|---------------|-----------------|-------------|
| Voice chat | "Modern multiplayer has voice" | Adds WebRTC infra (TURN/STUN, signaling, peer mesh or SFU), moderation burden 10x text (per [getstream voice moderation guide](https://getstream.io/blog/audio-voice-moderation/)), and a privacy/PII surface that doesn't fit a <50 CCU friends-and-test scope. ESRB now audits voice for compliance starting Aug 2025. | Text chat only. Players who want voice use Discord. |
| Mobile clients (iOS / Android native) | "Reach more players" | Tripled platform-test surface; each native runtime needs its own input model, UI scaling, and store compliance. Already excluded in `PROJECT.md`. | Chrome desktop only for v1. Mobile browser is best-effort, not blocking. |
| ML-based anti-cheat (behavior detection) | "Cheaters ruin online games" | At <50 CCU on a friends-and-test scope, the cost-benefit is wildly inverted — model training data doesn't exist, false-positive rate is unacceptable for a small community, and it doesn't address the actual cheats people will try. Already excluded. | Authoritative server (already locked) + per-input bounds checks + manual ban for confirmed cheaters. |
| Monetization (cosmetics, battle pass, ads) | "Need revenue" | Adds payment infra (Stripe + tax), age gates, refund logic, and changes the moral relationship with returning players who paid nothing for the original. | Free, no monetization for v1. Donation link if hosting cost becomes real. |
| Modding / user-generated content API | "Long tail content" | Sandboxing user code in the server is a research project; UGC moderation queue is a full-time job; backwards-compat constraints lock the rebuild before parity is even reached. Already excluded. | Defer indefinitely. If demanded post-parity, scope as a separate project. |
| 3D / VR | "Modern visuals" | Faithful 2D rebuild is the entire point — going 3D is a different game. Already excluded. | Stay 2D. |
| Email verification at signup (MVP) | "Prevent throwaway accounts" | Adds blocking SMTP dependency at MVP; at <50 CCU friend-and-test scope, the abuse vector isn't real yet | Make email optional at signup, required only if user opts into account recovery later. |
| Real-time everything (e.g., live presence updates streamed at 60Hz) | "Feels alive" | Wastes bandwidth + server CPU on info no one consumes; chat doesn't need <100ms latency | Push presence/friends-list updates as discrete events, not streams. Movement is the only true real-time channel. |
| Server-side replays / spectator mode | "Esports table stakes" | The original is not an esport. Recording infra, storage cost, replay-deterministic-engine constraints all real. | Skip unless concrete demand emerges post-parity. |
| Cross-server / federation | "Bigger world" | Not needed below sharding threshold; see "Room/zone management" below. Single Fly machine is the explicit `PROJECT.md` v1 scale. | Single server. Revisit if CCU sustainably exceeds ~200. |
| Public account self-service username changes | "Quality-of-life" | Breaks identity continuity for the migrated original-BNO users — the entire "preserve usernames" decision is undone if anyone can rename. Also breaks friends list / message history references. | Display name (cosmetic, changeable) layered on top of immutable username. Defer past v1. |
| Aggressive AI profanity filter | "Toxicity is bad" | Matches generated by AI filters at MVP scale produce more false positives than catches; per [WebPurify gaming guide](https://www.webpurify.com/gaming/) and [Greip blog](https://greip.io/blog/From-Toxic-to-Terrific-A-Guide-to-Integrating-Profanity-Filters-in-Online-Gaming-247) effective filters need ongoing wordlist tuning that doesn't justify itself for friends-and-test scope. | Simple wordlist filter (toggleable per-user) + ignore/block list + report mechanism + admin mute. AI-based moderation can be layered in later if community grows. |

---

## Movement-Sync Recommendation

**Recommendation: client-side prediction for the local player + entity interpolation for remote players, with server reconciliation. Authoritative server. NO lockstep.**

### Rationale for the BNO rebuild's specific shape

Three constraints drive this:

1. **Casual 2D top-down movement.** Not a competitive shooter, not an RTS. Sub-frame tick precision is not needed; "remote players are visibly ~100ms in the past" is invisible at this game's pace.
2. **<50 CCU on a single Fly.io machine.** Server CPU and bandwidth budgets are generous per-player; we can afford to validate every input.
3. **WebSocket transport.** Reliable ordered TCP-like — no UDP for this stack — which removes the need for the more complex sequence-number / packet-loss handling that FPS-grade netcode requires.

### Why each technique is included

- **Client-side prediction (local player only).** Without it, the local player feels laggy on every input — keypress → wait for round-trip → see movement. Even at 50–100ms RTT this is unbearable for a top-down game where the player presses an arrow key and expects instant motion. Per [Gabriel Gambetta's reference series](https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html) and [KinematicSoup](https://kinematicsoup.com/news/2017/5/30/multiplayerprediction), this is industry-standard for any responsiveness-sensitive multiplayer.
- **Server reconciliation.** When the server's authoritative position diverges from the client's prediction (because the server clamped a wall hit, or the player got hit by a server-side event), the client smoothly snaps back. Implementation: client keeps a buffer of recent inputs with monotonic input IDs; server acknowledges with the input ID it last processed and the resulting authoritative position; client replays inputs after that ID against the server-corrected position.
- **Entity interpolation (remote players only).** Render remote players ~100–150ms behind the latest received state, interpolating between the two most recent server snapshots. Per [Gambetta's interpolation article](https://www.gabrielgambetta.com/entity-interpolation.html), this trades a small visual delay for completely smooth movement that handles dropped/late updates gracefully. No client-side prediction for remote entities — we don't try to predict where another player will move.

### Why NOT lockstep

Lockstep (every client simulates identically from a shared input stream) is great for RTS / fighting games where determinism matters and player count is small + fixed. For an MMO-style room with players joining/leaving, variable latency, and no need for frame-perfect replay, lockstep adds rigid sync requirements without payoff. Skip.

### Why NOT interpolation-only (no prediction)

Would work — and is genuinely simpler — *if* the local player's input latency was acceptable. At 80ms RTT on a typical Fly.io edge connection, the local player sees an 80ms delay between keypress and movement on screen. For a top-down WASD/arrow-key game that's noticeable and bad. Adding prediction is ~2 days of work and pays back forever.

### Tick rate

- **Server tick: 20 Hz (50ms).** Standard for non-FPS multiplayer. Sufficient for casual movement.
- **Client send rate: 20 Hz** (one input/intent message per server tick). Client batches keys-held-this-tick into one message.
- **Server broadcast rate: 20 Hz** authoritative positions per room.
- **Client render rate: 60 Hz** (or display Hz), interpolating between the two most recent server snapshots.

At 50 CCU split across rooms with maybe 10 players in the busiest room, broadcast cost = 10 players × 10 visible peers × 20 Hz × ~24 bytes per position update = ~48 KB/s outbound for that room. Trivial.

### Movement-sync feature dependencies

- Requires: server-side validation (table stakes), authoritative server model (locked).
- Enables: collision detection on server, zone-based culling (don't broadcast players in other rooms).
- Enabled by: WebSocket binary frames (locked), shared TS packet types between client and server.

---

## Chat Pattern Recommendation

### Channel model

Three channels at parity (one at MVP):

1. **Local / room** — broadcast to all players in the same room. **MVP: this only.**
2. **Whisper / DM** — direct user-to-user, addressed by username. Routed even across rooms.
3. **System / server** — server-emitted notifications (player joined, kicked, server restart in 5min). One-way, not user-sendable.

Defer global / world channel past v1 — it's a moderation cost multiplier with little gain at <50 CCU.

### Rate limiting

Per `getstream.io` and `WebPurify` chat-moderation references, the right pattern is per-user token bucket:

- **5 messages per 5 seconds per user**, burst-allowed up to ~10. Slow-mode on top: minimum 1s between messages in the same channel.
- Server enforces; client soft-enforces (greys out send button) to avoid surprising the user.
- Violation response: silently drop the message, send back a reason code (`RATE_LIMITED`).

Whisper rate limit is stricter (e.g., 3/10s) because it bypasses room-bound visibility and is a higher abuse vector.

### Profanity filter

**MVP: none.** Use ignore/block + report instead.

**Parity: simple wordlist + per-user toggle.**

- Server-side wordlist of ~100 common slurs + obvious variants. Substring match with word boundaries.
- Per-user toggle in account settings (default ON for room chat, OFF for whispers — whispers are consensual).
- Filtered messages render with replacement (`****`), not blocked entirely, so context is preserved.
- AI-based filtering deferred — see Anti-Features. Rebuild can integrate WebPurify / GGWP / Greip later if community grows past the friends-and-test scale.

### History persistence

- **In-memory rolling buffer per room: last 50 messages.** Sent to client on room enter so they don't see an empty channel.
- **DB persistence for whispers if offline delivery is enabled.** TTL 30 days.
- **DB persistence for room chat ONLY for moderation review.** Last 24h, accessible to admin tools, auto-purged. Never exposed to players.
- **Logs are PII.** Treat per the lessons in `CONCERNS.md`: never commit, encrypt at rest, access-controlled.

### Chat feature dependencies

- Requires: account system (sender identity), session auth (prevent impersonation), room model (scoping room channel).
- Enables: report mechanism, moderation, whispers, social signals.
- Enabled by: rate limiter, profanity wordlist (parity, not MVP).

---

## Room/Zone Management Recommendation

### Architecture

**Single Node process, multi-room, all rooms in-memory.** No sharding at v1.

```
NodeServer
├── RoomRegistry
│   ├── Room "town_square"  → { players: Set, tickLoop, chatBuffer }
│   ├── Room "shop_area"    → { players: Set, tickLoop, chatBuffer }
│   ├── Room "battle_zone"  → { players: Set, tickLoop, chatBuffer }
│   └── ...
├── SessionRegistry → { sessionId → { userId, ws, currentRoom } }
└── DB (SQLite/Postgres, decision deferred per PROJECT.md)
```

Rooms are not separate processes — that's premature complexity for <50 CCU. One Node event loop handles all rooms; the 20Hz tick per room is cheap.

### Room transitions

- Player sends `ENTER_ROOM { room_id }` (with a server-side gate: must be reachable from current room, or it's a teleport-class action requiring server consent).
- Server: removes from old room's player set, adds to new, broadcasts `PLAYER_LEFT` to old room and `PLAYER_JOINED` to new, sends new room's chat buffer + initial player snapshot to the entering player.
- Persistence: `last_room` field updated atomically.

### Sharding triggers — when to revisit

The single-process model holds until **one or more** of these is true:

- **CCU sustainably > ~200.** At this point Node event-loop latency from per-tick broadcast starts being measurable and you want process-per-region.
- **Single most-popular room > ~50 concurrent players.** Bandwidth from "everyone-sees-everyone" position broadcasts becomes O(n²) per tick.
- **CPU profiling shows the tick loop blocking > 5ms.** Gives the green light for splitting into worker threads or processes.
- **Multi-region requirement.** Latency to Asia/EU users from one Fly region becomes a complaint. Solve with regional Fly machines + cross-region presence sync — but that's a v2+ problem.

None of these are remotely close at v1's <50 CCU friend-and-test scope. Fly.io single machine is the right answer per `PROJECT.md` Constraints.

### Room/zone feature dependencies

- Requires: persistence layer (for `last_room`), session registry, account system.
- Enables: scoped chat, scoped movement broadcasts, world-map navigation, per-room moderation rules.
- Enabled by: server tick architecture, WebSocket session routing.

---

## Account Migration UX Recommendation

The original `localList.txt` / `remoteList.txt` files contain ~298 plaintext (username, password) pairs (per `CONCERNS.md`). These are PII and **must never leave a local-only environment unredacted**. The migration design has to respect that.

### Recommended approach: **transparent re-hash on first successful login, with a forced reset escape hatch.**

1. **Pre-migration (offline, local-only):**
   - Read `localList.txt` (the more recent "remote" list per the 2021 provenance note).
   - For each `(username, password)` row: compute `bcrypt(password)` and store `(username, bcrypt_hash, original_hash_format='plaintext_migrated', migrated_at)` in the new DB.
   - The plaintext password is **never written to the new DB** — only the bcrypt hash.
   - Source `localList.txt` is then deleted from the migration host (or re-redacted to `[REDACTED]` per `CONCERNS.md` recommendation).

2. **At first login (returning player):**
   - User enters their original username + password.
   - Server bcrypt-verifies. Success → log them in, set `migrated_at_login=now()`, optionally surface a one-time toast: "Your account was migrated from BNO classic. We've upgraded the security on your password — no action needed. You can change your password anytime in Settings."
   - Failure → standard "wrong password" path. (We are NOT keeping the plaintext, so we cannot help them recover the original password — but they can use email-recovery if they registered an email, or admin-assisted reset.)

3. **Forced-reset escape hatch:**
   - If a returning user logs in successfully but the original password fails our modern strength rule (e.g., < 8 chars, top-1000 common password — many of the original passwords in `CONCERNS.md` examples qualify: `123456`, `harrypotter`), prompt for a new password before allowing in. Do NOT block them — let them choose a new one inline.
   - Rationale: many original passwords are weak by current standards. We migrate identity (the username) without forcing migration of weak credentials.

4. **Email collection (optional, post-MVP):**
   - First login does not require email. User can add one later for recovery purposes.
   - When they do, send a verification link before the email is usable for recovery.

### Why this UX wins

- **No "click this link in your email" gate before they can play.** Many of the original players are 10+ years gone; their old emails may be dead. Forcing email verification before first login orphans them.
- **Username preserved** — addresses the `PROJECT.md` "preserve identity continuity for returning players" decision.
- **Modernizes the hash silently** — no scary "your password was insecure" lecture, just a silent re-hash. The "we upgraded security" toast is informative, not blame-shifting.
- **Weak-password remediation happens at the most teachable moment** (post-login, in-context).
- **Survives the migration database being lost.** Bcrypt hashes are derivative; if we lose them we can re-run the migration from a fresh redaction-pass on `localList.txt` if the source artifact is preserved. (The source `localList.txt` files in `legacy/servers/*` are exactly that backup — see `CONCERNS.md` for the inventory and PII handling requirements.)

### Migration feature dependencies

- Requires: bcrypt/argon2 infrastructure, DB user table, login flow.
- Enables: returning-player re-engagement (the entire reason for the rebuild per `PROJECT.md` Core Value).
- Blocked by: nothing technical — but operationally requires a one-time, isolated migration host that handles the plaintext source list.

---

## Feature Dependencies

```
Account creation ──┬──> Login ──┬──> Session token
                   │            │       │
                   │            │       └──> Authenticated WS connection
                   │            │              │
                   │            │              ├──> Chat (any channel)
                   │            │              ├──> Movement (intent → server validate)
                   │            │              ├──> Persistent character state
                   │            │              └──> Room transitions
                   │            │
                   │            └──> Account migration (returning users)
                   │
                   ├──> Account settings (audio, keybinds, etc.)
                   └──> Email recovery (optional, post-MVP)

Movement (server-validated)
   ├──requires──> Authoritative server tick loop
   ├──requires──> Room model (scope broadcasts)
   ├──enables──> Collision (server-side)
   └──enables──> Visible remote players (interpolation)

Chat (room channel)
   ├──requires──> Authenticated session
   ├──requires──> Room model
   ├──requires──> Rate limiter
   ├──enables──> Whispers (DMs)
   ├──enables──> Reports (uses chat history snapshots)
   └──enables──> Mute (admin action on chat)

Friends list ──requires──> Account system
   └──enables──> Online presence + Whisper-from-friends-list

Moderation
   ├──requires──> Account system (target identity)
   ├──requires──> Admin role flag (server-side)
   ├──requires──> Chat history (for evidence)
   └──provides──> Mute / Kick / Ban (each with their own server enforcement path)

Persistence layer
   ├──required-by──> EVERYTHING that survives reconnect
   └──blocked-by──> Stage 3 schema decision (SQLite vs Postgres)

Full character persistence (.bnu parity)
   └──blocked-by──> Stage 3 GML extraction (.bnu schema unknown until then)
```

### Dependency notes

- **Persistent character state (full parity) is blocked on Stage 3.** Until the original `.bnu` schema is extracted, only minimal state (`{username, last_room, x, y}`) can be persisted at MVP. This is enough for the MVP gate but not for Stage 7. **Implication for roadmap: the parity-feature inventory cannot be finalized until Stage 3 completes.**
- **Friends list, whispers, reports all require account system + chat infra to be solid.** Chase MVP first, layer these as a "social" milestone after.
- **Moderation tools MUST exist before any non-friend user is invited.** The original game's "Ctrl+E run clipboard as superuser" approach is a security nightmare flagged in `CONCERNS.md`. Build modern admin commands as part of the same milestone that opens signups beyond friends.
- **Rate limiter is an MVP+1 feature.** At MVP (two friends testing) it's not enforced; first time real users join, it must be on. Don't ship a non-rate-limited chat to the public.
- **Email recovery's hard dependency is SMTP infra**, not code. Choose a provider (Resend / Postmark / SES) at the time you decide to build recovery — not earlier.

---

## MVP Definition

### Launch With (v1 — Stage 6 MVP gate)

The strict reading of `PROJECT.md` Stage 6: "two players join the same room, see each other move, exchange chat messages."

- [ ] Account creation (new accounts only — migration can wait a beat after MVP if it streamlines the gate)
- [ ] Login + session token issuance
- [ ] Persistent username (DB-backed, bcrypt hash)
- [ ] WebSocket connection authenticated by session token
- [ ] Single room (start with one — MVP doesn't need the world map yet)
- [ ] Movement: client-side prediction for local + interpolation for remote + server reconciliation + server-side validation
- [ ] Chat: room/local channel, server broadcasts to all in room
- [ ] Player nameplate visible above sprite
- [ ] Disconnect/reconnect handling with grace window
- [ ] Sane error messages (login failure, server down, kicked)
- [ ] One placeholder sprite, one placeholder room — no asset parity required at MVP

That's it. **Not** at MVP: friends list, whispers, moderation, full parity rooms, full sprite library, message board, account migration UI, chat history persistence, profanity filter, email recovery.

### Add After Validation (v1.x — between MVP and full parity ship)

Triggered by: MVP runs reliably for two test users for a week without intervention.

- [ ] Account migration flow for returning BNO usernames (bcrypt re-hash on first login)
- [ ] Multi-room support with room-transition mechanics
- [ ] Player report mechanism + admin queue
- [ ] Chat rate limiting (turn it on before any non-tester joins)
- [ ] Ignore/block list
- [ ] Friends list + online presence
- [ ] Whisper / DM
- [ ] Wordlist profanity filter (toggleable per-user)
- [ ] Modernized admin tools (web UI for kick/mute/ban — replacing the original RCE-style server console)
- [ ] Account settings (audio, keybinds, accessibility)
- [ ] Account recovery (email-based reset)
- [ ] Chat history (rolling buffer per room, sent on enter)
- [ ] Session reconnect across browser refresh (grace window, no re-login)

### Future Consideration (Stage 7 full parity, then beyond)

Triggered by: Stage 3 GML extraction completes; parity feature inventory becomes definitive.

- [ ] Full character state persistence per `.bnu` schema (TBD post-Stage 3)
- [ ] All original rooms / world map ported
- [ ] All original sprites + animations
- [ ] Message board (`MB_News`, `MB_Log`) reimplemented against modern DB
- [ ] Per-user settings persistence (`Settings.bno` parity, schema TBD post-Stage 3)
- [ ] Whatever gameplay-specific features Stage 3 surfaces (combat? trades? NPCs? — currently unknown)
- [ ] Offline message delivery (whispers persist for offline recipients)
- [ ] Emotes
- [ ] Party / grouping (only if friends list + whispers prove insufficient — likely defer)

### Beyond v1 — Explicitly Out (and staying out)

Per `PROJECT.md` Out of Scope and the Anti-Features section above: voice chat, mobile native, ML anti-cheat, monetization, modding API, 3D/VR, server replays, federation, AI profanity filtering, public username changes.

---

## Feature Prioritization Matrix

| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| Account creation + login | HIGH | MEDIUM | P1 |
| Session-authenticated WS | HIGH | LOW | P1 |
| Movement sync (prediction + interpolation) | HIGH | MEDIUM | P1 |
| Server-side movement validation | HIGH | MEDIUM | P1 |
| Room/local chat | HIGH | LOW | P1 |
| Single room model | HIGH | LOW | P1 |
| Disconnect/reconnect handling | HIGH | MEDIUM | P1 |
| Persistent username + min character state | HIGH | LOW | P1 |
| Player nameplates | MEDIUM | LOW | P1 |
| Sane error UX | HIGH | LOW | P1 |
| Account migration (returning players) | HIGH | MEDIUM | P2 |
| Multi-room + transitions | HIGH | MEDIUM | P2 |
| Chat rate limiting | MEDIUM | LOW | P2 (must precede public access) |
| Friends list + presence | MEDIUM | MEDIUM | P2 |
| Whispers (DMs) | MEDIUM | LOW | P2 |
| Ignore/block | MEDIUM | LOW | P2 |
| Player report mechanism | MEDIUM | MEDIUM | P2 |
| Admin tools (kick/mute/ban) | HIGH (operationally) | MEDIUM | P2 (must precede public access) |
| Wordlist profanity filter | MEDIUM | LOW | P2 |
| Account recovery (email reset) | MEDIUM | MEDIUM | P2 |
| Chat history (rolling buffer) | MEDIUM | LOW | P2 |
| Account-level settings | MEDIUM | MEDIUM | P2 |
| Full `.bnu` parity | HIGH (for returning players) | LARGE | P3 (blocked on Stage 3) |
| All original rooms + sprites | HIGH (parity) | LARGE | P3 |
| Message board | MEDIUM | MEDIUM | P3 |
| Emotes | LOW | LOW | P3 |
| Offline message delivery | LOW | MEDIUM | P3 |
| Party / grouping | LOW | LARGE | P3 (defer indefinitely) |
| Voice chat | LOW (in this game's scope) | LARGE | OUT |
| Mobile native | LOW (in v1) | LARGE | OUT |
| ML anti-cheat | LOW | LARGE | OUT |
| Monetization | LOW | LARGE | OUT |
| Modding API | LOW | LARGE | OUT |
| 3D / VR | LOW | LARGE | OUT |

**Priority key:**
- P1: MVP gate (Stage 6)
- P2: Required before public access / full parity ship (Stage 7)
- P3: Parity-driven, scope confirmed by Stage 3 extraction
- OUT: Anti-feature, locked out per `PROJECT.md`

---

## Competitor / Reference Analysis

The "competitors" here are reference points rather than market rivals — this is a faithful rebuild of a specific game, not a market product. References inform the modernization layer.

| Feature | BN Online (original) | Generic 2D MMO (e.g., Habbo / Tibia / browser-MMO patterns) | Our Approach |
|---------|----------------------|----------------------------------------|--------------|
| Auth | Plaintext passwords in `localList.txt`; no hashing | bcrypt/argon2, optional 2FA | bcrypt/argon2 with username preservation + transparent re-hash on first login |
| Transport | 39dll over raw TCP, no encryption | TLS WebSocket | WSS (WebSocket Secure) — TLS terminated at Fly.io edge |
| Movement | `[parity-inferred]` Authoritative server with `writebyte`/`writedouble` move packets per `decomp/wiki/08-39dll-networking.md` | Mix of authoritative + client-prediction depending on game | Authoritative server + client-side prediction + interpolation |
| Chat | `[parity-inferred]` Room-broadcast chat, no persistence beyond `MB_Log.bnb` board | Multi-channel (local/global/whisper), AI-moderated, rate-limited | Room channel at MVP; whispers + system + wordlist filter at parity |
| Admin | "Ctrl+E run clipboard as superuser" RCE-by-design (`,ServerCommands.txt`) | Web admin panel with role-gated commands | Web admin panel, role-gated, no clipboard exec |
| Friends list | None apparent in legacy sources | Standard | Add as differentiator |
| Account migration | N/A | N/A | Custom — silent re-hash, weak-password upgrade prompt |
| Modding | None | Some games support; most do not | Out of scope |
| Hosting | Single home/colo Windows machine | Cloud (AWS/GCP/Fly/Railway) | Fly.io single machine (`PROJECT.md` locked) |
| Persistence | Bespoke `.bno`/`.bnu`/`.bnb` flat files | SQL or NoSQL | SQLite + Litestream OR Postgres (deferred per `PROJECT.md`) |

---

## Sources

- `C:\Users\decid\Documents\projects\rebno\.planning\PROJECT.md` — Stages, constraints, Out of Scope, Auth/Hosting decisions (HIGH confidence: project-internal source of truth)
- `C:\Users\decid\Documents\projects\rebno\.planning\codebase\CONCERNS.md` — PII handling for `localList.txt`, original RCE-by-design admin model, plaintext credential inventory (HIGH confidence)
- `C:\Users\decid\Documents\projects\rebno\decomp\wiki\08-39dll-networking.md` — Original transport pattern, "structure = call order" packet semantics (HIGH confidence)
- `C:\Users\decid\Documents\projects\rebno\decomp\wiki\16-bno-bnb-notes.md` — `.bno`/`.bnb`/`.bnu` parsing strategy, Stage 3 dependency (HIGH confidence)
- [Gabriel Gambetta — Client-Side Prediction and Server Reconciliation](https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html) — Reference for MVP movement netcode (HIGH confidence: industry-standard reference series)
- [Gabriel Gambetta — Entity Interpolation](https://www.gabrielgambetta.com/entity-interpolation.html) — Reference for remote-player smoothing
- [KinematicSoup — Client-side Prediction for Smooth Multiplayer Gameplay](https://kinematicsoup.com/news/2017/5/30/multiplayerprediction) — Cross-source confirmation
- [GetStream — Chat Moderation 101](https://getstream.io/blog/chat-moderation/) — Rate limiting + moderation patterns (MEDIUM confidence: vendor blog, but methodology is industry-standard)
- [WebPurify — Moderating Gaming Websites and Apps](https://www.webpurify.com/gaming/) — Profanity filter approach
- [Greip — Integrating Profanity Filters in Online Gaming](https://greip.io/blog/From-Toxic-to-Terrific-A-Guide-to-Integrating-Profanity-Filters-in-Online-Gaming-247) — Cross-source on filter limitations at small scale
- [GetStream — Audio and Voice Moderation Implementation Guide](https://getstream.io/blog/audio-voice-moderation/) — Rationale for excluding voice from v1

---
*Feature research for: 2D top-down multiplayer online game (BN Online rebuild)*
*Researched: 2026-05-01*
