211 lines
9.4 KiB
GDScript
211 lines
9.4 KiB
GDScript
# 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(world._atlas.size() > 0, "40250 AtlasInfo chooses the current map")
|
||
if world.terrain:
|
||
report["terrain_base_cm"] = str(world.terrain.call("get_map_base_cm"))
|
||
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()
|
||
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)
|
||
var ground_low := 1.0
|
||
var ground_high := 0.0
|
||
for y in range(image.get_height() * 3 / 4, image.get_height() - 20, 17):
|
||
for x in range(30, image.get_width() - 30, 23):
|
||
var luminance := image.get_pixel(x, y).get_luminance()
|
||
ground_low = minf(ground_low, luminance)
|
||
ground_high = maxf(ground_high, luminance)
|
||
report["ground_luminance_range"] = ground_high - ground_low
|
||
_check(ground_high - ground_low > 0.08, "textured ground visible in screenshot")
|
||
# 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)
|