# client_main —— 打包客户端的入口场景根(main_scene = res://client_main.tscn)。 # # 默认走 port 路由(PORT-PLAN §2 总开关):40250 system.py 在内嵌解释器里跑,界面画在 # python_ui_surface.gd、RenderGame 的 3D 绘制画在 python_3d_surface.gd。 # MT_PORT_PATH=legacy 回到旧的 GDScript AppFlow(登录 → 选人 → 进游戏 串场控制器); # MT_TEST_MODE=smoke/playable 是 AppFlow 的真服测试,固定走 legacy。 # 资源目录由 AssetRoot 决定:默认 res://../assets(开发树),打包后设环境变量 # MT_ASSETS 指向真实资源目录即可(见 project/asset_root.gd)。 extends Node const AppFlow = preload("res://app_flow.gd") const LiveSmokeTest = preload("res://live_smoke_test.gd") const PackageRenderTest = preload("res://package_render_test.gd") const PlayableLiveTest = preload("res://playable_live_test.gd") const ForestMapRenderTest = preload("res://forest_map_render_test.gd") const NetTrace = preload("res://net_trace.gd") func _ready() -> void: if OS.get_environment("MT_TEST_MODE") == "particle_exit": add_child(preload("res://particle_exit_test.gd").new()) return # MT_TEST_MODE=render:包内离线渲染自检,不起 AppFlow、不连服务器。 # 导出模板不接受 --script,这是导出包里唯一能跑离线渲染回归的入口。 if OS.get_environment("MT_TEST_MODE") == "render": AssetPack.ensure() var t := PackageRenderTest.new() t.name = "PackageRenderTest" add_child(t) return # MT_TEST_MODE=python:在 app 进程里起内嵌解释器跑 system.py(PORT-PLAN 批次 2P step 4)。 # 导出模板不接受 --script,这是在真机 APK/IPA 里跑这条检查的唯一入口。 if OS.get_environment("MT_TEST_MODE") == "python": AssetPack.ensure() var fail: int = preload("res://python_host_check.gd").new().run() if fail == 0: print("PASS: python_host_check (app 进程内 system.py)") else: printerr("%d check(s) failed" % fail) get_tree().quit(1 if fail else 0) return if OS.get_environment("MT_TEST_MODE") == "forest_render": var forest := ForestMapRenderTest.new() forest.name = "ForestMapRenderTest" add_child(forest) return # port 路由:40250 system.py 的界面直接画在 Godot Canvas 上(PORT-PLAN 2V0-f), # Logo → 登录 → 选人 → 进游戏,RenderGame 的 3D 绘制由 python_3d_surface.gd 画出(2V2-e), # 方向键/鼠标点地走动走 40250 原路径(2V3);app.Exit() 退出进程。 # MT_TEST_MODE=python_ui 是切换默认值之前的入口,保留为 MT_PORT_PATH=port 的别名。 var mode := OS.get_environment("MT_TEST_MODE") var route := OS.get_environment("MT_PORT_PATH") if mode == "python_ui": route = "port" elif mode == "smoke" or mode == "playable": route = "legacy" if route != "legacy": if route != "" and route != "port": push_warning("[client] 未知的 MT_PORT_PATH=%s,按 port 处理" % route) if _start_port_route(route != ""): return _start_legacy_route() # Returns false when the port route cannot start and the default (unset MT_PORT_PATH) should fall # back to legacy; an explicit MT_PORT_PATH=port quits with the error instead. func _start_port_route(explicit: bool) -> bool: AssetPack.ensure() if not explicit and not (ClassDB.class_exists("Metin2PythonHost") and AssetRoot.pack_ready()): push_warning("[client] port 路由缺 Metin2PythonHost 或 40250 Client/pack(%s),退回 legacy AppFlow" % AssetRoot.client_path()) return false var surface: Control = preload("res://python_ui_surface.gd").new() surface.name = "PythonUISurface" add_child(surface) # The UI surface runs CPythonApplication::Process in its _process; the 3D surface reads that # frame's draws after it, so it comes second in the tree. var world: Node3D = preload("res://python_3d_surface.gd").new() world.name = "Python3DSurface" add_child(world) var err: String = surface.run_app() if err != "": printerr("port: " + err) get_tree().quit(1) return true func _start_legacy_route() -> void: # 打包 app 从 Finder/open 启动时没有 shell 环境;40250 服务器使用 classic # DH2/CTR 协议,因此默认固定到 classic,同时保留 MT_PROTOCOL 覆盖能力。 if OS.get_environment("MT_PROTOCOL").is_empty(): OS.set_environment("MT_PROTOCOL", "classic") # 移动端:APK/IPA 里没有资源 —— 挂载外部的 assets.zip(adb push / ios-deploy 上传)。 AssetPack.ensure() var root := AssetRoot.path() if not AssetRoot.available(): push_warning("[client] 资源目录不存在:%s —— MT_ASSETS 环境变量 / assets.zip(adb push 到 files/)" % root) var af := AppFlow.new() af.name = "AppFlow" add_child(af) af.start(root) # 主角 vid 生命周期取证(见 net_trace.gd):排在 AppFlow 之后,保证每帧 # 在 client 派发完信号之后再读 main_vid。 if af.client != null: var trace := NetTrace.new() trace.name = "NetTrace" add_child(trace) trace.setup(af.client) if OS.get_environment("MT_TEST_MODE") == "smoke": var smoke := LiveSmokeTest.new() smoke.name = "LiveSmokeTest" add_child(smoke) smoke.setup(af, af.client) if OS.get_environment("MT_TEST_MODE") == "playable": var playable := PlayableLiveTest.new() playable.name = "PlayableLiveTest" add_child(playable) playable.setup(af, af.client)