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>
90 lines
3.2 KiB
Python
Executable File
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))
|