OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_poll_no_reemit.gd

194 lines · gd ref:
extends SceneTree
## Gate: the 3 s room poll re-emits NOTHING that did not change.
##
## hyper-games #88 Tier 1 #1. KeetGameState._fetch_all ended every poll with
##
##     state_loaded.emit(room_id, entries)      # ALL entries, changed or not
##
## and GameState._on_durable_loaded answers that with state_changed for EVERY key.
## Downstream that is not idle chatter:
##
##   TerrainEditManager  every edit but the newest has seq < _max_applied_seq, so
##                       each one requests a re-fold -> clear_edits() and a replay
##                       of the WHOLE journal, every edited chunk remeshed
##   LevelPersistence    any world:level: key sets _dirty -> the level refolds
##
## every 3 s, on every peer, in a room. The first fetch after watch() had the
## opposite fault too: each key went out once as state_updated (stored and emitted
## one at a time, in Dictionary order) and then again in the bulk state_loaded --
## the order #77 item 5 made atomic in _on_durable_loaded, undone one signal early.
##
## The observable is GameState.state_changed, the bus every subsystem listens to,
## driven through the real KeetGameState -> GameState wiring with a fake daemon.

var ok := true
var _ran := false

const MAP := "gate_poll_map"
const ROOM := "gate-poll-room"


class FakeClient extends KeetUdsClient:
	## What state.list answers with: stored key -> var_to_str value, as the daemon
	## returns it (v3 flat shape).
	var entries: Dictionary = {}
	## hold = true: a state.list SNAPSHOTS `entries` now but answers only on
	## release() -- the reply of a poll that was in flight while other RPCs landed.
	var hold := false
	var _held: Array = []

	func call_rpc(method: String, params: Dictionary = {}, callback: Callable = Callable(), _timeout_sec: float = -1.0) -> void:
		if not callback.is_valid():
			return
		if method == "state.list":
			var snap := {"entries": entries.duplicate()}
			if hold:
				_held.append([callback, snap])
			else:
				callback.call(snap, null)
		elif method == "state.set":
			entries[String(params["key"])] = String(params["value"])
			callback.call({}, null)
		elif method == "state.del":
			entries.erase(String(params["key"]))
			callback.call({}, null)
		else:
			callback.call(null, "not faked")

	func release() -> void:
		hold = false
		var h := _held
		_held = []
		for c: Array in h:
			(c[0] as Callable).call(c[1], null)


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 poll_no_reemit: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)
	return true


var _changed: Array = []


func _run() -> void:
	var gs: Node = root.get_node_or_null("GameState")
	_p(gs != null, "GameState autoload present")
	if gs == null:
		return
	# game.gd installs the routes; a --script gate never loads it (see
	# gate_durable_load_order for what happens without this).
	gs.install_routes()
	gs.current_map_id = MAP

	var fake := FakeClient.new()
	fake.is_connected = true
	var e1 := {"pos": [1.0, 2.0, 3.0], "radius": 1.5, "str": -1.0, "type": "sphere", "op": 1}
	var e2 := {"pos": [4.0, 5.0, 6.0], "radius": 2.0, "str": -1.0, "type": "sphere", "op": 1}
	var k1 := "map:%s:world:terrain:edit:alice:100" % MAP
	var k2 := "map:%s:world:terrain:edit:alice:101" % MAP
	fake.entries = {k1: var_to_str(e1), k2: var_to_str(e2)}

	var kgs := KeetGameState.new()
	kgs.auto_poll = false          # the gate drives _poll_state itself
	kgs.client = fake
	root.add_child(kgs)            # _ready builds the poll timer + overlay
	kgs.state_loaded.connect(gs._on_durable_loaded)
	kgs.state_updated.connect(gs._on_durable_updated)
	gs.state_changed.connect(func(key: String, value: Variant, _ch: int) -> void:
		if key.begins_with("world:terrain:edit:"):
			_changed.append([key, value]))

	# 1. The first fetch after watch(): every key once, as one atomic load.
	_changed.clear()
	kgs.watch(ROOM)
	_p(_changed.size() == 2,
		"initial load emits each of the 2 keys exactly once (got %d)" % _changed.size())
	_p(bool(gs.room_state_loaded), "and marks the room loaded")

	# 2. A poll where nothing changed: silence.
	_changed.clear()
	kgs._poll_state()
	_p(_changed.is_empty(),
		"an UNCHANGED poll emits nothing (got %d state_changed)" % _changed.size())

	# 3. A poll with one new key and one removed key: exactly those two.
	var e3 := {"pos": [7.0, 8.0, 9.0], "radius": 1.0, "str": -1.0, "type": "sphere", "op": 1}
	var k3 := "map:%s:world:terrain:edit:bob:102" % MAP
	fake.entries = {k1: var_to_str(e1), k3: var_to_str(e3)}
	_changed.clear()
	kgs._poll_state()
	var keys: Array = []
	for c: Array in _changed:
		keys.append(c[0])
	_p(_changed.size() == 2, "a poll with 1 added + 1 removed emits exactly 2 (got %d: %s)"
		% [_changed.size(), str(keys)])
	_p(keys.has("world:terrain:edit:bob:102"), "the added key is emitted")
	var removed_null := false
	for c: Array in _changed:
		if c[0] == "world:terrain:edit:alice:101" and c[1] == null:
			removed_null = true
	_p(removed_null, "the removed key is emitted as null")

	# 4. A poll IN FLIGHT while a local write lands (hyper-games #108, 2026-10-07).
	#    poll sent -> carve written + confirmed -> the poll's snapshot, taken BEFORE
	#    the write, arrives without the key. That is not a deletion. Reported as one
	#    (null), TerrainEditManager re-folded: clear_edits + a full terrain rebuild +
	#    replay of the whole journal, after the player's own carve (4 in one session).
	var e4 := {"pos": [1.0, 1.0, 1.0], "radius": 3.0, "str": -3.0, "type": "sphere", "op": 1}
	var k4 := "map:%s:world:terrain:edit:alice:103" % MAP
	fake.hold = true
	kgs._poll_state()                         # snapshot WITHOUT k4, answer held
	kgs.set_value(k4, e4)                     # the carve: written and confirmed
	_changed.clear()
	fake.release()                            # the stale answer arrives
	var nulled := false
	for c: Array in _changed:
		if c[0] == "world:terrain:edit:alice:103" and c[1] == null:
			nulled = true
	_p(not nulled, "a poll sent BEFORE a local write does not report that key erased (got %s)"
		% str(_changed))
	_changed.clear()
	kgs._poll_state()                         # a poll sent AFTER the write sees it
	_p(_changed.is_empty(), "and the next poll, which includes it, emits nothing (got %s)"
		% str(_changed))

	#    The mirror: a poll in flight while a local ERASE lands must not resurrect it.
	fake.hold = true
	kgs._poll_state()                         # snapshot WITH k4, answer held
	kgs.erase_value(k4)                       # undo: erased and confirmed
	_changed.clear()
	fake.release()
	var revived := false
	for c: Array in _changed:
		if c[0] == "world:terrain:edit:alice:103" and c[1] != null:
			revived = true
	_p(not revived, "a poll sent BEFORE a local erase does not bring the key back (got %s)"
		% str(_changed))
	#    A REAL remote delete still arrives: k1 gone from a poll sent after any write.
	fake.entries.erase(k1)
	_changed.clear()
	kgs._poll_state()
	var k1_null := false
	for c: Array in _changed:
		if c[0] == "world:terrain:edit:alice:100" and c[1] == null:
			k1_null = true
	_p(k1_null, "a key deleted elsewhere is still reported erased (got %s)" % str(_changed))

	# 5. A poll arriving for a room we no longer watch is dropped.
	kgs.watch("other-room")
	_changed.clear()
	kgs._fetch_all(ROOM)
	_p(_changed.is_empty(), "a stale fetch for a previous room emits nothing (got %d)" % _changed.size())