6.6 KiB
Font rendering map + double-byte patch
Font object layout (size 0x191C, embedded by value in a manager struct)
Two UI font objects live at manager + 0x3A8 and manager + 0x1CC4 (difference = 0x191C).
+0x00 texture pointer (D3D texture object)
+0x04 max width
+0x08 max height
+0x0C SpaceWidth
+0x10 LineDistance
+0x14 CharDistance
+0x18 flag (1 = built)
+0x1C 256 glyph entries x 16 bytes = 4 floats (u1,v1,u2,v2) normalized UV
+0x101C 256 metric entries x 8 bytes = { int advance, int height }
+0x181C 256 "defined" bytes
The table stride differs (16 vs 8 vs 1), which is why the tables cannot be enlarged in place and a side table is used for CJK.
Key code
| VA | role |
|---|---|
0x4EBF90 |
parse/load Fonts.dat (generic config reader) |
0x4E7DC0 |
set font texture |
0x4E7E30 |
AddChar(code, x1,y1,x2,y2) — normalizes rect → stores glyph float[4] + metric[2] |
0x4EC168 |
mov cl,[ebp] — parser takes only the first byte of Code |
0x4E7FD0 |
measure text; loop body 0x4E7FF0; metric lookups 0x4E802C, 0x4E8033 |
0x4EAC40 |
draw text; loop head |
0x4EACB0 |
movzx ecx,al — glyph lookup site in draw loop |
0x4EACDC,0x4EAE9A |
metric lookups in draw loop |
0x4E56A0 |
IsCharDefined(font, code) |
Texture is bound once per draw call ([font] + vtable +0xF4), so ASCII and CJK glyphs must
share one texture atlas.
Patch design (implemented)
A new PE section .cjk holds two code caves and two big tables:
.cjk RVA 0x566000 (VA 0x966000), size 0x1C0000
0x00 draw_cave
0x0100 metrics_cave
0x0200 cjk_glyph : 65536 x 16 bytes (u1,v1,u2,v2 floats)
0x100200 cjk_metric : 65536 x 8 bytes (advance,height ints)
Hooks (5-byte jmp rel32):
0x4EACB0 -> draw_cave(wasmovzx ecx,al)0x4E8026 -> metrics_cave(wasmov ebp,[ecx+0x14])
Behaviour of each cave:
al < 0x80: execute the original instructions, jump back to the next instruction.al >= 0x80: decode a 2-byte code(al<<8)|[ptr+1]; copy the glyph float[4] into font glyph slot0xFFand the metric into slot0xFF; setecx = 0xFF, advance the string pointer by one extra byte; jump back into the engine's unchanged quad-building code.
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](alis free after theshl)
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); done via the shared atlas above.
- Lead bytes >=
0x80are reserved for DBCS. ASCII is untouched. - Only the
UIfont atlas is extended; theOldUI/UISmall/ Russian fonts still point at their own textures, so CJK drawn with those would need their own atlas/tables too.
Tools
Option-title centering
Four original title sites push reference x=620 and directly draw text. The
panel/Bind button center is reference x=640 (0x4D8646). Patch the title
anchors to x=640 and redirect their draw calls to .cjk+0x180500:
| Page | Anchor instruction | Draw call |
|---|---|---|
| Audio | 0x4D748C |
0x4D74A6 |
| Game | 0x4D9A1C |
0x4D9A36 |
| Video | 0x4DDAB4 |
0x4DDACE |
| Controls | 0x4DE8C8 |
0x4DE8E2 |
The wrapper preserves registers, measures the supplied font/text using
0x4E7FD0, subtracts half the measured width from the already transformed
screen point, and tail-jumps to 0x4EAB70. Ordinary option rows and unrelated
draw calls retain their existing placement. Input opcodes are checked before
these title edits. tools/test_cjk_titles.py tests the actual call-site code,
coordinate transformation, measurement, and resulting draw positions.
Wrapped-text layout correction
0x4ED380 wraps/aligns tooltip paragraphs separately from the plain draw and
measure loops. Its one-byte temporary string must be extended to a full GB2312
character, otherwise a Chinese-only line has zero measured glyph height.
New hooks (all addresses apply to the documented Steam executable):
| Hook | Cave offset in .cjk | Purpose |
|---|---|---|
0x4ED430 |
0x180200 |
Prepare a NUL-terminated one/two-byte character; preserve control-code flags |
0x4ED50E |
0x180300 |
Append complete character, advance byte index, retain width/height at CJK break |
0x4ED4E9 |
0x180400 |
Preserve an overwide first CJK glyph instead of consuming only its lead byte |
Layout locals: esp+0x18..0x1A character plus NUL, +0x1B byte length,
+0x14 source byte index, +0x24 output byte count, +0x1C last wrap
checkpoint. Empty output initializes the checkpoint to source_index-1.
The caves use spare space after the metric table; UVs and atlas pixels are
unchanged. tools/test_cjk_layout.py runs the original layout and measurement
machine code in Unicorn and captures line bytes and coordinates at the draw
call boundary. This checks layout behavior without replacing it with a Python
reimplementation.
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_metricfrom a charset.tools/make_cn_strings.py+tools/cn_strings.py— GB2312 translation ofStrings.dat.tools/install_cjk.py— deploy/restore everything into the game folder.tools/decode_dat.py— decode/encode the XOR.datfiles.tools/fontparse.py— parseFonts.datinto blocks + glyph rects.tools/xrefs.py,tools/disasm.py,tools/scan_disp.py— RE helpers.