What this page covers

Every town's dialogue, doors and triggers run as bytecode in the field/event VM (a small interpreter for per-scene scripts), and each scene ships its script in a container called the MAN (the scene's script + data sub-asset). An inn stay is nothing more than one such script: a gold check, a debit, a restore, a dream. Because the price is data inside the script rather than a row in a table, finding "what does this inn charge?" means scanning the script bytes - which is exactly what the clean-room engine does at scene load.

The clean-room engine runs that script rather than reimplementing it: walking up to an innkeeper and answering “Yes” steps the innkeeper's own record through the ported field VM, and the gold that leaves your purse and the HP that comes back are the script's own instructions doing it. See The trigger.

There is also an engine-side InnSession prompt (engine-core::inn, with its own cost window) which nothing on that path opens - see Open items.

The trigger: the picker's own jump table

There is no “is this dialogue an inn?” test anywhere - no opcode, no flag, no table of innkeepers. The hand-off from the offer to the restore is the option picker's per-option jump: each menu entry is a signed relative displacement, and the innkeeper's record is laid out so the Yes entry points at the gold gate. Answering the question is the trigger.

The shape, read off the retock innkeeper's record (the one carrying that scene's scanned charge):

1F <greeting> 00  1F <price line> 00  1F <offer question> 00
2A <rel16 yes> <rel16 no> 24        ; 2-option picker + post-page continue
1F <"yes" label> 00  1F <"no" label> 00
  ; yes-jump target:
A2 F8 <clip>  AC F8 08  AD F8 08    ; play a player clip, clear its end
                                    ;   latch, spin until it re-latches
... 26 <rel16>                      ; -> the gate
4E <pp> 30 <cost u16> <skip u16>    ; if gold < cost, skip to the refusal
3A <sext24(-cost)>                  ; take the money
1F <thank-you> 00 1F <good night> 00
35 / 34 / 4A / 36                   ; BGM release, fade, waits, cue
4C 82 00  4C 82 01  4C 82 02        ; the restore, one op per party slot

The No jump lands on the decline line and the gate's skip lands on the can't-afford line, both inside the same record - so a stay never leaves the field VM.

Two details of that layout are easy to get wrong, and either one alone makes the inn unreachable in a port:

  • The menu open byte is 0x2A, not 0x27. FUN_80038050 - the handler that applies the chosen option's jump - treats 0x27, 0x28, 0x29 and 0x2A in one arm. On the pager side 0x2A enters state 0x11 where 0x27 enters 0x13, and the shared cursor handler at 0x801D941C reads the option count off the active state (0x18 = 4, 0x16 = 3, otherwise 2). So 0x2A is a 2-option menu that animates the box geometry first, and its cursor clamps instead of wrapping. A decoder that stops at 0x29 finds no inn menu at all - and misses a large share of the field corpus's other menus with it.
  • AD F8 08 is a spin, not an end. 0x2D tests a bit of the clip-control word actor+0x62, and bit 8 is the “end” flag the actor tick FUN_800204F8 latches when a clip finishes - so A2 F8 <clip> / AC F8 08 / AD F8 08 means “play it, clear the latch, wait for it”. Retail's dialog state machine returns there every frame and re-enters until the latch lands. Reading that halt as “the conversation is over” stops one instruction into the Yes branch, before the gate.

Engine path: World::trigger_field_interactWorld::drive_inline_dialogueWorld::step_inline_dialoguelegaia_engine_vm::field::step. The runner parks on the clip-end spin and latches the tested bit once the cued player clip finishes; the gate reads the live purse through FieldHost::party_bank_value, and the debit and the restore are the record's own ADD_MONEY and 4C 82 <slot> ops. Disc-gated oracle: crates/engine-core/tests/inn_stay_field_vm_disc.rs, which drives the real record from the interact call and checks the gold delta and the party pools on the Yes, No and can't-afford branches.

Retail cost source (field-VM script literals)

An inn stay is a scripted gold-gate + debit pair inline in the scene MAN (asset type 0x03), the same place the town gold-shop stock lives (see Shop). In the disassembly below, each line is one script instruction: an opcode byte (0x4E, 0x3A) followed by its operands:

0x4E <pp> 0x30 <cost u16> <skip u16>   ; if gold < cost, jump +skip
...                                    ;   (the "can't afford" reply)
0x3A <sext24(-cost)>                   ; ADD_MONEY: gold -= cost

Op 0x4E sub-op 3 (operand byte 1, high nibble) loads the party gold _DAT_8008459C and compares it against the u16 literal at operand +2 (low nibble 0 = jump when gold < literal - the can't-afford branch); sub-op 10 is the 32-bit sibling (literal lo16 at +2 / hi16 at +6, 9 bytes) used where a price can exceed 65535 (the casino gold-to-coin counter). Provenance: the op-0x4E inner jump table (the array of handler addresses the interpreter jumps through) at field-overlay VA 0x801CEE30 (12 entries) - the sub-3 arm at 0x801E0AEC loads _DAT_8008459C, sub-2 at 0x801E0AC0 loads a per-character level byte (+0x130), sub-9 at 0x801E0B34 loads the casino coin bank _DAT_800845A4 (see ghidra/scripts/funcs/overlay_0897_801de840.txt; the decompiled-C case labels collapse these arms - the disassembly + jump-table words are ground truth). Op 0x3A (ADD_MONEY, see the field/event VM) applies the signed 24-bit delta.

After the debit the same script continues in-line: the innkeeper's thank-you text, per-party-slot 0x4C records on slots 0/1/2 (the restore), and a 0x3F transition whose destination name is DREAM@@ - the inn dream sequences. So cost prompt, gate, debit, restore, and dream hand-off are all one field-VM dialogue; no menu-overlay sub-screen is involved in retail.

The shared scanner is legaia_asset::inn_costs: a byte scan (robust to the dialogue-picker jump tables that desync a linear walk) for a gold compare whose literal reappears as the magnitude of a negative ADD_MONEY within a few ops of the gate (retail sites sit 7..~16 bytes apart). Swept disc-wide by crates/asset/tests/inn_costs_disc.rs: the pair resolves in the inn / paid-lodging scenes (e.g. the 200 G innkeeper sites in the ropeway and balden blocks, rayman2's 200 G stay, retock's 240 G stay), the paid tours and the 3,000 G station3 train ticket (sub-3, u16 costs), and the casino gold-to-coin counters (koin*; koin4 carries the only sub-10 u32 sites, 8,500..90,000 G). Free rests (Rim Elm's bed, Biron) simply have no gate + debit pair in their scripts.

Flow

This section describes the port's alternative presentation - a menu session with its own cost window - which nothing on the reachable path opens (see Open items). The in-game stay is the field-VM record above.

PhaseStateDescription
Cost promptInnConfirmShows cost for one stay and a Yes / No cursor.
Rest fadeInnSleepTransient screen that plays the rest fade after a Yes, then closes.
Commit-Deducts gold; restores all active party members’ HP and MP to their current maxima.

On commit: InnSession::can_afford(world_money) gates the transaction. If affordable, gold is deducted (World::money -= cost) and each of the first World::party_count actor slots whose active flag is set gets battle.hp = battle.max_hp and battle.mp restored to the roster record’s mp_max; inactive slots and the reserve bench are untouched. save_party() is called to sync the roster records.

The menu state machine (engine-vm::menu) routes the prompt: InnConfirm Yes (slot 0) commits the rest and routes to the transient InnSleep fade, which auto-advances to the menu’s Closing state after transient_hold_frames; No (slot 1) and Triangle route straight to Closing. Either way the inn session is cleared.

InnSession

InnSession { cost: u32 } is installed on MenuRuntime by open_scene_inn(&SceneHost) (resolves the loaded scene's scanned cost and enters InnConfirm; returns None and installs nothing for free-rest scenes) or directly by open_inn(cost). can_afford(world_money: i32) -> bool checks world_money ≥ cost. Neither entry has a production caller - see Open items.

Open items

  • No cost table exists. A per-scene cost table in the menu overlay's DATA segment looks plausible; there is none - every cost is a field-VM script literal in the scene MAN (gate 0x4E sub-3 + debit 0x3A, see Retail cost source above). In the port, SceneHost::load_scene scans the cached MAN into scene_gold_charges (scene_inn_cost() = the first sub-3 charge). Disc-gated oracle: crates/engine-core/tests/inn_cost_scene_disc.rs (retock's 240 G stay resolves; free-rest town01 opens nothing).
  • Trigger - resolved, wired. The stay runs as the innkeeper's own field-VM record, reached by walking up and talking (see The trigger). The port had the ops and the cost scan but not the path: the 0x2A menu open byte decoded as nothing, the gold gate read an empty purse because the engine had no party_bank_value, and the clip-end spin ended the conversation one instruction into the Yes branch.
  • InnSession has no production caller - disclosed, not wired. MenuRuntime::open_inn / open_scene_inn and the InnConfirm / InnSleep sub-screens are an engine-side prompt with its own cost window and commit kernel. Retail has no such screen and the reachable path does not pass through one, so wiring a host into it would replace faithful innkeeper dialogue with an invented panel rather than fill a gap. It stays as the direct entry for tests and tooling. Deleting it is a legitimate future call; inventing a caller for it is not.
  • The DREAM hand-off is not mirrored. Some inns append a story-flag-gated tail that warps to a DREAM scene after the restore. The restore runs first and unconditionally, so a stay is complete without it, but the dream scenes themselves are not yet reached.

Full reference

Complete flow and provenance at docs/subsystems/inn.md. Source: crates/engine-core/src/inn.rs, scanner legaia_asset::inn_costs.

See also