158 lines
6.5 KiB
GDScript
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)
|