Files
DragonHD 9eed020634 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.
2026-09-20 10:22:49 +08:00

197 lines
6.3 KiB
Python

"""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("<I", data, 0)[0]
p = 4
entries = []
for _ in range(count):
ln = struct.unpack_from("<I", data, p)[0]
p += 4
name = data[p:p + ln].decode("latin1")
p += ln
off, size = struct.unpack_from("<II", data, p)
p += 8
entries.append([name, data[off:off + size]])
hdr = 4
for name, _ in entries:
hdr += 4 + len(name) + 8
data_start = (hdr + 15) & ~15
out = bytearray(struct.pack("<I", len(entries)))
index = bytearray()
payload = bytearray()
offset = data_start
for i, (name, blob) in enumerate(entries):
blob = repl.get(name, blob)
nb = name.encode("latin1")
index += struct.pack("<I", len(nb)) + nb + struct.pack("<II", offset, len(blob))
payload += blob
if i != len(entries) - 1:
pad = ((len(blob) + 15) & ~15) - len(blob)
payload += b"\x00" * pad
offset += len(blob) + pad
else:
offset += len(blob)
out += index
out += b"\x00" * (data_start - len(out))
out += payload
return bytes(out)
def install(args):
os.makedirs(BACKUP, exist_ok=True)
state_path = os.path.join(BACKUP, "state.json")
state = {}
if os.path.exists(state_path):
state = json.load(open(state_path))
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)
def bak(name):
return os.path.join(BACKUP, name)
for src, dst in [(game_exe, bak(EXE)),
(game_res, bak("Textures.res")),
(game_str, bak("Strings.dat"))]:
if backup_once(src, dst):
print("backup", os.path.basename(dst))
if "loose_existed" not in state:
state["loose_existed"] = os.path.exists(game_loose)
if state["loose_existed"]:
backup_once(game_loose, bak("Shrift_gb_Germany.TGA"))
json.dump(state, open(state_path, "w"))
# 1. tables + patched exe (build against the pristine exe)
os.makedirs(os.path.join(PATCH, "tables"), exist_ok=True)
for t in ("cjk_glyph.bin", "cjk_metric.bin"):
shutil.copy2(os.path.join(BUILD, t), os.path.join(PATCH, "tables", t))
patch_out = os.path.join(PATCH, "TheIOfTheDragon_cjk.exe")
apply_cjk.main(bak(EXE), patch_out, os.path.join(PATCH, "tables"))
shutil.copy2(patch_out, game_exe)
print("patched exe ->", 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()