From 868e1842697c563661a9ed377abad2df9d59f9f1 Mon Sep 17 00:00:00 2001 From: tindrew Date: Sun, 25 Jan 2026 01:21:25 -0600 Subject: [PATCH 1/9] added Signalize --- core/object/object.cpp | 10 + core/object/object.h | 23 + editor/SCsub | 2 + editor/debugger/script_editor_debugger.cpp | 63 + editor/debugger/script_editor_debugger.h | 7 +- editor/docks/signalize_dock.cpp | 3142 ++++++++++++++++++++ editor/docks/signalize_dock.h | 311 ++ editor/editor_interface.h | 1 + editor/editor_node.cpp | 8 + editor/register_editor_types.cpp | 4 +- editor/script/script_text_editor.h | 3 + scene/debugger/scene_debugger.cpp | 180 ++ scene/debugger/scene_debugger.h | 9 + scene/debugger/signal_viewer_runtime.cpp | 384 +++ scene/debugger/signal_viewer_runtime.h | 101 + scene/register_scene_types.cpp | 5 + 16 files changed, 4251 insertions(+), 2 deletions(-) create mode 100644 editor/docks/signalize_dock.cpp create mode 100644 editor/docks/signalize_dock.h create mode 100644 scene/debugger/signal_viewer_runtime.cpp create mode 100644 scene/debugger/signal_viewer_runtime.h diff --git a/core/object/object.cpp b/core/object/object.cpp index 42c54cb0791..9cfcbfb2097 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -42,6 +42,10 @@ #include "core/string/translation_server.h" #include "core/variant/typed_array.h" +// Static member initialization for signal emission callback +Object::SignalEmissionCallback Object::signal_emission_callback = nullptr; +bool Object::signal_emission_callback_enabled = false; + #ifdef DEBUG_ENABLED struct _ObjectDebugLock { @@ -1237,6 +1241,12 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int return ERR_UNAVAILABLE; } + // STEP 1: Signal emission hook - Call the registered callback if enabled + // This allows editor tools like the Signal Viewer to track signal emissions globally + if (signal_emission_callback_enabled && signal_emission_callback) { + signal_emission_callback(this, p_name, p_args, p_argcount); + } + if (s->slot_map.size() > MAX_SLOTS_ON_STACK) { slot_callables = (Callable *)memalloc(sizeof(Callable) * s->slot_map.size()); slot_flags = (uint32_t *)memalloc(sizeof(uint32_t) * s->slot_map.size()); diff --git a/core/object/object.h b/core/object/object.h index e64aa13180a..8dd320534d2 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -791,6 +791,29 @@ class Object { public: static constexpr bool _class_is_enabled = true; + // Signal tracking callback for editor tools (e.g., Signal Viewer) + // This callback is invoked whenever a signal is emitted, allowing tools to track signal activity + // The callback is only active when explicitly registered to minimize performance impact + typedef void (*SignalEmissionCallback)(Object *p_emitter, const StringName &p_signal, const Variant **p_args, int p_argcount); + +private: + static SignalEmissionCallback signal_emission_callback; + static bool signal_emission_callback_enabled; + +public: + static void set_signal_emission_callback(SignalEmissionCallback p_callback) { + signal_emission_callback = p_callback; + signal_emission_callback_enabled = (p_callback != nullptr); + } + + static SignalEmissionCallback get_signal_emission_callback() { + return signal_emission_callback; + } + + static bool is_signal_emission_callback_enabled() { + return signal_emission_callback_enabled; + } + void notify_property_list_changed(); static void *get_class_ptr_static() { diff --git a/editor/SCsub b/editor/SCsub index 6e2f1f2a966..5d7cb6016bc 100644 --- a/editor/SCsub +++ b/editor/SCsub @@ -87,6 +87,7 @@ if env.editor_build: env.add_source_files(env.editor_sources, "*.cpp") env.add_source_files(env.editor_sources, gen_exporters) + SConscript("animation/SCsub") SConscript("asset_library/SCsub") SConscript("audio/SCsub") @@ -111,5 +112,6 @@ if env.editor_build: SConscript("translations/SCsub") SConscript("version_control/SCsub") + lib = env.add_library("editor", env.editor_sources) env.Prepend(LIBS=[lib]) diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 552cf313d41..f3bb50675dd 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -43,6 +43,7 @@ #include "editor/debugger/editor_visual_profiler.h" #include "editor/docks/filesystem_dock.h" #include "editor/docks/inspector_dock.h" +#include "editor/docks/signalize_dock.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" @@ -489,6 +490,52 @@ void ScriptEditorDebugger::_msg_servers_drawn(uint64_t p_thread_id, const Array can_request_idle_draw = true; } +void ScriptEditorDebugger::_msg_signal_viewer_signal_emitted(uint64_t p_thread_id, const Array &p_data) { + // Forward signal emission data to SignalizeDock + // Data format: [emitter_id, node_name, node_class, signal_name, count, connections_array] + + if (p_data.size() < 6) { + print_line("[ScriptEditorDebugger] WARNING: Invalid signal_viewer data, expected 6 elements"); + return; + } + + // Extract the data + ObjectID emitter_id = p_data[0]; + String node_name = p_data[1]; + String node_class = p_data[2]; + String signal_name = p_data[3]; + int count = p_data[4]; + Array connections = p_data[5]; + + // Get SignalizeDock singleton and update it + SignalizeDock *signal_viewer = SignalizeDock::get_singleton(); + if (signal_viewer) { + signal_viewer->_on_runtime_signal_emitted(emitter_id, node_name, node_class, signal_name, count, connections); + } else { + print_line("[ScriptEditorDebugger] WARNING: No SignalizeDock singleton"); + } +} + +void ScriptEditorDebugger::_msg_signal_viewer_node_signal_data(uint64_t p_thread_id, const Array &p_data) { + // Forward node signal data response to SignalizeDock + // Data format: [node_id, node_name, node_class, [signals_data]] + + if (p_data.is_empty()) { + print_line("[ScriptEditorDebugger] WARNING: Invalid node_signal_data, empty array"); + return; + } + + print_line(vformat("[ScriptEditorDebugger] Received node signal data with %d elements", p_data.size())); + + // Get SignalizeDock singleton and forward the data + SignalizeDock *signal_viewer = SignalizeDock::get_singleton(); + if (signal_viewer) { + signal_viewer->_on_node_signal_data_received(p_data); + } else { + print_line("[ScriptEditorDebugger] WARNING: No SignalizeDock singleton for node_signal_data"); + } +} + void ScriptEditorDebugger::_msg_stack_dump(uint64_t p_thread_id, const Array &p_data) { DebuggerMarshalls::ScriptStackDump stack; stack.deserialize(p_data); @@ -942,14 +989,28 @@ void ScriptEditorDebugger::_msg_embed_next_frame(uint64_t p_thread_id, const Arr void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread_id, const Array &p_data) { emit_signal(SNAME("debug_data"), p_msg, p_data); + // DEBUG: Log signal_viewer messages + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] Received message: '%s'", p_msg)); + } + ParseMessageFunc *fn_ptr = parse_message_handlers.getptr(p_msg); if (fn_ptr) { + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] Found handler for: '%s'", p_msg)); + } (this->**fn_ptr)(p_thread_id, p_data); } else { + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] No handler found for: '%s', trying plugins_capture", p_msg)); + } int colon_index = p_msg.find_char(':'); ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received"); bool parsed = EditorDebuggerNode::get_singleton()->plugins_capture(this, p_msg, p_data); + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] plugins_capture returned: %s", parsed ? "TRUE" : "FALSE")); + } if (!parsed) { WARN_PRINT("Unknown message: " + p_msg); } @@ -970,6 +1031,8 @@ void ScriptEditorDebugger::_init_parse_message_handlers() { #endif // DISABLE_DEPRECATED parse_message_handlers["servers:memory_usage"] = &ScriptEditorDebugger::_msg_servers_memory_usage; parse_message_handlers["servers:drawn"] = &ScriptEditorDebugger::_msg_servers_drawn; + parse_message_handlers["signal_viewer:signal_emitted"] = &ScriptEditorDebugger::_msg_signal_viewer_signal_emitted; + parse_message_handlers["signal_viewer:node_signal_data"] = &ScriptEditorDebugger::_msg_signal_viewer_node_signal_data; parse_message_handlers["stack_dump"] = &ScriptEditorDebugger::_msg_stack_dump; parse_message_handlers["stack_frame_vars"] = &ScriptEditorDebugger::_msg_stack_frame_vars; parse_message_handlers["stack_frame_var"] = &ScriptEditorDebugger::_msg_stack_frame_var; diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index d0859d746c5..5c94356e2e2 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -210,6 +210,8 @@ class ScriptEditorDebugger : public MarginContainer { #endif // DISABLE_DEPRECATED void _msg_servers_memory_usage(uint64_t p_thread_id, const Array &p_data); void _msg_servers_drawn(uint64_t p_thread_id, const Array &p_data); + void _msg_signal_viewer_signal_emitted(uint64_t p_thread_id, const Array &p_data); + void _msg_signal_viewer_node_signal_data(uint64_t p_thread_id, const Array &p_data); void _msg_stack_dump(uint64_t p_thread_id, const Array &p_data); void _msg_stack_frame_vars(uint64_t p_thread_id, const Array &p_data); void _msg_stack_frame_var(uint64_t p_thread_id, const Array &p_data); @@ -275,7 +277,6 @@ class ScriptEditorDebugger : public MarginContainer { void _item_menu_id_pressed(int p_option); void _tab_changed(int p_tab); - void _put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID); void _export_csv(); void _clear_execution(); @@ -305,8 +306,12 @@ class ScriptEditorDebugger : public MarginContainer { void clear_inspector(bool p_send_msg = true); + // Send message to game process - made public for SignalizeDock + void _put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID); + // Needed by _live_edit_set, buttons state. void set_editor_remote_tree(const Tree *p_tree) { editor_remote_tree = p_tree; } + const Tree *get_editor_remote_tree() const { return editor_remote_tree; } void request_remote_tree(); const SceneDebuggerTree *get_remote_tree(); diff --git a/editor/docks/signalize_dock.cpp b/editor/docks/signalize_dock.cpp new file mode 100644 index 00000000000..be5ea8b2daf --- /dev/null +++ b/editor/docks/signalize_dock.cpp @@ -0,0 +1,3142 @@ +#include "signalize_dock.h" + +#include "editor/editor_node.h" +#include "editor/editor_interface.h" +#include "editor/settings/editor_settings.h" +#include "editor/inspector/editor_inspector.h" +#include "editor/debugger/editor_debugger_node.h" +#include "editor/debugger/script_editor_debugger.h" +#include "editor/run/editor_run_bar.h" +#include "editor/script/script_editor_plugin.h" +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/tree.h" +#include "core/debugger/engine_debugger.h" +#include "core/object/script_language.h" +#include "core/object/script_instance.h" +#include "core/variant/callable.h" +#include "core/input/input_event.h" +#include "core/object/class_db.h" +#include "scene/gui/control.h" +#include "scene/gui/label.h" +#include "scene/gui/color_picker.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/option_button.h" +#include "scene/resources/style_box_flat.h" +#include "scene/gui/box_container.h" +#include "scene/main/timer.h" +#include "editor/gui/window_wrapper.h" +#include "core/templates/hash_set.h" + +// Static member initialization +SignalizeDock *SignalizeDock::singleton_instance = nullptr; +const String SignalizeDock::MESSAGE_SIGNAL_EMITTED = "signal_viewer:signal_emitted"; +const String SignalizeDock::MESSAGE_NODE_SIGNAL_DATA = "signal_viewer:node_signal_data"; + +SignalizeDock::SignalizeDock() { + // Set singleton instance for global callback access + singleton_instance = this; + set_name("Signalize"); + set_h_size_flags(SIZE_EXPAND_FILL); + set_v_size_flags(SIZE_EXPAND_FILL); + + // Create WindowWrapper (not added yet - only added when floating) + window_wrapper = memnew(WindowWrapper); + window_wrapper->set_margins_enabled(true); + window_wrapper->set_window_title(TTR("Signalize - Signal Viewer")); + + // Create a content container that holds all the UI + // This container can be reparented between SignalizeDock (docked) and WindowWrapper (floating) + content_container = memnew(VBoxContainer); + content_container->set_h_size_flags(SIZE_EXPAND_FILL); + content_container->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(content_container); + + // Top bar with search, refresh, and floating button + HBoxContainer *top_bar = memnew(HBoxContainer); + content_container->add_child(top_bar); + + title_label = memnew(Label("Signalize")); + title_label->set_h_size_flags(SIZE_EXPAND_FILL); + top_bar->add_child(title_label); + + search_box = memnew(LineEdit); + search_box->set_placeholder("Filter nodes..."); + search_box->set_h_size_flags(SIZE_EXPAND_FILL); + search_box->connect("text_changed", callable_mp(this, &SignalizeDock::_on_search_changed)); + top_bar->add_child(search_box); + + refresh_button = memnew(Button); + refresh_button->set_text("Build Graph"); + refresh_button->set_tooltip_text("Rebuild the signal graph from the edited scene"); + refresh_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_refresh_pressed)); + top_bar->add_child(refresh_button); + + // Per-node inspection: Add button to inspect selected remote node + Button *inspect_button = memnew(Button); + inspect_button->set_text("Inspect Selected Node"); + // Note: Button doesn't have set_tooltip in Godot 4.x, tooltip is set via theme or custom logic + inspect_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_inspect_selected_button_pressed)); + top_bar->add_child(inspect_button); + + // Connection color picker + connection_color_button = memnew(ColorPickerButton); + + // Load saved color from editor settings + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + Variant saved_color = editor_settings->get("signalize/connection_color"); + if (saved_color.get_type() == Variant::COLOR) { + custom_connection_color = saved_color; + } + + // Load verbosity level from editor settings + Variant saved_verbosity = editor_settings->get("signalize/verbosity_level"); + if (saved_verbosity.get_type() == Variant::INT) { + verbosity_level = saved_verbosity; + } + } + + connection_color_button->set_pick_color(custom_connection_color); + connection_color_button->set_tooltip_text("Connection line color"); + connection_color_button->connect("color_changed", callable_mp(this, &SignalizeDock::_on_connection_color_changed)); + top_bar->add_child(connection_color_button); + + // Settings button + settings_button = memnew(Button); + settings_button->set_theme_type_variation(SceneStringName(FlatButton)); + settings_button->set_tooltip_text("Signalize Settings"); + settings_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_settings_pressed)); + top_bar->add_child(settings_button); + + // Make Floating button (using icon like ScriptEditor) + make_floating_button = memnew(Button); + make_floating_button->set_theme_type_variation(SceneStringName(FlatButton)); + // Icon will be set in NOTIFICATION_THEME_CHANGED + make_floating_button->set_tooltip_text("Make Signalize floating (Alt+F)"); + make_floating_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_make_floating_pressed)); + top_bar->add_child(make_floating_button); + + // Graph view + graph_edit = memnew(GraphEdit); + graph_edit->set_h_size_flags(SIZE_EXPAND_FILL); + graph_edit->set_v_size_flags(SIZE_EXPAND_FILL); + graph_edit->set_zoom(0.8); + graph_edit->set_show_zoom_label(true); + content_container->add_child(graph_edit); + + // Connect to play/stop signals to rebuild graph with runtime nodes + EditorRunBar *run_bar = EditorRunBar::get_singleton(); + if (run_bar) { + run_bar->connect("play_pressed", callable_mp(this, &SignalizeDock::_on_play_pressed)); + run_bar->connect("stop_pressed", callable_mp(this, &SignalizeDock::_on_stop_pressed)); + } else { + ERR_PRINT("[Signalize] Could not connect to EditorRunBar signals"); + } + + // Try to connect to debugger for message handling + EditorDebuggerNode *debugger_node = EditorDebuggerNode::get_singleton(); + if (debugger_node) { + // Debugger node available + } + + // Create timer to check when game is running and send start_tracking message + game_start_check_timer = memnew(Timer); + game_start_check_timer->set_wait_time(0.5); // Check every 0.5 seconds + game_start_check_timer->set_one_shot(false); + game_start_check_timer->connect("timeout", callable_mp(this, &SignalizeDock::_on_game_start_check_timer_timeout)); + game_start_check_timer->set_autostart(false); + content_container->add_child(game_start_check_timer); + game_start_check_timer->set_process_internal(true); // Make sure timer processes + + // NOTE: Global signal tracking DISABLED by default + // We'll only enable it when a node is being inspected during gameplay + tracking_enabled = false; + was_playing_last_frame = false; + remote_scene_root_id = ObjectID(); // Initialize to invalid ID + + // Register inspector plugin to detect when nodes are clicked in remote tree + inspector_plugin = memnew(SignalizeInspectorPlugin); + inspector_plugin->set_signal_viewer_dock(this); + EditorInspector::add_inspector_plugin(inspector_plugin); + + // Register message capture to receive signal emissions from game process + if (EngineDebugger::get_singleton()) { + EngineDebugger::register_message_capture("signal_viewer", EngineDebugger::Capture(nullptr, _capture_signal_viewer_messages)); + // Also register for "scene" messages to detect node selection in remote tree + EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, _capture_signal_viewer_messages)); + } + + // Build initial graph from edited scene (only works when game is not running) + _build_graph(); +} + +void SignalizeDock::_on_test_signal() { + // Test handler - can be removed in production +} + +void SignalizeDock::_on_refresh_pressed() { + EditorDebuggerNode *debugger_node = EditorDebuggerNode::get_singleton(); + ScriptEditorDebugger *debugger = nullptr; + + if (debugger_node) { + debugger = debugger_node->get_current_debugger(); + } + + // If game is running, don't allow full graph refresh + if (debugger && debugger->is_session_active()) { + ERR_PRINT("[Signalize] Cannot refresh full graph while game is running. Use 'Inspect Selected Node' instead."); + return; + } + + // Clear the existing graph and rebuild from the edited scene + _clear_inspection(); + _build_graph(); +} + +void SignalizeDock::_on_make_floating_pressed() { + if (!window_wrapper || !content_container) { + return; + } + + if (!is_floating) { + // Create a shortcut for toggling floating + Ref make_floating_shortcut; + make_floating_shortcut.instantiate(); + make_floating_shortcut->set_name("signalize/make_floating"); + + Ref key_event; + key_event.instantiate(); + key_event->set_keycode(Key::F); + key_event->set_alt_pressed(true); + + Array events; + events.push_back(key_event); + make_floating_shortcut->set_events(events); + + // Reparent content_container from SignalizeDock to WindowWrapper + remove_child(content_container); + window_wrapper->set_wrapped_control(content_container, make_floating_shortcut); + + // Add WindowWrapper to the scene tree as a child of SignalizeDock's parent + if (get_parent()) { + get_parent()->add_child(window_wrapper); + } + + // Enable floating mode + window_wrapper->set_window_enabled(true); + is_floating = true; + } else { + // Disable floating mode first + window_wrapper->set_window_enabled(false); + + // Reparent content_container back to SignalizeDock + window_wrapper->release_wrapped_control(); + add_child(content_container); + + // Remove WindowWrapper from scene tree + if (window_wrapper->get_parent()) { + window_wrapper->get_parent()->remove_child(window_wrapper); + } + + is_floating = false; + } +} + +void SignalizeDock::_on_search_changed(const String &p_text) { + // Show/hide nodes based on search + String search_lower = p_text.to_lower(); + + for (const KeyValue &kv : node_graph_nodes) { + GraphNode *gn = kv.value; + if (!gn) { + continue; + } + + String node_name = gn->get_title(); + bool visible = search_lower.is_empty() || node_name.to_lower().contains(search_lower); + gn->set_visible(visible); + } +} + +void SignalizeDock::_on_connection_color_changed(const Color &p_color) { + // Update the custom connection color + custom_connection_color = p_color; + + // Save to editor settings + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + editor_settings->set("signalize/connection_color", p_color); + editor_settings->save(); + } + + + // Note: We don't rebuild the graph here because: + // 1. Rebuilding triggers the color_changed signal again, creating an infinite loop + // 2. The new color will be applied automatically when the graph is next rebuilt for any reason + // 3. Connection highlights during runtime will use the new color immediately + // The user can force a rebuild by clicking the "Build Graph" button if they want to see the change immediately +} + +void SignalizeDock::_on_settings_pressed() { + // Create settings dialog if it doesn't exist + if (!settings_dialog) { + settings_dialog = memnew(AcceptDialog); + settings_dialog->set_title("Signalize Settings"); + settings_dialog->set_min_size(Size2(300, 100)); + add_child(settings_dialog); + + VBoxContainer *vbox = memnew(VBoxContainer); + settings_dialog->add_child(vbox); + + // Verbosity setting + HBoxContainer *verbosity_row = memnew(HBoxContainer); + vbox->add_child(verbosity_row); + + Label *verbosity_label = memnew(Label("Verbosity Level:")); + verbosity_label->set_h_size_flags(SIZE_EXPAND_FILL); + verbosity_row->add_child(verbosity_label); + + OptionButton *verbosity_option = memnew(OptionButton); + verbosity_option->add_item("Silent (errors only)", 0); + verbosity_option->add_item("Quiet (graph stats)", 1); + verbosity_option->add_item("Normal (inspector updates)", 2); + verbosity_option->add_item("Verbose (full output)", 3); + verbosity_option->select(verbosity_level); + verbosity_option->connect("item_selected", callable_mp(this, &SignalizeDock::_on_verbosity_changed)); + verbosity_row->add_child(verbosity_option); + + // Pulse duration setting + HBoxContainer *duration_row = memnew(HBoxContainer); + vbox->add_child(duration_row); + + Label *duration_label = memnew(Label("Connection Pulse Duration (seconds):")); + duration_label->set_h_size_flags(SIZE_EXPAND_FILL); + duration_row->add_child(duration_label); + + SpinBox *duration_spin = memnew(SpinBox); + duration_spin->set_min(0.1); + duration_spin->set_max(10.0); + duration_spin->set_step(0.1); + duration_spin->set_value(connection_pulse_duration); + duration_spin->connect("value_changed", callable_mp(this, &SignalizeDock::_on_pulse_duration_changed)); + duration_row->add_child(duration_spin); + } + + settings_dialog->popup_centered(); +} + +void SignalizeDock::_on_pulse_duration_changed(double p_value) { + connection_pulse_duration = p_value; +} + +void SignalizeDock::_on_verbosity_changed(int p_level) { + verbosity_level = p_level; + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + editor_settings->set("signalize/verbosity_level", verbosity_level); + editor_settings->save(); + } +} + +void SignalizeDock::_on_open_function_button_pressed(ObjectID p_node_id, const String &p_method_name) { + // Get the node + Object *obj = ObjectDB::get_instance(p_node_id); + if (!obj) { + ERR_PRINT(vformat("[Signalize] Cannot open function: node not found (ID: %s)", String::num_uint64((uint64_t)p_node_id))); + return; + } + + Node *node = Object::cast_to(obj); + if (!node) { + ERR_PRINT(vformat("[Signalize] Cannot open function: object is not a Node")); + return; + } + + // Get the script attached to this node + Ref