Inn
What happens when you pay to rest at an inn - the "spend some gold, restore the party's HP and MP" flow. The headline finding: retail Legaia has no inn system at all. There is no inn overlay (no dedicated chunk of loadable code) and no inn cost table anywhere on the disc. Each inn is just an ordinary dialogue in its town's script - the innkeeper's conversation checks your gold, deducts the price, restores the party, and hands off to the dream sequence, all as plain script instructions - and the price is a literal number embedded in that script.
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, not0x27.FUN_80038050- the handler that applies the chosen option's jump - treats0x27,0x28,0x29and0x2Ain one arm. On the pager side0x2Aenters state0x11where0x27enters0x13, and the shared cursor handler at0x801D941Creads the option count off the active state (0x18= 4,0x16= 3, otherwise 2). So0x2Ais a 2-option menu that animates the box geometry first, and its cursor clamps instead of wrapping. A decoder that stops at0x29finds no inn menu at all - and misses a large share of the field corpus's other menus with it. AD F8 08is a spin, not an end.0x2Dtests a bit of the clip-control wordactor+0x62, and bit 8 is the “end” flag the actor tickFUN_800204F8latches when a clip finishes - soA2 F8 <clip>/AC F8 08/AD F8 08means “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_interact → World::drive_inline_dialogue → World::step_inline_dialogue → legaia_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.
| Phase | State | Description |
|---|---|---|
| Cost prompt | InnConfirm | Shows cost for one stay and a Yes / No cursor. |
| Rest fade | InnSleep | Transient 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
0x4Esub-3 + debit0x3A, see Retail cost source above). In the port,SceneHost::load_scenescans the cached MAN intoscene_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-resttown01opens 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
0x2Amenu open byte decoded as nothing, the gold gate read an empty purse because the engine had noparty_bank_value, and the clip-end spin ended the conversation one instruction into the Yes branch. InnSessionhas no production caller - disclosed, not wired.MenuRuntime::open_inn/open_scene_innand theInnConfirm/InnSleepsub-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
DREAMhand-off is not mirrored. Some inns append a story-flag-gated tail that warps to aDREAMscene 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.