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()