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
+62 -7
View File
@@ -20,6 +20,30 @@ const KEY_TO_DIK := {
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
}
# True when this surface started system.py itself (run_app) and so owns the interpreter's lifetime.
var _owns_app := false
# 40250 WinMain → RunMainScript: start the interpreter, give it the canvas size (app.Create reads it),
# run system.py until it parks in app.Loop(). Returns "" or the error RunApp reported.
func run_app() -> String:
if not AssetRoot.pack_ready():
return "no 40250 Client/pack at %s" % AssetRoot.client_path()
var err: String = Metin2PythonHost.start("")
if err != "":
return err
_sync_size()
err = Metin2PythonHost.run_main_script("")
if err != "":
Metin2PythonHost.stop()
return err
_owns_app = true
return ""
func _exit_tree() -> void:
if _owns_app:
_owns_app = false
Metin2PythonHost.stop()
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
@@ -31,9 +55,17 @@ func _notification(what: int) -> void:
func _sync_size() -> void:
if ClassDB.class_exists("Metin2PythonHost"):
Metin2PythonHost.set_ui_size(int(size.x), int(size.y))
# Before the first layout pass a full-rect Control is still 0x0; it will fill the viewport.
var canvas := size if size.x > 0 and size.y > 0 else get_viewport_rect().size
Metin2PythonHost.set_ui_size(int(canvas.x), int(canvas.y))
func _process(_delta: float) -> void:
# app.Exit()/Abort(): Loop() returned and RunApp() finished; 40250 WinMain then ends the process.
if _owns_app and not Metin2PythonHost.is_app_looping():
_owns_app = false
Metin2PythonHost.stop()
get_tree().quit(0)
return
if Metin2PythonHost.is_running():
Metin2PythonHost.ui_update()
queue_redraw()
@@ -84,12 +116,35 @@ func _draw() -> void:
elif command["kind"] == "image":
var texture: Texture2D = UiAssets.load_tex(AssetRoot.path(), command["text"])
if texture != null:
var dest := Rect2(p1, p2 - p1)
var visible := dest.intersection(clip)
if visible.has_area() and dest.has_area():
var scale := texture.get_size() / dest.size
var source := Rect2((visible.position - dest.position) * scale, visible.size * scale)
draw_texture_rect_region(texture, visible, source, color)
var clipped := _clip_quad(command["quad"], command["uv"], clip)
if clipped[0].size() >= 3:
draw_polygon(clipped[0], PackedColorArray([color]), clipped[1], texture)
# 40250's image quad (TL, TR, BL, BR) clipped to the viewport. The quad is a
# parallelogram, so texture coordinates are an affine function of position and are recomputed for
# the clipped outline. Returns [points, uvs]; empty points when nothing is visible.
# PORT: 40250's SCREEN / COLOR_DODGE / MODULATE blend states (command["blend"]) are not applied yet;
# the canvas draws every quad with normal alpha blending.
func _clip_quad(quad: PackedVector2Array, uv: PackedVector2Array, clip: Rect2) -> Array:
var outline := PackedVector2Array([quad[0], quad[1], quad[3], quad[2]])
var edge_u := quad[1] - quad[0]
var edge_v := quad[2] - quad[0]
var det := edge_u.cross(edge_v)
if is_zero_approx(det):
return [PackedVector2Array(), PackedVector2Array()]
var rect := PackedVector2Array([clip.position, Vector2(clip.end.x, clip.position.y),
clip.end, Vector2(clip.position.x, clip.end.y)])
var pieces := Geometry2D.intersect_polygons(outline, rect)
if pieces.is_empty():
return [PackedVector2Array(), PackedVector2Array()]
var points: PackedVector2Array = pieces[0]
var uvs := PackedVector2Array()
for point in points:
var d := point - quad[0]
var a := d.cross(edge_v) / det
var b := edge_u.cross(d) / det
uvs.push_back(uv[0] + (uv[1] - uv[0]) * a + (uv[2] - uv[0]) * b)
return [points, uvs]
func _clip_line(a: Vector2, b: Vector2, clip: Rect2) -> PackedVector2Array:
var delta := b - a