# Bounded, read-only release probe on the same native 40250 path as normal play. # Credentials stay in the process environment and are never written to the report. extends Node var _cases: Array[Dictionary] = [] var _failed := false func _ready() -> void: call_deferred("_run") func _check(ok: bool, name: String) -> bool: _cases.append({"id": name, "status": "PASS" if ok else "FAIL"}) if not ok: _failed = true printerr("BETA_PROBE: FAIL " + name) return ok func _py(source: String) -> bool: return Metin2PythonHost.run_line(source) == "" func _until(seconds: float, condition: Callable) -> bool: var deadline := Time.get_ticks_msec() + int(seconds * 1000.0) while Time.get_ticks_msec() < deadline: if condition.call(): return true await get_tree().process_frame return condition.call() func _key_state(keycode: Key, unicode: int, pressed: bool) -> void: var event := InputEventKey.new() event.keycode = keycode event.physical_keycode = keycode event.unicode = unicode event.pressed = pressed get_tree().root.push_input(event) func _key(keycode: Key, unicode: int) -> void: _key_state(keycode, unicode, true) _key_state(keycode, unicode, false) func _type(value: String) -> void: for i in value.length(): var codepoint := value.unicode_at(i) _key(OS.find_keycode_from_string(value[i].to_upper()), codepoint) func _finish(output: String, report: Dictionary) -> void: report["cases"] = _cases report["status"] = "FAIL" if _failed else "PASS" var file := FileAccess.open(output.path_join("client-report.json"), FileAccess.WRITE) if file == null: printerr("BETA_PROBE: cannot write client report") get_tree().quit(1) return file.store_string(JSON.stringify(report, " ")) file.close() print("BETA_PROBE: " + report["status"]) if not _py("import app\napp.Exit()"): Metin2PythonHost.stop() get_tree().quit(1) return # PythonUISurface observes app.Exit, stops the interpreter and ends this process. func _run() -> void: printerr("BETA_STAGE: start") var output := OS.get_environment("MT_BETA_OUTPUT") if output.is_empty() or not output.is_absolute_path(): printerr("BETA_PROBE: MT_BETA_OUTPUT must be an absolute directory") get_tree().quit(2) return DirAccess.make_dir_recursive_absolute(output) var report := {"schema_version": 1, "route": "native_40250", "map": "", "draw_count": 0} var host := OS.get_environment("MT_LIVE_HOST") var login := OS.get_environment("MT_LIVE_LOGIN") var password := OS.get_environment("MT_LIVE_PASSWORD") if not _check(not host.is_empty() and not login.is_empty() and not password.is_empty(), "credentials_present"): _finish(output, report) return if OS.get_environment("MT_BETA_REQUIRE_BUNDLE") == "1": var bundle := OS.get_executable_path().get_base_dir().path_join("../Resources/40250/Client").simplify_path() if not _check(AssetRoot.client_path() == bundle, "bundled_client_selected"): _finish(output, report) return if not _check(AssetRoot.pack_ready() and Metin2PythonHost.is_app_looping(), "bundled_pack_and_python_loop"): _finish(output, report) return if not _check(await _until(20, func(): return _py( "import __main__, networkModule, introLogin, introSelect, game\n" + "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]\n" + "assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)")), "login_window"): _finish(output, report) return printerr("BETA_STAGE: login_window") if not _check(_py("import os\n" + "_env = os.environ\n" + "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n" + "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), " + "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n" + "_w = _stream.curPhaseWindow\n" + "_w._LoginWindow__OpenLoginBoard()\n" + "_w.idEditLine.SetText('')\n" + "_w.pwdEditLine.SetText('')\n" + "_w.idEditLine.SetFocus()"), "login_board"): _finish(output, report) return _type(login) _key(KEY_TAB, 0) _type(password) if not _check(_py("assert _w.idEditLine.GetText() == _env['MT_LIVE_LOGIN']\n" + "assert _w.pwdEditLine.GetText() == _env['MT_LIVE_PASSWORD']\n" + "del _env, _w"), "keyboard_input"): _finish(output, report) return _key(KEY_ENTER, 0) printerr("BETA_STAGE: login_submitted") var selecting := await _until(30, func(): return _py( "assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)")) if not _check(selecting, "character_select"): _finish(output, report) return printerr("BETA_STAGE: character_select") if not _check(_py("import net\n" + "assert net.GetAccountCharacterSlotDataString(_slot, net.ACCOUNT_CHARACTER_SLOT_NAME)\n" + "_stream.curPhaseWindow.SelectSlot(_slot)\n" + "_stream.curPhaseWindow.StartGame()"), "selected_character"): _finish(output, report) return var in_game := await _until(70, func(): return _py( "assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()")) if not _check(in_game, "game_window"): _finish(output, report) return printerr("BETA_STAGE: game_window") var world: Node = get_parent().get_node("Python3DSurface") _check(await _until(10, func(): return world.draw_count > 0), "character_draws") _check(not Metin2PythonHost.current_map_name().is_empty(), "map_selected") _check(int(world.terrain_report.get("chunks_splatted", 0)) > 0, "terrain_textured") _check(_py("assert net.IsConnect()"), "network_connected") if _check(_py("import player\n_beta_start_pos = player.GetMainCharacterPosition()"), "movement_start_position"): printerr("BETA_STAGE: move_start") _key_state(KEY_UP, 0, true) await get_tree().create_timer(1.5).timeout _key_state(KEY_UP, 0, false) await get_tree().create_timer(0.5).timeout _check(_py("_beta_end_pos = player.GetMainCharacterPosition()\n" + "assert abs(_beta_end_pos[0] - _beta_start_pos[0])" + " + abs(_beta_end_pos[1] - _beta_start_pos[1]) > 20"), "movement_position_changed") printerr("BETA_STAGE: move_end") # Let the loading fade finish before assessing what a player actually sees. await get_tree().create_timer(2.0).timeout report["draw_count"] = world.draw_count report["map"] = Metin2PythonHost.current_map_name() report["terrain_chunks"] = int(world.terrain_report.get("chunks_splatted", 0)) if DisplayServer.get_name() != "headless": await RenderingServer.frame_post_draw var screenshot := get_viewport().get_texture().get_image() _check(screenshot.save_png(output.path_join("game.png")) == OK, "screenshot") var lit := 0 for y in range(screenshot.get_height() / 4, screenshot.get_height() * 3 / 4, 12): for x in range(screenshot.get_width() / 4, screenshot.get_width() * 3 / 4, 12): if screenshot.get_pixel(x, y).get_luminance() > 0.05: lit += 1 report["centre_lit_samples"] = lit _check(lit > 30, "scene_visible_after_loading") _finish(output, report)