Added Glitch Room Source code to Gitty

This commit is contained in:
Jeff Luke committed 2026-10-04 11:31:17 -07:00
commit 1ac9eb5644
505 files changed
+28504

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
class_name GameEvents
const STARTED: StringName = &"game_started"
const WON: StringName = &"game_won"
const LOST: StringName = &"game_lost"
const RESET: StringName = &"game_reset"
+1
View File
@@ -0,0 +1 @@
uid://cs0xs0amg3lrv
+81
View File
@@ -0,0 +1,81 @@
class_name GameHost
extends Node
signal game_loaded(game: ArcadeGame)
signal game_unloaded
var current_game: ArcadeGame
func load_game(game_definition: GameDefinition) -> ArcadeGame:
unload_game()
if game_definition == null:
push_error("GameHost received a null GameDefinition.")
return null
if game_definition.scene == null:
push_error(
"Game '%s' has no scene assigned."
% game_definition.game_id
)
return null
var instance := game_definition.scene.instantiate()
if not instance is ArcadeGame:
push_error(
"Game scene '%s' does not inherit from ArcadeGame."
% game_definition.game_id
)
instance.queue_free()
return null
current_game = instance as ArcadeGame
add_child(current_game)
current_game.game_started.connect(_on_game_started)
current_game.game_won.connect(_on_game_won)
current_game.game_lost.connect(_on_game_lost)
current_game.game_reset.connect(_on_game_reset)
game_loaded.emit(current_game)
print(
"[GAME HOST] Loaded '%s'"
% game_definition.display_name
)
return current_game
func unload_game() -> void:
if current_game == null:
return
print("[GAME HOST] Unloading current game.")
current_game.queue_free()
current_game = null
game_unloaded.emit()
func _on_game_started() -> void:
RoomRuntime.fire_game_event(GameEvents.STARTED)
func _on_game_won() -> void:
RoomRuntime.fire_game_event(GameEvents.WON)
func _on_game_lost() -> void:
RoomRuntime.fire_game_event(GameEvents.LOST)
func _on_game_reset() -> void:
RoomRuntime.fire_game_event(GameEvents.RESET)
+1
View File
@@ -0,0 +1 @@
uid://cp6novpbgl1uk
+77
View File
@@ -0,0 +1,77 @@
extends Node
signal action_activated(cabinet_id: StringName, action_id: StringName, context: Dictionary)
signal action_deactivated(cabinet_id: StringName, action_id: StringName, context: Dictionary)
signal status_changed(message: String)
var active_cabinet: CabinetProfile
var real_gpio := false
var status := "GPIO simulation (use --gpio=real on the Pi)"
var client: Node
var generations: Dictionary = {}
func _ready() -> void:
real_gpio = "--gpio=real" in OS.get_cmdline_user_args()
if real_gpio:
client = preload("res://core/runtime/pi_gpio_client.gd").new()
client.status_changed.connect(_status)
add_child(client)
_status("GPIO real mode — local Pi bridge required")
if "--gpio-test" in OS.get_cmdline_user_args():
test_led.call_deferred()
func _status(message: String) -> void:
status = message
print("[GPIO] ", message)
status_changed.emit(message)
func configure(cabinet: CabinetProfile) -> void:
active_cabinet = cabinet
func trigger(action_id: StringName, context: Dictionary = {}) -> void:
if active_cabinet == null:
_status("No active cabinet for GPIO action")
return
trigger_for_cabinet(active_cabinet, action_id, context)
func trigger_for_cabinet(cabinet: CabinetProfile, action_id: StringName, context: Dictionary = {}) -> void:
if cabinet == null or cabinet.hardware_config == null:
_status("Missing cabinet hardware configuration")
return
var action := cabinet.hardware_config.get_action(action_id)
if action == null:
_status("Unknown hardware action: %s" % action_id)
return
_send_action(cabinet, action, context)
func _send_action(cabinet: CabinetProfile, action: HardwareAction, context: Dictionary) -> void:
var key := "%s:%s" % [cabinet.hardware_config.controller_id, action.gpio_pin]
var generation := int(generations.get(key, 0)) + 1
generations[key] = generation
var pulse := action.action_type == HardwareAction.ActionType.PULSE_OUTPUT
var active := true if pulse else bool(context.get("active", true))
if real_gpio:
var ok: bool = await client.send({
"controller": str(cabinet.hardware_config.controller_id),
"pin": action.gpio_pin,
"active_high": action.active_high,
"mode": "pulse" if pulse else "set",
"duration_ms": action.pulse_duration_ms,
"active": active,
})
if not ok: return
else:
print("[MOCK GPIO] %s cabinet=%s action=%s BCM=%d" % ["PULSE" if pulse else "SET", cabinet.cabinet_id, action.action_id, action.gpio_pin])
if active:
action_activated.emit(cabinet.cabinet_id, action.action_id, context)
else:
action_deactivated.emit(cabinet.cabinet_id, action.action_id, context)
if pulse:
await get_tree().create_timer(action.pulse_duration_ms / 1000.0).timeout
if int(generations.get(key, 0)) == generation:
# Physical timing belongs to the bridge, even if the game exits mid-pulse.
action_deactivated.emit(cabinet.cabinet_id, action.action_id, context)
func test_led() -> void:
var cabinet = load("res://data/cabinets/cabinet_01.tres").duplicate(true)
var action: HardwareAction = cabinet.hardware_config.get_action(&"coin_release")
action.pulse_duration_ms = 2000
_send_action(cabinet, action, {"source": "gpio_led_test"})
+1
View File
@@ -0,0 +1 @@
uid://5d4k46jwlo4y
+48
View File
@@ -0,0 +1,48 @@
class_name LaunchConfig
var cabinet_id: StringName
var scenario_id: StringName
func _init(
default_cabinet: StringName = &"",
default_scenario: StringName = &""
) -> void:
cabinet_id = default_cabinet
scenario_id = default_scenario
static func from_command_line(
default_cabinet: StringName = &"",
default_scenario: StringName = &""
) -> LaunchConfig:
var config := LaunchConfig.new(
default_cabinet,
default_scenario
)
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--cabinet="):
config.cabinet_id = StringName(
arg.trim_prefix("--cabinet=")
)
elif arg.begins_with("--scenario="):
config.scenario_id = StringName(
arg.trim_prefix("--scenario=")
)
return config
func print_debug() -> void:
print("")
print("========= LAUNCH CONFIG =========")
print("Cabinet: ", cabinet_id)
print("Scenario: ", scenario_id)
print("=================================")
print("")
+1
View File
@@ -0,0 +1 @@
uid://brd536grsx38f
+19
View File
@@ -0,0 +1,19 @@
class_name LogicPathId
static func make(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName
) -> StringName:
return StringName(
"%s::%s::%s::%s"
% [
cabinet_id,
event_id,
action_id,
device_id
]
)
+1
View File
@@ -0,0 +1 @@
uid://cy746kl1sgsrn
+58
View File
@@ -0,0 +1,58 @@
extends Node
signal completed(id: int, ok: bool)
signal status_changed(message: String)
const URL := "http://127.0.0.1:8765"
var pending: Array[Dictionary] = []
var serial := 0
var busy := false
var heartbeat: HTTPRequest
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
heartbeat = HTTPRequest.new()
heartbeat.timeout = 1.0
add_child(heartbeat)
var timer := Timer.new()
timer.wait_time = 1.0
timer.timeout.connect(_heartbeat)
add_child(timer)
timer.start()
func _heartbeat() -> void:
if heartbeat.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED:
heartbeat.request(URL + "/heartbeat", [], HTTPClient.METHOD_POST, "{}")
func send(payload: Dictionary) -> bool:
if pending.size() >= 32:
status_changed.emit("GPIO output queue is full")
return false
serial += 1
var id := serial
pending.append({"id": id, "payload": payload})
_pump.call_deferred()
while true:
var result: Array = await completed
if result[0] == id: return result[1]
return false
func _pump() -> void:
if busy or pending.is_empty(): return
busy = true
var item: Dictionary = pending[0]
var request := HTTPRequest.new()
request.timeout = 2.0
add_child(request)
var ok := false
var message := "GPIO bridge unavailable — start pi_gpio_bridge.py on this Pi"
var error := request.request(URL + "/output", ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(item.payload))
if error == OK:
var response: Array = await request.request_completed
var body = JSON.parse_string(response[3].get_string_from_utf8())
ok = response[0] == HTTPRequest.RESULT_SUCCESS and response[1] == 200 and body is Dictionary and body.get("ok", false) == true
if body is Dictionary and body.has("error"): message = str(body.error)
request.queue_free()
pending.pop_front()
busy = false
status_changed.emit("GPIO %s accepted by Pi bridge" % item.payload.pin if ok else message)
completed.emit(item.id, ok)
_pump.call_deferred()
+1
View File
@@ -0,0 +1 @@
uid://74k3b8polfi2
+50
View File
@@ -0,0 +1,50 @@
@tool
class_name ProjectDatabase
extends Resource
@export var scenarios: Array[ScenarioDefinition] = []
@export var cabinets: Array[CabinetProfile] = []
@export var games: Array[GameDefinition] = []
func get_scenario(
scenario_id: StringName
) -> ScenarioDefinition:
for scenario in scenarios:
if scenario == null:
continue
if scenario.scenario_id == scenario_id:
return scenario
return null
func get_cabinet(
cabinet_id: StringName
) -> CabinetProfile:
for cabinet in cabinets:
if cabinet == null:
continue
if cabinet.cabinet_id == cabinet_id:
return cabinet
return null
func get_game(
game_id: StringName
) -> GameDefinition:
for game in games:
if game == null:
continue
if game.game_id == game_id:
return game
return null
+1
View File
@@ -0,0 +1 @@
uid://diqnrxrhwx2ug
+315
View File
@@ -0,0 +1,315 @@
class_name RoomController
extends Node
signal scenario_loaded(scenario: ScenarioDefinition)
signal validation_completed(
result: ValidationResult
)
signal cabinet_status_changed(
cabinet_id: StringName,
status: StringName
)
signal room_status_changed(
status: StringName
)
var database: ProjectDatabase
var active_scenario: ScenarioDefinition
var cabinet_statuses: Dictionary = {}
var room: RoomDefinition
func configure(
project_database: ProjectDatabase,
room_definition: RoomDefinition = null
) -> void:
database = project_database
room = room_definition
func load_scenario(
scenario_id: StringName
) -> bool:
if database == null:
push_error(
"RoomController has no ProjectDatabase."
)
return false
var scenario := database.get_scenario(
scenario_id
)
if scenario == null:
push_error(
"Scenario '%s' not found."
% scenario_id
)
return false
active_scenario = scenario
_initialize_cabinet_statuses()
print("")
print("======= ROOM CONTROLLER =======")
print(
"Loaded scenario: %s v%d"
% [
scenario.display_name,
scenario.version
]
)
print("===============================")
print("")
scenario_loaded.emit(
active_scenario
)
room_status_changed.emit(
&"LOADED"
)
return true
func validate_active_scenario() -> ValidationResult:
var result := ValidationResult.new()
result.merge(
ScenarioValidator.validate(
active_scenario
)
)
if room != null:
result.merge(
RoomValidator.validate(
room,
database
)
)
validation_completed.emit(
result
)
if result.is_valid():
room_status_changed.emit(
&"VALID"
)
else:
room_status_changed.emit(
&"INVALID"
)
return result
func prep_room() -> bool:
if active_scenario == null:
push_error(
"Cannot prep room: no scenario loaded."
)
return false
var validation := validate_active_scenario()
if not validation.is_valid():
print(
"[ROOM CONTROLLER] Prep aborted: "
+ validation.get_summary()
)
room_status_changed.emit(
&"PREP_FAILED"
)
return false
room_status_changed.emit(
&"PREPARING"
)
print("")
print("=========== PREP ROOM ===========")
for assignment in active_scenario.assignments:
_prep_assignment(assignment)
print("=================================")
print("")
room_status_changed.emit(
&"READY"
)
return true
func _prep_assignment(
assignment: ScenarioAssignment
) -> void:
var cabinet := assignment.cabinet
var game := assignment.game
_set_cabinet_status(
cabinet.cabinet_id,
&"PREPARING"
)
print(
"[ROOM CONTROLLER] Cabinet '%s'"
% cabinet.cabinet_id
)
print(
" Game: %s"
% game.game_id
)
print(
" Variant: %s"
% assignment.variant
)
print(
" Hardware: %s"
% cabinet.hardware_config.controller_id
)
#
# Networking will eventually happen here.
#
# send_to_cabinet(
# cabinet.cabinet_id,
# scenario,
# assignment
# )
#
print(
" [SIMULATION] Cabinet ready."
)
_set_cabinet_status(
cabinet.cabinet_id,
&"READY"
)
func _initialize_cabinet_statuses() -> void:
cabinet_statuses.clear()
if active_scenario == null:
return
for assignment in active_scenario.assignments:
if assignment == null:
continue
if assignment.cabinet == null:
continue
_set_cabinet_status(
assignment.cabinet.cabinet_id,
&"NOT_PREPARED"
)
func _set_cabinet_status(
cabinet_id: StringName,
status: StringName
) -> void:
cabinet_statuses[cabinet_id] = status
cabinet_status_changed.emit(
cabinet_id,
status
)
func get_cabinet_status(
cabinet_id: StringName
) -> StringName:
return cabinet_statuses.get(
cabinet_id,
&"UNKNOWN"
)
func simulate_game_event(
cabinet_id: StringName,
event_name: StringName
) -> void:
if active_scenario == null:
push_error(
"Cannot simulate event: no active scenario."
)
return
var assignment := (
active_scenario
.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
push_error(
"No assignment exists for cabinet '%s'."
% cabinet_id
)
return
print("")
print(
"[ROOM SIMULATION] %s -> %s"
% [
cabinet_id,
event_name
]
)
var actions := (
assignment
.get_actions_for_event(
event_name
)
)
if actions.is_empty():
print(
"[ROOM SIMULATION] No actions mapped."
)
return
for action_id in actions:
print(
"[ROOM SIMULATION] %s -> %s"
% [
event_name,
action_id
]
)
HardwareManager.trigger_for_cabinet(
assignment.cabinet,
action_id,
{
"game_event": event_name
}
)
+1
View File
@@ -0,0 +1 @@
uid://dbe24sbmbh6tt
+69
View File
@@ -0,0 +1,69 @@
extends Node
signal game_event_fired(
event_name: StringName,
cabinet_id: StringName
)
var active_assignment: ScenarioAssignment
func configure(assignment: ScenarioAssignment) -> void:
if assignment == null:
push_error("Cannot configure RoomRuntime with null assignment.")
return
active_assignment = assignment
HardwareManager.configure(assignment.cabinet)
print("")
print("======= ROOM RUNTIME =======")
print("Cabinet: ", assignment.cabinet.display_name)
print("Game: ", assignment.game.display_name)
print("Variant: ", assignment.variant)
print("============================")
print("")
func fire_game_event(event_name: StringName) -> void:
if active_assignment == null:
push_error("RoomRuntime has no active assignment.")
return
print("")
print(
"[GAME EVENT] %s -> %s"
% [
active_assignment.game.game_id,
event_name
]
)
game_event_fired.emit(
event_name,
active_assignment.cabinet.cabinet_id
)
var actions := active_assignment.get_actions_for_event(
event_name
)
if actions.is_empty():
print("[SCENARIO] No hardware actions mapped.")
return
for action_id in actions:
print(
"[SCENARIO] %s -> %s"
% [event_name, action_id]
)
HardwareManager.trigger(
action_id,
{
"game_event": event_name
}
)
+1
View File
@@ -0,0 +1 @@
uid://eelj57byt0su
+223
View File
@@ -0,0 +1,223 @@
class_name RoomValidator
static func validate(
room: RoomDefinition,
database: ProjectDatabase
) -> ValidationResult:
var result := ValidationResult.new()
if room == null:
result.add_error(
"RoomDefinition is null."
)
return result
_validate_cabinet_placements(
room,
database,
result
)
_validate_devices(
room,
result
)
_validate_bindings(
room,
database,
result
)
return result
static func _validate_cabinet_placements(
room: RoomDefinition,
database: ProjectDatabase,
result: ValidationResult
) -> void:
var seen_ids: Dictionary = {}
for placement in room.cabinet_placements:
if placement == null:
result.add_warning(
"Room contains a null cabinet placement."
)
continue
var cabinet_id := placement.cabinet_id
if cabinet_id == &"":
result.add_error(
"Room contains a cabinet placement with no cabinet_id."
)
continue
if seen_ids.has(cabinet_id):
result.add_error(
"Cabinet '%s' is placed in the room more than once."
% cabinet_id
)
else:
seen_ids[cabinet_id] = true
if database != null:
if database.get_cabinet(
cabinet_id
) == null:
result.add_error(
"Room references unknown cabinet '%s'."
% cabinet_id
)
static func _validate_devices(
room: RoomDefinition,
result: ValidationResult
) -> void:
var seen_ids: Dictionary = {}
for device in room.devices:
if device == null:
result.add_warning(
"Room contains a null device."
)
continue
if device.device_id == &"":
result.add_error(
"Room contains a device with no device_id."
)
continue
if seen_ids.has(
device.device_id
):
result.add_error(
"Room device '%s' appears more than once."
% device.device_id
)
else:
seen_ids[
device.device_id
] = true
static func _validate_bindings(
room: RoomDefinition,
database: ProjectDatabase,
result: ValidationResult
) -> void:
var seen_bindings: Dictionary = {}
for binding in room.bindings:
if binding == null:
result.add_warning(
"Room contains a null device binding."
)
continue
var cabinet_id := binding.cabinet_id
var action_id := binding.hardware_action_id
var device_id := binding.device_id
if cabinet_id == &"":
result.add_error(
"A room binding has no cabinet_id."
)
continue
if action_id == &"":
result.add_error(
"A room binding for cabinet '%s' has no hardware action."
% cabinet_id
)
continue
if device_id == &"":
result.add_error(
"A room binding for cabinet '%s' has no device_id."
% cabinet_id
)
continue
if room.get_cabinet_placement(
cabinet_id
) == null:
result.add_error(
(
"Binding references cabinet '%s', "
+ "but that cabinet is not placed in the room."
)
% cabinet_id
)
if room.get_device(
device_id
) == null:
result.add_error(
"Binding references unknown room device '%s'."
% device_id
)
var cabinet: CabinetProfile = null
if database != null:
cabinet = database.get_cabinet(
cabinet_id
)
if cabinet == null:
result.add_error(
"Binding references unknown cabinet '%s'."
% cabinet_id
)
else:
if cabinet.hardware_config == null:
result.add_error(
"Cabinet '%s' has no HardwareConfig."
% cabinet_id
)
elif not cabinet.hardware_config.has_action(
action_id
):
result.add_error(
(
"Room binding references unknown hardware action '%s' "
+ "on cabinet '%s'."
)
% [
action_id,
cabinet_id
]
)
var binding_key := (
"%s::%s::%s"
% [
cabinet_id,
action_id,
device_id
]
)
if seen_bindings.has(
binding_key
):
result.add_warning(
"Duplicate room binding '%s'."
% binding_key
)
else:
seen_bindings[
binding_key
] = true
+1
View File
@@ -0,0 +1 @@
uid://bbu1ejey4q6vx
+211
View File
@@ -0,0 +1,211 @@
class_name ScenarioValidator
static func validate(
scenario: ScenarioDefinition
) -> ValidationResult:
var result := ValidationResult.new()
if scenario == null:
result.add_error("Scenario is null.")
return result
if scenario.assignments.is_empty():
result.add_error(
"Scenario has no cabinet assignments."
)
return result
var seen_cabinets: Dictionary = {}
for assignment in scenario.assignments:
_validate_assignment(
assignment,
seen_cabinets,
result
)
return result
static func _validate_assignment(
assignment: ScenarioAssignment,
seen_cabinets: Dictionary,
result: ValidationResult
) -> void:
if assignment == null:
result.add_error(
"Scenario contains a null assignment."
)
return
if assignment.cabinet == null:
result.add_error(
"An assignment has no cabinet."
)
return
var cabinet := assignment.cabinet
var cabinet_id := cabinet.cabinet_id
if cabinet_id == &"":
result.add_error(
"A cabinet has no cabinet_id."
)
if seen_cabinets.has(cabinet_id):
result.add_error(
"Cabinet '%s' appears more than once."
% cabinet_id
)
else:
seen_cabinets[cabinet_id] = true
if assignment.game == null:
result.add_error(
"Cabinet '%s' has no game assigned."
% cabinet_id
)
return
var game := assignment.game
if game.game_id == &"":
result.add_error(
"Cabinet '%s' has a game with no game_id."
% cabinet_id
)
if game.scene == null:
result.add_error(
"Game '%s' has no scene assigned."
% game.game_id
)
if not game.can_run_on(cabinet):
result.add_error(
"Game '%s' cannot run on cabinet '%s'."
% [
game.game_id,
cabinet_id
]
)
if cabinet.hardware_config == null:
result.add_error(
"Cabinet '%s' has no HardwareConfig."
% cabinet_id
)
return
_validate_event_mappings(
assignment,
result
)
static func _validate_event_mappings(
assignment: ScenarioAssignment,
result: ValidationResult
) -> void:
var cabinet := assignment.cabinet
var cabinet_id := cabinet.cabinet_id
var hardware := cabinet.hardware_config
var game:= assignment.game
_validate_game_events(
game,
result
)
for mapping in assignment.event_mappings:
if mapping == null:
result.add_warning(
"Cabinet '%s' contains a null event mapping."
% cabinet_id
)
continue
if mapping.game_event == &"":
result.add_warning(
"Cabinet '%s' has a mapping with no game event."
% cabinet_id
)
else:
if not game.has_event(
mapping.game_event
):
result.add_error(
(
"Game '%s' does not define event '%s' "
+ "used by cabinet '%s'."
)
% [
game.game_id,
mapping.game_event,
cabinet_id
]
)
if mapping.hardware_actions.is_empty():
result.add_warning(
"Event '%s' on cabinet '%s' has no actions."
% [
mapping.game_event,
cabinet_id
]
)
for action_id in mapping.hardware_actions:
if not hardware.has_action(action_id):
result.add_error(
"Cabinet '%s': event '%s' references unknown hardware action '%s'."
% [
cabinet_id,
mapping.game_event,
action_id
]
)
static func _validate_game_events(
game: GameDefinition,
result: ValidationResult
) -> void:
var seen_events: Dictionary = {}
for event in game.events:
if event == null:
result.add_warning(
"Game '%s' contains a null event definition."
% game.game_id
)
continue
if event.event_id == &"":
result.add_error(
"Game '%s' contains an event with no event_id."
% game.game_id
)
continue
if seen_events.has(
event.event_id
):
result.add_error(
"Game '%s' defines event '%s' more than once."
% [
game.game_id,
event.event_id
]
)
else:
seen_events[
event.event_id
] = true
+1
View File
@@ -0,0 +1 @@
uid://dhcgaiviprvg8
+53
View File
@@ -0,0 +1,53 @@
@tool
class_name ValidationResult
extends RefCounted
var errors: Array[String] = []
var warnings: Array[String] = []
func add_error(message: String) -> void:
errors.append(message)
func add_warning(message: String) -> void:
warnings.append(message)
func is_valid() -> bool:
return errors.is_empty()
func has_warnings() -> bool:
return not warnings.is_empty()
func get_summary() -> String:
if not is_valid():
return "%d error(s), %d warning(s)" % [
errors.size(),
warnings.size()
]
if has_warnings():
return "Valid with %d warning(s)" % warnings.size()
return "Valid"
func merge(
other: ValidationResult
) -> void:
if other == null:
return
errors.append_array(
other.errors
)
warnings.append_array(
other.warnings
)
var result := ValidationResult.new()
+1
View File
@@ -0,0 +1 @@
uid://bey4rq671pjlp