MES dialog format Confirmed
MES is the container for Legaia's dialog text - every line an NPC speaks lives in one. The text is not ASCII: each byte names a tile in the dialog font, and a few prefix bytes splice in live data - the hero's name, an item, an art - at display time. Two on-disc variants share an offset table + bytecode tail, and unusually the interpreter is in the main executable rather than an overlay.
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: recurring0x44 0x78marker pairs, no fixed header - Size / stride
- Glyph bytes are 1 byte; escapes
0xC0..0xCFare 2 bytes; a line is0x1F <glyphs> 0x00 - Parser
crates/mes(CLImes); box packinglegaia_mes::dialog_box; pickerslegaia_mes::picker- Engine
engine_core::dialog+inline_dialogue; pager SM inengine-vm- 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
Container variants
| Variant | Discriminator | Where |
|---|---|---|
| Compact | First u16 = 0x0404 | Small blobs embedded inline in efect.dat and similar |
| Records | First two bytes 0x44 0x78 | Larger dialog blobs; the form found in RAM in town overlays |
Compact header
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | magic | 0x00000404 |
+0x04 | 0x24 | - | Unused padding |
+0x28 | 4 | back_ptr | Runtime pointer, patched on load |
+0x2C | 4 | forward_ptr | Runtime pointer |
+0x30 | 4 | expanded_size | Byte count of the expanded blob |
+0x34 | 4 | count | Number of messages |
+0x38 | 16 | metrics | i16[8] per-line metrics |
+0x48 | 26 | - | Further header fields (unknown) |
+0x62 | 3 × N | offset table | u24 offsets from 0xC8; up to 86 entries |
+0xC8 | - | bytecode | Message 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.
| Byte | Stride | Meaning |
|---|---|---|
0x00..0x1E | 1 | Line terminator; walkers stop here |
0x1F..0x5D, 0x5F..0xBF, 0xD0..0xFE | 1 | Glyph (font tile index); 0x1F is also the line lead |
0x5E XX | 2 | Alias, rewritten to 0xCE (XX - 0x2D) |
0xC0 XX, 0xC6 XX, 0xC8..0xCD XX | 2 | Wide glyph, no substitution |
0xC1 XX | 2 | Character name from the save record XX; 99 = current party leader |
0xC2 XX, 0xC4 XX | 2 | Item name (item table); two consumer sites, one table |
0xC3 XX | 2 | Magic name (spell table) |
0xC5 XX | 2 | Tactical Art name (arts-name table), key (character = XX >> 6, art = XX & 0x3F) |
0xC7 XX | 2 | Terrain / quest name (8-byte stride table) |
0xCE XX | 2 | Spacing: XX is a horizontal offset |
0xCF XX | 2 | Passthrough: XX renders alone |
0xFF | 1 | Alias, 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.
The four interpreters
- Stride walker - 16 instructions returning the glyph count to the next terminator; the pager uses it for line lengths.
- Width measurer - the same walk plus substitution dispatch, recursing into the substituted string.
- 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. Rust port: engine-core's dialog_window (rows, scroll, confirm) and dialog_pacing (the typewriter), driven by dialog::OwnedDialogPanel on the path both play hosts share.
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
0x1Flines are one page: a line right after a line's0x00means "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 in0x00..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_pagedecodes a page;pack_box/pack_boxescut 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 PCSX-Redux trace pins every arm, and the engine reproduces it vsync for vsync.
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 byte | Pager states | Effect |
|---|---|---|
0x24 | 3 → 4 | Next line, same box (rows preserved) |
0x48 | 9 → 0xA | Box reset, open animation - a fresh box |
0x25 | 0 → 1 | Box reset, row buffer cleared (teardown) |
0x4C 0xFF | 6 → 7 | Box reset, row buffer cleared (teardown) |
0x2A | 0x11 → 0x12 | 2-option menu in a small top-right box that slides in from the right edge |
0x27 | 0x13 → 0x14 | 2-option Yes / No menu; the box slides up from the bottom edge |
0x28 | 0x15 → 0x16 | 3-option menu, same slide |
0x29 | 0x17 → 0x18 | 4-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.
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:
[ .. 0x1F prompt segment .. 0x00 ] the box text shown above the menu
O open byte (0x27 / 0x28 / 0x29 / 0x2A)
O+1 .. O+N*2 N jump entries, 2 bytes each (i16 LE)
O+N*2+1 continuation byte (0x24/0x25/0x48/0x4C 0xFF)
OR the first label's 0x1F (immediate labels)
N * [ 0x1F label segment 0x00 ] the on-screen option labels
- 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
0x1Fsegments 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
izumibook menu) or the first label's0x1Fdirectly (Rim Elm's spar menu). The parser accepts both. 0x2Ais a real 2-option picker whose cursor clamps instead of wrapping. Every inn's Yes / No offer is one (inn); a decoder that stops at0x29finds no inn menus.
Engine: OwnedDialogPanel::from_inline_dialog attaches the picker, confirm_menu applies the jump. The faithful path, inline_dialogue, drives the whole interaction script through the real field VM (flag tests, scene changes) and pauses only at each 0x1F segment - default in play-window, --simple-dialogue opts out.
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.
History: labels between the open byte and the continuation
An earlier note read the 2-byte entries after the open byte as the option labels. They are the jump table; the labels follow the continuation byte. An earlier parser also required the dispatch-byte continuation and so found zero pickers in town01. See do-not-re-walk.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Stride walker | FUN_8003CA38 | Byte classification: terminator <= 0x1E, (b & 0xF0) == 0xC0 is 2-byte | funcs/8003ca38.txt |
| Width measurer | FUN_80036044 | Substitution gate (b + 0x40) < 8; name-table pointers 0x80084708 + XX*0x414, PTR_DAT_8007436C, PTR_s_Magic_800754D0, DAT_80075EC4, DAT_80073F24 | funcs/80036044.txt |
| Substitution expander | FUN_80036514 | 0x5E / 0xFF aliases; inline name copy | funcs/80036514.txt |
| Renderer | FUN_80036888 | Glyph emit + spacing forward | funcs/80036888.txt |
| Pager | FUN_801D84D0; dispatch chain 0x801D8FDC, jump table 0x801CEBC0 | 26 states (_DAT_801F2734), rows at _DAT_801F3540, lines-per-box _DAT_801F2740 = 3, control-byte table | funcs/overlay_*_801d84d0.txt |
| Picker cursor handler | 0x801D941C | Option count by state (0x18 → 4, 0x16 → 3, else 2); state 0x12 clamps | same dump |
| Actor dialog SM | FUN_80039B7C | Advance loop treats 0xC? + argument as one token | funcs/80039b7c.txt |
| Inline-script control handler | FUN_80038050 | Applies the relative jump on confirm; lists 0x2A with 0x27..0x29; cursor at DAT_801C6EA4+0xC, PC at actor[+0x9E] | funcs/80038050.txt |
| Box emitter | FUN_8002C69C, skin FUN_80034B6C, hands FUN_8002B994 | 9-slice frame from DAT_80073A00; double gouraud fill | funcs/8002c69c.txt |
| Disc-gated tests | field_dialog_boxpack_disc, field_dialog_pickers_disc, tetsu_spar_picker_disc | Box packing, jump targets inside their own script, immediate-labels form | crates/mes/tests |
| Live blob | RAM 0x80109270, 3893 bytes | Town-overlay capture validating the parser end to end | save 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.