OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_map_visibility.gd

137 lines · gd ref:
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()