Files
Void-Bastion/tests/online_integration_test.gd

187 lines
14 KiB
GDScript

extends SceneTree
var matched_a = null
var matched_b = null
var snapshot_a: Dictionary = {}
var snapshot_b: Dictionary = {}
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var nakama = root.get_node("Nakama")
var test_host := OS.get_environment("VOID_BASTION_TEST_HOST")
var test_scheme := OS.get_environment("VOID_BASTION_TEST_SCHEME")
var test_port_text := OS.get_environment("VOID_BASTION_TEST_PORT")
if test_host.is_empty():
test_host = "192.168.178.20"
if test_scheme.is_empty():
test_scheme = "http"
var test_port := int(test_port_text) if not test_port_text.is_empty() else 7350
var client_a: NakamaClient = nakama.create_client("voidbastion-public-v1", test_host, test_port, test_scheme, 5, NakamaLogger.LOG_LEVEL.ERROR)
var client_b: NakamaClient = nakama.create_client("voidbastion-public-v1", test_host, test_port, test_scheme, 5, NakamaLogger.LOG_LEVEL.ERROR)
var suffix := str(Time.get_ticks_usec())
var session_a: NakamaSession = await client_a.authenticate_device_async("voidbastion_godot_test_a_" + suffix, "godot_a_" + suffix, true)
var session_b: NakamaSession = await client_b.authenticate_device_async("voidbastion_godot_test_b_" + suffix, "godot_b_" + suffix, true)
assert(not session_a.is_exception() and session_a.is_valid(), "Godot-Client A muss ein Gastkonto anmelden können: %s" % session_a)
assert(not session_b.is_exception() and session_b.is_valid(), "Godot-Client B muss ein Gastkonto anmelden können: %s" % session_b)
var info = await client_a.rpc_async(session_a, "server_info", "{}")
assert(not info.is_exception(), "Die Godot-Anbindung muss Server-RPCs erreichen.")
var info_data = JSON.parse_string(info.payload)
assert(info_data is Dictionary and info_data.get("status") == "online", "Der Server muss seinen Onlinestatus melden.")
assert(info_data.get("version") == "0.27", "Der Server muss die aktuelle Runtime 0.27 geladen haben: %s" % info_data)
var deletion_client: NakamaClient = nakama.create_client("voidbastion-public-v1", test_host, test_port, test_scheme, 5, NakamaLogger.LOG_LEVEL.ERROR)
var deletion_session: NakamaSession = await deletion_client.authenticate_device_async("voidbastion_delete_test_" + suffix, "delete_test_" + suffix, true)
assert(not deletion_session.is_exception() and deletion_session.is_valid(), "Ein Wegwerf-Testkonto muss angelegt werden können.")
var renamed = await deletion_client.rpc_async(deletion_session, "update_display_name", JSON.stringify({"display_name": "Testpilot Ägir"}))
assert(not renamed.is_exception(), "Ein gültiger Spielername muss serverseitig gespeichert werden können.")
var renamed_data = JSON.parse_string(renamed.payload)
assert(renamed_data is Dictionary and bool(renamed_data.get("accepted", false)), "Der Server muss die Namensänderung bestätigen.")
var invalid_name = await deletion_client.rpc_async(deletion_session, "update_display_name", JSON.stringify({"display_name": "ADMIN"}))
assert(not invalid_name.is_exception(), "Ein ungültiger Spielername muss kontrolliert abgelehnt werden.")
var invalid_name_data = JSON.parse_string(invalid_name.payload)
assert(invalid_name_data is Dictionary and not bool(invalid_name_data.get("accepted", true)), "Reservierte Spielernamen dürfen nicht angenommen werden.")
var deleted = await deletion_client.rpc_async(deletion_session, "delete_account", "{}")
assert(not deleted.is_exception(), "Das Wegwerf-Testkonto muss im Spiel löschbar sein.")
var public_leaderboard_name := "Rangpilot %s" % suffix.right(6)
var renamed_a = await client_a.rpc_async(session_a, "update_display_name", JSON.stringify({"display_name": public_leaderboard_name}))
assert(not renamed_a.is_exception() and bool(JSON.parse_string(renamed_a.payload).get("accepted", false)), "Der Ranglistenpilot muss einen öffentlichen Namen setzen können.")
var run_payload := JSON.stringify({"map": 1, "difficulty": 1, "mode": "mission", "wave": 4, "score": 4700, "client_version": "0.25-integration"})
var submitted = await client_a.rpc_async(session_a, "submit_run", run_payload)
assert(not submitted.is_exception(), "Ein plausibles Ergebnis muss serverseitig angenommen werden.")
var records = await client_a.list_leaderboard_records_async(session_a, "mission_map1_difficulty1", [session_a.user_id], null, 10)
assert(not records.is_exception() and (records.records.size() >= 1 or records.owner_records.size() >= 1), "Das Ergebnis muss über das Godot-SDK aus der Rangliste lesbar sein.")
var own_record = null
for record in records.owner_records:
if record.owner_id == session_a.user_id:
own_record = record
break
assert(own_record != null and own_record.username == public_leaderboard_name, "Die Rangliste muss den öffentlichen Spielernamen statt der technischen Gast-ID zeigen.")
var socket_a: NakamaSocket = nakama.create_socket_from(client_a)
var socket_b: NakamaSocket = nakama.create_socket_from(client_b)
assert(not (await socket_a.connect_async(session_a, true, 5)).is_exception(), "Spieler A benötigt eine Echtzeitverbindung.")
assert(not (await socket_b.connect_async(session_b, true, 5)).is_exception(), "Spieler B benötigt eine Echtzeitverbindung.")
var friend_request = await client_a.add_friends_async(session_a, null, PackedStringArray([session_b.username]))
assert(not friend_request.is_exception(), "Spieler A muss eine Freundschaftsanfrage senden können.")
var friend_accept = await client_b.add_friends_async(session_b, PackedStringArray([session_a.user_id]))
assert(not friend_accept.is_exception(), "Spieler B muss die Freundschaftsanfrage bestätigen können.")
var friend_list = await client_a.list_friends_async(session_a, 0, 100)
assert(not friend_list.is_exception() and friend_list.friends.size() == 1 and friend_list.friends[0].user.id == session_b.user_id,
"Die bestätigte Freundschaft muss beim Gastgeber sichtbar sein.")
var received_invites: Array = []
socket_b.received_notification.connect(func(note):
if int(note.code) == 101:
received_invites.append(JSON.parse_string(note.content)))
socket_a.received_matchmaker_matched.connect(func(value): matched_a = value)
socket_b.received_matchmaker_matched.connect(func(value): matched_b = value)
socket_a.received_match_state.connect(func(value): _capture_snapshot(value, true))
socket_b.received_match_state.connect(func(value): _capture_snapshot(value, false))
var props := {"game": "void_bastion", "mode": "quick"}
var production_query := "+properties.game:void_bastion +properties.mode:quick +properties.difficulty:1"
var ticket_a = await socket_a.add_matchmaker_async(production_query, 2, 2, props, {"difficulty": 1.0})
var ticket_b = await socket_b.add_matchmaker_async(production_query, 2, 2, props, {"difficulty": 1.0})
assert(not ticket_a.is_exception() and not ticket_b.is_exception(), "Beide Spieler müssen die Mitspielersuche starten können.")
for attempt in 300:
if matched_a != null and matched_b != null:
break
await create_timer(0.05).timeout
assert(matched_a != null and matched_b != null, "Der Server muss zwei passende Spieler vermitteln.")
var match_a = await socket_a.join_matched_async(matched_a)
var match_b = await socket_b.join_matched_async(matched_b)
assert(not match_a.is_exception() and not match_b.is_exception(), "Beide Spieler müssen der autoritativen Koop-Partie beitreten können.")
assert(match_a.match_id == match_b.match_id, "Beide Spieler müssen dieselbe Koop-Partie erhalten.")
await create_timer(0.2).timeout
assert(not bool(snapshot_a.get("started", false)), "Die Partie darf vor der Bereitschaft beider Spieler nicht starten.")
await socket_a.send_match_state_async(match_a.match_id, 14, JSON.stringify({"ready": true, "name": "TESTPILOT ALPHA"}))
await socket_b.send_match_state_async(match_b.match_id, 14, JSON.stringify({"ready": true, "name": "TESTPILOT BETA"}))
for attempt in 100:
if bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)):
break
await create_timer(0.05).timeout
assert(bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)), "Beide Clients müssen den gestarteten Serverzustand empfangen.")
var slot_a := int((snapshot_a.get("slots", {}) as Dictionary).get(session_a.user_id, -1))
var slot_b := int((snapshot_b.get("slots", {}) as Dictionary).get(session_b.user_id, -1))
assert(slot_a != slot_b and slot_a in [0, 1] and slot_b in [0, 1], "Der Server muss beiden Spielern unterschiedliche Seiten zuweisen.")
var build_a := {"tower_type": 0, "x": 200.0 if slot_a == 0 else 2760.0, "y": 200.0}
var build_b := {"tower_type": 1, "x": 200.0 if slot_b == 0 else 2760.0, "y": 200.0}
await socket_a.send_match_state_async(match_a.match_id, 10, JSON.stringify(build_a))
await socket_b.send_match_state_async(match_b.match_id, 10, JSON.stringify(build_b))
await socket_a.send_match_state_async(match_a.match_id, 13, "{}")
for attempt in 120:
if int(snapshot_a.get("wave", 0)) == 1 and (snapshot_a.get("towers", []) as Array).size() == 2 and int(snapshot_b.get("wave", 0)) == 1:
break
await create_timer(0.05).timeout
assert(int(snapshot_a.get("wave", 0)) == 1, "Der Server muss die gemeinsame Welle starten.")
assert((snapshot_a.get("towers", []) as Array).size() == 2, "Beide autorisierten Bauaufträge müssen serverseitig sichtbar sein.")
assert(JSON.stringify(snapshot_a.get("towers", [])) == JSON.stringify(snapshot_b.get("towers", [])), "Beide Clients müssen denselben Turmzustand erhalten.")
assert(int(snapshot_a.get("reactor_lives", 0)) == int(snapshot_b.get("reactor_lives", -1)), "Beide Clients müssen dieselben gemeinsamen Reaktorleben sehen.")
assert(not (await socket_a.leave_match_async(match_a.match_id)).is_exception(), "Spieler A muss die schnelle Partie verlassen können.")
assert(not (await socket_b.leave_match_async(match_b.match_id)).is_exception(), "Spieler B muss die schnelle Partie verlassen können.")
await create_timer(0.2).timeout
snapshot_a = {}
snapshot_b = {}
var created_room = await client_a.rpc_async(session_a, "create_private_room", JSON.stringify({"difficulty": 2}))
assert(not created_room.is_exception(), "Spieler A muss einen privaten Raum erstellen können.")
var created_data = JSON.parse_string(created_room.payload)
assert(created_data is Dictionary, "Der private Raum muss gültige Daten liefern.")
var room_code := String(created_data.get("room_code", ""))
var private_match_id := String(created_data.get("match_id", ""))
assert(room_code.length() == 6 and not private_match_id.is_empty(), "Der Server muss einen sechsstelligen Raumcode und eine Partie liefern.")
var private_match_a = await socket_a.join_match_async(private_match_id)
assert(not private_match_a.is_exception(), "Der Gastgeber muss seinem privaten Raum beitreten können.")
var invite = await client_a.rpc_async(session_a, "invite_friend", JSON.stringify({"friend_id": session_b.user_id, "room_code": room_code}))
assert(not invite.is_exception() and bool(JSON.parse_string(invite.payload).get("accepted", false)), "Der Gastgeber muss einen bestätigten Freund einladen können.")
for attempt in 30:
if not received_invites.is_empty():
break
await create_timer(0.05).timeout
assert(not received_invites.is_empty() and received_invites[0] is Dictionary and String(received_invites[0].get("room_code", "")) == room_code,
"Die Einladung muss als Echtzeitbenachrichtigung mit dem richtigen Raumcode ankommen.")
var found_room = await client_b.rpc_async(session_b, "join_private_room", JSON.stringify({"room_code": room_code.to_lower()}))
assert(not found_room.is_exception(), "Der Mitspieler muss den Raumcode unabhängig von Groß-/Kleinschreibung finden können.")
var found_data = JSON.parse_string(found_room.payload)
assert(found_data is Dictionary and String(found_data.get("match_id", "")) == private_match_id, "Der Raumcode muss genau zur erstellten Partie führen: %s" % found_data)
var private_match_b = await socket_b.join_match_async(private_match_id)
assert(not private_match_b.is_exception(), "Der Mitspieler muss dem privaten Raum beitreten können.")
await create_timer(0.2).timeout
assert(not bool(snapshot_a.get("started", false)), "Auch private Räume müssen auf die Bereitschaft beider Spieler warten.")
var lobby_players: Array = snapshot_a.get("players", [])
assert(lobby_players.size() == 2 and lobby_players[0] is Dictionary and lobby_players[1] is Dictionary, "Die Lobby muss beide Spielerplätze anzeigen.")
assert(not (await socket_b.leave_match_async(private_match_id)).is_exception(), "Ein Mitspieler muss die Lobby verlassen können.")
await create_timer(0.2).timeout
lobby_players = snapshot_a.get("players", [])
assert(lobby_players.size() == 2 and not bool(lobby_players[1].get("connected", true)), "Die Lobby muss einen getrennten Mitspieler erkennen.")
private_match_b = await socket_b.join_match_async(private_match_id)
assert(not private_match_b.is_exception(), "Der reservierte Spieler muss demselben Raum wieder beitreten können.")
await socket_a.send_match_state_async(private_match_id, 14, JSON.stringify({"ready": true, "name": "TESTPILOT ALPHA"}))
await socket_b.send_match_state_async(private_match_id, 14, JSON.stringify({"ready": true, "name": "TESTPILOT BETA"}))
for attempt in 100:
if bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)):
break
await create_timer(0.05).timeout
assert(bool(snapshot_a.get("started", false)) and bool(snapshot_b.get("started", false)), "Der private Raum muss mit zwei Spielern die gemeinsame Partie starten.")
assert(int(snapshot_a.get("difficulty", -1)) == 2 and int(snapshot_b.get("difficulty", -1)) == 2, "Die private Partie muss den Schwierigkeitsgrad des Gastgebers übernehmen.")
assert(private_match_a.match_id == private_match_b.match_id, "Beide Spieler müssen über den Raumcode dieselbe private Partie erhalten.")
socket_a.close()
socket_b.close()
print("VOID_BASTION_ONLINE_INTEGRATION_OK")
quit()
func _capture_snapshot(message, client_a: bool) -> void:
if int(message.op_code) != 1:
return
var parsed = JSON.parse_string(message.data)
if not parsed is Dictionary:
return
if client_a:
snapshot_a = parsed
else:
snapshot_b = parsed