Files

1277 lines
19 KiB
GDScript

@tool
class_name LogicGraph
extends GraphEdit
signal scenario_mapping_changed
signal edit_rejected(
message: String
)
signal logic_path_selected(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName
)
var room: RoomDefinition
var scenario: ScenarioDefinition
var graph_nodes: Dictionary = {}
var game_port_events: Dictionary = {}
var hardware_node_data: Dictionary = {}
var mapping_node_data: Dictionary = {}
func _ready() -> void:
minimap_enabled = true
show_grid = true
snapping_enabled = true
snapping_distance = 20
connection_lines_thickness = 4.0
add_theme_color_override(
"activity",
Color(0.0, 1.0, 0.278, 1.0)
)
if not connection_request.is_connected(
_on_connection_request
):
connection_request.connect(
_on_connection_request
)
if not disconnection_request.is_connected(
_on_disconnection_request
):
disconnection_request.connect(
_on_disconnection_request
)
func configure(
room_definition: RoomDefinition,
scenario_definition: ScenarioDefinition
) -> void:
room = room_definition
scenario = scenario_definition
rebuild()
func rebuild() -> void:
clear_connections()
var children_to_remove: Array[Node] = []
for child in get_children():
if child is GraphNode:
children_to_remove.append(child)
for child in children_to_remove:
#
# Remove it from the tree immediately so its
# node name becomes available before we rebuild.
#
remove_child(child)
child.queue_free()
graph_nodes.clear()
game_port_events.clear()
hardware_node_data.clear()
mapping_node_data.clear()
if room == null:
return
if scenario == null:
return
_build_graph()
func _create_graph_node(
node_name: StringName,
title: String,
position: Vector2
) -> GraphNode:
var node := GraphNode.new()
node.name = node_name
node.title = title
node.position_offset = position
node.custom_minimum_size = Vector2(
220.0,
100.0
)
add_child(node)
#
# Always register using the actual Node name
# after it enters the tree.
#
graph_nodes[node.name] = node
return node
func _add_row(
node: GraphNode,
text: String
) -> Label:
var label := Label.new()
label.text = text
label.custom_minimum_size.y = 28.0
node.add_child(label)
return label
func _set_ports(
node: GraphNode,
row_index: int,
left_enabled: bool,
right_enabled: bool
) -> void:
node.set_slot(
row_index,
left_enabled,
0,
Color.WHITE,
right_enabled,
0,
Color.WHITE
)
func _build_graph() -> void:
var row_index := 0
for assignment in scenario.assignments:
if assignment == null:
continue
if assignment.cabinet == null:
continue
if assignment.game == null:
continue
_build_assignment(
assignment,
row_index
)
row_index += 1
func _build_assignment(
assignment: ScenarioAssignment,
row_index: int
) -> void:
var base_y := (
float(row_index) * 500.0
)
var game_node := _create_game_node(
assignment,
Vector2(
50.0,
base_y
)
)
var hardware_nodes := (
_build_available_hardware(
assignment,
base_y
)
)
var mapping_number := 0
for mapping in assignment.event_mappings:
if mapping == null:
continue
for action_id in mapping.hardware_actions:
if not hardware_nodes.has(
action_id
):
continue
var offset_y := (
base_y
+ float(mapping_number) * 150.0
)
var mapping_node := (
_create_mapping_node(
assignment,
mapping,
action_id,
Vector2(
380.0,
offset_y
)
)
)
var hardware_node: GraphNode = (
hardware_nodes[
action_id
]
)
var device := (
room.get_device_for_action(
assignment.cabinet.cabinet_id,
action_id
)
)
var device_node: GraphNode = null
if device != null:
device_node = (
_create_device_node(
device,
assignment.cabinet.cabinet_id,
action_id,
Vector2(
1050.0,
hardware_node.position_offset.y
)
)
)
_connect_chain(
game_node,
mapping,
mapping_node,
hardware_node,
device_node
)
mapping_number += 1
func _create_game_node(
assignment: ScenarioAssignment,
position: Vector2
) -> GraphNode:
var node_name := StringName(
"game__%s"
% assignment.cabinet.cabinet_id
)
var existing := graph_nodes.get(
node_name
) as GraphNode
if existing != null:
return existing
var node := _create_graph_node(
node_name,
"GAME — %s"
% assignment.cabinet.display_name,
position
)
_add_row(
node,
assignment.game.display_name
)
var port_events: Array[StringName] = []
for event_definition in assignment.game.events:
if event_definition == null:
continue
var event_row := _add_row(
node,
"%s [%s]"
% [
event_definition.display_name,
event_definition.event_id
]
)
_set_ports(
node,
event_row.get_index(),
false,
true
)
port_events.append(
event_definition.event_id
)
game_port_events[
node.name
] = port_events
return node
func _create_mapping_node(
assignment: ScenarioAssignment,
mapping: ScenarioEventMapping,
action_id: StringName,
position: Vector2
) -> GraphNode:
var node_name := StringName(
"mapping__%s__%s__%s"
% [
assignment.cabinet.cabinet_id,
mapping.game_event,
action_id
]
)
var node := _create_graph_node(
node_name,
"SCENARIO MAPPING",
position
)
mapping_node_data[
node.name
] = {
"cabinet_id": assignment.cabinet.cabinet_id,
"event_id": mapping.game_event,
"action_id": action_id
}
var event_row := _add_row(
node,
String(mapping.game_event)
)
_set_ports(
node,
event_row.get_index(),
true,
false
)
var action_row := _add_row(
node,
String(action_id)
)
_set_ports(
node,
action_row.get_index(),
false,
true
)
node.node_selected.connect(
func() -> void:
_emit_mapping_selection(
node
)
)
return node
func _create_hardware_node(
assignment: ScenarioAssignment,
action_id: StringName,
position: Vector2
) -> GraphNode:
var cabinet := assignment.cabinet
var hardware := cabinet.hardware_config
var action: HardwareAction = null
if hardware != null:
action = hardware.get_action(
action_id
)
var node_name := StringName(
"hardware__%s__%s"
% [
cabinet.cabinet_id,
action_id
]
)
var node := _create_graph_node(
node_name,
"HARDWARE",
position
)
hardware_node_data[
node.name
] = {
"cabinet_id": cabinet.cabinet_id,
"action_id": action_id
}
var input_row := _add_row(
node,
String(action_id)
)
_set_ports(
node,
input_row.get_index(),
true,
false
)
var description := (
"Cabinet: %s"
% cabinet.display_name
)
_add_row(
node,
description
)
var gpio_text := "GPIO: ?"
if action != null:
gpio_text = (
"GPIO: %d"
% action.gpio_pin
)
var output_row := _add_row(
node,
gpio_text
)
_set_ports(
node,
output_row.get_index(),
false,
true
)
return node
func _create_device_node(
device: RoomDeviceDefinition,
cabinet_id: StringName,
action_id: StringName,
position: Vector2
) -> GraphNode:
var node_name := StringName(
"device__%s__%s__%s"
% [
cabinet_id,
action_id,
device.device_id
]
)
var node := _create_graph_node(
node_name,
"ROOM DEVICE",
position
)
var row := _add_row(
node,
device.display_name
)
_set_ports(
node,
row.get_index(),
true,
false
)
return node
func _connect_chain(
game_node: GraphNode,
mapping: ScenarioEventMapping,
mapping_node: GraphNode,
hardware_node: GraphNode,
device_node: GraphNode
) -> void:
var game_port := _get_game_event_port(
game_node.name,
mapping.game_event
)
if game_port >= 0:
connect_node(
game_node.name,
game_port,
mapping_node.name,
0
)
connect_node(
mapping_node.name,
0,
hardware_node.name,
0
)
if device_node != null:
connect_node(
hardware_node.name,
0,
device_node.name,
0
)
func _get_game_event_port(
game_node_name: StringName,
event_id: StringName
) -> int:
if not game_port_events.has(
game_node_name
):
return -1
var events: Array = (
game_port_events[
game_node_name
]
)
for index in range(
events.size()
):
if events[index] == event_id:
return index
return -1
func set_path_active(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName,
active: bool
) -> void:
var game_name := StringName(
"game__%s"
% cabinet_id
)
var mapping_name := StringName(
"mapping__%s__%s__%s"
% [
cabinet_id,
event_id,
action_id
]
)
var hardware_name := StringName(
"hardware__%s__%s"
% [
cabinet_id,
action_id
]
)
var device_name := StringName(
"device__%s__%s__%s"
% [
cabinet_id,
action_id,
device_id
]
)
var amount := (
1.0
if active
else 0.0
)
#
# The game node has one output port per GameEventDefinition,
# so find the correct one for this event.
#
var game_port := _get_game_event_port(
game_name,
event_id
)
if (
game_port >= 0
and graph_nodes.has(game_name)
and graph_nodes.has(mapping_name)
):
print(
"[LOGIC PATH] cabinet=%s event=%s action=%s device=%s game_port=%d active=%s"
% [
cabinet_id,
event_id,
action_id,
device_id,
game_port,
active
]
)
set_connection_activity(
game_name,
game_port,
mapping_name,
0,
amount
)
print("")
print("======= PATH HIGHLIGHT DEBUG =======")
print("game_name: ", game_name)
print("mapping_name: ", mapping_name)
print("hardware_name: ", hardware_name)
print("device_name: ", device_name)
print("game_port: ", game_port)
print(
"game->mapping exists: ",
is_node_connected(
game_name,
game_port,
mapping_name,
0
)
)
print(
"mapping->hardware exists: ",
is_node_connected(
mapping_name,
0,
hardware_name,
0
)
)
print(
"hardware->device exists: ",
is_node_connected(
hardware_name,
0,
device_name,
0
)
)
print("activity: ", amount)
print("====================================")
print("")
if (
graph_nodes.has(mapping_name)
and graph_nodes.has(hardware_name)
):
set_connection_activity(
mapping_name,
0,
hardware_name,
0,
amount
)
if (
graph_nodes.has(hardware_name)
and graph_nodes.has(device_name)
):
set_connection_activity(
hardware_name,
0,
device_name,
0,
amount
)
func _build_available_hardware(
assignment: ScenarioAssignment,
base_y: float
) -> Dictionary:
var result: Dictionary = {}
var cabinet := assignment.cabinet
if cabinet.hardware_config == null:
return result
var index := 0
for action in cabinet.hardware_config.actions:
if action == null:
continue
var position := Vector2(
720.0,
base_y + float(index) * 140.0
)
var node := _create_hardware_node(
assignment,
action.action_id,
position
)
result[
action.action_id
] = node
index += 1
return result
func _on_connection_request(
from_node: StringName,
from_port: int,
to_node: StringName,
to_port: int
) -> void:
print("")
print("======= CONNECTION REQUEST =======")
print("FROM NODE: ", from_node)
print("FROM PORT: ", from_port)
print("TO NODE: ", to_node)
print("TO PORT: ", to_port)
print("Game nodes known: ", game_port_events.keys())
print("Hardware nodes known: ", hardware_node_data.keys())
print("==================================")
print("")
# ... existing function continues ...
if scenario == null:
return
if not game_port_events.has(
from_node
):
_reject_edit(
"Connections must start from a game event."
)
return
if not hardware_node_data.has(
to_node
):
_reject_edit(
"Game events can only connect to cabinet hardware actions."
)
return
var events: Array = (
game_port_events[from_node]
)
if (
from_port < 0
or
from_port >= events.size()
):
_reject_edit(
"Invalid game event port."
)
return
var event_id: StringName = (
events[from_port]
)
var hardware_data: Dictionary = (
hardware_node_data[to_node]
)
var cabinet_id: StringName = (
hardware_data["cabinet_id"]
)
var action_id: StringName = (
hardware_data["action_id"]
)
var source_cabinet_id := (
_get_game_node_cabinet_id(
from_node
)
)
if source_cabinet_id != cabinet_id:
_reject_edit(
"Cannot connect events between different cabinets."
)
return
if _mapping_exists(
cabinet_id,
event_id,
action_id
):
_reject_edit(
"This scenario mapping already exists."
)
return
_add_mapping(
cabinet_id,
event_id,
action_id
)
func _get_game_node_cabinet_id(
game_node_name: StringName
) -> StringName:
for assignment in scenario.assignments:
if assignment == null:
continue
if assignment.cabinet == null:
continue
var expected_name := StringName(
"game__%s"
% assignment.cabinet.cabinet_id
)
if expected_name == game_node_name:
return assignment.cabinet.cabinet_id
return &""
func _mapping_exists(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName
) -> bool:
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
return false
for mapping in assignment.event_mappings:
if mapping == null:
continue
if mapping.game_event != event_id:
continue
if mapping.hardware_actions.has(
action_id
):
return true
return false
func _add_mapping(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName
) -> void:
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
_reject_edit(
"Cabinet assignment could not be found."
)
return
if assignment.game == null:
return
if not assignment.game.has_event(
event_id
):
_reject_edit(
"Game '%s' does not define event '%s'."
% [
assignment.game.game_id,
event_id
]
)
return
if assignment.cabinet.hardware_config == null:
return
if not assignment.cabinet.hardware_config.has_action(
action_id
):
_reject_edit(
"Cabinet '%s' does not define hardware action '%s'."
% [
cabinet_id,
action_id
]
)
return
var target_mapping: ScenarioEventMapping = null
for mapping in assignment.event_mappings:
if mapping == null:
continue
if mapping.game_event == event_id:
target_mapping = mapping
break
if target_mapping == null:
target_mapping = (
ScenarioEventMapping.new()
)
target_mapping.game_event = event_id
assignment.event_mappings.append(
target_mapping
)
target_mapping.hardware_actions.append(
action_id
)
print(
"[LOGIC EDITOR] Added mapping: %s :: %s -> %s"
% [
cabinet_id,
event_id,
action_id
]
)
scenario_mapping_changed.emit()
rebuild()
func _reject_edit(
message: String
) -> void:
print(
"[LOGIC EDITOR] REJECTED: ",
message
)
edit_rejected.emit(
message
)
func _on_disconnection_request(
from_node: StringName,
from_port: int,
to_node: StringName,
to_port: int
) -> void:
var mapping_node_name: StringName = &""
if mapping_node_data.has(
from_node
):
mapping_node_name = from_node
elif mapping_node_data.has(
to_node
):
mapping_node_name = to_node
else:
_reject_edit(
"This connection belongs to the RoomDefinition and cannot be removed here."
)
return
var data: Dictionary = (
mapping_node_data[
mapping_node_name
]
)
_remove_mapping(
data["cabinet_id"],
data["event_id"],
data["action_id"]
)
func _remove_mapping(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName
) -> void:
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
return
for mapping in assignment.event_mappings:
if mapping == null:
continue
if mapping.game_event != event_id:
continue
mapping.hardware_actions.erase(
action_id
)
#
# Remove empty mapping Resources.
#
if mapping.hardware_actions.is_empty():
assignment.event_mappings.erase(
mapping
)
print(
"[LOGIC EDITOR] Removed mapping: %s :: %s -> %s"
% [
cabinet_id,
event_id,
action_id
]
)
scenario_mapping_changed.emit()
rebuild()
return
func _emit_mapping_selection(
node: GraphNode
) -> void:
if node == null:
return
if not mapping_node_data.has(
node.name
):
return
var data: Dictionary = (
mapping_node_data[
node.name
]
)
var cabinet_id: StringName = (
data["cabinet_id"]
)
var event_id: StringName = (
data["event_id"]
)
var action_id: StringName = (
data["action_id"]
)
var device_id: StringName = &""
if room != null:
var device := (
room.get_device_for_action(
cabinet_id,
action_id
)
)
if device != null:
device_id = device.device_id
logic_path_selected.emit(
cabinet_id,
event_id,
action_id,
device_id
)
func apply_editor_selection(
selection: EscapeRoomEditorSelection
) -> void:
if selection == null:
return
_clear_editor_selection_highlight()
if selection.cabinet_id != &"":
_highlight_cabinet_logic(
selection.cabinet_id
)
if (
selection.event_id != &""
and selection.action_id != &""
):
_highlight_specific_logic_path(
selection.cabinet_id,
selection.event_id,
selection.action_id,
selection.device_id
)
func _highlight_cabinet_logic(
cabinet_id: StringName
) -> void:
for node_name in graph_nodes:
var node: GraphNode = (
graph_nodes[
node_name
]
)
if node == null:
continue
var node_cabinet_id := (
_get_graph_node_cabinet_id(
node_name
)
)
if node_cabinet_id == cabinet_id:
node.modulate = Color.WHITE
else:
node.modulate = Color(
0.5,
0.5,
0.5,
1.0
)
func _clear_editor_selection_highlight() -> void:
for node_name in graph_nodes:
var node: GraphNode = graph_nodes[node_name]
if node == null:
continue
node.modulate = Color.WHITE
func _highlight_specific_logic_path(
_cabinet_id: StringName,
_event_id: StringName,
_action_id: StringName,
_device_id: StringName
) -> void:
#
# Exact editor-selection connection styling
# will be separate from simulation activity.
#
pass
func _get_graph_node_cabinet_id(
node_name: StringName
) -> StringName:
#
# Game nodes already have a reliable lookup.
#
if game_port_events.has(
node_name
):
return _get_game_node_cabinet_id(
node_name
)
#
# Scenario mapping nodes.
#
if mapping_node_data.has(
node_name
):
var mapping_data: Dictionary = (
mapping_node_data[
node_name
]
)
return mapping_data[
"cabinet_id"
]
#
# Hardware nodes.
#
if hardware_node_data.has(
node_name
):
var hardware_data: Dictionary = (
hardware_node_data[
node_name
]
)
return hardware_data[
"cabinet_id"
]
#
# Device node names are:
# device__cabinet_id__action_id__device_id
#
var node_text := String(
node_name
)
if node_text.begins_with(
"device__"
):
var parts := node_text.split(
"__"
)
if parts.size() >= 4:
return StringName(
parts[1]
)
return &""