# 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:" → [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 := {} # 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 _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])