At a glance

Where
Inline in each scene's MAN (the scene's script-and-data bundle) as the actors' interaction scripts; in data\battle\efect.dat-style bundles; as type 4 in the asset-type dispatcher
Magic
Compact: first u32 0x00000404. Records: recurring 0x44 0x78 marker pairs, no fixed header
Size / stride
Glyph bytes are 1 byte; escapes 0xC0..0xCF are 2 bytes; a line is 0x1F <glyphs> 0x00
Parser
crates/mes (CLI mes); box packing legaia_mes::dialog_box; pickers legaia_mes::picker
Engine
engine_dialog::dialog + engine_field::inline_dialogue; pager SM in engine-dialog
Confidence
Confirmed - four interpreter functions statically linked in SCUS_942.54; pager and pickers traced in the dialog overlay and pinned on disc bytes
Used by
field/event VM, translation packs, NPC browser

Anatomy of a line

How one dialog line becomes text on screen: glyph bytes index the dialog font, a two-byte escape pulls a name from a game table, the zero byte ends the line and the control byte after the page tells the pager what the next button press does. one line, as stored 0x1F glyph bytes 0xC1 XX glyph bytes 0x00 control dialog font byte = tile index + its width name table hero, item, magic or art name pager next page, close, or menu dialog box three rows of proportional text
Nothing in a line is ASCII. Glyph bytes are positions in the dialog font; a substitution escape is resolved when the line is drawn, which is why renaming the hero changes every line that mentions him.

A line is glyph indices with 2-byte escapes. Consecutive lines fill a three-row box; the control byte after the page says what the button press does - next page, close, or open a choice menu.

Container variants

VariantDiscriminatorWhere
CompactFirst u16 = 0x0404Small blobs embedded inline in efect.dat and similar
RecordsFirst two bytes 0x44 0x78Larger dialog blobs; the form found in RAM in town overlays

Compact header

OffsetSizeFieldMeaning
+0x004magic0x00000404
+0x040x24-Unused padding
+0x284back_ptrRuntime pointer, patched on load
+0x2C4forward_ptrRuntime pointer
+0x304expanded_sizeByte count of the expanded blob
+0x344countNumber of messages
+0x3816metricsi16[8] per-line metrics
+0x4826-Further header fields (unknown)
+0x623 × Noffset tableu24 offsets from 0xC8; up to 86 entries
+0xC8-bytecodeMessage bodies

The Records variant has no fixed header: the parser finds record boundaries by scanning for 0x44 0x78 markers (at least four). Its per-record structure is Unknown.

Bytecode encoding

Most bytes are glyphs. Prefix bytes 0xC0..0xCF (plus two aliases) take a data byte and either name a wide glyph, insert spacing, or substitute a string from one of the game's name tables. All four interpreter functions share this classification; only the action per byte differs.

ByteStrideMeaning
0x00..0x1E1Line terminator; walkers stop here
0x1F..0x5D, 0x5F..0xBF, 0xD0..0xFE1Glyph (font tile index); 0x1F is also the line lead
0x5E XX2Alias, rewritten to 0xCE (XX - 0x2D)
0xC0 XX, 0xC6 XX, 0xC8..0xCD XX2Wide glyph, no substitution
0xC1 XX2Character name from the save record XX; 99 = current party leader
0xC2 XX, 0xC4 XX2Item name (item table); two consumer sites, one table
0xC3 XX2Magic name (spell table)
0xC5 XX2Tactical Art name (arts-name table), key (character = XX >> 6, art = XX & 0x3F)
0xC7 XX2Terrain / quest name (8-byte stride table)
0xCE XX2Spacing: XX is a horizontal offset
0xCF XX2Colour: the text after it draws in ink XX; the pair itself draws nothing
0xFF1Alias, rewritten to 0xCF

The substitution gate is the integer test (byte + 0x40) < 8, catching 0xC0..0xC7; 0xC0 and 0xC6 fall through to "no substitution" at 2-byte stride.

Speaker tags are coloured. When a Ra-Seru speaks, the line opens on its name wrapped in two colour pairs: CF 04 C7 01 CF 07 prints Terra in green and switches back to the dialogue white for the colon and the line; Meta takes ink 9 (orange) and Ozma ink 5 (teal), and art names ink 6 (gold). The field pager resets the ink to white once a frame before it draws its rows, so an ink left standing at the end of a row carries into the next one.

Japanese build: count-led Shift-JIS lines

The table above is the Latin builds' encoding (USA and PAL). The Japanese disc frames the same script positions differently, measured from its scene MANs:

Latin buildsJapanese build
Line0x1F <glyph bytes> 0x00count byte N (0x01..=0x1F) then exactly N two-byte tokens, no terminator
Blank row1F 20 0000
SubstitutionsC1 xx name, C2 xx item, …moved to F0..FF: F1 xx party name, F2 xx item
Glyphssingle bytesShift-JIS double-byte characters

Both satisfy the pager's line test (b & 0x7F) < 0x20, which is why the Latin builds kept 0x1F - the Japanese build's longest count - as their lead byte. Three Latin-build scene MANs still carry Japanese-framed lines that no Latin script reaches.

The four interpreters

  • Stride walker - 16 instructions returning the glyph count to the next terminator; the pager uses it for line lengths.
  • Glyph counter - the same walk plus substitution dispatch, counting the substituted name's glyphs too. It returns a count, not a pixel width (it never reads the font width table): the count paces the typewriter reveal. Nothing wraps text at run time - line breaks are authored (dialog font).
  • Substitution expander - normalises the two aliases, then inlines each substituted name byte by byte into a working buffer.
  • Renderer - expands first, then emits glyphs into the text-actor buffer and forwards spacing ops to the cursor.

The dialog window pager

Above the byte-level interpreter sits the per-frame state machine that opens the speech box, scrolls lines in, waits for a button press, and turns the page. It lives in the dialog overlay: 26 states covering load / scroll / drain / wait / done, with per-line bytecode pointers in a 16-line buffer. In the port: engine-core's dialog_window (rows, scroll, confirm) and dialog_pacing (the typewriter).

Box packing

  • The window shows 3 rows; three box-open states each pin the limit to 3. It is a window height, not a page length.
  • Consecutive 0x1F lines are one page: a line right after a line's 0x00 means "same page, next row". A fourth line scrolls the window up a row with no button press; the page ends only at a control byte.
  • A 0xC? escape whose argument falls in 0x00..0x1E (e.g. 0xC1 0x00) does not end the line; the advance loop consumes the argument as part of the token.
  • Multi-page speech is several pages chained by 0x24. dialog_box::pack_page decodes a page; pack_box / pack_boxes cut the same lines into window-sized chunks. The Rim Elm sparring partner's narration packs into three full pages then a 2-row box opening a 4-option menu.

Row window and scrolling

The row table is a scrolling window. State 0x0B types the last row; a line after a full window scrolls it (0x0C, 15 px per row at 0x24 * dt sixteenths a call); at a page end, rows the previous page left above scroll away (0x0F) before the page waits (0x19). A 0x24 page turn keeps the rows and types the next page beneath them. A confirm press while a row types or holds latches 0x25 and completes the page (0x0D, then 0x0E for overflow). The text is clipped to the box's rows band; the frame does not scroll. A town01 emulator trace pins every arm.

Post-page control byte

When the page is full and the player confirms, the pager reads the next control byte (masked & 0x7F) and picks the follow-on state:

Control bytePager statesEffect
0x243 → 4Next line, same box (rows preserved)
0x489 → 0xABox reset, open animation - a fresh box
0x250 → 1Box reset, row buffer cleared (teardown)
0x4C 0xFF6 → 7Box reset, row buffer cleared (teardown)
0x2A0x11 → 0x122-option menu in a small top-right box that slides in from the right edge
0x270x13 → 0x142-option Yes / No menu; the box slides up from the bottom edge
0x280x15 → 0x163-option menu, same slide
0x290x17 → 0x184-option menu, same slide

The pager decides the box, not the conversation: teardown clears rows and stops. Whether the dialogue session ends is decided by the actor dialog SM and the field VM.

The pager runs and draws every game tick, gated on its state alone, so a box outlives the press that dismisses it. Every dispatch draws the box once more, without the page mark. After 0x25 the next state draws nothing, so the box lasts that one frame; after 0x24 the idle state keeps the rows on screen while the script runs its opcodes, until the next line loads beneath them. A picker's commit dispatches on its continuation byte the same way: an inn's Yes / No offer ends on 0x24, so the question stays up, without the picker, while Vahn nods. The port keeps the dismissed box as a held panel both hosts draw until the next line (one tick for the other bytes).

No menu takes input on the press that opens it. The box slides from off screen to its place over a span of 24 frame-step units - twelve pager calls at the field's 30 fps, 10 px a call for the inn's top-right box - and the option hand appears on the call where it lands, with input read from the call after. A capture of an inn's stay offer measures 26 vsyncs from the press to a usable menu. Tetsu's 4-option list in Rim Elm rises the same way from below the screen, y 240 to 148 at 8, 8, 7 px a call, with the same 26 vsyncs to input.

Choice menus (pickers)

Four open bytes - 0x27, 0x28, 0x29 and 0x2A - open a menu. Relative to the open byte at index O:

  • Each jump entry is a signed 16-bit relative jump applied on confirm: new_pc = (O + 1 + index*2) + entry[index], relative to that option's own entry.
  • Labels are ordinary 0x1F segments after the continuation byte, drawn under the prompt at a 15-px row pitch.
  • Two continuation forms exist: a dispatch byte before the labels (the izumi book menu) or the first label's 0x1F directly (Rim Elm's spar menu). The parser accepts both.
  • 0x2A is a real 2-option picker whose cursor clamps instead of wrapping. Every inn's Yes / No offer is one (inn); a decoder that stops at 0x29 finds no inn menus.

In the port: legaia_mes::picker decodes the menus; engine-field::inline_dialogue runs the whole interaction script through the field VM (flag tests, scene changes) and pauses only at each 0x1F segment, applying the picked jump on confirm.

The option cursor

Every box shares one cursor word, *(DAT_801C6EA4 + 0xC). The pager's Up / Down handler at 0x801D941C is its only stepper and never resets it. The reset is the script runner's: each time FUN_80039B7C hands a line to the pager it copies the preset word +0xE into the cursor and zeroes the preset (lhu v1,0xe(v0) / sh zero,0xe(v0) / sh v1,0xc(v0) at 0x80039F90..0x80039F98).

  • Every picker therefore opens on row 0, unless op 4C E0 <n> with n >= 100 set the preset to n - 100 ahead of it (op 0x4C). izumi's statue pages do (4C E0 65 .. 67), the only carriers on the disc.
  • A box carried on over a 0x24 continuation keeps its rows but not its cursor: teien's Mist question loops back to itself on No, and the second ask opens on Yes.

Port: OwnedDialogPanel::seat_picker_cursor, called with the spent preset from World::apply_dialog_window_format on every line load.

Box geometry and render

The three box-open init arms (states 0, 6, 9) are near-copies differing in one word: the successor state each writes (1, 7, 0xA). States 1 and 7 clear the 16-entry row buffer; state 4 (from 0x24) returns early and preserves it; state 0xA ramps _DAT_801F274C to 0x1000, which the draw reads as the box collapsing to its centre line, then hides the box for 0x18 steps and drops into state 1.

Picker box rect: x = 0x26, y = 0x94 + ((4-N)*0xF)/2, w = 0xF4, h = 0x38 - (4-N)*0xF - 244 wide, shrinking 15 px per absent option. The main reading box is emitted as (ctx+0x12, ctx+0x14 + d, 0xF4, lines*0xF - 3) with skin 0x61, where d moves only with the 0x48 collapse animation - the rows scroll, the frame does not.

The box emitter composes a 9-slice gold frame from the system-UI sheet plus an interior fill of two identical semi-transparent gouraud quads (mode B/2 + F/2, top RGB (0x18,0x18,0x28), bottom (0x40,0x40,0xA0)), which composes to 0.25 back + 0.75 gradient. The engine bakes the gradient at alpha 191/255 and draws it once. The page-advance hand draws at (0x10A, box_y + lines*0xF - 0x13); the picker hand at (box_x - 6, box_y + cursor*0xF); labels render CLUT 7 at box_x + 0x10.

How we know

Provenance: the four interpreters, the pager, the picker handlers and the box emitter
FunctionAddressWhat it provesDump
Stride walkerFUN_8003CA38Byte classification: terminator <= 0x1E, (b & 0xF0) == 0xC0 is 2-bytefuncs/8003ca38.txt
Glyph counterFUN_80036044Returns a glyph count over a counter advanced only by 1 or an expanded-substitution count; substitution gate (b + 0x40) < 8; name-table pointers 0x80084708 + XX*0x414, PTR_DAT_8007436C, PTR_s_Magic_800754D0, DAT_80075EC4, DAT_80073F24funcs/80036044.txt
Substitution expanderFUN_800365140x5E / 0xFF aliases; inline name copyfuncs/80036514.txt
RendererFUN_80036888Glyph emit + spacing forwardfuncs/80036888.txt
PagerFUN_801D84D0; dispatch chain 0x801D8FDC, jump table 0x801CEBC026 states (_DAT_801F2734), rows at _DAT_801F3540, lines-per-box _DAT_801F2740 = 3, control-byte tablefuncs/overlay_*_801d84d0.txt
Picker cursor handler0x801D941COption count by state (0x18 → 4, 0x16 → 3, else 2); state 0x12 clampssame dump
Actor dialog SMFUN_80039B7CAdvance loop treats 0xC? + argument as one tokenfuncs/80039b7c.txt
Inline-script control handlerFUN_80038050Applies the relative jump on confirm; lists 0x2A with 0x27..0x29; cursor at DAT_801C6EA4+0xC, PC at actor[+0x9E]funcs/80038050.txt
Box emitterFUN_8002C69C, skin FUN_80034B6C, hands FUN_8002B9949-slice frame from DAT_80073A00; double gouraud fillfuncs/8002c69c.txt
Live blobRAM 0x80109270, 3893 bytesTown-overlay capture validating the parser end to endsave state

Field dialogue has no dedicated opcode: it is the actor's inline interaction-script text, triggered by the touch / button-press interaction rather than an opcode (op 0x3E with op0 < 100 is the scripted-battle install). Op 0x3F is the scene change, not a dialog op.

CLI

mes info       <PATH>             # detect variant + report header
mes disasm     <PATH>             # walk the bytecode, print decoded ops
mes json       <PATH>             # emit machine-readable JSON
mes events     <PATH> [--index N] # walk the interpreter for one message
mes stats-all  <PATH>             # event-type histogram across every message

Source of record: docs/formats/mes.md.

See also