fix: correct CJK tooltip layout and center option titles

This commit is contained in:
DragonHD
2026-09-20 11:25:04 +08:00
parent ca5748821c
commit 253cc47bc5
6 changed files with 450 additions and 1 deletions
+147
View File
@@ -15,6 +15,21 @@ 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):
@@ -130,6 +145,124 @@ def main(orig=ORIG, out=OUT, tbl=TBL):
meas_code = bytes(meas_code)
print("draw_cave %d bytes @0x%x, metrics_cave %d bytes @0x%x" % (len(draw_code), draw_va, len(meas_code), meas_va))
# Locals in 0x4ED380: +18..1A = character + NUL, +1B = byte count.
# +14 is the source byte index, +24 the output byte count. The two
# formerly unused bytes at +1A/+1B precede the +1C wrap checkpoint.
# Preserve flags from cmp al,7: the original continuation uses jne.
layout_read_asm = """
mov byte ptr [esp + 0x18], al
mov word ptr [esp + 0x19], 0
mov byte ptr [esp + 0x1B], 1
pushfd
push eax
cmp dword ptr [esp + 0x2C], 0
jne checkpoint_ready
mov eax, ecx
dec eax
mov [esp + 0x24], eax
mov al, byte ptr [esp + 0x20]
checkpoint_ready:
cmp al, 0xA1
jb done
cmp al, 0xF7
ja done
cmp ecx, edx
jae done
mov eax, [esp + 0x450]
mov al, byte ptr [eax + ecx + 1]
cmp al, 0xA1
jb done
cmp al, 0xFE
ja done
mov byte ptr [esp + 0x21], al
mov byte ptr [esp + 0x23], 2
done:
pop eax
popfd
jmp 0x4ED439
"""
# Consume the second source byte only AFTER the character fits. On
# overflow the stock wrap code must retry the lead byte on the next line.
layout_append_asm = """
mov eax, [esp + 0x24]
mov cl, byte ptr [esp + 0x12]
mov byte ptr [esp + eax + 0x4C], cl
inc eax
cmp byte ptr [esp + 0x1B], 2
jne done
mov cl, byte ptr [esp + 0x19]
mov byte ptr [esp + eax + 0x4C], cl
inc eax
inc dword ptr [esp + 0x14]
done:
mov [esp + 0x24], eax
mov byte ptr [esp + eax + 0x4C], 0
mov ecx, [esp + 0x20]
mov eax, [esp + 0x40]
cmp eax, ecx
jle height_done
mov [esp + 0x20], eax
height_done:
mov esi, edx
cmp byte ptr [esp + 0x1B], 2
jne appended
mov eax, [esp + 0x14]
mov [esp + 0x1C], eax
mov ebp, esi
mov eax, [esp + 0x20]
mov [esp + 0x2C], eax
appended:
xor bl, bl
jmp 0x4ED67C
"""
# A box narrower than a glyph must still consume the whole character.
# Emit it on its own line instead of dropping its lead byte. The caller
# retains its normal clipping behavior for the overwide line.
layout_fit_asm = """
lea edx, [eax + esi]
cmp edx, ecx
jle fits
cmp dword ptr [esp + 0x24], 0
jne overflow
cmp byte ptr [esp + 0x1B], 2
jne overflow
fits:
jmp 0x4ED4F0
overflow:
jmp 0x4ED53D
"""
layout_read_code = bytes(ks.asm(layout_read_asm, sec_abs + OFF_LAYOUT_READ)[0])
layout_append_code = bytes(ks.asm(layout_append_asm, sec_abs + OFF_LAYOUT_APPEND)[0])
layout_fit_code = bytes(ks.asm(layout_fit_asm, sec_abs + OFF_LAYOUT_FIT)[0])
# Same ABI as DrawText: point/font/color/text/justification. The incoming
# point is already transformed to screen pixels; subtract the measured
# width in pixels AFTER transformation (glyphs do not scale with it).
title_center_asm = """
pushad
sub esp, 8
mov ecx, [esp + 0x30]
mov eax, [esp + 0x38]
mov edx, esp
push 0
push eax
push edx
call 0x4E7FD0
mov eax, [esp]
shr eax, 1
mov edx, [esp + 0x2C]
sub [edx], eax
add esp, 8
popad
jmp 0x4EAB70
"""
title_center_code = bytes(ks.asm(title_center_asm, sec_abs + OFF_TITLE_CENTER)[0])
assert len(draw_code) <= OFF_MEAS - OFF_DRAW
assert len(meas_code) <= OFF_GLYPH - OFF_MEAS
assert len(layout_read_code) <= OFF_LAYOUT_APPEND - OFF_LAYOUT_READ
assert len(layout_append_code) <= OFF_LAYOUT_FIT - OFF_LAYOUT_APPEND
assert len(layout_fit_code) <= OFF_TITLE_CENTER - OFF_LAYOUT_FIT
assert OFF_TITLE_CENTER + len(title_center_code) <= SEC_SIZE
if so + (nsec + 1) * 40 > secs[0][2]:
print("no room for section header", file=sys.stderr)
sys.exit(3)
@@ -151,6 +284,10 @@ def main(orig=ORIG, out=OUT, tbl=TBL):
glyph, metric = load_tables(tbl)
data[sec_off + OFF_GLYPH: sec_off + OFF_GLYPH + len(glyph)] = glyph
data[sec_off + OFF_METRIC: sec_off + OFF_METRIC + len(metric)] = metric
data[sec_off + OFF_LAYOUT_READ: sec_off + OFF_LAYOUT_READ + len(layout_read_code)] = layout_read_code
data[sec_off + OFF_LAYOUT_APPEND: sec_off + OFF_LAYOUT_APPEND + len(layout_append_code)] = layout_append_code
data[sec_off + OFF_LAYOUT_FIT: sec_off + OFF_LAYOUT_FIT + len(layout_fit_code)] = layout_fit_code
data[sec_off + OFF_TITLE_CENTER: sec_off + OFF_TITLE_CENTER + len(title_center_code)] = title_center_code
def hook(va, target):
off = va - imagebase
@@ -164,6 +301,16 @@ def main(orig=ORIG, out=OUT, tbl=TBL):
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)