extends TestCase ## A host and guests in one process, each with its own GwSession and ## NetLink, connected over the loopback interface. var host_key := PackedByteArray([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) var guest_key := PackedByteArray([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) var host: SessionLink ## Notices per SessionLink, collected while frames pass. var notices: Dictionary[SessionLink, Array] = {} func _start_host() -> void: var session := GwSession.new() session.start_new(7) session.join(host_key, "Host") session.step() var link := NetLink.new() check_eq(link.host(0, "127.0.0.1"), OK, "the host listens") host = SessionLink.new(session, link) notices[host] = [] func _start_guest(key: PackedByteArray, name: String) -> SessionLink: var session := GwSession.new() session.start_guest(key, name) var link := NetLink.new() link.join("127.0.0.1", host.link.local_port()) var guest := SessionLink.new(session, link) notices[guest] = [] return guest ## Lets `frames` frames pass. The host runs a tick per frame if `ticking`; ## every guest runs what arrived. func _play(guests: Array[SessionLink], frames: int, ticking := true) -> void: for frame in frames: notices[host].append_array(host.pump(1.0 / 60.0)) if ticking: host.session.step() host.flush() for guest in guests: notices[guest].append_array(guest.pump(1.0 / 60.0)) for i in guest.session.pending_ticks(): guest.session.step() await tree.process_frame func _types(link: SessionLink) -> Array: return notices[link].map(func(notice: Dictionary) -> String: return notice["type"]) func _first(link: SessionLink, type: String) -> Dictionary: for notice: Dictionary in notices[link]: if notice["type"] == type: return notice return {} func _close(links: Array[SessionLink]) -> void: for link in links: link.link.close() func test_a_guest_joins_over_the_network_and_follows_the_host() -> void: _start_host() var guest := _start_guest(guest_key, "Grace") await _play([guest], 40) await _play([guest], 10, false) check(_types(host).has("guest_arrived"), "the host sees the guest arrive") check_eq(_first(guest, "world_received").get("resync"), false, "the guest got the host's world") check_eq(guest.session.local_player(), 1, "the guest is player 1") check_eq(guest.session.player_name(0), "Host", "the guest knows the host's name") check_eq(guest.session.tick(), host.session.tick(), "the guest is at the host's tick") check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same") check_eq(guest.session.save().size(), 0, "a guest does not save the host's world") _close([host, guest]) func test_a_hit_by_the_guest_reaches_the_host() -> void: _start_host() var guest := _start_guest(guest_key, "Grace") await _play([guest], 30) var rock := host.session.spawn_point() + Vector2i(7, -2) guest.session.dig(rock.x, rock.y) await _play([guest], 10) await _play([guest], 10, false) var index := rock.y * host.session.map_width() + rock.x check_eq(host.session.damage_codes()[index], 1, "the host's rock took the hit") check_eq(guest.session.state_hash_hex(), host.session.state_hash_hex(), "both worlds are the same") _close([host, guest]) func test_figures_travel_to_the_other_machines() -> void: _start_host() var guest := _start_guest(guest_key, "Grace") var other := _start_guest(PackedByteArray([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), "Linus") await _play([guest, other], 30) host.send_figure(Vector2(10.0, 20.0)) guest.send_figure(Vector2(5.5, 6.5)) await _play([guest, other], 10) check_eq(_first(host, "figure"), {"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}, "the host sees the guest") var seen := notices[other].filter(func(notice: Dictionary) -> bool: return notice["type"] == "figure") check(seen.has({"type": "figure", "player": 0, "position": Vector2(10.0, 20.0)}), "the other guest sees the host") check(seen.has({"type": "figure", "player": 1, "position": Vector2(5.5, 6.5)}), "and the first guest, passed on by the host") check(not notices[guest].any(func(notice: Dictionary) -> bool: return notice["type"] == "figure" and notice["player"] == 1), "nobody gets their own figure back") _close([host, guest, other]) func test_a_guest_with_the_key_of_a_player_in_the_world_is_turned_away() -> void: _start_host() var twin := _start_guest(host_key, "Host") await _play([twin], 30) check_eq(_first(twin, "refused").get("reason"), GwSession.REFUSED_ALREADY_PLAYING, "the reason is given") check(_types(twin).has("host_lost"), "then the host closes the connection") check(not twin.session.is_running(), "the guest never got a world") _close([host, twin]) func test_a_guest_that_quits_leaves_the_world() -> void: _start_host() var guest := _start_guest(guest_key, "Grace") await _play([guest], 30) check_eq(host.session.online_players(), PackedInt64Array([0, 1]), "two players online") guest.link.close() await _play([], 10) check(_types(host).has("guest_left"), "the host sees the guest leave") check_eq(host.session.online_players(), PackedInt64Array([0]), "only the host is online") _close([host]) func test_a_guest_learns_when_the_host_is_gone() -> void: _start_host() var guest := _start_guest(guest_key, "Grace") await _play([guest], 30) host.link.close() await _play([guest], 10, false) check(_types(guest).has("host_lost"), "the guest notices") _close([guest])