281 lines
7.8 KiB
GDScript
281 lines
7.8 KiB
GDScript
@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
|