- 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>
169 lines
6.5 KiB
GDScript
169 lines
6.5 KiB
GDScript
# Canvas adapter for the 40250 CWindowManager. The window tree remains native;
|
|
# this Control forwards Godot input and draws the native UI command stream.
|
|
extends Control
|
|
|
|
const AssetRoot = preload("res://asset_root.gd")
|
|
const UiAssets = preload("res://ui/ui_assets.gd")
|
|
|
|
const KEY_TO_DIK := {
|
|
KEY_ESCAPE: 0x01, KEY_1: 0x02, KEY_2: 0x03, KEY_3: 0x04,
|
|
KEY_4: 0x05, KEY_5: 0x06, KEY_6: 0x07, KEY_7: 0x08,
|
|
KEY_8: 0x09, KEY_9: 0x0a, KEY_0: 0x0b, KEY_BACKSPACE: 0x0e,
|
|
KEY_TAB: 0x0f, KEY_Q: 0x10, KEY_W: 0x11, KEY_E: 0x12,
|
|
KEY_R: 0x13, KEY_T: 0x14, KEY_Y: 0x15, KEY_U: 0x16,
|
|
KEY_I: 0x17, KEY_O: 0x18, KEY_P: 0x19, KEY_ENTER: 0x1c,
|
|
KEY_A: 0x1e, KEY_S: 0x1f, KEY_D: 0x20, KEY_F: 0x21,
|
|
KEY_G: 0x22, KEY_H: 0x23, KEY_J: 0x24, KEY_K: 0x25,
|
|
KEY_L: 0x26, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e,
|
|
KEY_V: 0x2f, KEY_B: 0x30, KEY_N: 0x31, KEY_M: 0x32,
|
|
KEY_SPACE: 0x39, KEY_UP: 0xc8, KEY_LEFT: 0xcb,
|
|
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
|
|
_sync_size()
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_RESIZED and is_inside_tree():
|
|
_sync_size()
|
|
|
|
func _sync_size() -> void:
|
|
if ClassDB.class_exists("Metin2PythonHost"):
|
|
# 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()
|
|
|
|
func _gui_input(event: InputEvent) -> void:
|
|
if not Metin2PythonHost.is_running():
|
|
return
|
|
if event is InputEventMouseMotion:
|
|
Metin2PythonHost.ui_mouse_move(int(event.position.x), int(event.position.y))
|
|
elif event is InputEventMouseButton:
|
|
if event.button_index >= MOUSE_BUTTON_LEFT and event.button_index <= MOUSE_BUTTON_MIDDLE:
|
|
Metin2PythonHost.ui_mouse_button(event.button_index, event.pressed,
|
|
int(event.position.x), int(event.position.y))
|
|
|
|
func _unhandled_key_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and Metin2PythonHost.is_running():
|
|
var dik: int = KEY_TO_DIK.get(event.physical_keycode, 0)
|
|
if dik != 0:
|
|
Metin2PythonHost.ui_key(dik, event.pressed)
|
|
|
|
func _draw() -> void:
|
|
if not Metin2PythonHost.is_running():
|
|
return
|
|
for command in Metin2PythonHost.ui_render_commands():
|
|
var argb: int = command["argb"]
|
|
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
|
|
argb & 255, (argb >> 24) & 255)
|
|
var p1 := Vector2(command["x1"], command["y1"])
|
|
var p2 := Vector2(command["x2"], command["y2"])
|
|
var clip := Rect2(Vector2(command["clip_x1"], command["clip_y1"]),
|
|
Vector2(command["clip_x2"] - command["clip_x1"],
|
|
command["clip_y2"] - command["clip_y1"]))
|
|
if command["kind"] == "bar":
|
|
var visible := Rect2(p1, p2 - p1).intersection(clip)
|
|
if visible.has_area():
|
|
draw_rect(visible, color, true)
|
|
elif command["kind"] == "line":
|
|
var segment := _clip_line(p1, p2, clip)
|
|
if segment.size() == 2:
|
|
draw_line(segment[0], segment[1], color)
|
|
elif command["kind"] == "text":
|
|
var font: Font = ThemeDB.fallback_font
|
|
if font != null:
|
|
var baseline := p1 + Vector2(0, font.get_ascent(14))
|
|
if clip.has_point(p1):
|
|
draw_string(font, baseline, command["text"], HORIZONTAL_ALIGNMENT_LEFT,
|
|
clip.end.x - p1.x, 14, color)
|
|
elif command["kind"] == "image":
|
|
var texture: Texture2D = UiAssets.load_tex(AssetRoot.path(), command["text"])
|
|
if texture != null:
|
|
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
|
|
var p := [-delta.x, delta.x, -delta.y, delta.y]
|
|
var q := [a.x - clip.position.x, clip.end.x - a.x,
|
|
a.y - clip.position.y, clip.end.y - a.y]
|
|
var enter := 0.0
|
|
var leave := 1.0
|
|
for i in range(4):
|
|
if is_zero_approx(p[i]):
|
|
if q[i] < 0.0:
|
|
return PackedVector2Array()
|
|
else:
|
|
var t: float = q[i] / p[i]
|
|
if p[i] < 0.0:
|
|
enter = maxf(enter, t)
|
|
else:
|
|
leave = minf(leave, t)
|
|
if enter > leave:
|
|
return PackedVector2Array()
|
|
return PackedVector2Array([a + delta * enter, a + delta * leave])
|