Files
room_glitch/core/scenarios/scenario_edit_session.gd
Jeff Luke c1b2195834 Expand Glitch operations hub, cabinet authoring, deployment, and RFID support
Operator hub
- Introduce Escape Gallery branding and Glitch: the Dark Machine room views
- Add cabinet illustrations, Pac-Man marquee/gameplay artwork, and waiting states
- Add device feedback panels with RFID readings, input/output states, and stale status
- Add logged operator hardware interventions with request deduplication

Room and cabinet authoring
- Add a visual custom-PCB connection editor with per-cabinet device inventory
- Support device IDs, bindings, wiring notes, input polarity, and validation
- Add physical-room label/dimension toggles and editable doorway annotations
- Preserve doorway changes through undo, save, and revert
- Expand cabinet profiles, room layout, and Glitch scenario resources

Scenarios and gameplay
- Add per-assignment game configuration and runtime feature validation
- Add timed test wins and Frogger individual-home objectives
- Add the three-cabinet timed/score/Frogger test scenario
- Add Cabinet 1 Pac-Man RFID credits, credit sound, and CREDIT 0/1 display
- Require physical Start after an accepted scan and prevent duplicate credits

Pi hardware and deployment
- Add GPIO feedback sampling and a separate multiplexed RFID helper
- Validate reader pin ownership and input/output conflicts
- Add SSH cabinet startup and Pi desktop-session service support
- Add master packaging, site setup, SSH enrollment, and staged cabinet releases
- Verify deployment manifests and support explicit activation/status/log commands

Documentation and verification
- Consolidate the root README around the current architecture and setup workflow
- Add the complete master/Pi/RFID export and deployment walkthrough
- Document network isolation, credentials, card identification, and rollback
- Add PCB, room-editor, feedback, credit, deployment, and controller tests
- Isolate protocol tests from designer-edited cabinet configurations

Known limitations
- RFID helper compilation and physical Pi commissioning remain unverified
- Fleet provisioning does not yet configure RFID-only installations completely
- Broader Pac-Man tests retain a ghost-capture assertion failure
- Staged exports must be rebuilt and checked before deploying runtime 2.3
2026-10-17 14:53:54 -07:00

324 lines
5.2 KiB
GDScript

class_name ScenarioEditSession
extends RefCounted
signal scenario_changed(
scenario: ScenarioDefinition
)
signal dirty_changed(
is_dirty: bool
)
signal saved
signal reverted
var source_scenario: ScenarioDefinition
var working_scenario: ScenarioDefinition
var is_dirty: bool = false
var saved_state: Dictionary = {}
func begin(
scenario: ScenarioDefinition
) -> bool:
if scenario == null:
push_error(
"ScenarioEditSession cannot begin with a null scenario."
)
return false
source_scenario = scenario
_create_working_copy()
return true
func mark_dirty() -> void:
if working_scenario == null:
_set_dirty(
false
)
return
var current_state := (
_capture_scenario_state(
working_scenario
)
)
_set_dirty(
current_state != saved_state
)
func save() -> bool:
if source_scenario == null:
push_error(
"ScenarioEditSession has no source scenario."
)
return false
if working_scenario == null:
push_error(
"ScenarioEditSession has no working scenario."
)
return false
_apply_working_copy_to_source()
var path := source_scenario.resource_path
if path.is_empty():
push_error(
"Source ScenarioDefinition has no resource path."
)
return false
var error := ResourceSaver.save(
source_scenario,
path
)
if error != OK:
push_error(
"Failed to save ScenarioDefinition. Error: %s"
% error
)
return false
saved_state = (
_capture_scenario_state(
working_scenario
)
)
_set_dirty(
false
)
saved.emit()
return true
func revert() -> void:
if source_scenario == null:
return
_create_working_copy()
reverted.emit()
print(
"[SCENARIO EDITOR] Reverted unsaved changes."
)
func _create_working_copy() -> void:
var copy := ScenarioDefinition.new()
copy.scenario_id = source_scenario.scenario_id
copy.display_name = source_scenario.display_name
copy.version = source_scenario.version
for source_assignment in source_scenario.assignments:
if source_assignment == null:
continue
var assignment := (
ScenarioAssignment.new()
)
#
# Preserve references to the actual
# game/cabinet definitions.
#
assignment.cabinet = (
source_assignment.cabinet
)
assignment.game = (
source_assignment.game
)
assignment.variant = (
source_assignment.variant
)
assignment.activate_after_win = source_assignment.activate_after_win
assignment.game_config = source_assignment.game_config.duplicate(true)
assignment.event_mappings = (
_duplicate_mappings(
source_assignment.event_mappings
)
)
copy.assignments.append(
assignment
)
working_scenario = copy
#
# This working copy is now our clean,
# saved baseline.
#
saved_state = (
_capture_scenario_state(
working_scenario
)
)
_set_dirty(
false
)
scenario_changed.emit(
working_scenario
)
func _duplicate_mappings(
source_mappings: Array[ScenarioEventMapping]
) -> Array[ScenarioEventMapping]:
var result: Array[ScenarioEventMapping] = []
for source_mapping in source_mappings:
if source_mapping == null:
continue
var mapping := ScenarioEventMapping.new()
mapping.game_event = (
source_mapping.game_event
)
mapping.hardware_actions = (
source_mapping.hardware_actions.duplicate()
)
result.append(mapping)
return result
func _apply_working_copy_to_source() -> void:
for working_assignment in working_scenario.assignments:
if working_assignment == null:
continue
if working_assignment.cabinet == null:
continue
var source_assignment := (
source_scenario.get_assignment_for_cabinet(
working_assignment.cabinet.cabinet_id
)
)
if source_assignment == null:
continue
source_assignment.event_mappings = (
_duplicate_mappings(
working_assignment.event_mappings
)
)
func _capture_scenario_state(
scenario: ScenarioDefinition
) -> Dictionary:
if scenario == null:
return {}
var state := {
"scenario_id": scenario.scenario_id,
"display_name": scenario.display_name,
"version": scenario.version,
"assignments": [],
}
for assignment in scenario.assignments:
if assignment == null:
state["assignments"].append(
null
)
continue
var assignment_state := {
"cabinet_id": StringName(),
"game_id": StringName(),
"variant": assignment.variant,
"activate_after_win": assignment.activate_after_win,
"game_config": assignment.game_config.duplicate(true),
"event_mappings": [],
}
if assignment.cabinet != null:
assignment_state["cabinet_id"] = (
assignment.cabinet.cabinet_id
)
if assignment.game != null:
assignment_state["game_id"] = (
assignment.game.game_id
)
for mapping in assignment.event_mappings:
if mapping == null:
assignment_state[
"event_mappings"
].append(
null
)
continue
var mapping_state := {
"game_event": mapping.game_event,
"hardware_actions": [],
}
for action_id in mapping.hardware_actions:
mapping_state[
"hardware_actions"
].append(
action_id
)
assignment_state[
"event_mappings"
].append(
mapping_state
)
state["assignments"].append(
assignment_state
)
return state
func _set_dirty(
value: bool
) -> void:
if is_dirty == value:
return
is_dirty = value
dirty_changed.emit(
is_dirty
)