A flat 256-entry table turns the game's own character encoding into a 34-value "voice code" alphabet. This is the only place text is looked at.
Messages are stored one byte per character in a custom encoding
(include/m_font.h). It is ASCII-compatible over exactly three ranges —
0-9 at 48–57, A-Z at 65–90, a-z at 97–122 — and
diverges everywhere else. Notable non-ASCII placements:
| Code | Character | Note |
|---|---|---|
| 0–31 | uppercase accented Latin, ¡ ¿ Ç Ð Ñ ß Þ | where ASCII control codes would be |
| 43 | ♥ | where ASCII has + |
| 47 | ♪ | where ASCII has / |
| 59 | droplet symbol | where ASCII has ; |
| 92 | annoyed symbol | where ASCII has \ |
| 127 | CHAR_CONTROL_CODE | escape byte; a control code follows |
| 128 | CHAR_MESSAGE_TAG | escape byte |
| 210, 211 | short space, wide space | layout-only spaces |
mMsg_sound_voice_get(code) (src/game/m_msg_sound.c_inc:1) is a
256-byte lookup with two hard-coded exceptions:
if (code == CHAR_CONTROL_CODE || code == CHAR_MESSAGE_TAG)
return -1; // no sound, and no state change
return voice_array[code];
The full 256-byte array is reproduced verbatim in the reference tables; this page describes its shape.
Those are the only two entries in the array holding 0xFF, so
0xFF is never actually returned from this function — it arrives at
Na_VoiceSe only as a padding value for the unused b/c
slots.
| Voice code | Meaning |
|---|---|
0x53–0x5C | digits 0–9 |
0x5D–0x76 | letters a–z |
0x80 | ♪ — plays the speaker's melody, bypasses everything |
0x81 | space |
0x82 | ? — raises pitch |
0x83 | ! — raises volume |
0x84 | newline (injected, never in the table) |
0x85 | mute — the catch-all for punctuation and symbols |
0x86 | click — plays NA_SE_BEBE |
0xFF | empty slot in a window |
The split at 0x77 matters: Sou_BoinShiinCheck and several rules in
Na_VoiceSe test > 0x77 to mean "this is punctuation, not a letter".
Upper and lower case map to the same code, and every accented Latin letter folds onto its base letter. Some of the folds are worth knowing about:
| Character(s) | Folds to |
|---|---|
À Á Â Ã Å à á â ã ä å, ª (feminine ordinal) | a |
ß (eszett) | b — a glyph-shape fold, not a phonetic one |
Ç ç | c |
Ð ð | d |
È É Ê Ë è é ê ë | e |
Ì Í Î Ï ì í î ï | i |
Ñ ñ | n |
Ò Ó Ô Õ Ö ò ó ô õ ö, º (masculine ordinal) | o |
Þ þ (thorn) | p |
Ù Ú Û Ü ù ú û ü | u |
Ý ý ÿ | y |
Œ œ | mute — the only accented Latin pair that is not folded |
0x81.0x86, the click. It is the only character in the whole
table that maps to the click code.0x85, mute.if (code == CHAR_NEW_LINE) return 0x84; return mMsg_sound_voice_get(code);
Newline has no entry in the table; the wrapper injects 0x84. This is the
function the scheduler actually calls, so 0x84 reaches the engine while the
raw table never produces it. m_msg_sound.c_inc:44
sound = mMsg_sound_voice_get(code);
switch (sound) {
case 0x80: case 0x82: case 0x83: case 0x85: return 0x86;
default: return sound;
}
In the text editor, ♪, ?, ! and every mute character all become
the click. See the editor path.
m_msg_sound.c_inc:26
a-z0-9 are all gone by
the time audio sees anything. Whatever "understanding of English" the animalese has,
it lives entirely in the digraph table on the next page.