Room Transitions — Entering & Leaving a Room

Every room change runs through one ordered sequence, GrogVM's reconstruction of SCUMM's startScene. Getting the order wrong is what breaks transitions: a room's entry/exit scripts, the ego's placement, and the resource swap all interlock, and several MI1 scenes only work if each step happens at the right moment. This note is that sequence and the gotchas that pin each step in place.

The two entry points:

Both funnel into the same transition; loadRoomWithEgo adds the ego handling described in §3.

1. The transition sequence

On a room change, in this exact order:

  1. Clear the draw queue and per-object draw positions. A fresh room starts with nothing queued; its entry script repopulates what should be visible.
  2. Run the exit side, nested: the exit hook, the previous room's EXCD, the second exit hook. SCUMM brackets each room's own script with two global hook scripts whose ids live in VAR_EXIT_SCRIPT / VAR_EXIT_SCRIPT2; everything here runs to completion before the transition returns to the caller — not deferred as a normal slot (see §4). MI1 points the exit hook at #7, which records the room being left in g101 — entry scripts branch on it (Hook Isle's side-dependent touchability, the Voodoo Lady's entrance choreography that closes the door behind you).
  3. Stop the old room's local + object/verb scripts. Room-scoped scripts (WIO_ROOM / WIO_FLOBJECT) die on a room change; globals survive, as do the verb scripts of carried objects (they belong to the inventory item, not the departing room). The purge also spares any slot that owns an active cutscene frame, so the cutscene reaches its own endCutscene instead of stranding its frame on the stack and freezing control — the same spare freezeScripts grants the cutscene's caller (cutscenes §3). Two MI1 shapes ride on it: a Look/Use handler on an inventory object that swaps rooms (a parchment close-up loads its own room, polls, then returns and ends the cutscene), and a verb on a room object whose sibling drives the change (the general-store exit runs the open-door verb's cutScene … endCutScene while the walk-to-door verb calls loadRoomWithEgo; kill the still-animating open-door cutscene at the room change and its frame strands). Without the purge itself, an old room's ambient/animation loop keeps running into the new room and tries to start locals that don't exist there. The same purge covers a previous ENCD/EXCD still yielded mid-slice: a stale entry-script slice that survives resumes against the new room's local table and starts whatever script owns that id there (a previous room's entry script resuming two rooms later is a VM halt, not a glitch).
  4. Reset per-room box flags to the new room's on-disk values (the entry script re-applies any door locks).
  5. Set currentRoom and VAR_ROOM to the requested id — the raw id even for a pseudo-room (the forest maze keeps VAR_ROOM at 201–220; see room §7b).
  6. Load the room's resources. Decode the background, palette, z-planes, scripts; resolve a pseudo-room alias if the id has no physical room (room §7b); re-apply the persistent UI-palette overrides over the freshly-decoded CLUT; and re-queue every object already in a non-zero, image-backed state (so a door left open stays drawn open across re-entry and save/restore).
  7. Resolve box + scale for every actor already placed in the room. A putActor into a room that isn't current can't resolve a walk box — those boxes aren't loaded — so the room load is itself a placement event (see walk-boxes §"Perspective-scale recompute timing"). The intro is the witness: the boot script parks ego on the cliff path (room 38) while the title room is still current; the path room's first frame must already show him at path scale, not full-size.
  8. Bring the entering ego into the new room (loadRoomWithEgo only — §3). Its room membership is set here, before the entry script, so an ENCD that branches on getActorRoom(ego) sees it already arrived; its screen position and entry walk land later, after the entry script's first slice (§3).
  9. Run the entry side, nested: the entry hook (VAR_ENTRY_SCRIPT), the new room's ENCD to its first breakHere (see §4), then the second entry hook (VAR_ENTRY_SCRIPT2). MI1 boots #5/#6 into the entry hooks; #6 re-runs the verb-bar scripts and clears pending sentences on every entry, so the verb panel arrives consistent in each room.

Screen-effect fades bracket the transition but render as instant cuts today (state modelled, animation deferred) — see screen effects.

2. VAR_ROOM is the raw id

VAR_ROOM holds the id the script asked for, untranslated — including a pseudo-room id with its high bit. The forest maze depends on this: its single shared room branches on VAR_ROOM == 201..220 to compose the right "screen," so collapsing the id (e.g. to & 0x7F) would feed the entry script the wrong screen. The pseudo-room alias affects only which resources load, never the id the scripts see. (VAR_WALKTO_OBJ, below, is the other variable a room's entry script reads to know how the ego arrived.)

3. Bringing the ego in — loadRoomWithEgo

loadRoomWithEgo obj N x y places the ego relative to the entry object and lets the new room's entry script walk it the rest of the way:

The forest fork is the worked example. The map node runs loadRoomWithEgo obj=687 room=218: object 687 is the right-edge path trunk, so the ego is placed at the right edge, and room 58's entry script — seeing VAR_WALKTO_OBJ == 687 — walks it left into the clearing. Enter via a different edge object and the ego comes in from that edge instead.

4. Why EXCD and ENCD run nested

Both scripts run nested — inline, finishing (or yielding at the first breakHere) before the loadRoom/loadRoomWithEgo opcode returns — rather than being queued as ordinary cooperative slots. The reason is ordering: the transition's caller (a global script) keeps executing its own opcodes right after the room change, and those opcodes assume the entry/exit scripts have already set the scene up.

Cutscene freezing, override, and the cooperative slot model these scripts run under are covered in cutscenes; the boot-time first room entry in boot.