2V2-e.2: Godot consumer of the 3D draw commands, offline screenshot
python_3d_surface.gd turns RenderGame's recorded draws into meshes under a camera read back from SetPerspective; textures come from the 40250 packs. GetFarClip and the CMapManager ctor ported verbatim. Until the environment is ported, lit draws with no light/ambient are shown unshaded (stand-in). script/python_game_render_test.sh runs system.py login -> select -> game against port_fake_login_server (or --live via MT_LIVE_*) and screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5bfa13c5cc
commit
1fe8a4b643
@@ -0,0 +1,172 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user