extends Node # Explicit source-only experiment. Never connects to the production room controller. var server := TCPServer.new() var peers: Array[Dictionary] = [] var game: Node var session := "" var phase := "powered_off" var scans := 0 var last_credits := 0 var seen: Dictionary = {} var rfid_device := "cab1_rfid1" var inject_rfid := false func _ready() -> void: var args := OS.get_cmdline_user_args() if not "--glitch-lab" in args: push_error("This scene requires --glitch-lab") get_tree().quit(2) return inject_rfid = "--lab-inject-rfid" in args if inject_rfid and HardwareManager.real_gpio: push_error("Choose physical RFID or lab injection, never both") get_tree().quit(2) return for arg in args: if arg.begins_with("--lab-rfid-device="): rfid_device = arg.trim_prefix("--lab-rfid-device=") session = Crypto.new().generate_random_bytes(16).hex_encode() RoomRuntime.active_assignment = null HardwareManager.active_cabinet = null game = load("res://games/pacman/pacman.gd").new() # Set before _ready to prevent automatic boot. game.run_config = _config() add_child(game) game.game_started.connect(func(): phase = "playing") game.game_won.connect(func(): phase = "won") game.game_lost.connect(func(): phase = "lost") game.power_state_changed.connect(_power) reset() if server.listen(18768, "127.0.0.1") != OK: push_error("Godot lab port 18768 unavailable") get_tree().quit(2) return print("GLITCH GODOT LAB READY (no room controller or reward outputs)") func _config() -> Dictionary: return {"power_on_device": "__lab_operator_only__", "rfid_credit_device": rfid_device, "score_to_win": 500, "boards_to_win": 0, "starting_lives": 3} func _power(value: String) -> void: phase = {"off": "powered_off", "booting": "booting", "ready": "ready"}.get(value, value) func reset() -> void: game.configure(_config()) scans = 0 last_credits = 0 if inject_rfid: game.receive_device_feedback({"fresh": true, "events": [], "devices": []}) func feed_rfid(feedback: Dictionary) -> void: # Only RFID input is admitted. No button (including BCM23) can power this lab on. var filtered := feedback.duplicate(true) filtered["events"] = feedback.get("events", []).filter(func(e): return e.get("kind") == "rfid") game.receive_device_feedback(filtered) _sync_credit() func _sync_credit() -> void: if game.credits > last_credits: scans += 1 last_credits = game.credits func snapshot() -> Dictionary: _sync_credit() return {"session": session, "cabinet_id": "cabinet_01", "phase": phase, "credits": game.credits, "scans": scans, "simulated": false, "rfid_injection": inject_rfid, "score": game.model.score} func reply(ok: bool, duplicate: bool, message: String) -> Dictionary: return {"accepted": ok, "duplicate": duplicate, "message": message, "state": snapshot()} func execute(cmd: Dictionary) -> Dictionary: var action := str(cmd.get("action", "")) if action == "snapshot": return reply(true, false, "Real Godot cabinet state") var id := str(cmd.get("id", "")) if id.is_empty() or id.length() > 128 or cmd.get("session") != session: return reply(false, false, "Invalid command ID or stale session") if seen.has(id): var previous: Dictionary = seen[id] if previous.command != cmd: return reply(false, false, "Command ID reused with different contents") return reply(previous.accepted, true, previous.message) var now := int(Time.get_unix_time_from_system() * 1000) var expires := int(cmd.get("expires_ms", 0)) if expires <= now or expires - now > 30000: return reply(false, false, "Expired command") var ok := false match action: "power_on": if phase == "powered_off": ok = game.power_on() "start": if phase == "ready": ok = game.start_game() "scan": if inject_rfid and phase == "ready" and game.credits == 0: # Synthetic fixture passes through the real admission logic, never sets credits. game.receive_device_feedback({"fresh": true, "events": [], "devices": []}) feed_rfid({"fresh": true, "events": [{"id": id, "kind": "rfid", "device": rfid_device, "value": "LAB-CARD", "age_ms": 0}], "devices": [{"id": rfid_device, "kind": "rfid", "value": "LAB-CARD", "age_ms": 0}]}) ok = game.credits == 1 "reset": reset() ok = true var message := "Executed by Godot" if ok else "Action unavailable; scan a physical card or play to the score target" seen[id] = {"command": cmd.duplicate(true), "accepted": ok, "message": message} if seen.size() > 256: seen.erase(seen.keys()[0]) return reply(ok, false, message) func _process(_delta: float) -> void: if game == null: return if HardwareManager.real_gpio and HardwareManager.client != null: feed_rfid(HardwareManager.client.get_feedback()) while server.is_connection_available(): var peer := server.take_connection() if peers.size() >= 8: peer.disconnect_from_host() else: peers.append({"peer": peer, "data": "", "deadline": Time.get_ticks_msec() + 1800}) for item in peers.duplicate(): var peer: StreamPeerTCP = item.peer peer.poll() var available := peer.get_available_bytes() if available > 0: var packet := peer.get_data(mini(available, 8193)) if packet[0] == OK: item.data += packet[1].get_string_from_utf8() if item.data.length() > 8192 or Time.get_ticks_msec() > item.deadline or peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: peer.disconnect_from_host() peers.erase(item) elif "\n" in item.data: var cmd = JSON.parse_string(item.data.get_slice("\n", 0)) var result := execute(cmd) if cmd is Dictionary else reply(false, false, "Invalid JSON command") peer.put_data((JSON.stringify(result) + "\n").to_utf8_buffer()) peer.disconnect_from_host() peers.erase(item) func _exit_tree() -> void: server.stop() for item in peers: item.peer.disconnect_from_host()