MagicNStuff/source/addons/panku_console/modules/variable_tracker/module.gd
2025-02-25 22:07:11 +01:00

169 lines
5.6 KiB
GDScript

class_name PankuModuleVariableTracker extends PankuModule
## Module to register and update some common environments.
##
## On module startup current scene root node registered as 'current' environment var,
## for more convenient access from interactive shell. Module constantly monitoring node
## tree afterwards and auto rebind 'current' environment in case of current scene changed.
## Also all user autoload singletons registered with its root node names.
const PROJECT_AUTOLOAD_PREFIX := "autoload/"
const CURRENT_SCENE_ENV := "current"
const SHELL_MODULE_NAME := "interactive_shell"
const DEFAULT_TRACKING_DELAY := 0.5
const CURRENT_REGISTERED_TIP := "[tip] Node '%s' registered as current scene, you can access it by [b]%s[/b]."
const CURRENT_REMOVED_TIP := "[tip] No current scene found, [b]%s[/b] keyword is no longer available."
const USER_AUTOLOADS_TIP := "[tip] Accessible user singleton modules: [b]%s[/b]"
var _raw_exceptions_string: String = ""
var _nodes_exception_regexp: RegEx
var _reverse_root_nodes_order: bool
var _current_scene_root:Node
var _user_singleton_files := []
var _tween_loop:Tween
var _loop_call_back:CallbackTweener
func init_module():
get_module_opt().tracking_delay = load_module_data("tracking_delay", DEFAULT_TRACKING_DELAY)
_reverse_root_nodes_order = load_module_data("use_last_as_current", true)
_raw_exceptions_string = load_module_data("root_node_exceptions", _raw_exceptions_string)
await core.get_tree().process_frame # not sure if it is necessary
update_exceptions_regexp()
_update_project_singleton_files()
_setup_scene_root_tracker()
_check_autoloads()
# Build root node exceptions regular expression
func update_exceptions_regexp() -> void:
if _raw_exceptions_string.is_empty():
_nodes_exception_regexp = RegEx.new() # not valid expression
return
_nodes_exception_regexp = RegEx.create_from_string(_raw_exceptions_string)
if not _nodes_exception_regexp.is_valid():
push_error("Can't parse '%s' expression for variable tracker" % _raw_exceptions_string)
# Parse project setting and collect and autoload files.
func _update_project_singleton_files() -> void:
_user_singleton_files.clear()
for property in ProjectSettings.get_property_list():
if property.name.begins_with(PROJECT_AUTOLOAD_PREFIX):
_user_singleton_files.append(ProjectSettings.get_setting(property.name).trim_prefix("*"))
# Check if given node is autoload singleton.
func _is_singleton(node: Node) -> bool:
# Comparing scene file and script file with list of autoload files
# from project settings. I'm not sure that approach hundred percent perfect,
# but it works so far.
if node.scene_file_path in _user_singleton_files:
return true
var script = node.get_script()
if script and (script.get_path() in _user_singleton_files):
return true
return false
# Setup monitoring loop for current scene root node.
func _setup_scene_root_tracker() -> void:
_check_current_scene()
# The whole idea looping something in the background
# while dev console is not even opened does not feel so right.
# Have no idea how to make it more elegant way,
# so lets make loop interval user controllable at least.
var tracking_delay = get_module_opt().tracking_delay
_tween_loop = core.create_tween()
_loop_call_back = _tween_loop.set_loops().tween_callback(_check_current_scene).set_delay(tracking_delay)
## Set current scene root node monitoring interval.
func change_tracking_delay(delay: float) -> void:
if _loop_call_back:
_loop_call_back.set_delay(delay)
# Update current scene root node environment.
func _check_current_scene() -> void:
var scene_root_found: Node = get_scene_root()
if scene_root_found:
if scene_root_found != _current_scene_root:
core.gd_exprenv.register_env(CURRENT_SCENE_ENV, scene_root_found)
_print_to_interactive_shell(CURRENT_REGISTERED_TIP % [scene_root_found.name, CURRENT_SCENE_ENV])
else:
if _current_scene_root:
core.gd_exprenv.remove_env(CURRENT_SCENE_ENV)
_print_to_interactive_shell(CURRENT_REMOVED_TIP % CURRENT_SCENE_ENV)
_current_scene_root = scene_root_found
## Find the root node of current active scene.
func get_scene_root() -> Node:
# Assuming current scene is the first node in tree that is not autoload singleton.
for node in _get_valid_root_nodes():
if not _is_singleton(node):
return node
return null
# Get list of tree root nodes filtered and sorted according module settings
func _get_valid_root_nodes() -> Array:
var nodes: Array = core.get_tree().root.get_children().filter(_root_nodes_filter)
if _reverse_root_nodes_order:
nodes.reverse()
return nodes
# Filter function for tree root nodes
func _root_nodes_filter(node: Node) -> bool:
# skip panku plugin itself
if node.name == core.SingletonName:
return false
# skip user defined exceptions
if _nodes_exception_regexp.is_valid() and _nodes_exception_regexp.search(node.name):
return false
return true
# Find all autoload singletons and bind its to environment vars.
func _check_autoloads() -> void:
var _user_singleton_names := []
for node in _get_valid_root_nodes():
if _is_singleton(node):
# register user singleton
_user_singleton_names.append(node.name)
core.gd_exprenv.register_env(node.name, node)
if not _user_singleton_names.is_empty():
_print_to_interactive_shell(USER_AUTOLOADS_TIP % ",".join(_user_singleton_names))
# Print a tip to interactive shell module, modules load order does matter.
func _print_to_interactive_shell(message: String) -> void:
if core.module_manager.has_module(SHELL_MODULE_NAME):
var ishell = core.module_manager.get_module(SHELL_MODULE_NAME)
ishell.interactive_shell.output(message)
func quit_module():
_tween_loop.kill()
super.quit_module()