---
mvp: yes
subsystem: scene-room-model
---

# Scene & Room Model

A "room" in GameMaker 5.x is the unit of scene management. Each room owns:

- **Dimensions** (width × height in pixels)
- **Speed** (steps per second; 30 in BNO)
- **Background layers** (up to 8; each with image, tiling flags, scroll speed)
- **Tiles** (static sprite cells laid out in the editor)
- **Instances** (object instances with starting `x, y, depth`)
- **Views** (camera viewports — BNO uses one)
- **Creation code** (a single GML script that runs once when the room enters)
- **Persistent flag** (whether instance state survives a room exit/re-entry)

`extracted/client-5-8/rooms/` contains 16 rooms. They are:

| ID | Name | Role |
|----|------|------|
| 0 | Test | dev-only test room |
| 3 | Main_Menu | landing screen |
| 4 | Online_Lobby | server lobby (player list, chat) |
| 6 | Settings_Menu | options screen |
| 7 | Online_Command_Screen | admin command console (Ctrl+E surface — see [admin-anti-port.md](admin-anti-port.md)) |
| 46 | Prairie_Flats | open world zone |
| 47 | Database | inside-zone (datacenter aesthetic) |
| 48 | Emptiness_Hull | inside-zone |
| 49 | Digital_Abyss | open world zone |
| 52 | Traverse_Core | hub zone |
| 53 | Bahoo | open world zone |
| 54 | Noiya | open world zone |
| 55 | Schweisstar | open world zone |
| 56 | Floes_of_Ghennam | open world zone |
| 57 | Main_Menu_New | redesigned main menu (presumed beta) |
| 58 | BNCentral | hub zone |

The transition primitives are minimal:

- **`room_goto(room)`** — transition to a specific room. Persistent objects survive; non-persistent are destroyed.
- **`room_restart()`** — re-enter the current room with a fresh instance set.
- **`room_next()` / `room_previous()`** — only meaningful if you index by GM's internal room order; BNO does not use them.

The transition triggers in BNO live in **Script 0117 `change_area`** (called from per-room edge collisions) and **Script 0277 `room_update`** (the post-area-change handler). Script `0095 online_room` is the lobby-specific room init. Script `0326 zone_room` and `0328 zone_name` map (room ↔ zone) for the shared-state protocol — the server tracks which zone each player is in and the client uses these helpers to interconvert.

Script `0336 room_zone` is the inverse — given a room name, return the canonical zone identifier the server expects.

The world rooms (46-58) follow a naming convention from the BN universe (prairie, database, abyss, traverse, etc.). Each is a hand-laid tile grid with NPC instances, hexport beacons, and zone-edge triggers. The Online_Lobby (4) is structurally different — it's a chat/list UI rather than a walking zone — but it uses the same Room primitive.

## Key idioms

- **One transition per call.** `room_goto` does not stack or queue; calling it from a Step event commits the change between this step and the next.
- **Persistent objects** carry state across rooms. The player (0042) is persistent; chat objects (0223 chatob) are persistent; tile objects are not.
- **Zone vs room.** "Zone" is the server-side name (`Prairie`, `Database`, `Abyss`, ...); "room" is the GM-level resource. The client maps via `0326 zone_room` and `0328 zone_name`. Phase 4 server adopts the same vocabulary.
- **Room speed = tick rate.** All BNO rooms run at 30 steps/sec. Phase 4 server-tick alignment matches this.

## Canonical floor tile: **44 × 40 px**

> **Locked extracted constant.** The world floor tile in BN Online is **44 px wide × 40 px tall**. Use these exact dimensions for any room rendering, asset pipeline atlas slicing, collision grid arithmetic, or test-room synthesis. Do not assume 32×32 or any other power-of-two value.

**Source of truth:** `extracted/client-5-8/sprites/0023-Tile1/meta.json`

```json
{
  "name": "Tile1",
  "width": 44,
  "height": 40,
  "bboxLeft": 0, "bboxTop": 0, "bboxRight": 43, "bboxBottom": 39,
  "originX": 0, "originY": 0
}
```

Companion sprite `0024-TSide1` (vertical wall edge) is also **44 × 4 px** — same tile pitch on the X axis, thin Y because it's an edge trim. Sprite IDs `0023`..`0030`-ish in the Tile* / TSide* / HBorder family share the 44-px column pitch.

**Implications for Phase 6+:**

- **MVP room synthesis:** a 20×20 floor grid is 880 × 800 px (20·44 by 20·40). Snap player spawn, walls, and camera bounds to this grid.
- **Asset pipeline (AST-01):** atlas frame width = 44, frame height = 40 for any Tile* / TSide* / HBorder sprite. `meta.json` `width`/`height` are authoritative — read them, don't hard-code.
- **Collision grid:** if a Phase 6/7 collision system uses a uniform cell grid, the cell is 44×40 — not square. Movement step constants from `pcode_mover.gml` must be reasoned about against this non-square cell.
- **Camera math:** room view is 640×480 (see BNCentral `meta.json`). That is **~14.5 tiles wide × 12 tiles tall** — fractional on purpose; do not "round" the view to a tile-multiple.

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0095 | online_room | 12 | — |
| 0117 | change_area | 11 | — |
| 0277 | room_update | 5 | — |
| 0326 | zone_room | 15 | — |
| 0328 | zone_name | 15 | — |
| 0336 | room_zone | 15 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `room_goto` | 2 | 0095-online_room | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `room_restart` | 1 | 0094-begin_client_receive | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `room_next` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `room_previous` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->

## Rebuild guidance

For the Phase 6 client:

- **Map "room" to a Phaser Scene (or Pixi container/stage).** Each scene owns its assets, instances, and tick handler.
- **Server owns zone state.** The client requests a zone change; the server validates and broadcasts. Don't `room_goto` based on local collision alone — that's the original BNO model and it loses authority.
- **CLI-08 MVP needs only Online_Lobby + ONE world room** (probably Prairie_Flats or Bahoo). Pick one, rebuild it. The other 14 are post-MVP.
- **Persistent flag → scene-survival logic.** Phaser keeps scenes alive when you `start` a new one only if you don't shutdown; pick the model that matches GM's `persistent: true` on a per-instance basis.
- **Zone ↔ room mapping is shared protocol.** Both client and server must agree on the canonical zone names. Define in `packages/protocol/src/zones.ts`.

## See also

- [client-networking.md](client-networking.md) — the protocol layer that drives zone changes
- [collision.md](collision.md) — area-edge collisions trigger room transitions
- [ui-and-menus.md](ui-and-menus.md) — Main_Menu and Online_Lobby UIs
- [decomp/wiki/07-gml-core-functions.md](../../decomp/wiki/07-gml-core-functions.md) — `room_*` reference
