OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_orphan_players.gd

79 lines · gd ref:
extends SceneTree
## Gate: player nodes belonging to no live peer are reaped.
##
## The bug, measured from a real ashatron join: on_game_started spawns the local
## player while the OfflineMultiplayerPeer is still active, so get_unique_id()
## returns 1 and the node is named "1". The mesh then assigns a real id and
## ensure_all_spawned spawns a SECOND local player, orphaning the first -- which
## keeps its two MultiplayerSynchronizers and is still iterated by
## _update_interest, producing set_visibility_for() calls for a peer that does not
## exist.
##
## Tests _reap_orphan_players directly against a stub players_node: instancing the
## real Game scene would pull in maps, a lobby camera and the whole autoload chain
## to prove a rule about which children survive.

var ok := true
var _ran := false


func _p(c: bool, m: String) -> void:
	print(("ok   " if c else "FAIL: ") + m)
	if not c:
		ok = false


func _process(_delta: float) -> bool:
	if _ran:
		return true
	_ran = true
	_run()
	print("GATE orphan_players: %s" % ("PASS" if ok else "FAIL"))
	return true


func _run() -> void:
	var gs: GDScript = load("res://scenes/game/game.gd")
	if gs == null:
		_p(false, "game.gd compiled")
		return
	var game: Node = gs.new()
	# IN the tree, because Node.multiplayer only resolves for a node inside it and
	# the reaper asks it for get_peers(). Processing is switched off immediately:
	# game.gd's _ready and _process expect a full scene (maps_node, peer_label, a
	# lobby camera) and would otherwise emit a stream of unrelated null errors that
	# have nothing to do with the rule under test.
	root.add_child(game)
	game.set_process(false)
	game.set_physics_process(false)
	# Node3D, NOT Node: players_node is declared `@onready var players_node: Node3D`
	# and a typed assignment through set() silently fails on a mismatch -- the stub
	# was simply never installed and the reaper ran against null.
	var players := Node3D.new()
	players.name = "Players"
	root.add_child(players)
	game.set("players_node", players)
	_p(game.get("players_node") == players, "players_node injected")

	for n: String in ["1", "576519484", "1597707962"]:
		var p := Node3D.new()
		p.name = n
		players.add_child(p)
	_p(players.get_child_count() == 3, "three player nodes to start")

	# Our id is 576519484 and no peers are connected in a gate, so BOTH the stale
    # offline-peer node and the remote one are orphans here. The rule under test is
	# "belongs to a live peer", not "is named 1".
	game._reap_orphan_players(576519484)

	# queue_free is DEFERRED, so absence cannot be asserted in the same frame --
	# is_queued_for_deletion() is the in-frame truth and is what the reaper's
	# callers effectively see next frame.
	var mine: Node = players.get_node_or_null(^"576519484")
	var orphan: Node = players.get_node_or_null(^"1")
	_p(mine != null and not mine.is_queued_for_deletion(), "our own player survives")
	_p(orphan != null and orphan.is_queued_for_deletion(), "the offline-peer orphan is reaped")

	players.queue_free()
	game.queue_free()