extends TestCase ## What milestone 1 promises: two instances of the game, each in a process of ## its own, stay in lockstep. This process is the host; a second Godot ## process plays a guest with tools/guest_bot.gd. ## The tick at which the host stops and both compare their worlds. const UNTIL := 150 ## Milliseconds the test waits for the second instance. const PATIENCE_MSEC := 30000 func test_a_guest_in_another_process_stays_in_lockstep() -> void: var directory := TestFiles.temporary_dir("two-instances") var host: Node = load("res://main.tscn").instantiate() host.storage_dir = directory.path_join("host") host.command_line = PackedStringArray() tree.root.add_child(host) host.options.port = 0 host.listen_address = "127.0.0.1" check(host.play(true), "the world opens") var report_path := directory.path_join("guest.json") var arguments := PackedStringArray([ "--headless", "--path", ProjectSettings.globalize_path("res://"), "--log-file", ProjectSettings.globalize_path(directory.path_join("guest.log")), "-s", "res://tools/guest_bot.gd", "--", "--join=127.0.0.1:%d" % host.link.link.local_port(), "--storage=" + directory.path_join("guest"), "--until=%d" % UNTIL, "--out=" + report_path, ]) var process := OS.create_process(OS.get_executable_path(), arguments) check(process > 0, "the second instance starts") var started := Time.get_ticks_msec() while OS.is_process_running(process) and Time.get_ticks_msec() - started < PATIENCE_MSEC: if host.session.guest_count() > 0: # Once the guest is there, the test runs the host's ticks itself, # one per frame, instead of twenty a second, and stops at UNTIL. # A guest's arrival ends a pause, so the clock is stopped here. host.clock.paused = true if host.session.tick() < UNTIL: host._run_tick() await tree.process_frame if OS.is_process_running(process): OS.kill(process) check(false, "the second instance did not finish in time") var report: Variant = JSON.parse_string(FileAccess.get_file_as_string(report_path)) check(report is Dictionary and not report.has("error"), "the guest reports: %s" % [report]) if report is Dictionary and not report.has("error"): check_eq(int(report["tick"]), UNTIL, "the guest stopped where the host stopped") check_eq(report["hash"], host.session.state_hash_hex(), "both worlds are the same") check_eq(int(report["player"]), 1, "the guest is player 1") check_eq(int(report["stones"]), 1, "the guest broke the rock") check_eq(int(report["resyncs"]), 0, "without a divergence on the way") var rock: Vector2i = host.session.spawn_point() + Vector2i(7, -2) check_eq(host.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "the rock is gone in the host's world too") check_eq(host.session.player_name(1), "Bot", "the host knows the guest") tree.root.remove_child(host) host.link.link.close() host.free() TestFiles.remove_dir(directory)