TowerDefense/addons/signal_lens/autoload/signal_lens_autoload.gd

193 lines
8 KiB
GDScript3
Raw Normal View History

2025-08-25 23:23:03 +02:00
## This autoload is responsible for receiving the node path that will be inspected
## from the editor panel and then sending back all the signal data from that node
## parsed into a debugger friendly array
extends Node
## Reference to the currently targeted node in the remote tree
var target_node: Node = null
## On singleton ready in scene
## subscribe to the editor panel's request
func _ready() -> void:
EngineDebugger.register_message_capture("signal_lens", _on_node_signal_data_requested)
## This callback parses a node's signal data into an array that can be sent to the debugger
## The data is packaged in the following structure:
## Pseudo-code: [Name of target node, [All of the node's signals and each signal's respective callables], Class of target node]
## This request is received in the debugger with an array containing the target node's node path
## which will be used to retrieve the target node from the scene
func _on_node_signal_data_requested(prefix, data) -> bool:
var new_target_node = get_tree().root.get_node(data[0])
# If node is not found, return false
# If found, keep going
if new_target_node == null:
printerr("No node found in path " + str(data[0]))
return false
# Avoid error when trying to inspect root node
if new_target_node == get_tree().root:
EngineDebugger.send_message("signal_lens:incoming_node_signal_data", ["Root"])
return false
# Disconnect this autoload's callable connections from the previously targeted node's signals
if target_node != null:
if target_node != new_target_node:
for signal_name in target_node.get_signal_list().map(func(p_signal): return p_signal["name"]):
if target_node.is_connected(signal_name, _on_target_node_signal_emitted):
target_node.disconnect(signal_name, _on_target_node_signal_emitted)
# Acquire newly targeted node reference
target_node = new_target_node
# Initialize the first piece of data that will be sent to the debugger
# The unique name of the targeted node
# This will be used to set the name of the main graph node in the editor panel
var target_node_name: String = target_node.name
# Get target node class using Object.get_class()
var target_node_class: String = target_node.get_class()
# Initialize the array that will store the node's signal data
var target_node_signal_data: Array
# Get unparsed signal data from target node
var target_node_signal_list: Array[Dictionary] = target_node.get_signal_list()
# Iterate all signals in target node and parse signal data
# to debugger-friendly format
for i in range(target_node_signal_list.size()):
var raw_signal_data: Dictionary = target_node_signal_list[i]
# Raw signal data is formatted as:
# [name] is the name of the method, as a String
# [args] is an Array of dictionaries representing the arguments
# [default_args] is the default arguments as an Array of variants
# [flags] is a combination of MethodFlags
# [id] is the method's internal identifier int
# [return] is the returned value, as a Dictionary;
# Parse signal name
var parsed_signal_name: String = raw_signal_data["name"]
# Parse signal callables
var raw_signal_connections: Array[Dictionary] = target_node.get_signal_connection_list(raw_signal_data["name"])
# Raw signal connection is formatted as:
# [signal] is a reference to the Signal;
# [callable] is a reference to the connected Callable;
# [flags] is a combination of ConnectFlags.
var parsed_signal_callables = parse_signal_callables_to_debugger_format(raw_signal_connections)
# Create debugger-friendly signal data dictionary
var parsed_signal_data: Dictionary = {
"signal": parsed_signal_name,
"callables": parsed_signal_callables
}
# Append to overall signal data that will be sent to debugger
target_node_signal_data.append(parsed_signal_data)
# Connect this autoload's signal emission capture callable to currently iterated signal
# so we can send signal emissions to the editor panel
if not target_node.is_connected(parsed_signal_name, _on_target_node_signal_emitted):
var signal_args: Array = raw_signal_data["args"]
if signal_args.size() > 0:
target_node.connect(parsed_signal_name, _on_target_node_signal_emitted.bind(target_node_name, parsed_signal_name).unbind(signal_args.size()))
else:
target_node.connect(parsed_signal_name, _on_target_node_signal_emitted.bind(target_node_name, parsed_signal_name))
# On node data ready, prepare the array as per the debugger's specifications
EngineDebugger.send_message("signal_lens:incoming_node_signal_data", [target_node_name, target_node_signal_data, target_node_class])
return true
func parse_signal_callables_to_debugger_format(raw_signal_connections):
var parsed_signal_callables: Array[Dictionary]
# Iterate all connections of signal to parse callables
for raw_signal_connection: Dictionary in raw_signal_connections:
var parsed_callable_object: Object = raw_signal_connection["callable"].get_object()
var parsed_callable_object_name: String
# If object has property "name", get this property
# Otherwise, get the string value of the object
# This is important to allow parsing anonymous lambdas, which
# don't have name properties. The names in the nodes are not
# very user-friendly right now, so this is a good spot for a
# TODO: improve readability of anonymous lambda nodes
if not parsed_callable_object: return {"object_name": "ERROR: Couldn't parse node name."}
if parsed_callable_object.get("name") != null:
parsed_callable_object_name = parsed_callable_object.get("name")
else:
parsed_callable_object_name = parsed_callable_object.to_string()
var parsed_callable_method_name = str(raw_signal_connection["callable"].get_method())
# Don't parse callable that is in this autoload
if parsed_callable_method_name == "_on_target_node_signal_emitted": continue
var parsed_callable_data = {
"object_name": parsed_callable_object_name,
"method_name": parsed_callable_method_name
}
parsed_signal_callables.append(parsed_callable_data)
return parsed_signal_callables
## This callable receives all signal emissions from the currently targeted node
## and sends them to the editor panel
func _on_target_node_signal_emitted(node_name: String, signal_name: String):
var emission_data: Dictionary = {
"node_name": node_name,
"signal_name": signal_name,
"signal_arguments": [], # NOTE: Empty for now, will be available in a future release targeting Godot 4.5
"datetime": get_current_datetime_string(),
"timestamp": get_engine_ticks_string(),
"process_frames": Engine.get_process_frames(),
"physics_frames": Engine.get_physics_frames(),
}
EngineDebugger.send_message("signal_lens:incoming_node_signal_emission", [emission_data])
# NOTE: This function is compatible with Godot 4.5+ only, but 1.4.0 version of Signal Lens
# will still support 4.3+, so I'm keeping it here so it can reimplemented in a future release.
#func _on_target_node_signal_emitted(...args: Array):
#
#var parsed_args: Array
#var signal_args = args.slice(0, args.size() - 2)
#for arg in signal_args:
#parsed_args.append(str(arg))
#
#var emission_data: Dictionary = {
#"node_name": args[args.size() - 2],
#"signal_name": args[args.size() - 1],
#"datetime": get_current_datetime_string(),
#"timestamp": get_engine_ticks_string(),
#"process_frames": Engine.get_process_frames(),
#"physics_frames": Engine.get_physics_frames(),
#"signal_arguments": parsed_args
#}
#
#EngineDebugger.send_message("signal_lens:incoming_node_signal_emission", [emission_data])
func get_current_datetime_string() -> String:
return Time.get_datetime_string_from_system()
func get_engine_ticks_string() -> String:
var ticks: int = Time.get_ticks_msec()
# Convert milliseconds to total seconds
var total_seconds = ticks / 1000
var milliseconds = ticks % 1000
# Calculate hours, minutes, and seconds
var hours = total_seconds / 3600
var minutes = (total_seconds % 3600) / 60
var seconds = total_seconds % 60
# Format with leading zeros
return "%02d:%02d:%02d:%03d" % [hours, minutes, seconds, milliseconds]