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
324 lines
5.2 KiB
GDScript
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
|
|
)
|