extends SceneTree
## Gate: you only see the players who are in your map.
##
## Player nodes hang off Game, not off the map, so every peer's body existed in
## every map — a peer parked in map_flat was visible from map_test and followed
## you through a teleporter. Nothing filtered by map because change_map assumed
## the whole room moved together: it teleported EVERY player node to the
## destination spawn.
##
## That assumption does not hold, for a reason with no error attached.
## world:settings:current_map is read in exactly ONE place — _restore_saved_map,
## at game start — and game.gd subscribes to nothing, so a LIVE peer never
## follows a map change. Teleport and you diverge; the other peer stays where it
## is until it rejoins. Once two peers are genuinely in different maps, the
## one-map assumption that made filtering unnecessary is simply false.
##
## So a player is both a room member and map content: the node persists, and it
## is only VISIBLE where its peer actually is.
##
## ⚠ AN UNKNOWN MAP COUNTS AS SHARED. A peer running a build that does not
## publish player:map: would otherwise become invisible to everyone the day this
## ships, which is a worse failure than the one being fixed.
var ok := true
var _ran := false
const KEY := "player:map:"
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 map_visibility: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var gs_auto: Node = root.get_node_or_null("GameState")
_p(gs_auto != null, "GameState autoload present")
if gs_auto == null:
return
# ---- the route exists, room-global and realtime ----
var row: Dictionary = {}
for r: Dictionary in gs_auto.STATE_ROUTES:
if String(r.get("prefix", "")) == KEY:
row = r
break
_p(not row.is_empty(), "%s is routed" % KEY)
if row.is_empty():
return
_p(str(row.get("map_scope", "")) == "",
"%s is NOT map-scoped — it names a map, so scoping it to one is circular" % KEY)
_p(int(row.get("channel", -1)) == gs_auto.Channel.REALTIME,
"%s is realtime, like player:name: and player:pos:" % KEY)
# ---- the rule ----
var script: GDScript = load("res://scenes/game/game.gd")
_p(script != null, "game.gd compiles")
if script == null:
return
var game: Node = script.new()
# A "Maps" child BEFORE entering the tree. game.gd's _ready resolves
# @onready var maps_node := $Maps and iterates it on the headless branch, so a
# bare instance throws "get_children on a null value" the moment it is added.
# This removes that one; a bare instance still logs a null peer_label, which
# gate_orphan_players already explains — _ready expects a full scene. Those are
# noise, not the rule under test.
var maps := Node3D.new()
maps.name = "Maps"
game.add_child(maps)
root.add_child(game)
game.set_process(false)
game.set_physics_process(false)
# Node3D, not Node: players_node is typed, and a mismatched set() fails
# silently — gate_orphan_players found that the hard way.
var players := Node3D.new()
players.name = "Players"
root.add_child(players)
game.set("players_node", players)
var here := "map_flat"
var elsewhere := "map_test"
game.set("current_map_path", "res://scenes/maps/map_flat.tscn")
_p(game.has_method("_peer_shares_our_map"),
"game.gd exposes _peer_shares_our_map(peer_id)")
if not game.has_method("_peer_shares_our_map"):
return
gs_auto.set_state(KEY + "111", here)
gs_auto.set_state(KEY + "222", elsewhere)
_p(bool(game._peer_shares_our_map(111, here)), "a peer in our map shares it")
_p(not bool(game._peer_shares_our_map(222, here)), "a peer in another map does NOT")
_p(bool(game._peer_shares_our_map(333, here)),
"a peer that publishes nothing counts as shared, not invisible")
# ---- and the rule is applied to the nodes ----
for n: String in ["111", "222", "333"]:
var p := Node3D.new()
p.name = n
players.add_child(p)
_p(game.has_method("_update_map_visibility"), "game.gd exposes _update_map_visibility()")
if not game.has_method("_update_map_visibility"):
return
game._update_map_visibility()
var a: Node3D = players.get_node_or_null(^"111") as Node3D
var b: Node3D = players.get_node_or_null(^"222") as Node3D
var c: Node3D = players.get_node_or_null(^"333") as Node3D
_p(a != null and a.visible, "the peer in our map is visible")
_p(b != null and not b.visible, "the peer in another map is HIDDEN")
_p(c != null and c.visible, "the peer with no published map stays visible")
# SELF-CHECK: it must follow the state, not a fixed answer. Move 222 to our
# map and it must reappear; move 111 away and it must vanish.
gs_auto.set_state(KEY + "222", here)
gs_auto.set_state(KEY + "111", elsewhere)
game._update_map_visibility()
_p(b.visible, "self-check: a peer that joins our map becomes visible")
_p(not a.visible, "self-check: and one that leaves it is hidden")
game.free()
players.free()