# 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_MINUS: 0x0c, KEY_EQUAL: 0x0d, 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_BRACKETLEFT: 0x1a, KEY_BRACKETRIGHT: 0x1b, KEY_ENTER: 0x1c, KEY_CTRL: 0x1d, 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_SEMICOLON: 0x27, KEY_APOSTROPHE: 0x28, KEY_QUOTELEFT: 0x29, KEY_SHIFT: 0x2a, KEY_BACKSLASH: 0x2b, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e, KEY_V: 0x2f, KEY_B: 0x30, KEY_N: 0x31, KEY_M: 0x32, KEY_COMMA: 0x33, KEY_PERIOD: 0x34, KEY_SLASH: 0x35, KEY_ALT: 0x38, KEY_SPACE: 0x39, KEY_F1: 0x3b, KEY_F2: 0x3c, KEY_F3: 0x3d, KEY_F4: 0x3e, KEY_F5: 0x3f, KEY_F6: 0x40, KEY_F7: 0x41, KEY_F8: 0x42, KEY_F9: 0x43, KEY_F10: 0x44, KEY_F11: 0x57, KEY_F12: 0x58, KEY_HOME: 0xc7, KEY_UP: 0xc8, KEY_PAGEUP: 0xc9, KEY_LEFT: 0xcb, KEY_RIGHT: 0xcd, KEY_END: 0xcf, KEY_DOWN: 0xd0, KEY_PAGEDOWN: 0xd1, KEY_INSERT: 0xd2, KEY_DELETE: 0xd3, } # 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:" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the # "@" of a command's name moves on (a glyph was added to the page). var _memory_textures := {} var _tex_info_cache := {} var _color_cache := {} var _white_color_array := PackedColorArray([Color.WHITE]) var _asset_root_path := "" var _segments_used := 0 # 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() # The exported Godot executable starts in Contents/Resources. 40250 writes mouse.cfg, # metin2.cfg and other player settings relative to CWD, so move those writes to user://. var data_dir := OS.get_user_data_dir() if DirAccess.make_dir_recursive_absolute(data_dir) != OK: return "cannot create user data directory at %s" % data_dir OS.set_environment("MT_40250_CLIENT", AssetRoot.client_path()) OS.set_environment("MT_40250_USER_DATA_DIR", data_dir) 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 texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED _asset_root_path = AssetRoot.path() _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(): var profile_start := Time.get_ticks_usec() if _profile_frame else 0 Metin2PythonHost.ui_update() if profile_start != 0: profile_ui_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0 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)) elif event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: var factor: float = event.factor if event.factor > 0.0 else 1.0 Metin2PythonHost.ui_mouse_wheel(int(round(120.0 * factor))) elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: var factor: float = event.factor if event.factor > 0.0 else 1.0 Metin2PythonHost.ui_mouse_wheel(-int(round(120.0 * factor))) elif event is InputEventPanGesture: # macOS trackpad two-finger scroll: scrolling up has negative delta.y, zooming in var delta: int = -int(round(event.delta.y * 30.0)) if delta != 0: Metin2PythonHost.ui_mouse_wheel(delta) elif event is InputEventMagnifyGesture: # macOS trackpad pinch to zoom: factor > 1.0 is zoom in var delta: int = int(round((event.factor - 1.0) * 600.0)) if delta != 0: Metin2PythonHost.ui_mouse_wheel(delta) func _unhandled_key_input(event: InputEvent) -> void: if event is InputEventKey and Metin2PythonHost.is_running(): var code: int = event.physical_keycode var dik: int = KEY_TO_DIK.get(code, 0) if dik == 0 and event.keycode != 0: dik = KEY_TO_DIK.get(event.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 var _profile_frame := OS.has_environment("MT_PROFILE_FRAME") var profile_ui_update_ms := 0.0 var profile_draw_ms := 0.0 func _get_color_array(argb: int) -> PackedColorArray: if argb == 0xFFFFFFFF or argb == -1: return _white_color_array var arr: PackedColorArray = _color_cache.get(argb, PackedColorArray()) if not arr.is_empty(): return arr var c := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255) arr = PackedColorArray([c]) _color_cache[argb] = arr return arr func _get_tex_draw_info(name: String) -> Array: var info: Array = _tex_info_cache.get(name, []) if not info.is_empty(): return info var texture: Texture2D = _texture(name) if texture == null: _tex_info_cache[name] = [] return [] var tex_rid: RID = texture.get_rid() var is_atlas := false var u0 := 0.0 var v0 := 0.0 var du := 1.0 var dv := 1.0 var default_uv := PackedVector2Array() if texture is AtlasTexture: var at: AtlasTexture = texture if at.atlas != null: tex_rid = at.atlas.get_rid() var asize: Vector2 = at.atlas.get_size() if asize.x > 0 and asize.y > 0: is_atlas = true var r: Rect2 = at.region u0 = r.position.x / asize.x v0 = r.position.y / asize.y du = r.size.x / asize.x dv = r.size.y / asize.y default_uv = PackedVector2Array([ Vector2(u0, v0), Vector2(u0 + du, v0), Vector2(u0 + du, v0 + dv), Vector2(u0, v0 + dv) ]) if default_uv.is_empty(): default_uv = PackedVector2Array([ Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(0, 1) ]) info = [tex_rid, is_atlas, u0, v0, du, dv, default_uv, texture] _tex_info_cache[name] = info return info func _draw() -> void: var profile_start := Time.get_ticks_usec() if _profile_frame else 0 var fg_used := 0 var fg_plain := false var masked := 0 for i in _segments_used: RenderingServer.canvas_item_clear(_segments[i]) _segments_used = 0 if not Metin2PythonHost.is_running(): return var has_3d: bool = Metin2PythonHost.has_3d_draws() for command in Metin2PythonHost.ui_render_commands_batched(): # Skip background UI commands when 3D is active, as the 3D surface renders the background Quad if has_3d and bool(command.get(&"behind_3d", false)): continue var kind: Variant = command[&"kind"] if kind == &"glyph_batch": var info := _get_tex_draw_info(command[&"text"]) if info.is_empty(): continue if not fg_plain: fg_used = _segment(fg_used, RID()) fg_plain = true RenderingServer.canvas_item_add_triangle_array(_segments[fg_used - 1], command[&"indices"], command[&"points"], command[&"colors"], command[&"uvs"], PackedInt32Array(), PackedFloat32Array(), info[0]) continue var x1: float = command[&"x1"] var y1: float = command[&"y1"] var x2: float = command[&"x2"] var y2: float = command[&"y2"] var cx1: float = command[&"clip_x1"] var cy1: float = command[&"clip_y1"] var cx2: float = command[&"clip_x2"] var cy2: float = command[&"clip_y2"] # 1) Completely culled by scissor? if x2 <= cx1 or x1 >= cx2 or y2 <= cy1 or y1 >= cy2: continue var argb: int = command[&"argb"] var target_segments := _segments if kind == &"image": var text_name: String = command.get(&"text", "") var mask_name: String = command.get(&"mask", "") var mask_tex: Texture2D = _texture(mask_name) if not mask_name.is_empty() else null if text_name.is_empty() and mask_tex != null: text_name = "__white__" var info := _get_tex_draw_info(text_name) if info.is_empty(): continue var tex_rid: RID = info[0] var is_atlas: bool = info[1] var u0: float = info[2] var v0: float = info[3] var du: float = info[4] var dv: float = info[5] var quad: PackedVector2Array = command[&"quad"] var quad_uv: PackedVector2Array = command[&"uv"] var needs_clip := x1 < cx1 or x2 > cx2 or y1 < cy1 or y2 > cy2 var poly_pts: PackedVector2Array var poly_uv: PackedVector2Array if not needs_clip: poly_pts = PackedVector2Array([quad[0], quad[1], quad[3], quad[2]]) var is_full_uv := quad_uv[0] == Vector2.ZERO and quad_uv[1] == Vector2(1, 0) and quad_uv[2] == Vector2(0, 1) and quad_uv[3] == Vector2.ONE if is_full_uv: poly_uv = info[6] elif is_atlas: poly_uv = PackedVector2Array([ Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv), Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv), Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv), Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv) ]) else: poly_uv = PackedVector2Array([quad_uv[0], quad_uv[1], quad_uv[3], quad_uv[2]]) else: var mapped_uv: PackedVector2Array if is_atlas: mapped_uv = PackedVector2Array([ Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv), Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv), Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv), Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv) ]) else: mapped_uv = quad_uv var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1) var clipped := _clip_quad(quad, mapped_uv, clip_rect) if clipped[0].size() < 3: continue poly_pts = clipped[0] poly_uv = clipped[1] if mask_tex != null: var mat_rid := _mask_material(masked, mask_tex, quad_uv, command[&"mask_uv"]) fg_used = _segment(fg_used, mat_rid) fg_plain = false masked += 1 elif not fg_plain: fg_used = _segment(fg_used, RID()) fg_plain = true var seg_idx := fg_used - 1 RenderingServer.canvas_item_add_polygon(target_segments[seg_idx], poly_pts, _get_color_array(argb), poly_uv, tex_rid) elif kind == &"bar": var bx1 := maxf(x1, cx1) var by1 := maxf(y1, cy1) var bx2 := minf(x2, cx2) var by2 := minf(y2, cy2) if bx2 > bx1 and by2 > by1: if not fg_plain: fg_used = _segment(fg_used, RID()) fg_plain = true var seg_idx := fg_used - 1 var color := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255) RenderingServer.canvas_item_add_rect(target_segments[seg_idx], Rect2(bx1, by1, bx2 - bx1, by2 - by1), color) elif kind == &"gradient_bar": var bx1 := maxf(x1, cx1) var by1 := maxf(y1, cy1) var bx2 := minf(x2, cx2) var by2 := minf(y2, cy2) if bx2 > bx1 and by2 > by1: if not fg_plain: fg_used = _segment(fg_used, RID()) fg_plain = true var seg_idx := fg_used - 1 var color := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255) 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 := (by1 - y1) / (y2 - y1) if y2 != y1 else 0.0 var bottom_factor := (by2 - y1) / (y2 - y1) if y2 != y1 else 1.0 RenderingServer.canvas_item_add_polygon(target_segments[seg_idx], PackedVector2Array([Vector2(bx1, by1), Vector2(bx2, by1), Vector2(bx2, by2), Vector2(bx1, by2)]), PackedColorArray([color.lerp(bottom, top_factor), color.lerp(bottom, top_factor), color.lerp(bottom, bottom_factor), color.lerp(bottom, bottom_factor)])) elif kind == &"line": var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1) var p1 := Vector2(x1, y1) var p2 := Vector2(x2, y2) var segment := _clip_line(p1, p2, clip_rect) if segment.size() == 2: if not fg_plain: fg_used = _segment(fg_used, RID()) fg_plain = true var seg_idx := fg_used - 1 var color := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255) RenderingServer.canvas_item_add_line(target_segments[seg_idx], segment[0], segment[1], color) _segments_used = fg_used if profile_start != 0: profile_draw_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0 # Opens the next pooled child item (draw order = index) with the given material; returns the count used. func _segment(used: int, material: RID) -> int: var pool := _segments var parent_item: RID = get_canvas_item() if used == pool.size(): var item := RenderingServer.canvas_item_create() RenderingServer.canvas_item_set_parent(item, parent_item) RenderingServer.canvas_item_set_default_texture_repeat(item, RenderingServer.CANVAS_ITEM_TEXTURE_REPEAT_ENABLED) pool.push_back(item) var item := pool[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 name == "__white__": return _white_texture() if not name.begins_with("mem:"): var root := _asset_root_path if not _asset_root_path.is_empty() else AssetRoot.path() return UiAssets.load_tex(root, 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()] if clip.has_point(quad[0]) and clip.has_point(quad[1]) \ and clip.has_point(quad[2]) and clip.has_point(quad[3]): return [outline, PackedVector2Array([uv[0], uv[1], uv[3], uv[2]])] 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])