Files
mtgodot-poc/project/python_game_render_test.gd
T

247 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)