Port 2V0-f window input and UI render command bridge
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
# 桌面:python_host_test.gd(--script);真机:MT_TEST_MODE=python 从 client_main.gd 进来。
|
||||
extends RefCounted
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
|
||||
const EXPECTED_STOP := "Sorry, Your system does not support 3D graphics"
|
||||
|
||||
var fail := 0
|
||||
@@ -54,6 +56,104 @@ func run() -> int:
|
||||
"system.py 进入 prototype.RunApp(),Create() 报告待实现的图形设备")
|
||||
_ck(Metin2PythonHost.run_line("import net, player, item; assert net.__port_stub__ == 1 and player.__port_stub__ == 1 and item.__port_stub__ == 1") == "",
|
||||
"2V0 玩法模块桩已注册,调用会记录到 stderr")
|
||||
# 真正从 Godot 入口转发事件给 40250 的 CWindowManager,再由窗口树回调 Python。
|
||||
Metin2PythonHost.set_ui_size(800, 600)
|
||||
var probe_source := """
|
||||
import wndMgr
|
||||
_ui_events = []
|
||||
class _UIProbe:
|
||||
def OnMouseOverIn(self): _ui_events.append('in')
|
||||
def OnMouseOverOut(self): _ui_events.append('out')
|
||||
def OnMouseOver(self): pass
|
||||
def OnMouseLeftButtonDown(self): _ui_events.append('down')
|
||||
def OnMouseLeftButtonUp(self): _ui_events.append('up'); return 1
|
||||
def OnSetFocus(self): _ui_events.append('focus')
|
||||
def OnKillFocus(self): _ui_events.append('blur')
|
||||
def OnUpdate(self): _ui_events.append('update')
|
||||
def OnKeyDown(self, key): _ui_events.append('key:%d' % key); return 1
|
||||
def OnKeyUp(self, key): return 1
|
||||
_ui_probe = _UIProbe()
|
||||
_ui_window = wndMgr.Register(_ui_probe, 'UI')
|
||||
wndMgr.SetWindowSize(_ui_window, 100, 50)
|
||||
wndMgr.SetWindowPosition(_ui_window, 10, 20)
|
||||
wndMgr.Show(_ui_window)
|
||||
wndMgr.SetFocus(_ui_window)
|
||||
"""
|
||||
_ck(Metin2PythonHost.run_line(probe_source) == "", "40250 窗口测试对象已在 Godot 进程中创建")
|
||||
Metin2PythonHost.ui_mouse_move(15, 25)
|
||||
Metin2PythonHost.ui_mouse_button(1, true, 15, 25)
|
||||
Metin2PythonHost.ui_mouse_button(1, false, 15, 25)
|
||||
Metin2PythonHost.ui_key(42, true)
|
||||
Metin2PythonHost.ui_update()
|
||||
_ck(Metin2PythonHost.run_line("assert 'in' in _ui_events and 'down' in _ui_events and 'up' in _ui_events and 'focus' in _ui_events and 'key:42' in _ui_events and 'update' in _ui_events") == "",
|
||||
"Godot 输入、焦点和更新事件抵达 40250 Python 窗口")
|
||||
_ck(Metin2PythonHost.run_line("_ui_bar = wndMgr.RegisterBar(_UIProbe(), 'UI'); wndMgr.SetWindowSize(_ui_bar, 20, 10); wndMgr.SetWindowPosition(_ui_bar, 30, 40); wndMgr.SetColor(_ui_bar, 0x7f336699); wndMgr.Show(_ui_bar)") == "",
|
||||
"40250 Bar 窗口已创建")
|
||||
var draws: Array = Metin2PythonHost.ui_render_commands()
|
||||
var has_bar := false
|
||||
for draw in draws:
|
||||
if draw["kind"] == "bar" and draw["x1"] == 30.0 and draw["y1"] == 40.0 and draw["x2"] == 50.0 and draw["y2"] == 50.0 and draw["argb"] == 0x7f336699:
|
||||
has_bar = true
|
||||
_ck(has_bar, "40250 Bar 绘制调用进入 Godot Canvas 命令队列")
|
||||
var clip_source := """
|
||||
import grp
|
||||
class _ClipProbe(_UIProbe):
|
||||
def OnRender(self):
|
||||
_ui_events.append('renderclip')
|
||||
grp.SetViewport(0.0, 0.0, 0.05, 0.08)
|
||||
_ui_clip_probe = _ClipProbe()
|
||||
_ui_clip = wndMgr.Register(_ui_clip_probe, 'UI')
|
||||
wndMgr.SetWindowSize(_ui_clip, 800, 600)
|
||||
wndMgr.SetParent(_ui_bar, _ui_clip)
|
||||
wndMgr.Show(_ui_clip)
|
||||
"""
|
||||
_ck(Metin2PythonHost.run_line(clip_source) == "", "40250 裁剪窗口已创建")
|
||||
draws = Metin2PythonHost.ui_render_commands()
|
||||
_ck(Metin2PythonHost.run_line("assert 'renderclip' in _ui_events") == "", "父窗口 OnRender 已执行")
|
||||
var has_clip := false
|
||||
for draw in draws:
|
||||
if draw["kind"] == "bar" and draw["clip_x2"] == 40.0 and draw["clip_y2"] == 48.0:
|
||||
has_clip = true
|
||||
_ck(has_clip, "40250 viewport 裁剪范围传给 Godot Canvas")
|
||||
var surface := PythonUISurface.new()
|
||||
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 线段裁剪保留可见区间")
|
||||
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 已创建")
|
||||
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 命令队列")
|
||||
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_text)") == "", "TextLine 已销毁")
|
||||
var image_path := "locale/en/ui/vkey/key_backspace.tga"
|
||||
if Metin2Pack.exists(image_path):
|
||||
var packed_texture: Texture2D = preload("res://ui/ui_assets.gd").load_tex(AssetRoot.path(), image_path)
|
||||
_ck(packed_texture != null and packed_texture.get_width() > 0,
|
||||
"40250 pack 图片可解码为 Godot 纹理")
|
||||
var image_source := "_ui_image_probe = _UIProbe(); _ui_image = wndMgr.RegisterImageBox(_ui_image_probe, 'UI'); wndMgr.LoadImage(_ui_image, '%s'); wndMgr.SetWindowPosition(_ui_image, 120, 130); wndMgr.Show(_ui_image)" % image_path
|
||||
_ck(Metin2PythonHost.run_line(image_source) == "", "40250 pack 图片已加载到 ImageBox")
|
||||
draws = Metin2PythonHost.ui_render_commands()
|
||||
var has_image := false
|
||||
for draw in draws:
|
||||
if draw["kind"] == "image" and String(draw["text"]).replace("\\", "/") == image_path and draw["x1"] == 120.0 and draw["y1"] == 130.0 and draw["x2"] > draw["x1"] and draw["y2"] > draw["y1"]:
|
||||
has_image = true
|
||||
_ck(has_image, "40250 ImageBox 图片进入 Godot Canvas 命令队列")
|
||||
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_image)") == "", "ImageBox 已销毁")
|
||||
var sub_path := "d:/ymir work/ui/cursor/cursor.sub"
|
||||
if Metin2Pack.exists(sub_path):
|
||||
_ck(Metin2PythonHost.run_line("import grpImage; _ui_sub = grpImage.Generate('%s'); assert _ui_sub; grpImage.Delete(_ui_sub)" % sub_path) == "",
|
||||
"40250 .sub 子图可创建并释放")
|
||||
_ck(Metin2PythonHost.run_line("_ui_expanded = grpImage.GenerateExpanded('%s'); assert _ui_expanded; grpImage.DeleteExpanded(_ui_expanded)" % image_path) == "",
|
||||
"40250 ExpandedImage 可创建并释放")
|
||||
else:
|
||||
print("SKIP: 40250 image test(pack 缺少 %s)" % image_path)
|
||||
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_window)") == "", "窗口已销毁")
|
||||
else:
|
||||
print("SKIP: system.py 这一段(没有 40250 Client/pack:%s)" % AssetRoot.client_path())
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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,
|
||||
}
|
||||
|
||||
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"):
|
||||
Metin2PythonHost.set_ui_size(int(size.x), int(size.y))
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
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 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)
|
||||
|
||||
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])
|
||||
+27
-1
@@ -67,11 +67,37 @@ static func _load_uncached(assets_root: String, vpath: String) -> Texture2D:
|
||||
if real != "":
|
||||
break
|
||||
if real == "":
|
||||
return null
|
||||
return _load_pack_image(rel)
|
||||
if real.get_extension().to_lower() == "sub":
|
||||
return _load_sub(real)
|
||||
return _load_image_file(real)
|
||||
|
||||
static func _load_pack_image(rel: String) -> Texture2D:
|
||||
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
|
||||
return null
|
||||
if not Metin2Pack.exists(rel):
|
||||
return null
|
||||
var ext := rel.get_extension().to_lower()
|
||||
var image: Image
|
||||
if ext == "dds":
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
return null
|
||||
var helper: Object = ClassDB.instantiate("Metin2World")
|
||||
image = helper.call("load_dds", "pack://" + rel)
|
||||
helper.free()
|
||||
else:
|
||||
var bytes: PackedByteArray = Metin2Pack.get_bytes(rel)
|
||||
image = Image.new()
|
||||
var err := ERR_FILE_UNRECOGNIZED
|
||||
match ext:
|
||||
"tga": err = image.load_tga_from_buffer(bytes)
|
||||
"png": err = image.load_png_from_buffer(bytes)
|
||||
"jpg", "jpeg": err = image.load_jpg_from_buffer(bytes)
|
||||
"bmp": err = image.load_bmp_from_buffer(bytes)
|
||||
if err != OK:
|
||||
return null
|
||||
return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
|
||||
|
||||
static func _load_image_file(path: String) -> Texture2D:
|
||||
# .sub 文件可能引用了未随当前资源包发布的共享贴图(例如 Public.tga)。
|
||||
# 先做存在性检查,避免 headless/UI fallback 因缺失可选贴图刷错误日志。
|
||||
|
||||
Reference in New Issue
Block a user