Files
room_glitch/editor/workspace/editor_simulation_driver.gd

984 lines
14 KiB
GDScript

@tool
class_name EditorSimulationDriver
extends RefCounted
signal action_activated(
cabinet_id: StringName,
action_id: StringName,
context: Dictionary
)
signal action_deactivated(
cabinet_id: StringName,
action_id: StringName,
context: Dictionary
)
signal device_state_changed(
device_id: StringName,
active: bool
)
signal trace_entry_created(
entry: Dictionary
)
signal sequence_started(
sequence: TestSequence
)
signal sequence_finished(
sequence: TestSequence
)
signal sequence_stopped
var sequence_settling: bool = false
var room: RoomDefinition
var scenario: ScenarioDefinition
var logic_engine: RoomLogicEngine
var simulated_device_states: Dictionary = {}
var hardware_backend: SimulatedHardwareBackend
var pending_hardware_deactivations: Array[Dictionary] = []
var paused: bool = false
var active_sequence: TestSequence = null
var sequence_start_time: float = 0.0
var pending_sequence_steps: Array[Dictionary] = []
var sequence_running: bool = false
func start_sequence(
sequence: TestSequence
) -> void:
if sequence == null:
return
#
# Starting a rehearsal means starting from
# a clean simulated-room state.
#
reset_simulation()
active_sequence = sequence
sequence_start_time = (
_get_simulation_time()
)
pending_sequence_steps.clear()
for step in sequence.get_sorted_steps():
pending_sequence_steps.append(
{
"execute_at": (
sequence_start_time
+ step.time_offset_seconds
),
"step": step,
}
)
sequence_running = true
sequence_settling = false
trace_entry_created.emit(
{
"type": &"sequence",
"time": _get_simulation_time(),
"sequence_id": sequence.sequence_id,
"display_name": sequence.display_name,
"state": &"started",
}
)
sequence_started.emit(
sequence
)
#
# Execute all steps authored at 0.0 immediately.
#
_process_pending_sequence_steps()
func advance(
delta_seconds: float
) -> void:
if paused:
return
_advance_internal(
delta_seconds
)
func configure(
room_definition: RoomDefinition,
scenario_definition: ScenarioDefinition
) -> void:
room = room_definition
scenario = scenario_definition
pending_hardware_deactivations.clear()
simulated_device_states.clear()
logic_engine = RoomLogicEngine.new()
logic_engine.configure(
room_definition
)
logic_engine.action_requested.connect(
_on_logic_action_requested
)
logic_engine.trace_entry_created.connect(
_on_logic_trace_entry_created
)
hardware_backend = SimulatedHardwareBackend.new()
hardware_backend.pin_state_changed.connect(
_on_simulated_pin_state_changed
)
hardware_backend.trace_entry_created.connect(
_on_hardware_trace_entry_created
)
func simulate_game_event(
cabinet_id: StringName,
event_id: StringName
) -> void:
if scenario == null:
push_error(
"EditorSimulationDriver has no scenario."
)
return
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
push_error(
"No scenario assignment exists for cabinet '%s'."
% cabinet_id
)
return
#
# Feed the event into the room-level logic engine.
#
emit_room_event(
cabinet_id,
event_id
)
#
# Preserve the existing scenario mapping:
#
# game event
# -> cabinet hardware action
#
var actions := (
assignment.get_actions_for_event(
event_id
)
)
for action_id in actions:
_activate_action(
cabinet_id,
action_id,
{
"game_event": event_id
}
)
func _activate_action(
cabinet_id: StringName,
action_id: StringName,
context: Dictionary
) -> void:
if room == null:
return
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
return
if assignment.cabinet == null:
return
if assignment.cabinet.hardware_config == null:
return
var action := (
assignment.cabinet.hardware_config.get_action(
action_id
)
)
if action == null:
push_error(
"Editor simulation could not find hardware action '%s' on cabinet '%s'."
% [
action_id,
cabinet_id
]
)
return
action_activated.emit(
cabinet_id,
action_id,
context
)
hardware_backend.activate_action(
cabinet_id,
action,
context
)
match action.action_type:
HardwareAction.ActionType.PULSE_OUTPUT:
_schedule_deactivation(
cabinet_id,
action_id,
context,
action.pulse_duration_ms
)
HardwareAction.ActionType.DIGITAL_OUTPUT:
#
# For now, treat a digital output as latched.
# We'll add explicit ON/OFF editing later.
#
pass
func _schedule_deactivation(
cabinet_id: StringName,
action_id: StringName,
context: Dictionary,
duration_ms: int
) -> void:
var duration_seconds := (
float(duration_ms)
/ 1000.0
)
pending_hardware_deactivations.append(
{
"execute_at": (
_get_simulation_time()
+ duration_seconds
),
"cabinet_id": cabinet_id,
"action_id": action_id,
"context": context.duplicate(),
}
)
func _get_simulation_time() -> float:
if logic_engine == null:
return 0.0
return logic_engine.get_simulation_time()
func emit_room_event(
source_id: StringName,
event_id: StringName
) -> void:
if logic_engine == null:
return
logic_engine.emit_event(
source_id,
event_id
)
func _on_logic_action_requested(
target_id: StringName,
action_id: StringName,
rule_id: StringName
) -> void:
if room == null:
return
#
# First: direct room-device action.
#
var device := room.get_device(
target_id
)
if device != null:
_apply_device_action(
device,
action_id,
rule_id
)
return
#
# Second: cabinet hardware action.
#
var assignment := (
scenario.get_assignment_for_cabinet(
target_id
)
)
if assignment != null:
_activate_action(
target_id,
action_id,
{
"logic_rule": rule_id
}
)
return
print(
"[SIM] Unhandled logic action: ",
target_id,
".",
action_id
)
func _apply_device_action(
device: RoomDeviceDefinition,
action_id: StringName,
_rule_id: StringName
) -> void:
match action_id:
&"activate":
_set_device_active(
device.device_id,
true
)
_emit_device_event(
device.device_id,
&"activated"
)
&"deactivate":
_set_device_active(
device.device_id,
false
)
_emit_device_event(
device.device_id,
&"deactivated"
)
&"turn_on":
_set_device_active(
device.device_id,
true
)
_emit_device_event(
device.device_id,
&"turned_on"
)
&"turn_off":
_set_device_active(
device.device_id,
false
)
_emit_device_event(
device.device_id,
&"turned_off"
)
&"unlock":
_set_device_active(
device.device_id,
true
)
_emit_device_event(
device.device_id,
&"unlocked"
)
&"lock":
_set_device_active(
device.device_id,
false
)
_emit_device_event(
device.device_id,
&"locked"
)
_:
print(
"[SIM] Unsupported device action: ",
device.device_id,
".",
action_id
)
func _set_device_active(
device_id: StringName,
active: bool
) -> void:
var previous_active := bool(
simulated_device_states.get(
device_id,
false
)
)
simulated_device_states[
device_id
] = active
device_state_changed.emit(
device_id,
active
)
#
# Record the simulated physical-state transition.
#
if previous_active != active:
trace_entry_created.emit(
{
"type": &"device",
"time": _get_simulation_time(),
"device_id": device_id,
"active": active,
}
)
func _emit_device_event(
device_id: StringName,
event_id: StringName
) -> void:
if logic_engine == null:
return
logic_engine.emit_event(
device_id,
event_id
)
func _on_logic_trace_entry_created(
entry: Dictionary
) -> void:
var timed_entry := (
entry.duplicate(true)
)
timed_entry["time"] = (
_get_simulation_time()
)
print(
"[SIM TRACE] ",
timed_entry
)
trace_entry_created.emit(
timed_entry
)
func _propagate_hardware_action_to_devices(
cabinet_id: StringName,
action_id: StringName,
active: bool,
context: Dictionary
) -> void:
if room == null:
return
for binding in room.bindings:
if binding == null:
continue
if binding.cabinet_id != cabinet_id:
continue
if binding.hardware_action_id != action_id:
continue
var device := room.get_device(
binding.device_id
)
if device == null:
continue
_set_device_active(
device.device_id,
active
)
print(
"[SIM DEVICE PATH] ",
cabinet_id,
".",
action_id,
" -> ",
device.device_id,
" active=",
active,
" context=",
context
)
if active:
_emit_device_event(
device.device_id,
&"activated"
)
else:
_emit_device_event(
device.device_id,
&"deactivated"
)
func _on_simulated_pin_state_changed(
cabinet_id: StringName,
_gpio_pin: int,
active: bool,
action_id: StringName,
context: Dictionary
) -> void:
_propagate_hardware_action_to_devices(
cabinet_id,
action_id,
active,
context
)
func _on_hardware_trace_entry_created(
entry: Dictionary
) -> void:
var timed_entry := (
entry.duplicate(true)
)
timed_entry["time"] = (
_get_simulation_time()
)
print(
"[GPIO SIM] ",
timed_entry
)
trace_entry_created.emit(
timed_entry
)
func _process_pending_hardware_deactivations() -> void:
if pending_hardware_deactivations.is_empty():
return
var ready: Array[Dictionary] = []
var remaining: Array[Dictionary] = []
for pending in pending_hardware_deactivations:
var execute_at: float = (
pending.get(
"execute_at",
0.0
)
)
if execute_at <= _get_simulation_time():
ready.append(
pending
)
else:
remaining.append(
pending
)
pending_hardware_deactivations = remaining
for pending in ready:
_deactivate_hardware_action(
pending.get(
"cabinet_id",
&""
),
pending.get(
"action_id",
&""
),
pending.get(
"context",
{}
)
)
func _deactivate_hardware_action(
cabinet_id: StringName,
action_id: StringName,
context: Dictionary
) -> void:
if scenario == null:
return
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
return
if assignment.cabinet == null:
return
if assignment.cabinet.hardware_config == null:
return
var action := (
assignment.cabinet.hardware_config.get_action(
action_id
)
)
if action == null:
return
action_deactivated.emit(
cabinet_id,
action_id,
context
)
if hardware_backend != null:
hardware_backend.deactivate_action(
cabinet_id,
action,
context
)
func set_paused(
value: bool
) -> void:
paused = value
func is_paused() -> bool:
return paused
func step(
delta_seconds: float
) -> void:
if delta_seconds <= 0.0:
return
_advance_internal(
delta_seconds
)
func reset_simulation() -> void:
pending_sequence_steps.clear()
active_sequence = null
sequence_running = false
sequence_start_time = 0.0
sequence_settling = false
pending_hardware_deactivations.clear()
simulated_device_states.clear()
if logic_engine != null:
logic_engine.reset()
if hardware_backend != null:
hardware_backend.reset()
paused = false
func _advance_internal(
delta_seconds: float
) -> void:
if delta_seconds <= 0.0:
return
if logic_engine == null:
return
logic_engine.advance(
delta_seconds
)
_process_pending_sequence_steps()
_process_pending_hardware_deactivations()
_finish_sequence_if_ready()
func _process_pending_sequence_steps() -> void:
if not sequence_running:
return
if pending_sequence_steps.is_empty():
_finish_sequence_if_ready()
return
_finish_sequence_if_ready()
var current_time := (
_get_simulation_time()
)
var ready: Array[Dictionary] = []
var remaining: Array[Dictionary] = []
for pending in pending_sequence_steps:
var execute_at := float(
pending.get(
"execute_at",
0.0
)
)
if execute_at <= current_time:
ready.append(
pending
)
else:
remaining.append(
pending
)
pending_sequence_steps = remaining
for pending in ready:
var step := (
pending.get(
"step"
)
as TestSequenceStep
)
if step == null:
continue
_execute_sequence_step(
step
)
_finish_sequence_if_ready()
func _execute_sequence_step(
step: TestSequenceStep
) -> void:
trace_entry_created.emit(
{
"type": &"input",
"time": _get_simulation_time(),
"source_id": step.source_id,
"event_id": step.event_id,
"note": step.note,
}
)
#
# If the source is a scenario cabinet, this is a
# game/cabinet event and must travel through the
# existing scenario mapping as well as room logic.
#
if scenario != null:
var assignment := (
scenario.get_assignment_for_cabinet(
step.source_id
)
)
if assignment != null:
simulate_game_event(
step.source_id,
step.event_id
)
return
#
# Otherwise this is a generic room/world input.
#
emit_room_event(
step.source_id,
step.event_id
)
func _finish_sequence_if_ready() -> void:
if not sequence_running:
return
#
# Still waiting to inject authored inputs.
#
if not pending_sequence_steps.is_empty():
return
#
# All authored inputs have been injected.
# Enter settling state once.
#
if not sequence_settling:
sequence_settling = true
if active_sequence != null:
trace_entry_created.emit(
{
"type": &"sequence",
"time": _get_simulation_time(),
"sequence_id": active_sequence.sequence_id,
"display_name": active_sequence.display_name,
"state": &"settling",
}
)
#
# Room logic still has delayed work.
#
if (
logic_engine != null
and logic_engine.has_pending_actions()
):
return
#
# Hardware pulses are still active.
#
if not pending_hardware_deactivations.is_empty():
return
#
# Everything has settled.
#
var completed_sequence := active_sequence
sequence_running = false
sequence_settling = false
active_sequence = null
if completed_sequence == null:
return
trace_entry_created.emit(
{
"type": &"sequence",
"time": _get_simulation_time(),
"sequence_id": completed_sequence.sequence_id,
"display_name": completed_sequence.display_name,
"state": &"finished",
}
)
sequence_finished.emit(
completed_sequence
)
func stop_sequence() -> void:
if not sequence_running:
return
pending_sequence_steps.clear()
sequence_running = false
sequence_settling = false
active_sequence = null
trace_entry_created.emit(
{
"type": &"sequence",
"time": _get_simulation_time(),
"state": &"stopped",
}
)
sequence_stopped.emit()