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" OUT = r"E:\DragonHD\patch\TheIOfTheDragon_cjk.exe" TBL = r"E:\DragonHD\patch\tables" HOOK_DRAW_VA = 0x4EACB0 HOOK_MEAS_VA = 0x4E8026 BACK_DRAW_VA = 0x4EACB8 BACK_MEAS_VA = 0x4E8049 SEC_SIZE = 0x1C0000 OFF_DRAW = 0x0000 OFF_MEAS = 0x0100 OFF_GLYPH = 0x0200 OFF_METRIC = 0x100200 OFF_LAYOUT_READ = 0x180200 OFF_LAYOUT_APPEND = 0x180300 OFF_LAYOUT_FIT = 0x180400 OFF_TITLE_CENTER = 0x180500 # Fixed left edge at x=620 was tuned for English titles. The panel and Bind # button are centered at reference x=640 (scaled by the engine from 800px). TITLE_SITES = ((0x4D748C, 0x4D74A6), (0x4D9A1C, 0x4D9A36), (0x4DDAB4, 0x4DDACE), (0x4DE8C8, 0x4DE8E2)) # The wrapped-text path has its own one-byte temporary string and output # buffer. It must measure and append complete GB2312 characters too. HOOK_LAYOUT_READ_VA = 0x4ED430 HOOK_LAYOUT_APPEND_VA = 0x4ED50E HOOK_LAYOUT_FIT_VA = 0x4ED4E9 def align(x, a): return (x + a - 1) // a * a 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(orig=ORIG, out=OUT, tbl=TBL): data = bytearray(open(orig, "rb").read()) e_lfanew = struct.unpack_from(" secs[0][2]: print("no room for section header", file=sys.stderr) sys.exit(3) # add section header hdr = so + nsec * 40 struct.pack_into("<8sIIII", data, hdr, b".cjk".ljust(8, b"\x00"), SEC_SIZE, new_va, align(SEC_SIZE, file_align), new_raw) struct.pack_into(" 0x%x" % (va, target)) hook(HOOK_DRAW_VA, draw_va) hook(HOOK_MEAS_VA, meas_va) hook(HOOK_LAYOUT_READ_VA, sec_abs + OFF_LAYOUT_READ) hook(HOOK_LAYOUT_APPEND_VA, sec_abs + OFF_LAYOUT_APPEND) hook(HOOK_LAYOUT_FIT_VA, sec_abs + OFF_LAYOUT_FIT) for anchor, call_site in TITLE_SITES: anchor_off, call_off = anchor - imagebase, call_site - imagebase expected_call = bytes(ks.asm('call 0x4EAB70', call_site)[0]) if data[anchor_off:anchor_off + 5] != b'\x68\x6c\x02\x00\x00' or data[call_off:call_off + 5] != expected_call: raise ValueError('unexpected option-title instructions at 0x%x' % anchor) data[anchor_off:anchor_off + 5] = bytes(ks.asm('push 0x280', anchor)[0]) data[call_off:call_off + 5] = bytes(ks.asm('call %s' % hex(sec_abs + OFF_TITLE_CENTER), call_site)[0]) os.makedirs(os.path.dirname(out), exist_ok=True) open(out, "wb").write(data) print("wrote", out, len(data), "bytes") if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--orig", default=ORIG) ap.add_argument("--out", default=OUT) ap.add_argument("--tbl", default=TBL) a = ap.parse_args() main(a.orig, a.out, a.tbl)