Files

144 lines
5.7 KiB
GDScript

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