Text resources: extract, pack with relocatable pointers, round-trip #27

Merged
Gatchan merged 4 commits from res-text into main 2026-10-29 17:22:11 +00:00
Owner

Text resources: our own codec, extract to UTF-8 trees, pack with rebuilt pointer tables, core pack backend. Fixes #9

What

  • tools/textcodec.py (own code, decision D5): exact decoder and encoder for the US (DTE pairs, control codes, variables) and JP (kana, two-byte kanji, MTE words) text encodings, in the disassembly text syntax. Character tables tools/textcodec_tables.json; the dte/mte tables are built at run time from the game's own dte_tbl/mte_tbl resources (ROM content, not committed).
  • tools/extract.py text --version us_v10|jp_v10 [--out DIR] -> DIR/us10|jp10/text/*.txt + meta/text/*.json (per string: original bytes, SHA-256 of the extracted text, offset, shared flag) + extract.json. Default DIR others/extracted/ (workspace, never the repo).
  • tools/pack.py -> .ff6pack format v0 (docs/pack.md, writer tools/packfile.py): unedited strings keep Square's bytes, edited strings are re-encoded, strings block + pointer table + dialogue bank split (DlgBankInc, $CC:E600) rebuilt (tools/restext.py). Clear errors naming file and string (unknown char/control code, too long for a fixed field, missing {0}, beyond pointer range, changed header or string count, ambiguous encoding).
  • Catalogue re/assets.toml (31 text assets + the RNG table: ids, ROM ranges per version, pointer tables, headers, round-trip status) and re/text_labels.toml (labels only). tools/gen_res.py -> core/res/res_gen.{h,c} (resource ids, ROM locations; tools.gen_res.check keeps them in sync).
  • Core: core/res/res_pack.{h,c} reader (magic, format_major, game version, TOC bounds/order, payload CRC-32), ff6_config_t.pack_data (optional, copied), FF6_ERR_BAD_PACK; res_get = pack first, then ROM; res_rom_get/res_pack_get for comparisons. The emulated game keeps reading the ROM.
  • Docs: docs/pack.md, docs/resources.md, docs/build.md tools section.

Numbers

US v1.0 JP v1.0
text assets 30 29
resources (strings + pointer tables + bank split) 45 44
strings 6 007 5 947
string bytes 169 750 142 398
round-trip ROM -> extract -> pack, resources byte-identical 45/45 44/44
core pack backend == ROM backend 45 resources, 178 502 bytes 44 resources, 151 170 bytes
strings our encoder re-encodes to Square's bytes 3 279 / 6 007 (dlg: 2 728 differ, DTE choices) 5 941 / 5 947 (map_title: 6)
decode -> encode -> decode identical 6 007 / 6 007 5 947 / 5 947

Extracted text files are byte-identical to the disassembly's assets/text/*.txt (working tree) for all 59 files (oracle check, scratch script).

Edit test (rom.text_roundtrip.*): one dialogue string grows by 561 bytes (US) / 481 bytes (JP), 3 078 later pointers move by exactly that, bank split 1574 -> 1564 (US) / 1820 -> 1809 (JP), plus one battle message and one item name; every string of every resource read back through the pack's pointer tables decodes to the extracted text with the edits, unedited strings keep their bytes; five error cases checked.

Tests

  • CI (no ROM): tools.textcodec (18 cases: codec, text files, layout, container), unit.res_pack (synthetic pack, every error status), tools.gen_res.check.
  • ROM: rom.text_roundtrip.{US_V10,JP_V10} (Python, fixture producing the vanilla pack) and rom.res_pack.{US_V10,JP_V10} (C: pack backend == ROM backend for every text resource; a pack of the other version is refused).
  • debug --fast 20/20, release --fast 20/20; clang and mingw build clean (0 warnings), new tests pass on debug, clang and mingw (wine).

Decisions taken here

  • Meta sidecar keeps the original bytes per string (hex) and a SHA-256 of the extracted text: edit detection is by text hash, and the pack checks the sidecar still decodes to that hash (catches codec/table drift).
  • A string keeps every byte up to the next pointer (stale bytes after {0} included), so the decoder is lossless and the vanilla block is the concatenation of the strings; strings are laid out back to back in table order (all ROM tables are monotonic, checked at extraction).
  • Text resources are split into three ids (strings, pointer table, bank split) with payloads in ROM layout (type_version 0), so pack and ROM backends compare byte for byte.
  • dte_tbl/mte_tbl are not editable (unedited dialogue keeps its original codes); header lines and string counts are fixed by the catalogue.
  • The ROM space Square reserved per block does not limit a pack (only the 16-bit pointer range, 128 KiB for the two-bank dialogue).
  • Character tables are committed as factual code->glyph mappings in our own JSON file (same escape names as the disassembly for syntax compatibility).

🤖 Generated with Claude Code

Text resources: our own codec, extract to UTF-8 trees, pack with rebuilt pointer tables, core pack backend. Fixes #9 ## What - `tools/textcodec.py` (own code, decision D5): exact decoder and encoder for the US (DTE pairs, control codes, variables) and JP (kana, two-byte kanji, MTE words) text encodings, in the disassembly text syntax. Character tables `tools/textcodec_tables.json`; the `dte`/`mte` tables are built at run time from the game's own `dte_tbl`/`mte_tbl` resources (ROM content, not committed). - `tools/extract.py text --version us_v10|jp_v10 [--out DIR]` -> `DIR/us10|jp10/text/*.txt` + `meta/text/*.json` (per string: original bytes, SHA-256 of the extracted text, offset, shared flag) + `extract.json`. Default DIR `others/extracted/` (workspace, never the repo). - `tools/pack.py` -> `.ff6pack` format v0 (`docs/pack.md`, writer `tools/packfile.py`): unedited strings keep Square's bytes, edited strings are re-encoded, strings block + pointer table + dialogue bank split (`DlgBankInc`, $CC:E600) rebuilt (`tools/restext.py`). Clear errors naming file and string (unknown char/control code, too long for a fixed field, missing `{0}`, beyond pointer range, changed header or string count, ambiguous encoding). - Catalogue `re/assets.toml` (31 text assets + the RNG table: ids, ROM ranges per version, pointer tables, headers, round-trip status) and `re/text_labels.toml` (labels only). `tools/gen_res.py` -> `core/res/res_gen.{h,c}` (resource ids, ROM locations; `tools.gen_res.check` keeps them in sync). - Core: `core/res/res_pack.{h,c}` reader (magic, format_major, game version, TOC bounds/order, payload CRC-32), `ff6_config_t.pack_data` (optional, copied), `FF6_ERR_BAD_PACK`; `res_get` = pack first, then ROM; `res_rom_get`/`res_pack_get` for comparisons. The emulated game keeps reading the ROM. - Docs: `docs/pack.md`, `docs/resources.md`, `docs/build.md` tools section. ## Numbers | | US v1.0 | JP v1.0 | |---|---|---| | text assets | 30 | 29 | | resources (strings + pointer tables + bank split) | 45 | 44 | | strings | 6 007 | 5 947 | | string bytes | 169 750 | 142 398 | | round-trip ROM -> extract -> pack, resources byte-identical | 45/45 | 44/44 | | core pack backend == ROM backend | 45 resources, 178 502 bytes | 44 resources, 151 170 bytes | | strings our encoder re-encodes to Square's bytes | 3 279 / 6 007 (dlg: 2 728 differ, DTE choices) | 5 941 / 5 947 (map_title: 6) | | decode -> encode -> decode identical | 6 007 / 6 007 | 5 947 / 5 947 | Extracted text files are byte-identical to the disassembly's `assets/text/*.txt` (working tree) for all 59 files (oracle check, scratch script). Edit test (`rom.text_roundtrip.*`): one dialogue string grows by 561 bytes (US) / 481 bytes (JP), 3 078 later pointers move by exactly that, bank split 1574 -> 1564 (US) / 1820 -> 1809 (JP), plus one battle message and one item name; every string of every resource read back through the pack's pointer tables decodes to the extracted text with the edits, unedited strings keep their bytes; five error cases checked. ## Tests - CI (no ROM): `tools.textcodec` (18 cases: codec, text files, layout, container), `unit.res_pack` (synthetic pack, every error status), `tools.gen_res.check`. - ROM: `rom.text_roundtrip.{US_V10,JP_V10}` (Python, fixture producing the vanilla pack) and `rom.res_pack.{US_V10,JP_V10}` (C: pack backend == ROM backend for every text resource; a pack of the other version is refused). - debug `--fast` 20/20, release `--fast` 20/20; clang and mingw build clean (0 warnings), new tests pass on debug, clang and mingw (wine). ## Decisions taken here - Meta sidecar keeps the original bytes per string (hex) and a SHA-256 of the extracted text: edit detection is by text hash, and the pack checks the sidecar still decodes to that hash (catches codec/table drift). - A string keeps every byte up to the next pointer (stale bytes after `{0}` included), so the decoder is lossless and the vanilla block is the concatenation of the strings; strings are laid out back to back in table order (all ROM tables are monotonic, checked at extraction). - Text resources are split into three ids (strings, pointer table, bank split) with payloads in ROM layout (type_version 0), so pack and ROM backends compare byte for byte. - `dte_tbl`/`mte_tbl` are not editable (unedited dialogue keeps its original codes); header lines and string counts are fixed by the catalogue. - The ROM space Square reserved per block does not limit a pack (only the 16-bit pointer range, 128 KiB for the two-bank dialogue). - Character tables are committed as factual code->glyph mappings in our own JSON file (same escape names as the disassembly for syntax compatibility). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Gatchan/FF6Portage!27
No description provided.