Files
mtgodot-poc/project/beta_live_probe.gd
T

158 lines
6.5 KiB
GDScript

# Bounded, read-only release probe on the same native 40250 path as normal play.
# Credentials stay in the process environment and are never written to the report.
extends Node
var _cases: Array[Dictionary] = []
var _failed := false
func _ready() -> void:
call_deferred("_run")
func _check(ok: bool, name: String) -> bool:
_cases.append({"id": name, "status": "PASS" if ok else "FAIL"})
if not ok:
_failed = true
printerr("BETA_PROBE: FAIL " + name)
return ok
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _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 get_tree().process_frame
return condition.call()
func _key_state(keycode: Key, unicode: int, pressed: bool) -> void:
var event := InputEventKey.new()
event.keycode = keycode
event.physical_keycode = keycode
event.unicode = unicode
event.pressed = pressed
get_tree().root.push_input(event)
func _key(keycode: Key, unicode: int) -> void:
_key_state(keycode, unicode, true)
_key_state(keycode, unicode, false)
func _type(value: String) -> void:
for i in value.length():
var codepoint := value.unicode_at(i)
_key(OS.find_keycode_from_string(value[i].to_upper()), codepoint)
func _finish(output: String, report: Dictionary) -> void:
report["cases"] = _cases
report["status"] = "FAIL" if _failed else "PASS"
var file := FileAccess.open(output.path_join("client-report.json"), FileAccess.WRITE)
if file == null:
printerr("BETA_PROBE: cannot write client report")
get_tree().quit(1)
return
file.store_string(JSON.stringify(report, " "))
file.close()
print("BETA_PROBE: " + report["status"])
if not _py("import app\napp.Exit()"):
Metin2PythonHost.stop()
get_tree().quit(1)
return
# PythonUISurface observes app.Exit, stops the interpreter and ends this process.
func _run() -> void:
var output := OS.get_environment("MT_BETA_OUTPUT")
if output.is_empty() or not output.is_absolute_path():
printerr("BETA_PROBE: MT_BETA_OUTPUT must be an absolute directory")
get_tree().quit(2)
return
DirAccess.make_dir_recursive_absolute(output)
var report := {"schema_version": 1, "route": "native_40250", "map": "", "draw_count": 0}
var host := OS.get_environment("MT_LIVE_HOST")
var login := OS.get_environment("MT_LIVE_LOGIN")
var password := OS.get_environment("MT_LIVE_PASSWORD")
if not _check(not host.is_empty() and not login.is_empty() and not password.is_empty(), "credentials_present"):
_finish(output, report)
return
if OS.get_environment("MT_BETA_REQUIRE_BUNDLE") == "1":
var bundle := OS.get_executable_path().get_base_dir().path_join("../Resources/40250/Client").simplify_path()
if not _check(AssetRoot.client_path() == bundle, "bundled_client_selected"):
_finish(output, report)
return
if not _check(AssetRoot.pack_ready() and Metin2PythonHost.is_app_looping(), "bundled_pack_and_python_loop"):
_finish(output, report)
return
if not _check(await _until(20, func(): return _py(
"import __main__, networkModule, introLogin, introSelect, game\n"
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]\n"
+ "assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)")), "login_window"):
_finish(output, report)
return
if not _check(_py("import os\n"
+ "_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"
+ "_w = _stream.curPhaseWindow\n"
+ "_w._LoginWindow__OpenLoginBoard()\n"
+ "_w.idEditLine.SetText('')\n"
+ "_w.pwdEditLine.SetText('')\n"
+ "_w.idEditLine.SetFocus()"), "login_board"):
_finish(output, report)
return
_type(login)
_key(KEY_TAB, 0)
_type(password)
if not _check(_py("assert _w.idEditLine.GetText() == _env['MT_LIVE_LOGIN']\n"
+ "assert _w.pwdEditLine.GetText() == _env['MT_LIVE_PASSWORD']\n"
+ "del _env, _w"), "keyboard_input"):
_finish(output, report)
return
_key(KEY_ENTER, 0)
var selecting := await _until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"))
if not _check(selecting, "character_select"):
_finish(output, report)
return
if not _check(_py("import net\n"
+ "assert net.GetAccountCharacterSlotDataString(_slot, net.ACCOUNT_CHARACTER_SLOT_NAME)\n"
+ "_stream.curPhaseWindow.SelectSlot(_slot)\n"
+ "_stream.curPhaseWindow.StartGame()"), "selected_character"):
_finish(output, report)
return
var in_game := await _until(70, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"))
if not _check(in_game, "game_window"):
_finish(output, report)
return
var world: Node = get_parent().get_node("Python3DSurface")
_check(await _until(10, func(): return world.draw_count > 0), "character_draws")
_check(not Metin2PythonHost.current_map_name().is_empty(), "map_selected")
_check(int(world.terrain_report.get("chunks_splatted", 0)) > 0, "terrain_textured")
_check(_py("assert net.IsConnect()"), "network_connected")
if _check(_py("import player\n_beta_start_pos = player.GetMainCharacterPosition()"), "movement_start_position"):
_key_state(KEY_UP, 0, true)
await get_tree().create_timer(1.5).timeout
_key_state(KEY_UP, 0, false)
await get_tree().create_timer(0.5).timeout
_check(_py("_beta_end_pos = player.GetMainCharacterPosition()\n"
+ "assert abs(_beta_end_pos[0] - _beta_start_pos[0])"
+ " + abs(_beta_end_pos[1] - _beta_start_pos[1]) > 20"), "movement_position_changed")
# Let the loading fade finish before assessing what a player actually sees.
await get_tree().create_timer(2.0).timeout
report["draw_count"] = world.draw_count
report["map"] = Metin2PythonHost.current_map_name()
report["terrain_chunks"] = int(world.terrain_report.get("chunks_splatted", 0))
if DisplayServer.get_name() != "headless":
await RenderingServer.frame_post_draw
var screenshot := get_viewport().get_texture().get_image()
_check(screenshot.save_png(output.path_join("game.png")) == OK, "screenshot")
var lit := 0
for y in range(screenshot.get_height() / 4, screenshot.get_height() * 3 / 4, 12):
for x in range(screenshot.get_width() / 4, screenshot.get_width() * 3 / 4, 12):
if screenshot.get_pixel(x, y).get_luminance() > 0.05:
lit += 1
report["centre_lit_samples"] = lit
_check(lit > 30, "scene_visible_after_loading")
_finish(output, report)