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:
loadRoom N— swap to roomN, leave the ego wherever it is. Used for off-screen / scripted scene changes.loadRoomWithEgo obj N x y— swap to roomNand bring the ego in, positioned relative to objectobj. This is how the player walks between rooms (an exit object's walk-to script runs it) and how the map/menu jumps to a location.
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:
- Clear the draw queue and per-object draw positions. A fresh room starts with nothing queued; its entry script repopulates what should be visible.
- 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 inVAR_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 ing101— entry scripts branch on it (Hook Isle's side-dependent touchability, the Voodoo Lady's entrance choreography that closes the door behind you). - 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 ownendCutsceneinstead of stranding its frame on the stack and freezing control — the same sparefreezeScriptsgrants 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'scutScene … endCutScenewhile the walk-to-door verb callsloadRoomWithEgo; 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 previousENCD/EXCDstill 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). - Reset per-room box flags to the new room's on-disk values (the entry script re-applies any door locks).
- Set
currentRoomandVAR_ROOMto the requested id — the raw id even for a pseudo-room (the forest maze keepsVAR_ROOMat 201–220; see room §7b). - 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).
- Resolve box + scale for every actor already placed in the room. A
putActorinto 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. - Bring the entering ego into the new room (
loadRoomWithEgoonly — §3). Its room membership is set here, before the entry script, so anENCDthat branches ongetActorRoom(ego)sees it already arrived; its screen position and entry walk land later, after the entry script's first slice (§3). - Run the entry side, nested: the entry hook (
VAR_ENTRY_SCRIPT), the new room'sENCDto its firstbreakHere(see §4), then the second entry hook (VAR_ENTRY_SCRIPT2). MI1 boots#5/#6into the entry hooks;#6re-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 ego joins the new room before the entry script runs. SCUMM's
loadRoomWithEgosets the ego's room membership ahead of theENCD, so an entry script that gates on the ego having arrived sees it — the captain's-cabin spinning-key setup (room 72) runs its whole draw-the-key block only whilegetActorRoom(ego)is the cabin, and would skip it (leaving no key to take) if membership lagged the room change. Only the ego's position waits for the firstbreakHere(next bullet). VAR_WALKTO_OBJis set toobjacross the transition, so the new room'sENCDcan branch on which object/edge the ego came in through. It stays set through the entry script (the nextloadRoomWithEgooverwrites it).- The ego is placed at the entry object's walk-to point —
getObjectXYPos(the object'swalkX/walkY, not its image origin), shifted by anydrawObject … atreposition the object has had, then clamped into the walk boxes (SCUMM'sadjustXYToBeInBox). Placement happens after the entry script's first slice has run (step 9 begins the script; the placement reads the now-repositioned object), so the ego lands at the screen edge the entry object occupies — not at the object's design coordinates. - The ego is turned to face the room. Right after placement it is faced per
the entry object's
actorDir(CDHD byte 12, objects §2) and dropped into its stand pose. Without this it keeps whatever direction it was walking when the transition fired, resting side-on where the room expects it facing front or back. - The entry script walks the ego in. Gated on
VAR_WALKTO_OBJ, theENCDissues awalkActorToafter its firstbreakHere, pulling the ego from the edge to its resting spot. So the ego enters walking rather than snapping into place. - An explicit
(x, y)operand overrides the entry walk — when it isn't(-1, -1), the ego walks straight to that point instead.
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.
EXCDmust finish first. MI1's pirate-conversation script doesloadRoom 82then immediately sets the dialog verb-script variable. The bar's exit script resets that same variable to its default; queued as a deferred slot it would run after the caller's set and clobber it, leaving dialog clicks routed to the wrong script. RunningEXCDnested — before the opcode returns — restores the original ordering.ENCDruns to its firstbreakHere. The entry script's prologue (draw the scene, position actors) must be in effect before the caller's next opcode observes the room; anENCDthat spans frames still yields back to the per-frame scheduler after that prologue, exactly as the original does. The ego entry-walk (§3) lives after this firstbreakHere, which is why the ego's placement is read whileVAR_WALKTO_OBJis still set and isn't clobbered by it.
Cutscene freezing, override, and the cooperative slot model these scripts run
under are covered in cutscenes; the boot-time first
room entry in boot.