984 lines
14 KiB
GDScript
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()
|