Files
mtgodot-poc/project/python_game_render_test.gd
T
shenleiandClaude Opus 5.5 ac4c2aa4e0 2V2-e.3: live smoke of the Godot render adapter
Fix the material cache key: bools given to %d failed to format, so every
draw shared the first material (the face texture). The render test now
checks one texture per texture name. Offline and live (192.168.21.203)
show the dressed main character; live also shows the nearby mobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:42:24 +09:00

179 lines
7.8 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["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 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
report["surfaces_textured"] = surfaces_textured
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)
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)