# 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["terrain"] = world.terrain_report report["atlas_entries"] = world._atlas.size() _check(not Metin2PythonHost.current_map_name().is_empty(), "native AtlasInfo chooses the current map") if world.terrain: report["terrain_base_cm"] = str(world.terrain.call("get_map_base_cm")) report["native_map_name"] = Metin2PythonHost.current_map_name() _check(world.map_path == Metin2PythonHost.current_map_name(), "terrain uses native Warp map") var first_draws: Array = Metin2PythonHost.render3d_draws() if not first_draws.is_empty(): var camera_world: Vector3 = world.d3d_transform(first_draws[0]["view"]).affine_inverse().origin report["camera_world_cm"] = str(camera_world) report["terrain_height_at_camera_cm"] = float(world.terrain.call("sample_height", camera_world.x / 100.0, -camera_world.y / 100.0)) * 100.0 var tile_bounds := [] for tile in world.terrain.find_children("Terrain*", "MeshInstance3D", true, false): if tile_bounds.size() >= 4: break tile_bounds.append(str(tile.global_transform * tile.mesh.get_aabb())) report["terrain_camera_bounds"] = tile_bounds _check(world.terrain != null and int(world.terrain_report.get("chunks_built", 0)) > 0, "40250 terrain height tiles loaded") _check(int(world.terrain_report.get("chunks_splatted", 0)) > 0, "40250 terrain textures applied") _check(bool(world.terrain_report.get("env_ok", false)) and world.light.visible, "map .msenv lights the character") _check(world.unlit_stand_in_count == 0, "unlit stand-in removed") 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() var native_lit_draws := 0 var view_finite := true var actor_foot_cm := INF var foot_xy := Vector2.ZERO for draw in draws: if draw["light0"]: native_lit_draws += 1 for value in draw["view"]: if not is_finite(value): view_finite = false var world_xform: Transform3D = world.d3d_transform(draw["world"]) for vertex in draw["positions"]: var p: Vector3 = world_xform * vertex if p.z < actor_foot_cm: actor_foot_cm = p.z foot_xy = Vector2(p.x, -p.y) report["native_lit_draws"] = native_lit_draws report["view_finite"] = view_finite _check(view_finite, "native camera view stays finite") if world.terrain and not is_inf(actor_foot_cm): var ground_cm := float(world.terrain.call("sample_height", foot_xy.x / 100.0, foot_xy.y / 100.0)) * 100.0 report["actor_foot_cm"] = actor_foot_cm report["ground_at_foot_cm"] = ground_cm _check(absf(actor_foot_cm - ground_cm) < 120.0, "native actor feet meet map height (delta %.1f cm)" % absf(actor_foot_cm - ground_cm)) _check(not drawn or native_lit_draws == draws.size(), "native RenderGame light0 comes from map .msenv (%d/%d)" % [native_lit_draws, draws.size()]) 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 var lit_surfaces := 0 var distinct_textures := {} 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 distinct_textures[material.albedo_texture] = true if material and material.shading_mode == BaseMaterial3D.SHADING_MODE_PER_VERTEX: lit_surfaces += 1 report["surfaces_textured"] = surfaces_textured report["lit_surfaces"] = lit_surfaces report["distinct_textures"] = distinct_textures.size() # Every texture name gets its own texture (face, body, hair are not one material). _check(distinct_textures.size() == textures.size(), "one texture per texture name (%d of %d)" % [distinct_textures.size(), textures.size()]) 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) _check(lit_surfaces > 0, "character surfaces use directional lighting (%d)" % lit_surfaces) 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 var game_image := root.get_texture().get_image() game_image.save_png(output.path_join("game_3d.png")) var textured_pairs := 0 for y in range(game_image.get_height() * 3 / 4, game_image.get_height() - 20, 13): for x in range(30, game_image.get_width() - 35, 17): var left := game_image.get_pixel(x, y).get_luminance() var right := game_image.get_pixel(x + 5, y).get_luminance() if absf(left - right) > 0.04: textured_pairs += 1 report["ground_texture_pairs"] = textured_pairs _check(textured_pairs > 20, "textured ground visible in screenshot (%d local contrasts)" % textured_pairs) ui.visible = true await _finish(output, report)