2V0-f: run the real system.py app loop in Godot

- system.py runs app.Loop() on the script fiber; each UIUpdate is one
  CPythonApplication::Process(). Logo -> Login, popup ESC/click and
  app.Exit teardown (launcher cleared first, as in 40250 Main) work.
- Image commands are base-texture quads with UVs (a .sub shares its parent
  texture); ImageInstance/ExpandedImageInstance OnRender and SetRenderingRect
  ported verbatim, so scale/rotation/rendering rect are drawn.
- CGraphicBase::GetColor / grp.GenerateColor ported (ThinBoard background).
- port.app_loop test, python_host_test app-loop checks, MT_TEST_MODE=python_ui.
- TODO: .fnt/CGraphicText font pipeline, ExpandedImage blend modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 13:38:13 +09:00
co-authored by Claude Opus 5.5
parent 5590714b16
commit 141206cb69
20 changed files with 916 additions and 57 deletions
+47 -1
View File
@@ -3,7 +3,8 @@
# 和 ctest 的 `port.python_launcher` 跑的是同一条序列,区别只有一个:那边是独立可执行文件,这边在
# Godot 进程里,由 GDScript 发起 —— step 4 要验的就是这个差别。2V0 把 C++ 模块注册上之前,
# 2V0-e 的玩法桩让 system.py 真正走到 prototype.RunApp();
# 2V0-f 的应用 adapter 尚未创建图形设备,RunApp 捕获 CREATE_DEVICE 并给出原版提示。
# 没设画布尺寸时 app.Create 没有设备,RunApp 捕获 CREATE_DEVICE 并给出原版提示;设了尺寸以后
# system.py 停在 app.Loop() 里,由 ui_update 逐帧推进(_run_app_loop)。
#
# 真机上通常没有 40250 的 Client/pack(APK 里只有 python27.zip),所以分两段:解释器 + 标准库这一段
# 不需要 pack,任何平台都跑;system.py 这一段需要 pack,没有就跳过并说清楚。
@@ -119,6 +120,16 @@ wndMgr.Show(_ui_clip)
var segment: PackedVector2Array = surface._clip_line(Vector2(-10, 10), Vector2(50, 10), Rect2(0, 0, 30, 30))
_ck(segment.size() == 2 and segment[0] == Vector2(0, 10) and segment[1] == Vector2(30, 10),
"Godot Canvas 线段裁剪保留可见区间")
# ExpandedImage 的四边形(TL, TR, BL, BR)被 viewport 裁掉右半边,UV 随位置按仿射关系重算。
var quad := PackedVector2Array([Vector2(0, 0), Vector2(20, 0), Vector2(0, 10), Vector2(20, 10)])
var quad_uv := PackedVector2Array([Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)])
var clipped: Array = surface._clip_quad(quad, quad_uv, Rect2(0, 0, 10, 10))
var max_u := 0.0
for i in clipped[1].size():
max_u = maxf(max_u, clipped[1][i].x)
if not is_equal_approx(clipped[1][i].x, clipped[0][i].x / 20.0):
max_u = -1.0
_ck(clipped[0].size() >= 3 and is_equal_approx(max_u, 0.5), "ExpandedImage 四边形裁剪后 UV 同步收缩")
surface.free()
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_bar)") == "", "Bar 窗口已销毁")
_ck(Metin2PythonHost.run_line("grp.RestoreViewport(); wndMgr.Destroy(_ui_clip)") == "", "裁剪窗口已销毁")
@@ -159,4 +170,39 @@ wndMgr.Show(_ui_clip)
Metin2PythonHost.stop()
_ck(not Metin2PythonHost.is_running(), "stop() 之后 is_running() 为假")
if has_pack:
_run_app_loop()
return fail
# 2V0-f:有画布尺寸时 app.Create 成功,system.py 停在 app.Loop() 里(脚本线程),每次 ui_update 就是
# 一次 CPythonApplication::Process()。Logo 阶段 → LoginWindow → 弹窗 ESC 关闭 → stop 走完 RunApp 收尾。
func _run_app_loop() -> void:
_ck(Metin2PythonHost.start("") == "", "第二次 start()")
Metin2PythonHost.set_ui_size(800, 600)
var err: String = Metin2PythonHost.run_main_script("")
_ck(err == "", "system.py 进入 app.Loop():" + err)
_ck(Metin2PythonHost.is_app_looping(), "is_app_looping() 为真")
_ck(Metin2PythonHost.run_line("import __main__, gc, networkModule, introLogo\n__main__._stream = [o for o in gc.get_objects() if isinstance(o, networkModule.MainStream)][0]\nassert isinstance(_stream.curPhaseWindow, introLogo.LogoWindow)") == "",
"Logo 阶段已打开")
var reached := false
for i in 600:
if not Metin2PythonHost.is_app_looping():
break
Metin2PythonHost.ui_update()
if Metin2PythonHost.run_line("import introLogin\nif not isinstance(_stream.curPhaseWindow, introLogin.LoginWindow): raise SystemError") == "":
reached = true
break
_ck(reached, "帧推进后进入 LoginWindow")
_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 命令队列")
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() 并收尾")