144 lines
5.7 KiB
GDScript
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()
|