From 9eed02063429cd5fda4c8ebcc2975ffcfa3b93ba Mon Sep 17 00:00:00 2001 From: DragonHD Date: Sun, 20 Sep 2026 10:22:49 +0800 Subject: [PATCH] Chinese main menu: font atlas + GB2312 tables + Strings translation - build_cjk.py: 2048x1024 UI atlas (original ASCII at 0,0 + full GB2312 at 12px) and cjk_glyph/cjk_metric tables. - cn_strings.py + make_cn_strings.py: escape-aware GB2312 rewrite of Strings.dat (46 entries). - install_cjk.py: one-shot deploy/restore (exe patch, atlas loose+res, strings). - apply_cjk.py: fix metrics cave code byte order (was (second<<8)|first) so CJK width measures correctly and centred labels are no longer flushed right. - docs: DEVLOG + patch notes for the atlas and the fix. --- .gitignore | 4 + README.md | 4 + docs/DEVLOG.md | 28 +++++ docs/patch-notes.md | 36 +++++- tools/apply_cjk.py | 30 +++-- tools/build_cjk.py | 256 +++++++++++++++++++++++++++++++++++++++ tools/cn_strings.py | 65 ++++++++++ tools/install_cjk.py | 196 ++++++++++++++++++++++++++++++ tools/make_cn_strings.py | 67 ++++++++++ 9 files changed, 671 insertions(+), 15 deletions(-) create mode 100644 tools/build_cjk.py create mode 100644 tools/cn_strings.py create mode 100644 tools/install_cjk.py create mode 100644 tools/make_cn_strings.py diff --git a/.gitignore b/.gitignore index eab48df..62c88f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ !/.gitignore !/README.md +# Python caches +__pycache__/ +*.pyc + # Belt-and-suspenders: never track game / crack / media / build artifacts *.res *.iso diff --git a/README.md b/README.md index e94f62c..6bad348 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ Working data (not tracked): `original/`, `png/`, `report/`, `patch/`, `crack/`. | `tools/fontparse.py` | parse `Fonts.dat` into font blocks and glyph rects | | `tools/add_section.py` | add an executable section to the game PE | | `tools/apply_cjk.py` | apply the double-byte font-rendering patch | +| `tools/build_cjk.py` | build the 2048x1024 UI font atlas + `cjk_glyph`/`cjk_metric` tables | +| `tools/cn_strings.py` | simplified-Chinese translation table (`Id` -> text) | +| `tools/make_cn_strings.py` | patch the `English` column of `Strings.dat` to GB2312 | +| `tools/install_cjk.py` | deploy/restore the whole patch into the game folder | | `tools/xrefs.py` | find call/jmp xrefs in the game executable | See `docs/` for the reverse-engineered formats and addresses. diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index e36f72a..7b28129 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -3,6 +3,34 @@ Working log for the *The I of the Dragon* localization / HD project. Newest entries on top. +## 2026-09-20 — Chinese in the main menu (first playable) + +- Built `tools/build_cjk.py`: renders the full GB2312 set (7445 double-byte codes) as **12x12** + glyphs with simsun (crisp/binary, matching the game's pixel font) into a **2048x1024** TGA. + The original 256x128 ASCII artwork is copied to (0,0), so all existing `Fonts.dat` rects keep + working unchanged (verified byte-identical). Also emits `cjk_glyph.bin` / `cjk_metric.bin`. + Advance/height = 12 (the UI font cap height is ~11-12px), drawn 1px up to fit the cell. +- `tools/cn_strings.py` + `tools/make_cn_strings.py`: translate the `English` column of + `Strings.dat` to GB2312 (46 entries; escape-aware so `\"%s\"` formats survive). +- `tools/install_cjk.py`: one-shot deploy/restore (backs up exe/res/strings; rebuilds `Textures.res` + with the new atlas, drops the loose atlas, patches the exe and the strings). +- **Result**: the main menu renders Chinese — 开始新游戏 / 载入游戏 / 选项 / 教程 / 制作人员 / 退出 / + 载入上次存档:"…" — confirmed by the user on screen. + +### Fixed a byte-order bug in the metrics cave + +The draw cave decoded the code as `(first<<8)|second`, but the metrics cave used +`movzx eax,al; mov ah,[ebx+1]` which produced `(second<<8)|first`. Every CJK width measured as 0, +so centred labels (the "Start New Game" button) were flushed right. Fixed to +`movzx eax,al; shl eax,8; mov al,[ebx+1]`. Centring is now correct. + +### Notes / next + +- The menu button labels *are* dynamic (`Strings.dat`) — the earlier suspicion of baked button + images was wrong; the layer confusion came from the animated particle background. +- Remaining UI text that is baked into textures (credits, tutorial pics) still needs redrawing. +- Next: translate the rest of `Strings.dat`, scripts and `*Description.dat`; then HD textures. + ## 2026-09-20 — Double-byte font patch proven - Reverse-engineered the whole text pipeline (see `patch-notes.md`). diff --git a/docs/patch-notes.md b/docs/patch-notes.md index b933ef9..83546ae 100644 --- a/docs/patch-notes.md +++ b/docs/patch-notes.md @@ -64,17 +64,47 @@ Behaviour of each cave: Slot `0xFF` glyph lives at `font+0x100C`, its metric at `font+0x1814`; these do not collide with the real tables. +## Byte-order detail (important) + +Both caves must build the code identically as `(first << 8) | second`: + +- draw cave: `movzx ecx,al; shl ecx,8; movzx eax,[edx+1]; or ecx,eax` +- metrics cave: `movzx eax,al; shl eax,8; mov al,[ebx+1]` (`al` is free after the `shl`) + +The metrics cave originally used `mov ah,[ebx+1]` which yields `(second<<8)|first`; CJK then +measured as width 0 and centred labels were flushed right. Fixed. + +## Font atlas (implemented, `tools/build_cjk.py`) + +`AddChar` normalises rects by the **actual D3D texture size**, so replacing the UI font texture +with a larger atlas that still contains the original ASCII art at pixel (0,0) keeps every existing +`Fonts.dat` rect valid — no `Fonts.dat` edits needed. + +``` +Shrift_gb_Germany.TGA 2048 x 1024, 32bpp TGA (type 2, bottom-up, desc 0x08, 26-byte footer) + (0,0) 256x128 original ASCII/German artwork (byte-identical copy) + (0,128)+ 12x12 cells, 170 per row, full GB2312 (7445 codes), simsun 12px, binary alpha +``` + +`cjk_glyph` holds normalised UVs `(x/2048, y/1024, (x+12)/2048, (y+12)/1024)`; +`cjk_metric` holds `(12, 12)` (advance, height). Deployed as a loose file and inside +`Textures.res` (see `tools/install_cjk.py`). + ## Requirements / limits -- The font texture must contain **both** ASCII and CJK glyphs (one texture per draw). - Plan: replace the UI font `Texture` with a 2048x2048 atlas and re-point the ASCII `Char` rects. +- The font texture must contain **both** ASCII and CJK glyphs (one texture per draw); done via the + shared atlas above. - Lead bytes >= `0x80` are reserved for DBCS. ASCII is untouched. -- Confirmed end to end: a GB2312-coded menu string rendered the mapped glyph on screen. +- Only the `UI` font atlas is extended; the `OldUI` / `UISmall` / Russian fonts still point at their + own textures, so CJK drawn with those would need their own atlas/tables too. ## Tools - `tools/add_section.py` — append an executable PE section. - `tools/apply_cjk.py` — assemble the caves (keystone), add `.cjk`, write hooks and tables. +- `tools/build_cjk.py` — build the atlas TGA + `cjk_glyph`/`cjk_metric` from a charset. +- `tools/make_cn_strings.py` + `tools/cn_strings.py` — GB2312 translation of `Strings.dat`. +- `tools/install_cjk.py` — deploy/restore everything into the game folder. - `tools/decode_dat.py` — decode/encode the XOR `.dat` files. - `tools/fontparse.py` — parse `Fonts.dat` into blocks + glyph rects. - `tools/xrefs.py`, `tools/disasm.py`, `tools/scan_disp.py` — RE helpers. diff --git a/tools/apply_cjk.py b/tools/apply_cjk.py index 7df825f..851e1ac 100644 --- a/tools/apply_cjk.py +++ b/tools/apply_cjk.py @@ -1,4 +1,4 @@ -import struct, sys, os +import argparse, struct, sys, os from keystone import Ks, KS_ARCH_X86, KS_MODE_32 ORIG = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe" @@ -21,16 +21,16 @@ def align(x, a): return (x + a - 1) // a * a -def load_tables(): - g = open(os.path.join(TBL, "cjk_glyph.bin"), "rb").read() - m = open(os.path.join(TBL, "cjk_metric.bin"), "rb").read() +def load_tables(tbl): + g = open(os.path.join(tbl, "cjk_glyph.bin"), "rb").read() + m = open(os.path.join(tbl, "cjk_metric.bin"), "rb").read() assert len(g) == 65536 * 16, len(g) assert len(m) == 65536 * 8, len(m) return g, m -def main(): - data = bytearray(open(ORIG, "rb").read()) +def main(orig=ORIG, out=OUT, tbl=TBL): + data = bytearray(open(orig, "rb").read()) e_lfanew = struct.unpack_from(" vtable+0x38 surface desc). So if we replace the UI font texture +`UI\\Overhead\\Shrift_gb_Germany.TGA` (256x128) with a bigger atlas that still +contains the original ASCII artwork at pixel (0,0), every existing ASCII rect +keeps working unchanged. + +CJK glyphs are rendered as full-width cells at the bottom/right of the atlas and +described by the side tables consumed by the `.cjk` code caves: + + cjk_glyph : 65536 * float4 (u1,v1,u2,v2) + cjk_metric : 65536 * int2 (advance, height) + +`key = (lead_byte << 8) | trail_byte` -- exactly the code the cave builds. + +Outputs (into --out): + Shrift_gb_Germany.TGA new texture (TGA type 2, 32bpp, bottom-up, same desc) + cjk_glyph.bin + cjk_metric.bin + cells.json code -> cell / unicode, for diagnostics + atlas_preview.png downscaled preview (not deployed) +""" + +import argparse +import json +import os +import struct +import sys + +from PIL import Image, ImageDraw, ImageFont + +DEFAULT_GAME = r"F:\steam\steamapps\common\The I of the Dragon" +DEFAULT_OUT = r"E:\DragonHD\build\cjk" +TEX_NAME = r"UI\Overhead\Shrift_gb_Germany.TGA" +FONT_CANDIDATES = [ + (r"C:\Windows\Fonts\simsun.ttc", 0), + (r"C:\Windows\Fonts\simhei.ttf", 0), + (r"C:\Windows\Fonts\msyh.ttc", 0), +] + + +# ---------------------------------------------------------------- res reader +def read_res_entry(res_path, want_name): + data = open(res_path, "rb").read() + count = struct.unpack_from(" PIL +def tga_geometry(raw): + if len(raw) < 18: + raise SystemExit("not a TGA") + idlen = raw[0] + imgtype = raw[2] + w = struct.unpack_from(" unicode char, full GB2312 double byte space.""" + out = {} + for hi in range(0xA1, 0xF8): + for lo in range(0xA1, 0xFF): + try: + u = bytes([hi, lo]).decode("gb2312") + except Exception: + continue + out[(hi << 8) | lo] = u + return out + + +def chars_from_text(path, base): + """merge chars found in a utf-8 text file into the base set.""" + txt = open(path, encoding="utf-8").read() + used = {} + for ch in txt: + if ord(ch) < 0x80: + continue + try: + enc = ch.encode("gb2312") + except Exception: + print(" ! cannot gb2312-encode %r" % ch, file=sys.stderr) + continue + if len(enc) != 2: + continue + used[(enc[0] << 8) | enc[1]] = ch + return used + + +# ---------------------------------------------------------------- build +def pick_font(size): + for path, idx in FONT_CANDIDATES: + if os.path.exists(path): + return ImageFont.truetype(path, size, index=idx), path + raise SystemExit("no CJK ttf found") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--game", default=DEFAULT_GAME) + ap.add_argument("--out", default=DEFAULT_OUT) + ap.add_argument("--size", type=int, default=12, + help="CJK pixel size (12 matches the UI pixel font)") + ap.add_argument("--cell", type=int, default=12) + ap.add_argument("--atlas-w", type=int, default=2048) + ap.add_argument("--atlas-h", type=int, default=1024) + ap.add_argument("--cjk-y0", type=int, default=128) + ap.add_argument("--draw-dy", type=int, default=-1, + help="vertical offset of the glyph inside the cell") + ap.add_argument("--charset", default="gb2312", + choices=["gb2312", "text"], + help="gb2312 = every double byte code; text = only chars in --text") + ap.add_argument("--text", default=None) + args = ap.parse_args() + + os.makedirs(args.out, exist_ok=True) + + orig_raw = load_original_tga(args.game) + orig_img, desc = tga_to_image(orig_raw) + ow, oh = orig_img.size + footer = orig_raw[-26:] if len(orig_raw) >= 26 and orig_raw[-18:-1] == b"TRUEVISION-XFILE." else b"" + print("original %dx%d desc=0x%02x footer=%d" % (ow, oh, desc, len(footer))) + + base = iter_gb2312() + if args.charset == "text": + base = chars_from_text(args.text, base) + print("charset size: %d" % len(base)) + + cell = args.cell + cols = args.atlas_w // cell + rows = (args.atlas_h - args.cjk_y0) // cell + if len(base) > cols * rows: + raise SystemExit("charset %d does not fit in %dx%d (%d cells)" + % (len(base), cols, rows, cols * rows)) + + atlas = Image.new("RGBA", (args.atlas_w, args.atlas_h), (0, 0, 0, 0)) + atlas.paste(orig_img, (0, 0)) + + font, fontpath = pick_font(args.size) + print("font: %s @ %dpx" % (fontpath, args.size)) + d = ImageDraw.Draw(atlas) + + cells = {} + clipped = 0 + for i, code in enumerate(sorted(base)): + uni = base[code] + col = i % cols + row = i // cols + x = col * cell + y = args.cjk_y0 + row * cell + d.text((x, y + args.draw_dy), uni, font=font, fill=(255, 255, 255, 255)) + cells[code] = {"x": x, "y": y, "uni": uni} + # sanity: ink must fit the cell + bb = atlas.crop((x, y, x + cell, y + cell)).getbbox() + if bb is None: + clipped += 1 + + print("placed %d glyphs (%d empty), grid %dx%d" % (len(cells), clipped, cols, rows)) + + # tables + glyph = bytearray(65536 * 16) + metric = bytearray(65536 * 8) + aw, ah = args.atlas_w, args.atlas_h + for code, c in cells.items(): + x, y = c["x"], c["y"] + struct.pack_into("<4f", glyph, code * 16, + x / aw, y / ah, (x + cell) / aw, (y + cell) / ah) + struct.pack_into("<2i", metric, code * 8, cell, cell) + + open(os.path.join(args.out, "cjk_glyph.bin"), "wb").write(glyph) + open(os.path.join(args.out, "cjk_metric.bin"), "wb").write(metric) + + tga = image_to_tga(atlas, desc=desc, footer=footer) + open(os.path.join(args.out, "Shrift_gb_Germany.TGA"), "wb").write(tga) + + with open(os.path.join(args.out, "cells.json"), "w", encoding="utf-8") as f: + json.dump({"cell": cell, "atlas": [aw, ah], "cjk_y0": args.cjk_y0, + "size": args.size, "font": fontpath, "cells": cells}, f) + + meta = {"cell": cell, "atlas": [aw, ah], "cjk_y0": args.cjk_y0, + "size": args.size, "font": fontpath, "count": len(cells), + "advance": cell, "height": cell} + with open(os.path.join(args.out, "atlas_meta.json"), "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + + # preview + prev = atlas.resize((args.atlas_w // 4, args.atlas_h // 4), Image.NEAREST) + prev.save(os.path.join(args.out, "atlas_preview.png")) + + print("atlas TGA %d bytes, glyph/probe tables written to %s" % (len(tga), args.out)) + + +if __name__ == "__main__": + main() diff --git a/tools/cn_strings.py b/tools/cn_strings.py new file mode 100644 index 0000000..858bacb --- /dev/null +++ b/tools/cn_strings.py @@ -0,0 +1,65 @@ +"""Simplified Chinese UI strings for The I of the Dragon. + +Only *display* values are translated here (the `English` column of +`Data\\Misc\\Strings.dat`). Never translate `Id`s, event names, commands or +unit types -- doing so is what breaks mission triggers. + +`\\a` is an in-engine text control prefix; keep it verbatim. +Format specifiers (`%s`, `%d`, ...) must survive untouched. +""" + +CN_STRINGS = { + # window / main menu + "WindowCaption": "龙之眼", + "MainMenu_StartNewGame": "开始新游戏", + "MainMenu_SaveLoadGame": "保存或载入游戏", + "MainMenu_LoadGame": "载入游戏", + "MainMenu_ResumeGame": "继续游戏", + "MainMenu_ResumeSavedGame": "载入上次存档:\\\"%s\\\"", + "MainMenu_ResumeSavedGameAutoTip": "载入上次游戏", + "MainMenu_CannotResumeGame": "无法继续游戏", + "MainMenu_Options": "选项", + "MainMenu_Multi": "教程", + "MainMenu_Credits": "制作人员", + "MainMenu_Exit": "退出", + + # options + "OptionsMenu_ApplyDialog": "\\a应用更改?", + "OptionsMenu_Audio": "音频", + "OptionsMenu_Video": "视频", + "OptionsMenu_Controls": "操作", + "OptionsMenu_Game": "游戏选项", + "OptionsMenu_AudioTitle": "音频", + "OptionsMenu_VideoTitle": "视频", + "OptionsMenu_ControlsTitle": "操作", + "OptionsMenu_GameTitle": "游戏选项", + "OptionsMenu_Exit": "退出", + "OptionsMenu_Back": "返回", + "OptionsMenu_Okey": "应用", + "OptionsMenu_Reset": "重置", + + # save / load + "SaveLoadMenu_SaveGame": "保存游戏", + "SaveLoadMenu_CannotSaveGame": "无法保存游戏", + "SaveLoadMenu_LoadGame": "载入游戏", + "SaveLoadMenu_CannotLoadGame": "请先选择一个存档", + "SaveLoadMenu_Back": "返回", + "SaveLoadMenu_Delete": "删除", + "SaveLoadMenu_CannotDelete": "无法删除", + "SaveLoadMenu_Cancel": "取消", + "SaveLoadMenu_ReplaceDialog": "\\a替换?", + "SaveLoadMenu_DeleteDialog": "\\a删除?", + + # multiplayer / tutorial + "Multiplayer_Players": "玩家", + "Multiplayer_Host": "主机", + "Multiplayer_Join": "加入", + "Multiplayer_Ready": "准备", + "Multiplayer_Start": "开始", + "Multiplayer_RedDragon": "红龙 安诺斯", + "Multiplayer_BlackDragon": "黑龙 莫罗格", + "Multiplayer_BlueDragon": "蓝龙 巴洛斯", + "Multiplayer_IP": "IP:", + "Multiplayer_PlayerStateDescription": "%s - %d 经验值", + "Multiplayer_ClientSessionTerminated": "\\a游戏已被主机终止。", +} diff --git a/tools/install_cjk.py b/tools/install_cjk.py new file mode 100644 index 0000000..4325210 --- /dev/null +++ b/tools/install_cjk.py @@ -0,0 +1,196 @@ +"""Deploy / restore the Chinese font patch into the game directory. + +`install`: + 1. back up the pristine exe / Textures.res / Strings.dat (once, into backup_game) + 2. copy the generated `.cjk` tables, rebuild the patched exe (apply_cjk) + 3. drop the patched exe into the game folder + 4. install the font atlas (loose file + inside Textures.res) + 5. patch Strings.dat with the Chinese `English` column + +`restore`: + puts every original file back and removes anything we added. +""" + +import argparse +import hashlib +import json +import os +import shutil +import struct +import sys + +GAME = r"F:\steam\steamapps\common\The I of the Dragon" +BUILD = r"E:\DragonHD\build\cjk" +PATCH = r"E:\DragonHD\patch" +BACKUP = r"E:\DragonHD\backup_game" + +EXE = "TheIOfTheDragon.exe" +ATLAS_RES_NAME = r"UI\Overhead\Shrift_gb_Germany.TGA" +ATLAS_LOOSE = os.path.join("Data", "Textures", "Ui", "Overhead", "Shrift_gb_Germany.TGA") + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import apply_cjk # noqa: E402 +import make_cn_strings # noqa: E402 +from cn_strings import CN_STRINGS # noqa: E402 + + +def sha1(path): + h = hashlib.sha1() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def backup_once(src, dst): + if os.path.exists(dst): + return False + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(src, dst) + return True + + +def replace_res_entries(res_path, repl): + data = open(res_path, "rb").read() + count = struct.unpack_from("", game_exe) + + # 2. atlas: loose + res + atlas_src = os.path.join(BUILD, "Shrift_gb_Germany.TGA") + atlas_blob = open(atlas_src, "rb").read() + os.makedirs(os.path.dirname(game_loose), exist_ok=True) + open(game_loose, "wb").write(atlas_blob) + print("loose atlas ->", game_loose) + + if not args.skip_res: + open(game_res, "wb").write( + replace_res_entries(bak("Textures.res"), {ATLAS_RES_NAME: atlas_blob})) + print("res atlas updated ->", game_res) + + # 3. strings + raw = open(bak("Strings.dat"), "rb").read() + out, done, missing = make_cn_strings.patch(raw, CN_STRINGS) + open(game_str, "wb").write(out) + print("strings patched %d/%d" % (len(done), len(CN_STRINGS))) + if missing: + print("MISSING:", missing, file=sys.stderr) + sys.exit(2) + + print("install done. exe sha1", sha1(game_exe), "res", sha1(game_res)) + + +def restore(args): + game_exe = os.path.join(args.game, EXE) + game_res = os.path.join(args.game, "Data", "Textures.res") + game_str = os.path.join(args.game, "Data", "Misc", "Strings.dat") + game_loose = os.path.join(args.game, ATLAS_LOOSE) + state_path = os.path.join(BACKUP, "state.json") + state = json.load(open(state_path)) if os.path.exists(state_path) else {} + + for name, dst in [(EXE, game_exe), ("Textures.res", game_res), ("Strings.dat", game_str)]: + src = os.path.join(BACKUP, name) + if os.path.exists(src): + shutil.copy2(src, dst) + print("restored", dst) + + if os.path.exists(game_str + ".orig"): + os.remove(game_str + ".orig") + + if state.get("loose_existed"): + src = os.path.join(BACKUP, "Shrift_gb_Germany.TGA") + if os.path.exists(src): + shutil.copy2(src, game_loose) + print("restored loose atlas") + elif os.path.exists(game_loose): + os.remove(game_loose) + print("removed loose atlas") + + print("restore done. exe sha1", sha1(game_exe), "res", sha1(game_res)) + print("strings sha1", sha1(game_str)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("action", choices=["install", "restore"]) + ap.add_argument("--game", default=GAME) + ap.add_argument("--skip-res", action="store_true") + args = ap.parse_args() + if args.action == "install": + install(args) + else: + restore(args) + + +if __name__ == "__main__": + main() diff --git a/tools/make_cn_strings.py b/tools/make_cn_strings.py new file mode 100644 index 0000000..2b276c9 --- /dev/null +++ b/tools/make_cn_strings.py @@ -0,0 +1,67 @@ +"""Patch the `English` column of `Data\\Misc\\Strings.dat` with simplified Chinese. + +Reads the byte-exact file (single byte code page), replaces only the value that +follows `Id = ""` and `English = "..."`, and writes GB2312 double byte text. +The original is preserved next to it as `Strings.dat.orig` (once). +""" + +import argparse +import os +import re +import sys + +from cn_strings import CN_STRINGS + +DEFAULT_GAME = r"F:\steam\steamapps\common\The I of the Dragon" + + +def patch(raw, mapping): + done, missing = [], [] + for sid, cn in mapping.items(): + enc = cn.encode("gb2312") + pat = re.compile( + rb'(Id\s*=\s*"' + re.escape(sid.encode("latin1")) + rb'"' + rb'\s*[\r\n]+\s*English\s*=\s*")(?:[^"\\]|\\.)*(")' + ) + new, n = pat.subn(lambda m: m.group(1) + enc + m.group(2), raw, count=1) + if n: + raw = new + done.append(sid) + else: + missing.append(sid) + return raw, done, missing + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--game", default=DEFAULT_GAME) + ap.add_argument("--file", default=None) + ap.add_argument("--restore", action="store_true") + args = ap.parse_args() + + path = args.file or os.path.join(args.game, "Data", "Misc", "Strings.dat") + bak = path + ".orig" + + if args.restore: + if os.path.exists(bak): + open(path, "wb").write(open(bak, "rb").read()) + print("restored", path) + else: + print("no backup at", bak) + return + + if not os.path.exists(bak): + open(bak, "wb").write(open(path, "rb").read()) + print("backup ->", bak) + + raw = open(path, "rb").read() + out, done, missing = patch(raw, CN_STRINGS) + open(path, "wb").write(out) + print("patched %d/%d strings (%d bytes)" % (len(done), len(CN_STRINGS), len(out))) + if missing: + print("MISSING:", missing, file=sys.stderr) + sys.exit(2) + + +if __name__ == "__main__": + main()