2V0-g: port the .fnt font pipeline over a FreeType GDI emulation

CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies
verbatim; CGraphicTextInstance is verbatim except that its D3D draws become
UI render commands (one image quad per glyph, Bar for cursor/underline).

- platform/Win32Gdi.cpp: the GDI subset those files call, over vendored
  FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp
  rules; system font lookup with substitute and CJK fallback faces
- Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by
  hand, other code pages through iconv)
- CGraphicImageTexture memory textures: glyph pages reach Godot as
  "mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache
- Util.cpp code page / font face table, SetDefaultCodePage at boot,
  DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height
- TextTag, StringCodec, Arabic, Vietnamese copied from 40250
- tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop
  and python_host_test.gd now check glyph quads instead of text commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 14:17:25 +09:00
co-authored by Claude Opus 5.5
parent 141206cb69
commit afe63bfa61
272 changed files with 137097 additions and 316 deletions
+35 -12
View File
@@ -133,14 +133,32 @@ wndMgr.Show(_ui_clip)
surface.free()
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_bar)") == "", "Bar 窗口已销毁")
_ck(Metin2PythonHost.run_line("grp.RestoreViewport(); wndMgr.Destroy(_ui_clip)") == "", "裁剪窗口已销毁")
_ck(Metin2PythonHost.run_line("_ui_text_probe = _UIProbe(); _ui_text = wndMgr.RegisterTextLine(_ui_text_probe, 'UI'); wndMgr.SetWindowPosition(_ui_text, 70, 80); wndMgr.SetText(_ui_text, 'Hello'); wndMgr.SetFontColor(_ui_text, 1.0, 0.5, 0.0); wndMgr.Show(_ui_text)") == "",
"40250 TextLine 已创建")
_ck(Metin2PythonHost.run_line("_ui_text_probe = _UIProbe(); _ui_text = wndMgr.RegisterTextLine(_ui_text_probe, 'UI'); wndMgr.SetWindowPosition(_ui_text, 70, 80); wndMgr.SetFontName(_ui_text, 'Tahoma:12'); wndMgr.SetText(_ui_text, 'Hello'); wndMgr.SetFontColor(_ui_text, 1.0, 0.5, 0.0); wndMgr.Show(_ui_text)") == "",
"40250 TextLine 已创建(和 ui.TextLine.__init__ 一样先 SetFontName)")
draws = Metin2PythonHost.ui_render_commands()
var has_text := false
for draw in draws:
if draw["kind"] == "text" and draw["text"] == "Hello" and draw["x1"] == 70.0 and draw["y1"] == 80.0:
has_text = true
_ck(has_text, "40250 TextLine 文本进入 Godot Canvas 命令队列")
var glyphs: Array = draws.filter(func(d): return d["kind"] == "image" and String(d["text"]).begins_with("mem:"))
var min_x := INF
for draw in glyphs:
min_x = minf(min_x, draw["quad"][0].x)
_ck(glyphs.size() == 5 and absf(min_x - 70.0) <= 1.5,
"40250 TextLine 经 CGraphicFontTexture 字形页画成 5 个字形四边形(%d 个,左边 %.1f)" % [glyphs.size(), min_x])
if not glyphs.is_empty():
var page: Image = Metin2PythonHost.memory_texture(glyphs[0]["text"], -1)
var inked := false
if page != null:
for y in page.get_height():
for x in page.get_width():
if page.get_pixel(x, y).a > 0.0:
inked = true
break
if inked:
break
_ck(inked, "字形页(mem: 内存纹理)有字形像素")
var page_surface := preload("res://python_ui_surface.gd").new()
var page_texture: Texture2D = page_surface._texture(glyphs[0]["text"])
_ck(page_texture != null and page_texture == page_surface._texture(glyphs[0]["text"]),
"Canvas 按 mem:<id> 缓存字形页纹理")
page_surface.free()
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_text)") == "", "TextLine 已销毁")
var image_path := "locale/en/ui/vkey/key_backspace.tga"
if Metin2Pack.exists(image_path):
@@ -193,16 +211,21 @@ func _run_app_loop() -> void:
reached = true
break
_ck(reached, "帧推进后进入 LoginWindow")
var glyphs_before := _glyph_quads()
_ck(Metin2PythonHost.run_line("_stream.popupWindow.Open('POPUP_PROBE')\nassert _stream.popupWindow.IsShow()") == "", "PopupDialog 已打开")
Metin2PythonHost.ui_update()
var has_popup := false
for draw in Metin2PythonHost.ui_render_commands():
if draw["kind"] == "text" and draw["text"] == "POPUP_PROBE":
has_popup = true
_ck(has_popup, "弹窗文字进入 Godot Canvas 命令队列")
_ck(_glyph_quads() >= glyphs_before + 11, "弹窗文字的字形进入 Godot Canvas 命令队列")
Metin2PythonHost.ui_key(1, true) # DIK_ESCAPE
Metin2PythonHost.ui_key(1, false)
Metin2PythonHost.ui_update()
_ck(Metin2PythonHost.run_line("assert not _stream.popupWindow.IsShow()") == "", "ESC 关闭弹窗")
Metin2PythonHost.stop()
_ck(not Metin2PythonHost.is_running() and not Metin2PythonHost.is_app_looping(), "stop() 结束 app.Loop() 并收尾")
# CGraphicTextInstance draws one image quad per glyph from a "mem:<id>@<rev>" font page.
func _glyph_quads() -> int:
var count := 0
for draw in Metin2PythonHost.ui_render_commands():
if draw["kind"] == "image" and String(draw["text"]).begins_with("mem:"):
count += 1
return count