Files
room_glitch/core/simulation/room_edit_session.gd
T
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

423 lines
6.4 KiB
GDScript

class_name RoomEditSession
extends RefCounted
signal room_changed(room: RoomDefinition)
signal dirty_changed(is_dirty: bool)
signal saved
signal reverted
var source_room: RoomDefinition
var working_room: RoomDefinition
var is_dirty: bool = false
var saved_state: Dictionary = {}
func begin(room: RoomDefinition) -> bool:
if room == null:
push_error(
"RoomEditSession cannot begin with a null room."
)
return false
source_room = room
_create_working_copy()
return true
func mark_dirty() -> void:
if working_room == null:
_set_dirty(
false
)
return
var current_state := (
_capture_room_state(
working_room
)
)
_set_dirty(
current_state != saved_state
)
func save() -> bool:
if source_room == null:
push_error(
"RoomEditSession has no source room."
)
return false
if working_room == null:
push_error(
"RoomEditSession has no working room."
)
return false
_apply_working_copy_to_source()
var path := source_room.resource_path
if path.is_empty():
push_error(
"Source RoomDefinition has no resource path."
)
return false
var error := ResourceSaver.save(
source_room,
path
)
if error != OK:
push_error(
"Failed to save RoomDefinition. Error: %s"
% error
)
return false
print(
"[ROOM EDITOR] Saved room to '%s'."
% path
)
saved_state = (
_capture_room_state(
working_room
)
)
_set_dirty(
false
)
saved.emit()
return true
func revert() -> void:
if source_room == null:
return
_create_working_copy()
reverted.emit()
print(
"[ROOM EDITOR] Reverted unsaved changes."
)
func reload_from_disk() -> bool:
if source_room == null:
return false
var path := (
source_room.resource_path
)
if path.is_empty():
push_error(
"Source RoomDefinition has no resource path."
)
return false
var reloaded := (
ResourceLoader.load(
path,
"",
ResourceLoader.CACHE_MODE_REPLACE
)
as RoomDefinition
)
if reloaded == null:
push_error(
"Failed to reload RoomDefinition from '%s'."
% path
)
return false
source_room = reloaded
_create_working_copy()
print(
"[ROOM EDITOR] Reloaded room from disk: '%s'."
% path
)
return true
func _create_working_copy() -> void:
working_room = (
_duplicate_room(
source_room
)
)
saved_state = (
_capture_room_state(
working_room
)
)
_set_dirty(
false
)
room_changed.emit(
working_room
)
func _apply_working_copy_to_source() -> void:
for working_placement in working_room.cabinet_placements:
if working_placement == null:
continue
var source_placement := (
source_room.get_cabinet_placement(
working_placement.cabinet_id
)
)
if source_placement == null:
continue
source_placement.position = (
working_placement.position
)
source_placement.rotation_degrees = (
working_placement.rotation_degrees
)
for working_device in working_room.devices:
if working_device == null:
continue
var source_device := (
source_room.get_device(
working_device.device_id
)
)
if source_device == null:
continue
source_device.position = (
working_device.position
)
for working_binding in working_room.bindings:
if working_binding == null:
continue
var source_binding: RoomDeviceBinding = null
for candidate in source_room.bindings:
if candidate == null:
continue
if (
candidate.cabinet_id
== working_binding.cabinet_id
and candidate.hardware_action_id
== working_binding.hardware_action_id
and candidate.device_id
== working_binding.device_id
):
source_binding = candidate
break
if source_binding == null:
continue
source_binding.route_points = (
working_binding.route_points.duplicate()
)
#
# Copy authored room geometry.
#
source_room.boundary_points = (
working_room.boundary_points.duplicate()
)
source_room.walls = _duplicate_walls(
working_room.walls
)
func _duplicate_room(
source: RoomDefinition
) -> RoomDefinition:
var copy := (
source.duplicate(true)
as RoomDefinition
)
if copy == null:
return null
#
# Explicitly duplicate authored geometry so
# wall Resources are never shared with source_room.
#
copy.boundary_points = (
source.boundary_points.duplicate()
)
copy.walls = _duplicate_walls(
source.walls
)
return copy
func _duplicate_walls(
source: Array[RoomWallDefinition]
) -> Array[RoomWallDefinition]:
var result: Array[RoomWallDefinition] = []
for wall in source:
if wall == null:
continue
var copy := (
wall.duplicate(true)
as RoomWallDefinition
)
if copy != null:
result.append(
copy
)
return result
func _capture_room_state(
room: RoomDefinition
) -> Dictionary:
if room == null:
return {}
var state := {
"room_id": room.room_id,
"display_name": room.display_name,
"boundary_points": room.boundary_points.duplicate(),
"cabinet_placements": [],
"devices": [],
"bindings": [],
"walls": [],
}
for placement in room.cabinet_placements:
if placement == null:
state["cabinet_placements"].append(
null
)
continue
state["cabinet_placements"].append(
{
"cabinet_id": placement.cabinet_id,
"position": placement.position,
"rotation_degrees": (
placement.rotation_degrees
),
}
)
for device in room.devices:
if device == null:
state["devices"].append(
null
)
continue
state["devices"].append(
{
"device_id": device.device_id,
"display_name": device.display_name,
"device_type": device.device_type,
"behavior": device.behavior,
"initial_active": (
device.initial_active
),
"position": device.position,
}
)
for binding in room.bindings:
if binding == null:
state["bindings"].append(
null
)
continue
state["bindings"].append(
{
"cabinet_id": binding.cabinet_id,
"hardware_action_id": (
binding.hardware_action_id
),
"device_id": binding.device_id,
"route_points": (
binding.route_points.duplicate()
),
}
)
for wall in room.walls:
if wall == null:
state["walls"].append(
null
)
continue
state["walls"].append(
{
"wall_id": wall.wall_id,
"start": wall.start,
"end": wall.end,
"thickness": wall.thickness,
"is_doorway": wall.is_doorway,
"swing_reversed": wall.swing_reversed,
}
)
return state
func _set_dirty(
value: bool
) -> void:
if is_dirty == value:
return
is_dirty = value
dirty_changed.emit(
is_dirty
)