godot-parkour/addons/godot_state_charts/utilities/state_chart_debugger.gd

281 lines
7.8 KiB
GDScript3
Raw Normal View History

2024-02-23 20:37:00 +00:00
@icon("state_chart_debugger.svg")
extends Control
const DebuggerHistory = preload("debugger_history.gd")
## Whether or not the debugger is enabled.
@export var enabled:bool = true:
set(value):
enabled = value
if not Engine.is_editor_hint():
_setup_processing(enabled)
## The initial node that should be watched. Optional, if not set
## then no node will be watched. You can set the node that should
## be watched at runtime by calling debug_node().
@export var initial_node_to_watch:NodePath
## Maximum lines to display in the history. Keep at 300 or below
## for best performance.
@export var maximum_lines:int = 300
## If set to true, events will not be printed in the history panel.
## If you send a large amount of events then this may clutter the
## output so you can disable it here.
@export var ignore_events:bool = false
## If set to true, state changes will not be printed in the history
## panel. If you have a large amount of state changes, this may clutter
## the output so you can disable it here.
@export var ignore_state_changes:bool = false
## If set to true, transitions will not be printed in the history.
@export var ignore_transitions:bool = false
## The tree that shows the state chart.
@onready var _tree:Tree = %Tree
## The text field with the history.
@onready var _history_edit:TextEdit = %HistoryEdit
# the state chart we track
var _state_chart:StateChart
var _root:Node
# the states we are currently connected to
var _connected_states:Array[State] = []
# the transitions we are currently connected to
# key is the transition, value is the callable
var _connected_transitions:Dictionary = {}
# the debugger history in text form
var _history:DebuggerHistory = null
func _ready():
# always run, even if the game is paused
process_mode = Node.PROCESS_MODE_ALWAYS
# initialize the buffer
_history = DebuggerHistory.new(maximum_lines)
%CopyToClipboardButton.pressed.connect(func (): DisplayServer.clipboard_set(_history_edit.text))
%ClearButton.pressed.connect(_clear_history)
var to_watch = get_node_or_null(initial_node_to_watch)
if is_instance_valid(to_watch):
debug_node(to_watch)
# mirror the editor settings
%IgnoreEventsCheckbox.set_pressed_no_signal(ignore_events)
%IgnoreStateChangesCheckbox.set_pressed_no_signal(ignore_state_changes)
%IgnoreTransitionsCheckbox.set_pressed_no_signal(ignore_transitions)
## Adds an item to the history list.
func add_history_entry(text:String):
_history.add_history_entry(Engine.get_process_frames(), text)
## Sets up the debugger to track the given state chart. If the given node is not
## a state chart, it will search the children for a state chart. If no state chart
## is found, the debugger will be disabled.
func debug_node(root:Node) -> bool:
# if we are not enabled, we do nothing
if not enabled:
return false
_root = root
# disconnect all existing signals
_disconnect_all_signals()
var success = _debug_node(root)
# if we have no success, we disable the debugger
if not success:
push_warning("No state chart found. Disabling debugger.")
_setup_processing(false)
_state_chart = null
else:
# find all state nodes below the state chart and connect their signals
_connect_all_signals()
_clear_history()
_setup_processing(true)
return success
func _debug_node(root:Node) -> bool:
# if we have no root, we use the scene root
if not is_instance_valid(root):
return false
if root is StateChart:
_state_chart = root
return true
# no luck, search the children
for child in root.get_children():
if _debug_node(child):
# found one, return
return true
# no luck, return false
return false
func _setup_processing(enabled:bool):
process_mode = Node.PROCESS_MODE_ALWAYS if enabled else Node.PROCESS_MODE_DISABLED
visible = enabled
## Disconnects all signals from the currently connected states.
func _disconnect_all_signals():
if is_instance_valid(_state_chart):
if not ignore_events:
_state_chart.event_received.disconnect(_on_event_received)
for state in _connected_states:
# in case the state has been destroyed meanwhile
if is_instance_valid(state):
state.state_entered.disconnect(_on_state_entered)
state.state_exited.disconnect(_on_state_exited)
for transition in _connected_transitions.keys():
# in case the transition has been destroyed meanwhile
if is_instance_valid(transition):
transition.taken.disconnect(_connected_transitions.get(transition))
## Connects all signals from the currently processing state chart
func _connect_all_signals():
_connected_states.clear()
_connected_transitions.clear()
if not is_instance_valid(_state_chart):
return
_state_chart.event_received.connect(_on_event_received)
# find all state nodes below the state chart and connect their signals
for child in _state_chart.get_children():
if child is State:
_connect_signals(child)
func _connect_signals(state:State):
state.state_entered.connect(_on_state_entered.bind(state))
state.state_exited.connect(_on_state_exited.bind(state))
_connected_states.append(state)
# recurse into children
for child in state.get_children():
if child is State:
_connect_signals(child)
if child is Transition:
var callable = _on_before_transition.bind(child, state)
child.taken.connect(callable)
_connected_transitions[child] = callable
func _process(delta):
# Clear contents
_tree.clear()
if not is_instance_valid(_state_chart):
return
var root = _tree.create_item()
root.set_text(0, _root.name)
# walk over the state chart and find all active states
_collect_active_states(_state_chart, root )
# also show the values of all variables
var items = _state_chart._expression_properties.keys()
if items.size() <= 0:
return # nothing to show
# sort by name so it doesn't flicker all the time
items.sort()
var properties_root = root.create_child()
properties_root.set_text(0, "< Expression properties >")
for item in items:
var value = str(_state_chart._expression_properties.get(item))
var property_line = properties_root.create_child()
property_line.set_text(0, "%s = %s" % [item, value])
func _collect_active_states(root:Node, parent:TreeItem):
for child in root.get_children():
if child is State:
if child.active:
var state_item = _tree.create_item(parent)
state_item.set_text(0, child.name)
if is_instance_valid(child._pending_transition):
var transition_item = state_item.create_child()
transition_item.set_text(0, ">> %s (%.2f)" % [child._pending_transition.name, child._pending_transition_time])
_collect_active_states(child, state_item)
func _clear_history():
_history_edit.text = ""
_history.clear()
func _on_before_transition(transition:Transition, source:State):
if ignore_transitions:
return
_history.add_transition(Engine.get_process_frames(), transition.name, _state_chart.get_path_to(source), _state_chart.get_path_to(transition.resolve_target()))
func _on_event_received(event:StringName):
if ignore_events:
return
_history.add_event(Engine.get_process_frames(), event)
func _on_state_entered(state:State):
if ignore_state_changes:
return
_history.add_state_entered(Engine.get_process_frames(), state.name)
func _on_state_exited(state:State):
if ignore_state_changes:
return
_history.add_state_exited(Engine.get_process_frames(), state.name)
func _on_timer_timeout():
# ignore the timer if the history edit isn't visible
if not _history_edit.visible or not _history.dirty:
return
# fill the history field
_history_edit.text = _history.get_history_text()
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
func _on_ignore_events_checkbox_toggled(button_pressed):
ignore_events = button_pressed
func _on_ignore_state_changes_checkbox_toggled(button_pressed):
ignore_state_changes = button_pressed
func _on_ignore_transitions_checkbox_toggled(button_pressed):
ignore_transitions = button_pressed