# python_game_render_test — 2V2-e:Godot 里跑 40250 的 system.py,按 port_login_flow_test 的同一条路径 # 登录 → 选人 → 进游戏,把 RenderGame 的绘制命令(python_3d_surface.gd)画出来并截图。 # # godot --path project --script res://python_game_render_test.gd # # 服务器与账号只从环境变量读(MT_LIVE_HOST / MT_LIVE_LOGIN / MT_LIVE_PASSWORD,可选 MT_LIVE_AUTH_PORT # 11000、MT_LIVE_GAME_PORT 13000、MT_LIVE_SLOT 0);Python 侧也用 os.environ 取,不进源码文本。 # 离线由 script/python_game_render_test.sh 起 port_fake_login_server 并把这些变量指向它。 # 截图与 report.json 写到 MT_RENDER_OUTPUT(默认 build/rendering/python-game-<时间>)。 extends SceneTree const PythonUISurface = preload("res://python_ui_surface.gd") const Python3DSurface = preload("res://python_3d_surface.gd") var failed := false var ui: Control var world: Node3D func _initialize() -> void: call_deferred("run") func _check(ok: bool, message: String) -> void: if ok: print("ok: " + message) else: failed = true printerr("FAIL: " + message) func _py(source: String) -> bool: return Metin2PythonHost.run_line(source) == "" func _pump_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 process_frame return condition.call() func _finish(output: String, report: Dictionary) -> void: report["passed"] = not failed var file := FileAccess.open(output.path_join("report.json"), FileAccess.WRITE) if file: file.store_string(JSON.stringify(report, " ")) file.close() print("python_game_render_test: %s (%s)" % ["FAIL" if failed else "PASS", output]) ui.queue_free() await process_frame quit(1 if failed else 0) func run() -> void: root.size = Vector2i(1024, 768) var output := OS.get_environment("MT_RENDER_OUTPUT") if output.is_empty(): output = ProjectSettings.globalize_path("res://../build/rendering/python-game-%d" % Time.get_unix_time_from_system()) DirAccess.make_dir_recursive_absolute(output) var report := {"output": output, "headless": DisplayServer.get_name() == "headless"} var host := OS.get_environment("MT_LIVE_HOST") if host.is_empty() or OS.get_environment("MT_LIVE_LOGIN").is_empty() or OS.get_environment("MT_LIVE_PASSWORD").is_empty(): printerr("python_game_render_test: needs MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD") quit(2) return world = Python3DSurface.new() world.name = "Python3DSurface" ui = PythonUISurface.new() ui.name = "PythonUISurface" # The UI surface runs CPythonApplication::Process in its _process; the 3D surface reads the draws # of that frame after it, so it comes second in the tree. root.add_child(ui) root.add_child(world) var err: String = ui.run_app() _check(err == "", "system.py reached app.Loop() (%s)" % err) if err != "": await _finish(output, report) return _check(_py("import __main__, os, networkModule, introLogin, introSelect, introLoading, game\n" + "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"), "found networkModule.MainStream") var at_login := await _pump_until(20, func(): return _py( "assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)")) _check(at_login, "login window") # LoginWindow.Connect with the account from the environment. _check(_py("_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" + "_stream.curPhaseWindow.Connect(_env['MT_LIVE_LOGIN'], _env['MT_LIVE_PASSWORD'])\n" + "del _env"), "LoginWindow.Connect") var selecting := await _pump_until(30, func(): return _py( "assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)")) _check(selecting, "character select") if selecting: _check(_py("_stream.curPhaseWindow.SelectSlot(_slot)\n_stream.curPhaseWindow.StartGame()"), "StartGame") var in_game := selecting and await _pump_until(60, func(): return _py( "assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()")) _check(in_game, "game window") # The main character's draws reach the 3D surface. var drawn := in_game and await _pump_until(10, func(): return world.draw_count > 0) _check(drawn, "RenderGame draws shown (%d)" % world.draw_count) # Let the WAIT motion run a little before the picture. for i in 30: await process_frame report["draw_count"] = world.draw_count report["unlit_stand_in_count"] = world.unlit_stand_in_count report["camera_fov"] = world.camera.fov report["camera_near"] = world.camera.near report["camera_far"] = world.camera.far var textures := [] var draws: Array = Metin2PythonHost.render3d_draws() if not draws.is_empty(): report["proj"] = Array(draws[0]["proj"]) report["view"] = Array(draws[0]["view"]) # Each draw's render state (everything but the arrays and matrices). var states := [] for draw in draws: var state := {} for key in draw: if typeof(draw[key]) in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_COLOR, TYPE_VECTOR3]: state[key] = str(draw[key]) state["vertices"] = draw["positions"].size() if draw.has("uv0"): var lo := Vector2(INF, INF) var hi := -lo for uv in draw["uv0"]: lo = lo.min(uv) hi = hi.max(uv) state["uv0_range"] = str([lo, hi]) state["indices"] = draw["indices"].size() states.append(state) report["draw_states"] = states for draw in draws: if not draw["texture0"].is_empty() and not textures.has(draw["texture0"]): textures.append(draw["texture0"]) report["textures"] = textures var surfaces_textured := 0 for instance in world.find_children("Draw*", "MeshInstance3D", false, false): if instance.visible and instance.mesh: var material: StandardMaterial3D = instance.mesh.surface_get_material(0) if material and material.albedo_texture: surfaces_textured += 1 report["surfaces_textured"] = surfaces_textured var bounds := [] for instance in world.find_children("Draw*", "MeshInstance3D", false, false): if instance.visible and instance.mesh: bounds.append(str(instance.transform * instance.mesh.get_aabb())) report["camera_space_bounds"] = bounds _check(not drawn or surfaces_textured > 0, "character surfaces have their diffuse textures (%d)" % surfaces_textured) if DisplayServer.get_name() != "headless": await RenderingServer.frame_post_draw var image := root.get_texture().get_image() _check(image.save_png(output.path_join("game.png")) == OK, "screenshot saved") # The character is centred by __UpdateCamera: the middle of the picture is not background. var centre := Rect2i(image.get_width() * 2 / 5, image.get_height() * 3 / 10, image.get_width() / 5, image.get_height() * 2 / 5) var lit := 0 for y in range(centre.position.y, centre.end.y, 4): for x in range(centre.position.x, centre.end.x, 4): if image.get_pixel(x, y).get_luminance() > 0.05: lit += 1 report["centre_lit_samples"] = lit _check(lit > 50, "character pixels in the middle of the screen (%d samples)" % lit) # The 3D pass alone, for looking at the character without the UI. ui.visible = false await RenderingServer.frame_post_draw root.get_texture().get_image().save_png(output.path_join("game_3d.png")) ui.visible = true await _finish(output, report)