---
mvp: yes
subsystem: room-management
---

# Room Management

A "room" in BNO is a GameMaker room ID — there are 49 of them (the constants `Online_Command_Screen`, `BN_Central`, `Nature_Grounds`, `Whirlpool_Promenade`, …). Each player carries a current `p_room[pid]` integer; transitioning means writing a new value and telling the other clients in the source/destination rooms what changed. Server is authoritative for `p_room`; client sends a "request room change" intent.

## Room change opcode (1 c2s)

`0359-server_receive.gml` case 1 is the room-change handler:

```gml
// 0359-server_receive.gml — case 1 (room change)
case 1:
  updateonly = readbyte();
  tempint = readint();
  if(!updateonly)
  {
    global.p_room[pid] = tempint;
    if(tempint == 49) global.dabypass[global.p_uid[pid]] = 1;
    else global.dabypass[global.p_uid[pid]] = 0;
    dynamicaddline(global.p_name[pid] + " has changed to room " + string(tempint) + ".");
  }
  //Send the user a list of data from players in the room they entered:
  if(tempint != Online_Command_Screen)
  {
    clearbuffer();
    writebyte(11);
    // ... s2c "room snapshot" follows ...
```

Properties:

- **2-byte payload**: `updateonly` flag (1 byte) + `room_id` (4 bytes int, GM 5.3a `writeint`).
- **`updateonly == 1` is a no-op for state** — used by the client to *request* a snapshot of who's in the room without actually moving (idempotent re-sync). The server still emits the snapshot.
- **Room 49 = `Whirlpool_Promenade`** triggers the `dabypass` flag (a per-account "Disconnected Alley bypass" — Phase 7 PAR scope).
- **`Online_Command_Screen`** is special: the server's operator avatar lives there, so the snapshot is suppressed.

The room-snapshot s2c response (opcode 11) is described in [./protocol.md](./protocol.md). It enumerates every other player currently in the destination room — name, sprite, x/y, can-duo flag — so the joiner can render them immediately on first paint.

## Area-change vs room-change

`0117-change_area.gml` is the lighter sibling:

```gml
// 0117-change_area.gml
if(global.area != argument0)
{
  global.area = argument0;
  chatob.ara = 0;
  chatob.carea = 1;
}
```

An **area** is a logical chat-channel grouping that aggregates rooms. A "room" is a single GM room id; an "area" is a named region (Central, Nature, Whirlpool, etc.) that may span multiple GM rooms. The chat object (`0223-chatob`) routes messages by area, not room — so two players in adjacent connected rooms within the same area can chat as if co-located.

The 8 area-change objects are `0109-ac_centralsquare`, `0110-ac_naturegrounds`, `0111-ac_whirlpool`, `0112-ac_disconnectedalley`, `0114-ac_roseport`, `0115-ac_petalsquare`, `0116-ac_returnjuncture`, `0123-ac_abyssalpathway`, `0124-ac_divinesquare` — placed on room boundaries; their Step or Collision event calls `change_area()` when the player overlaps them.

## Persistence flags per area

The server saves per-user per-area state in `User_Area.bnu` (one row per `(uid, area)` pair). See [./persistence.md](./persistence.md) and [./save-formats.md](./save-formats.md) for the byte grammar of `uarea_backup` / `uarea_restore`.

## Rebuild guidance (Phase 4 + Phase 6)

- **Each GM room → Colyseus room schema.** Joining a room = Colyseus `joinOrCreate(roomId)`; leaving = `leave()`. Colyseus handles snapshot-on-join automatically via state sync.
- **Areas become tags on rooms.** Chat broadcast can filter by tag without a separate state machine.
- **Reject unknown room IDs.** The original trusts `readint()` blindly. Phase 4 SRV-04 will validate against an allow-list of known rooms before mutating state.
- **The `dabypass` flag becomes an account attribute.** Today it lives in a per-pid global; Phase 4 surfaces it via the player schema.

## See also

- [./protocol.md](./protocol.md) — opcodes 1 (c2s room change) + 11 (s2c room snapshot) + 5 (user log on/off broadcast)
- [./persistence.md](./persistence.md) — `uarea_*` save cadence
- [./save-formats.md](./save-formats.md) — `User_Area.bnu` grammar
- [../extracted-engine/scene-room-model.md](../extracted-engine/scene-room-model.md) — client-side room transition (room_goto)
- [./world-simulation.md](./world-simulation.md) — tick-loop context

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0117 | change_area | 6 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
| 0109 | ac_centralsquare | 135 | 67 | 1 |
| 0110 | ac_naturegrounds | 135 | 67 | 1 |
| 0111 | ac_whirlpool | 135 | 67 | 1 |
| 0112 | ac_disconnectedalley | 135 | 67 | 1 |
| 0114 | ac_roseport | 135 | 67 | 1 |
| 0115 | ac_petalsquare | 135 | 67 | 1 |
| 0116 | ac_returnjuncture | 135 | 67 | 1 |
| 0123 | ac_abyssalpathway | 135 | 67 | 1 |
| 0124 | ac_divinesquare | 135 | 67 | 1 |
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `room_goto` | 2 | 0004-CmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `change_area` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
