Files
mtgodot-poc/project/python_game_render_test.gd
T

211 lines
9.4 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(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)