Files
mtgodot-poc/project/python_ui_surface.gd
T

322 lines
13 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,
}
# Win32 virtual-key codes of the keys EditLine.OnIMEKeyDown handles (WM_KEYDOWN → OnIMEKeyDown);
# letters and digits use their ASCII code, as VK_A..VK_Z / VK_0..VK_9 do.
const KEY_TO_VK := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d,
KEY_ESCAPE: 0x1b, KEY_SPACE: 0x20, KEY_END: 0x23, KEY_HOME: 0x24,
KEY_LEFT: 0x25, KEY_UP: 0x26, KEY_RIGHT: 0x27, KEY_DOWN: 0x28, KEY_DELETE: 0x2e,
}
# The control characters Windows turns these keys into (WM_CHAR); Godot reports them with unicode 0.
const KEY_TO_CHAR := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d, KEY_ESCAPE: 0x1b,
}
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
# "@<revision>" of a command's name moves on (a glyph was added to the page).
var _memory_textures := {}
# 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:
for segment in _segments:
RenderingServer.free_rid(segment)
_segments.clear()
for material in _materials:
RenderingServer.free_rid(material)
_materials.clear()
if _mask_shader.is_valid():
RenderingServer.free_rid(_mask_shader)
_mask_shader = RID()
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)
if event.pressed:
_ime_key(event)
# 40250 gets every key press twice more through the window procedure: WM_KEYDOWN (a VK code, for
# OnIMEKeyDown) and then WM_CHAR (the typed character, for CPythonIME). Held keys repeat both.
func _ime_key(event: InputEventKey) -> void:
var keycode: int = event.keycode
var vk: int = KEY_TO_VK.get(keycode, 0)
if vk == 0 and ((keycode >= KEY_A and keycode <= KEY_Z) or (keycode >= KEY_0 and keycode <= KEY_9)):
vk = keycode
if vk != 0:
Metin2PythonHost.ui_ime_key(vk)
var ch: int = event.unicode
if ch == 0:
ch = KEY_TO_CHAR.get(keycode, 0)
if ch != 0:
Metin2PythonHost.ui_char(ch)
# The command stream is drawn into pooled child canvas items in order: a run of plain commands
# shares one item; a masked image (CPythonMiniMap's terrain tiles under minimap_image_filter.dds)
# gets its own item carrying the mask shader, since a canvas item has a single material.
const MASK_SHADER := """
shader_type canvas_item;
render_mode blend_mix;
uniform sampler2D mask_texture : repeat_disable, filter_linear;
uniform vec4 uv_rect; // su, sv, eu, ev of the image
uniform vec2 m0; // mask coordinates at the image's TL, TR and BL corners
uniform vec2 m1;
uniform vec2 m2;
void fragment() {
vec2 ab = (UV - uv_rect.xy) / max(uv_rect.zw - uv_rect.xy, vec2(1e-6));
vec4 mask = texture(mask_texture, m0 + (m1 - m0) * ab.x + (m2 - m0) * ab.y);
vec4 image = texture(TEXTURE, UV) * COLOR;
COLOR = vec4(image.rgb * mask.rgb, mask.a * COLOR.a);
}
"""
var _segments: Array[RID] = []
var _materials: Array[RID] = []
var _mask_shader := RID()
var _white: ImageTexture
func _draw() -> void:
var used := 0
var masked := 0
var plain := false
for segment in _segments:
RenderingServer.canvas_item_clear(segment)
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():
if not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_rect(_segments[used - 1], visible, color)
elif command["kind"] == "gradient_bar":
var visible := Rect2(p1, p2 - p1).intersection(clip)
if visible.has_area():
if not plain:
used = _segment(used, RID())
plain = true
var bottom_argb: int = command["end_argb"]
var bottom := Color8((bottom_argb >> 16) & 255, (bottom_argb >> 8) & 255,
bottom_argb & 255, (bottom_argb >> 24) & 255)
var top_factor := (visible.position.y - p1.y) / (p2.y - p1.y)
var bottom_factor := (visible.end.y - p1.y) / (p2.y - p1.y)
RenderingServer.canvas_item_add_polygon(_segments[used - 1],
PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y),
visible.end, Vector2(visible.position.x, visible.end.y)]),
PackedColorArray([color.lerp(bottom, top_factor), color.lerp(bottom, top_factor),
color.lerp(bottom, bottom_factor), color.lerp(bottom, bottom_factor)]))
elif command["kind"] == "line":
var segment := _clip_line(p1, p2, clip)
if segment.size() == 2:
if not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_line(_segments[used - 1], segment[0], segment[1], color)
elif command["kind"] == "image":
var texture: Texture2D = _texture(command["text"])
var mask: Texture2D = _texture(command["mask"]) if command.has("mask") else null
if mask != null and texture == null:
texture = _white_texture()
if texture != null:
var clipped := _clip_quad(command["quad"], command["uv"], clip)
if clipped[0].size() >= 3:
if mask != null:
used = _segment(used, _mask_material(masked, mask, command["uv"], command["mask_uv"]))
masked += 1
plain = false
elif not plain:
used = _segment(used, RID())
plain = true
RenderingServer.canvas_item_add_polygon(_segments[used - 1], clipped[0],
PackedColorArray([color]), clipped[1], texture.get_rid())
# Opens the next pooled child item (draw order = index) with the given material; returns the count used.
func _segment(used: int, material: RID) -> int:
if used == _segments.size():
var item := RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_parent(item, get_canvas_item())
_segments.push_back(item)
var item := _segments[used]
RenderingServer.canvas_item_set_draw_index(item, used)
RenderingServer.canvas_item_set_material(item, material)
return used + 1
func _mask_material(index: int, mask: Texture2D, uv: PackedVector2Array, mask_uv: PackedVector2Array) -> RID:
if not _mask_shader.is_valid():
_mask_shader = RenderingServer.shader_create()
RenderingServer.shader_set_code(_mask_shader, MASK_SHADER)
if index == _materials.size():
var material := RenderingServer.material_create()
RenderingServer.material_set_shader(material, _mask_shader)
_materials.push_back(material)
var material := _materials[index]
RenderingServer.material_set_param(material, "mask_texture", mask.get_rid())
RenderingServer.material_set_param(material, "uv_rect", Vector4(uv[0].x, uv[0].y, uv[3].x, uv[3].y))
RenderingServer.material_set_param(material, "m0", mask_uv[0])
RenderingServer.material_set_param(material, "m1", mask_uv[1])
RenderingServer.material_set_param(material, "m2", mask_uv[2])
return material
func _white_texture() -> Texture2D:
if _white == null:
var image := Image.create(1, 1, false, Image.FORMAT_RGBA8)
image.fill(Color.WHITE)
_white = ImageTexture.create_from_image(image)
return _white
func _texture(name: String) -> Texture2D:
if not name.begins_with("mem:"):
return UiAssets.load_tex(AssetRoot.path(), name)
var at := name.find("@")
var key := name.substr(0, at) if at >= 0 else name
var revision := int(name.substr(at + 1)) if at >= 0 else 0
var entry: Array = _memory_textures.get(key, [])
if not entry.is_empty() and entry[0] == revision:
return entry[1]
var image: Image = Metin2PythonHost.memory_texture(name, -1)
if image == null:
_memory_textures.erase(key)
return null
var texture: ImageTexture = entry[1] if not entry.is_empty() else null
if texture != null and texture.get_size() == Vector2(image.get_size()):
texture.update(image)
else:
texture = ImageTexture.create_from_image(image)
_memory_textures[key] = [revision, texture]
return 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])