Files
benilla-octo/scripts/gen-message-catalog.py
T
SamandClaude Opus 5.5 73427e2068 comments: the generators, the tools and the reference table headers
Docstrings keep their usage and exit codes; table headers keep the
column contract and the client addresses and lose the dates, the
correction history and every path into another repository. A
generated header changes in its generator's template, so each of the
four generators still reproduces its output byte for byte, and no data
row changed in any of the seven tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:32:13 +07:00

90 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
r"""Generate `crates/benilla-ui/src/messages/catalog.rs` from `reference/1.12-message-catalog.tsv`.
The table is the reference's message registry at `0xb4b498`, which `CGGameUI::DisplayError`
`0x496720` indexes by message id; this re-shapes it into a Rust static that ships with benilla.
python3 scripts/gen-message-catalog.py # the vendored table -> the .rs, in place
python3 scripts/gen-message-catalog.py <tsv> # another table
It refuses rather than emits on a non-dense id range, a duplicate key or an unknown kind.
"""
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TABLE = os.path.join(REPO, "reference", "1.12-message-catalog.tsv")
OUT = os.path.join(REPO, "crates", "benilla-ui", "src", "messages", "catalog.rs")
KINDS = {0: "Chat", 1: "Info", 2: "Error"}
HEADER = """\
//! The 1.12 client's message catalog, row for row.
//!
//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv`.
//! Do not edit by hand; regenerate.
use super::{MessageRecord, MsgKind};
/// Every row of `0xb4b498`, indexed by message id: the ids are dense `0..465`, so `CATALOG[id]`
/// is the reference's `0xb4b498 + 20*id`.
///
/// `rustfmt` is off: the generator emits one row per line, and a rewrap would drift from it.
#[rustfmt::skip]
pub static CATALOG: [MessageRecord; {count}] = [
"""
def main(path: str) -> int:
if not os.path.exists(path):
raise SystemExit(f"no table at {path}")
rows = []
for line in open(path):
if line.startswith("#") or line.startswith("id_hex"):
continue
f = line.rstrip("\n").split("\t")
if len(f) != 8:
raise SystemExit(f"malformed row (expected 8 fields, got {len(f)}): {line!r}")
rows.append(
{
"id": int(f[1]),
"key": f[3],
"kind": int(f[4]),
"sound": f[5],
"type_tag": int(f[6], 16),
"chat_type": int(f[7]),
}
)
for want, r in enumerate(rows):
if r["id"] != want:
raise SystemExit(f"ids are not dense: expected {want}, got {r['id']}")
keys = [r["key"] for r in rows]
if len(set(keys)) != len(keys):
raise SystemExit("duplicate key in the catalog — by_key would be ambiguous")
for r in rows:
if r["kind"] not in KINDS:
raise SystemExit(
f"row {r['id']} ({r['key']}) has kind {r['kind']}, which MsgKind does not model"
)
out = [HEADER.replace("{count}", str(len(rows)))]
for r in rows:
sound = "None" if r["sound"] == "NONE" else f'Some("{r["sound"]}")'
out.append(
f' MessageRecord {{ id: {r["id"]}, key: "{r["key"]}", '
f'kind: MsgKind::{KINDS[r["kind"]]}, sound: {sound}, '
f'type_tag: {r["type_tag"]:#04x}, chat_type: {r["chat_type"]} }},\n'
)
out.append("];\n")
with open(OUT, "w") as f:
f.write("".join(out))
print(f"wrote {os.path.relpath(OUT, REPO)} ({len(rows)} rows)")
return 0
if __name__ == "__main__":
if len(sys.argv) > 2:
raise SystemExit(__doc__)
sys.exit(main(sys.argv[1] if len(sys.argv) == 2 else TABLE))