---
mvp: yes
subsystem: world-simulation
---

# World Simulation (server-side tick model)

The BNO 5-4 server is a single-threaded GameMaker game loop. There is no separate simulation tick from the engine's render tick — the original GM 5.3a runtime drives ~30 Hz step events and every step is the entire authoritative simulation. The `0349-operations` object is the singleton that owns this loop on the server side: its Step event drains every TCP/UDP socket, dispatches per-opcode handlers, runs disconnect detection, and triggers per-player save cadence.

## The tick is the GameMaker step event

```gml
// 0349-operations.gml — singleton tick loop (excerpt)
if(global.pnum > 0)
{
  loopcount = 0;
  while(global.pnum > 0 && loopcount < 250)
  {
    var pid, size, upid;
    //Check for messages from all players (Server only)
    for(pid = -1; pid < global.pindex; pid += 1)
    {
      size = 0;
      upid = -1; //Used for UDP receives
      clearbuffer();
      if(pid == -1) //Check UDP socket for messages
      {
        size = receivemessage(global.udpsocket);
        if(size > 0) { upid = ip2pid(lastinIP()); break; }
      }
      else if(global.p_online[pid])
      {
        upid = pid;
        size = receivemessage(global.p_tcpsocket[pid]);
        if(size >= 0) break;
      }
    }
    if(size < 0) break;
    if(size == 0 && global.p_online[upid])
    {
      // disconnect detected
      uninit_user(upid);
      if(global.pindex == 0) break;
    }
    if(size != 0) messageid = readbyte();
    else messageid = -1;
    // ... `server_receive(messageid)` switch dispatch follows ...
  }
}
```

The salient properties:

- **No fixed-tick accumulator** — the simulation runs whatever the engine schedules. Phase 4 SRV-05 will add a fixed-tick simulation step (Colyseus default 50 ms / 20 Hz) decoupled from the network read loop.
- **Polling, not async** — every step pulls from every socket. There are no callbacks. A slow read blocks the entire game loop for the duration of the receive call.
- **`loopcount < 250` safety brake** — caps how many messages can be drained per tick. Above this, the loop yields and resumes next step. Phase 4 server replaces this with proper async I/O.
- **Server is itself a player slot.** The same `global.p_*` arrays the server uses for player state, the server-side "operator" GUI also writes to (via `commandob`, see [./admin-anti-port.md](./admin-anti-port.md)). `pid == -1` is the UDP socket; `pid == 0..global.pindex` are real players.

## Singleton objects

| Object | Role |
|--------|------|
| `0000-server` | The "operator" instance — the server-side avatar / control surface for any human running the binary |
| `0042-player` | The player object reused server-side for collision math (place_meeting / collision_rectangle on the server's tilemap) |
| `0349-operations` | Tick driver — owns Step, Alarm[1] (the maintenance/restart timer), and the receive-dispatch loop |
| `0342-startserver` | Boot-time init: opens TCP listen socket, opens UDP socket, calls `users_load` / `users_restore` |

## Tilemap collision (server-authoritative)

The server runs the same tile-collision math as the client. Scripts `0085-tileborder.gml`, `0086-tleftcheck.gml`, `0087-trightcheck.gml`, `0088-ttopcheck.gml`, `0089-tbottomcheck.gml` mirror the [client-side scene-room-model](../extracted-engine/scene-room-model.md) collision routines so the server can validate movement intent. Client sends "I want to move east"; server runs `tleftcheck`/`trightcheck`/etc. against the destination tile and either accepts or snaps the player back.

`0099-orpos_meeting.gml` checks if the player's bounding rectangle overlaps another instance — used for door / teleporter / area-trigger collision.

## Per-player state arrays

The world state lives in `global.p_*[pid]` parallel arrays. Per `0360-init_user.gml`:

```gml
global.p_online[argument0] = 1;
global.p_name[argument0] = "UNDEFINED";
global.p_spr[argument0] = NaviStandD;
global.p_room[argument0] = Online_Command_Screen;
global.p_canduo[argument0] = 0;
global.p_induo[argument0] = 0;
global.p_watchable[argument0] = 0;
global.p_sendalarm[argument0] = 900;
global.p_logalarm[argument0] = 2700;
```

`p_room` is the GameMaker room ID — see [./room-management.md](./room-management.md) for room transition mechanics. `p_sendalarm` / `p_logalarm` are countdowns that fire periodic save events (see [./persistence.md](./persistence.md)).

## Rebuild guidance (Phase 4 SRV-05)

For the rebuild server:

- **Fixed 50 ms tick** via Colyseus's built-in `setSimulationInterval`. Decoupled from network read — Colyseus drains the WebSocket separately on its own event loop.
- **Per-room state in Colyseus rooms.** Each original GM room becomes a Colyseus room schema; `p_*[pid]` arrays become `state.players` MapSchema.
- **Authoritative collision.** The same tile/border collision math runs server-side in TypeScript. Client sends `{type: 'walk', dir}`; server validates against tilemap before applying.
- **No per-step file I/O.** The original calls `save_settings()` / `all_backup()` from inside the step event — that is one of the worst latency hazards in the original. Phase 4 moves these to async background tasks (or relies on Litestream replication; see [./persistence.md](./persistence.md)).

## See also

- [./room-management.md](./room-management.md) — how `p_room[pid]` transitions
- [./packet-protocol.md](./packet-protocol.md) — what `server_receive` dispatches on each opcode
- [./persistence.md](./persistence.md) — when the tick triggers saves
- [../extracted-engine/scene-room-model.md](../extracted-engine/scene-room-model.md) — client-side mirror of room transitions

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0085 | tileborder | 16 | — |
| 0086 | tleftcheck | 5 | — |
| 0087 | trightcheck | 5 | — |
| 0088 | ttopcheck | 5 | — |
| 0089 | tbottomcheck | 5 | — |
| 0099 | orpos_meeting | 5 | — |
| 0116 | hbordercol | 10 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
| 0000 | server | 65 | 34 | 25 |
| 0020 | tile1 | 23 | -1 | 1 |
| 0021 | tside1 | 24 | -1 | 1 |
| 0022 | vborder | 25 | -1 | 1 |
| 0023 | hborder | 26 | -1 | 1 |
| 0025 | dirttile | 46 | -1 | 1 |
| 0026 | dtside | 47 | -1 | 1 |
| 0027 | door1vertical | 49 | -1 | 1 |
| 0028 | door1horizontal | 50 | 63 | 2 |
| 0030 | mtile1l | 53 | -1 | 1 |
| 0032 | mtile1u | 55 | -1 | 1 |
| 0033 | mtile1d | 56 | -1 | 1 |
| 0042 | player | 65 | 34 | 6 |
| 0048 | mtile1r | 54 | -1 | 1 |
| 0053 | mplat1_ud_d | 23 | -1 | 8 |
| 0054 | mplatparent | 23 | -1 | 0 |
| 0056 | mplat1_lr_r | 23 | -1 | 8 |
| 0057 | mplatstop | 67 | -1 | 0 |
| 0058 | teleporter1 | 69 | 34 | 1 |
| 0118 | alphaq | 59 | 59 | 2 |
| 0119 | whitetile | 136 | -1 | 1 |
| 0120 | wtside | 137 | -1 | 1 |
| 0121 | hiddentile | 23 | 141 | 5 |
| 0122 | htside | 140 | -1 | 2 |
| 0342 | startserver | 59 | -1 | 4 |
| 0349 | operations | 426 | -1 | 7 |
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `instance_create` | 1 | 0085-tileborder | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `instance_exists` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `place_meeting` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `collision_rectangle` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
