generated from mstar/godot-template
762 lines
26 KiB
GDScript3
762 lines
26 KiB
GDScript3
|
@tool
|
||
|
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd"
|
||
|
|
||
|
const StateMachine = preload("../src/states/StateMachine.gd")
|
||
|
const Transition = preload("../src/transitions/Transition.gd")
|
||
|
const State = preload("../src/states/State.gd")
|
||
|
const StateDirectory = preload("../src/StateDirectory.gd")
|
||
|
const StateNode = preload("state_nodes/StateNode.tscn")
|
||
|
const TransitionLine = preload("transition_editors/TransitionLine.tscn")
|
||
|
const StateNodeScript = preload("state_nodes/StateNode.gd")
|
||
|
const StateMachineEditorLayer = preload("StateMachineEditorLayer.gd")
|
||
|
const PathViewer = preload("PathViewer.gd")
|
||
|
|
||
|
signal inspector_changed(property) # Inform plugin to refresh inspector
|
||
|
signal debug_mode_changed(new_debug_mode)
|
||
|
|
||
|
const ENTRY_STATE_MISSING_MSG = {
|
||
|
"key": "entry_state_missing",
|
||
|
"text": "Entry State missing, it will never get started. Right-click -> \"Add Entry\"."
|
||
|
}
|
||
|
const EXIT_STATE_MISSING_MSG = {
|
||
|
"key": "exit_state_missing",
|
||
|
"text": "Exit State missing, it will never exit from nested state. Right-click -> \"Add Exit\"."
|
||
|
}
|
||
|
const DEBUG_MODE_MSG = {
|
||
|
"key": "debug_mode",
|
||
|
"text": "Debug Mode"
|
||
|
}
|
||
|
|
||
|
@onready var context_menu = $ContextMenu
|
||
|
@onready var state_node_context_menu = $StateNodeContextMenu
|
||
|
@onready var convert_to_state_confirmation = $ConvertToStateConfirmation
|
||
|
@onready var save_dialog = $SaveDialog
|
||
|
@onready var create_new_state_machine_container = $MarginContainer
|
||
|
@onready var create_new_state_machine = $MarginContainer/CreateNewStateMachine
|
||
|
@onready var param_panel = $ParametersPanel
|
||
|
var path_viewer = HBoxContainer.new()
|
||
|
var condition_visibility = TextureButton.new()
|
||
|
var unsaved_indicator = Label.new()
|
||
|
var message_box = VBoxContainer.new()
|
||
|
|
||
|
var editor_accent_color = Color.WHITE
|
||
|
var transition_arrow_icon
|
||
|
|
||
|
var undo_redo: EditorUndoRedoManager
|
||
|
|
||
|
var debug_mode: = false:
|
||
|
set = set_debug_mode
|
||
|
var state_machine_player:
|
||
|
set = set_state_machine_player
|
||
|
var state_machine:
|
||
|
set = set_state_machine
|
||
|
var can_gui_name_edit = true
|
||
|
var can_gui_context_menu = true
|
||
|
|
||
|
var _reconnecting_connection
|
||
|
var _last_index = 0
|
||
|
var _last_path = ""
|
||
|
var _message_box_dict = {}
|
||
|
var _context_node
|
||
|
var _current_state = ""
|
||
|
var _last_stack = []
|
||
|
|
||
|
|
||
|
func _init():
|
||
|
super._init()
|
||
|
|
||
|
path_viewer.mouse_filter = MOUSE_FILTER_IGNORE
|
||
|
path_viewer.set_script(PathViewer)
|
||
|
path_viewer.dir_pressed.connect(_on_path_viewer_dir_pressed)
|
||
|
top_bar.add_child(path_viewer)
|
||
|
|
||
|
condition_visibility.tooltip_text = "Hide/Show Conditions on Transition Line"
|
||
|
condition_visibility.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
|
||
|
condition_visibility.toggle_mode = true
|
||
|
condition_visibility.size_flags_vertical = SIZE_SHRINK_CENTER
|
||
|
condition_visibility.focus_mode = FOCUS_NONE
|
||
|
condition_visibility.pressed.connect(_on_condition_visibility_pressed)
|
||
|
condition_visibility.button_pressed = true
|
||
|
gadget.add_child(condition_visibility)
|
||
|
|
||
|
unsaved_indicator.size_flags_vertical = SIZE_SHRINK_CENTER
|
||
|
unsaved_indicator.focus_mode = FOCUS_NONE
|
||
|
gadget.add_child(unsaved_indicator)
|
||
|
|
||
|
message_box.set_anchors_and_offsets_preset(PRESET_BOTTOM_WIDE)
|
||
|
message_box.grow_vertical = GROW_DIRECTION_BEGIN
|
||
|
add_child(message_box)
|
||
|
|
||
|
content.get_child(0).name = "root"
|
||
|
|
||
|
set_process(false)
|
||
|
|
||
|
func _ready():
|
||
|
create_new_state_machine_container.visible = false
|
||
|
create_new_state_machine.pressed.connect(_on_create_new_state_machine_pressed)
|
||
|
context_menu.index_pressed.connect(_on_context_menu_index_pressed)
|
||
|
state_node_context_menu.index_pressed.connect(_on_state_node_context_menu_index_pressed)
|
||
|
convert_to_state_confirmation.confirmed.connect(_on_convert_to_state_confirmation_confirmed)
|
||
|
save_dialog.confirmed.connect(_on_save_dialog_confirmed)
|
||
|
|
||
|
func _process(delta):
|
||
|
if not debug_mode:
|
||
|
set_process(false)
|
||
|
return
|
||
|
if not is_instance_valid(state_machine_player):
|
||
|
set_process(false)
|
||
|
set_debug_mode(false)
|
||
|
return
|
||
|
var stack = state_machine_player.get("Members/StackPlayer.gd/stack")
|
||
|
if ((stack == []) or (stack==null)):
|
||
|
set_process(false)
|
||
|
set_debug_mode(false)
|
||
|
return
|
||
|
|
||
|
if stack.size() == 1:
|
||
|
set_current_state(state_machine_player.get("Members/StackPlayer.gd/current"))
|
||
|
else:
|
||
|
var stack_max_index = stack.size() - 1
|
||
|
var prev_index = stack.find(_current_state)
|
||
|
if prev_index == -1:
|
||
|
if _last_stack.size() < stack.size():
|
||
|
# Reproduce transition, for example:
|
||
|
# [Entry, Idle, Walk]
|
||
|
# [Entry, Idle, Jump, Fall]
|
||
|
# Walk -> Idle
|
||
|
# Idle -> Jump
|
||
|
# Jump -> Fall
|
||
|
var common_index = -1
|
||
|
for i in _last_stack.size():
|
||
|
if _last_stack[i] == stack[i]:
|
||
|
common_index = i
|
||
|
break
|
||
|
if common_index > -1:
|
||
|
var count_from_last_stack = _last_stack.size()-1 - common_index -1
|
||
|
_last_stack.reverse()
|
||
|
# Transit back to common state
|
||
|
for i in count_from_last_stack:
|
||
|
set_current_state(_last_stack[i + 1])
|
||
|
# Transit to all missing state in current stack
|
||
|
for i in range(common_index + 1, stack.size()):
|
||
|
set_current_state(stack[i])
|
||
|
else:
|
||
|
set_current_state(stack.back())
|
||
|
else:
|
||
|
set_current_state(stack.back())
|
||
|
else:
|
||
|
# Set every skipped state
|
||
|
var missing_count = stack_max_index - prev_index
|
||
|
for i in range(1, missing_count + 1):
|
||
|
set_current_state(stack[prev_index + i])
|
||
|
_last_stack = stack
|
||
|
var params = state_machine_player.get("Members/_parameters")
|
||
|
var local_params = state_machine_player.get("Members/_local_parameters")
|
||
|
if params == null:
|
||
|
params = state_machine_player.get("Members/StateMachinePlayer.gd/_parameters")
|
||
|
local_params = state_machine_player.get("Members/StateMachinePlayer.gd/_local_parameters")
|
||
|
param_panel.update_params(params, local_params)
|
||
|
get_focused_layer(_current_state).debug_update(_current_state, params, local_params)
|
||
|
|
||
|
func _on_path_viewer_dir_pressed(dir, index):
|
||
|
var path = path_viewer.select_dir(dir)
|
||
|
select_layer(get_layer(path))
|
||
|
|
||
|
if _last_index > index:
|
||
|
# Going backward
|
||
|
var end_state_parent_path = StateMachinePlayer.path_backward(_last_path)
|
||
|
var end_state_name = StateMachinePlayer.path_end_dir(_last_path)
|
||
|
var layer = content.get_node_or_null(NodePath(end_state_parent_path))
|
||
|
if layer:
|
||
|
var node = layer.content_nodes.get_node_or_null(NodePath(end_state_name))
|
||
|
if node:
|
||
|
var cond_1 = (not ("states" in node.state)) or (node.state.states=={}) # states property not defined or empty
|
||
|
# Now check if, for some reason, there are an Entry and/or an Exit node inside this node
|
||
|
# not registered in the states variable above.
|
||
|
var nested_layer = content.get_node_or_null(NodePath(_last_path))
|
||
|
var cond_2 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.ENTRY_STATE)) == null) # there is no entry state in the node
|
||
|
var cond_3 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.EXIT_STATE)) == null) # there is no exit state in the node
|
||
|
if (cond_1 and cond_2 and cond_3):
|
||
|
# Convert state machine node back to state node
|
||
|
convert_to_state(layer, node)
|
||
|
|
||
|
_last_index = index
|
||
|
_last_path = path
|
||
|
|
||
|
func _on_context_menu_index_pressed(index):
|
||
|
var new_node = StateNode.instantiate()
|
||
|
new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color
|
||
|
match index:
|
||
|
0: # Add State
|
||
|
## Handle state name duplication (4.x changed how duplicates are
|
||
|
## automatically handled and gave a random index instead of
|
||
|
## a progressive one)
|
||
|
var default_new_state_name = "State"
|
||
|
var state_dup_index = 0
|
||
|
var new_name = default_new_state_name
|
||
|
for state_name in current_layer.state_machine.states:
|
||
|
if (state_name == new_name):
|
||
|
state_dup_index += 1
|
||
|
new_name = "%s%s" % [default_new_state_name, state_dup_index]
|
||
|
new_node.name = new_name
|
||
|
1: # Add Entry
|
||
|
if State.ENTRY_STATE in current_layer.state_machine.states:
|
||
|
push_warning("Entry node already exist")
|
||
|
return
|
||
|
new_node.name = State.ENTRY_STATE
|
||
|
2: # Add Exit
|
||
|
if State.EXIT_STATE in current_layer.state_machine.states:
|
||
|
push_warning("Exit node already exist")
|
||
|
return
|
||
|
new_node.name = State.EXIT_STATE
|
||
|
new_node.position = content_position(get_local_mouse_position())
|
||
|
add_node(current_layer, new_node)
|
||
|
|
||
|
func _on_state_node_context_menu_index_pressed(index):
|
||
|
if not _context_node:
|
||
|
return
|
||
|
|
||
|
match index:
|
||
|
0: # Copy
|
||
|
_copying_nodes = [_context_node]
|
||
|
_context_node = null
|
||
|
1: # Duplicate
|
||
|
duplicate_nodes(current_layer, [_context_node])
|
||
|
_context_node = null
|
||
|
2: # Delete
|
||
|
remove_node(current_layer, _context_node.name)
|
||
|
for connection_pair in current_layer.get_connection_list():
|
||
|
if connection_pair.from == _context_node.name or connection_pair.to == _context_node.name:
|
||
|
disconnect_node(current_layer, connection_pair.from, connection_pair.to).queue_free()
|
||
|
_context_node = null
|
||
|
3: # Separator
|
||
|
_context_node = null
|
||
|
4: # Convert
|
||
|
convert_to_state_confirmation.popup_centered()
|
||
|
|
||
|
func _on_convert_to_state_confirmation_confirmed():
|
||
|
convert_to_state(current_layer, _context_node)
|
||
|
_context_node.queue_redraw() # Update outlook of node
|
||
|
# Remove layer
|
||
|
var path = str(path_viewer.get_cwd(), "/", _context_node.name)
|
||
|
var layer = get_layer(path)
|
||
|
if layer:
|
||
|
layer.queue_free()
|
||
|
_context_node = null
|
||
|
|
||
|
func _on_save_dialog_confirmed():
|
||
|
save()
|
||
|
|
||
|
func _on_create_new_state_machine_pressed():
|
||
|
var new_state_machine = StateMachine.new()
|
||
|
|
||
|
undo_redo.create_action("Create New StateMachine")
|
||
|
|
||
|
undo_redo.add_do_reference(new_state_machine)
|
||
|
undo_redo.add_do_property(state_machine_player, "state_machine", new_state_machine)
|
||
|
undo_redo.add_do_method(self, "set_state_machine", new_state_machine)
|
||
|
undo_redo.add_do_property(create_new_state_machine_container, "visible", false)
|
||
|
undo_redo.add_do_method(self, "check_has_entry")
|
||
|
undo_redo.add_do_method(self, "emit_signal", "inspector_changed", "state_machine")
|
||
|
|
||
|
undo_redo.add_undo_property(state_machine_player, "state_machine", null)
|
||
|
undo_redo.add_undo_method(self, "set_state_machine", null)
|
||
|
undo_redo.add_undo_property(create_new_state_machine_container, "visible", true)
|
||
|
undo_redo.add_undo_method(self, "check_has_entry")
|
||
|
undo_redo.add_undo_method(self, "emit_signal", "inspector_changed", "state_machine")
|
||
|
|
||
|
undo_redo.commit_action()
|
||
|
|
||
|
func _on_condition_visibility_pressed():
|
||
|
for line in current_layer.content_lines.get_children():
|
||
|
line.vbox.visible = condition_visibility.button_pressed
|
||
|
|
||
|
func _on_debug_mode_changed(new_debug_mode):
|
||
|
if new_debug_mode:
|
||
|
param_panel.show()
|
||
|
add_message(DEBUG_MODE_MSG.key, DEBUG_MODE_MSG.text)
|
||
|
set_process(true)
|
||
|
# mouse_filter = MOUSE_FILTER_IGNORE
|
||
|
can_gui_select_node = false
|
||
|
can_gui_delete_node = false
|
||
|
can_gui_connect_node = false
|
||
|
can_gui_name_edit = false
|
||
|
can_gui_context_menu = false
|
||
|
else:
|
||
|
param_panel.clear_params()
|
||
|
param_panel.hide()
|
||
|
remove_message(DEBUG_MODE_MSG.key)
|
||
|
set_process(false)
|
||
|
can_gui_select_node = true
|
||
|
can_gui_delete_node = true
|
||
|
can_gui_connect_node = true
|
||
|
can_gui_name_edit = true
|
||
|
can_gui_context_menu = true
|
||
|
|
||
|
func _on_state_machine_player_changed(new_state_machine_player):
|
||
|
if not state_machine_player:
|
||
|
return
|
||
|
if new_state_machine_player.get_class() == "EditorDebuggerRemoteObject":
|
||
|
return
|
||
|
|
||
|
if new_state_machine_player:
|
||
|
create_new_state_machine_container.visible = !new_state_machine_player.state_machine
|
||
|
else:
|
||
|
create_new_state_machine_container.visible = false
|
||
|
|
||
|
func _on_state_machine_changed(new_state_machine):
|
||
|
var root_layer = get_layer("root")
|
||
|
path_viewer.select_dir("root") # Before select_layer, so path_viewer will be updated in _on_layer_selected
|
||
|
select_layer(root_layer)
|
||
|
clear_graph(root_layer)
|
||
|
# Reset layers & path viewer
|
||
|
for child in root_layer.get_children():
|
||
|
if child is FlowChartLayer:
|
||
|
root_layer.remove_child(child)
|
||
|
child.queue_free()
|
||
|
if new_state_machine:
|
||
|
root_layer.state_machine = state_machine
|
||
|
var validated = StateMachine.validate(new_state_machine)
|
||
|
if validated:
|
||
|
print_debug("gd-YAFSM: Corrupted StateMachine Resource fixed, save to apply the fix.")
|
||
|
draw_graph(root_layer)
|
||
|
check_has_entry()
|
||
|
|
||
|
func _gui_input(event):
|
||
|
super._gui_input(event)
|
||
|
|
||
|
if event is InputEventMouseButton:
|
||
|
match event.button_index:
|
||
|
MOUSE_BUTTON_RIGHT:
|
||
|
if event.pressed and can_gui_context_menu:
|
||
|
context_menu.set_item_disabled(1, current_layer.state_machine.has_entry())
|
||
|
context_menu.set_item_disabled(2, current_layer.state_machine.has_exit())
|
||
|
context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position())
|
||
|
context_menu.popup()
|
||
|
|
||
|
func _input(event):
|
||
|
# Intercept save action
|
||
|
if visible:
|
||
|
if event is InputEventKey:
|
||
|
match event.keycode:
|
||
|
KEY_S:
|
||
|
if event.ctrl_pressed and event.pressed:
|
||
|
save_request()
|
||
|
|
||
|
func create_layer(node):
|
||
|
# Create/Move to new layer
|
||
|
var new_state_machine = convert_to_state_machine(current_layer, node)
|
||
|
# Determine current layer path
|
||
|
var parent_path = path_viewer.get_cwd()
|
||
|
var path = str(parent_path, "/", node.name)
|
||
|
var layer = get_layer(path)
|
||
|
path_viewer.add_dir(node.state.name) # Before select_layer, so path_viewer will be updated in _on_layer_selected
|
||
|
if not layer:
|
||
|
# New layer to spawn
|
||
|
layer = add_layer_to(get_layer(parent_path))
|
||
|
layer.name = node.state.name
|
||
|
layer.state_machine = new_state_machine
|
||
|
draw_graph(layer)
|
||
|
_last_index = path_viewer.get_child_count()-1
|
||
|
_last_path = path
|
||
|
return layer
|
||
|
|
||
|
func open_layer(path):
|
||
|
var dir = StateDirectory.new(path)
|
||
|
dir.goto(dir.get_end_index())
|
||
|
dir.back()
|
||
|
var next_layer = get_next_layer(dir, get_layer("root"))
|
||
|
select_layer(next_layer)
|
||
|
return next_layer
|
||
|
|
||
|
# Recursively get next layer
|
||
|
func get_next_layer(dir, base_layer):
|
||
|
var next_layer = base_layer
|
||
|
var np = dir.next()
|
||
|
if np:
|
||
|
next_layer = base_layer.get_node_or_null(NodePath(np))
|
||
|
if next_layer:
|
||
|
next_layer = get_next_layer(dir, next_layer)
|
||
|
else:
|
||
|
var to_dir = StateDirectory.new(dir.get_current())
|
||
|
to_dir.goto(to_dir.get_end_index())
|
||
|
to_dir.back()
|
||
|
var node = base_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end()))
|
||
|
next_layer = get_next_layer(dir, create_layer(node))
|
||
|
return next_layer
|
||
|
|
||
|
func get_focused_layer(state):
|
||
|
var current_dir = StateDirectory.new(state)
|
||
|
current_dir.goto(current_dir.get_end_index())
|
||
|
current_dir.back()
|
||
|
return get_layer(str("root/", current_dir.get_current()))
|
||
|
|
||
|
func _on_state_node_gui_input(event, node):
|
||
|
if node.state.is_entry() or node.state.is_exit():
|
||
|
return
|
||
|
|
||
|
if event is InputEventMouseButton:
|
||
|
match event.button_index:
|
||
|
MOUSE_BUTTON_LEFT:
|
||
|
if event.pressed:
|
||
|
if event.double_click:
|
||
|
if node.name_edit.get_rect().has_point(event.position) and can_gui_name_edit:
|
||
|
# Edit State name if within LineEdit
|
||
|
node.enable_name_edit(true)
|
||
|
accept_event()
|
||
|
else:
|
||
|
var layer = create_layer(node)
|
||
|
select_layer(layer)
|
||
|
accept_event()
|
||
|
MOUSE_BUTTON_RIGHT:
|
||
|
if event.pressed:
|
||
|
# State node context menu
|
||
|
_context_node = node
|
||
|
state_node_context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position())
|
||
|
state_node_context_menu.popup()
|
||
|
state_node_context_menu.set_item_disabled(4, not (node.state is StateMachine))
|
||
|
accept_event()
|
||
|
|
||
|
func convert_to_state_machine(layer, node):
|
||
|
# Convert State to StateMachine
|
||
|
var new_state_machine
|
||
|
if node.state is StateMachine:
|
||
|
new_state_machine = node.state
|
||
|
else:
|
||
|
new_state_machine = StateMachine.new()
|
||
|
new_state_machine.name = node.state.name
|
||
|
new_state_machine.graph_offset = node.state.graph_offset
|
||
|
layer.state_machine.remove_state(node.state.name)
|
||
|
layer.state_machine.add_state(new_state_machine)
|
||
|
node.state = new_state_machine
|
||
|
return new_state_machine
|
||
|
|
||
|
func convert_to_state(layer, node):
|
||
|
# Convert StateMachine to State
|
||
|
var new_state
|
||
|
if node.state is StateMachine:
|
||
|
new_state = State.new()
|
||
|
new_state.name = node.state.name
|
||
|
new_state.graph_offset = node.state.graph_offset
|
||
|
layer.state_machine.remove_state(node.state.name)
|
||
|
layer.state_machine.add_state(new_state)
|
||
|
node.state = new_state
|
||
|
else:
|
||
|
new_state = node.state
|
||
|
return new_state
|
||
|
|
||
|
func create_layer_instance():
|
||
|
var layer = Control.new()
|
||
|
layer.set_script(StateMachineEditorLayer)
|
||
|
layer.editor_accent_color = editor_accent_color
|
||
|
return layer
|
||
|
|
||
|
func create_line_instance():
|
||
|
var line = TransitionLine.instantiate()
|
||
|
line.theme.get_stylebox("focus", "FlowChartLine").shadow_color = editor_accent_color
|
||
|
line.theme.set_icon("arrow", "FlowChartLine", transition_arrow_icon)
|
||
|
return line
|
||
|
|
||
|
# Request to save current editing StateMachine
|
||
|
func save_request():
|
||
|
if not can_save():
|
||
|
return
|
||
|
|
||
|
save_dialog.dialog_text = "Saving StateMachine to %s" % state_machine.resource_path
|
||
|
save_dialog.popup_centered()
|
||
|
|
||
|
# Save current editing StateMachine
|
||
|
func save():
|
||
|
if not can_save():
|
||
|
return
|
||
|
|
||
|
unsaved_indicator.text = ""
|
||
|
ResourceSaver.save(state_machine, state_machine.resource_path)
|
||
|
|
||
|
# Clear editor
|
||
|
func clear_graph(layer):
|
||
|
clear_connections()
|
||
|
|
||
|
for child in layer.content_nodes.get_children():
|
||
|
if child is StateNodeScript:
|
||
|
layer.content_nodes.remove_child(child)
|
||
|
child.queue_free()
|
||
|
|
||
|
queue_redraw()
|
||
|
unsaved_indicator.text = "" # Clear graph is not action by user
|
||
|
|
||
|
# Intialize editor with current editing StateMachine
|
||
|
func draw_graph(layer):
|
||
|
for state_key in layer.state_machine.states.keys():
|
||
|
var state = layer.state_machine.states[state_key]
|
||
|
var new_node = StateNode.instantiate()
|
||
|
new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color
|
||
|
new_node.name = state_key # Set before add_node to let engine handle duplicate name
|
||
|
add_node(layer, new_node)
|
||
|
# Set after add_node to make sure UIs are initialized
|
||
|
new_node.state = state
|
||
|
new_node.state.name = state_key
|
||
|
new_node.position = state.graph_offset
|
||
|
for state_key in layer.state_machine.states.keys():
|
||
|
var from_transitions = layer.state_machine.transitions.get(state_key)
|
||
|
if from_transitions:
|
||
|
for transition in from_transitions.values():
|
||
|
connect_node(layer, transition.from, transition.to)
|
||
|
layer._connections[transition.from][transition.to].line.transition = transition
|
||
|
queue_redraw()
|
||
|
unsaved_indicator.text = "" # Draw graph is not action by user
|
||
|
|
||
|
# Add message to message_box(overlay text at bottom of editor)
|
||
|
func add_message(key, text):
|
||
|
var label = Label.new()
|
||
|
label.text = text
|
||
|
_message_box_dict[key] = label
|
||
|
message_box.add_child(label)
|
||
|
return label
|
||
|
|
||
|
# Remove message from message_box
|
||
|
func remove_message(key):
|
||
|
var control = _message_box_dict.get(key)
|
||
|
if control:
|
||
|
_message_box_dict.erase(key)
|
||
|
message_box.remove_child(control)
|
||
|
# Weird behavior of VBoxContainer, only sort children properly after changing grow_direction
|
||
|
message_box.grow_vertical = GROW_DIRECTION_END
|
||
|
message_box.grow_vertical = GROW_DIRECTION_BEGIN
|
||
|
return true
|
||
|
return false
|
||
|
|
||
|
# Check if current editing StateMachine has entry, warns user if entry state missing
|
||
|
func check_has_entry():
|
||
|
if not current_layer.state_machine:
|
||
|
return
|
||
|
if not current_layer.state_machine.has_entry():
|
||
|
if not (ENTRY_STATE_MISSING_MSG.key in _message_box_dict):
|
||
|
add_message(ENTRY_STATE_MISSING_MSG.key, ENTRY_STATE_MISSING_MSG.text)
|
||
|
else:
|
||
|
if ENTRY_STATE_MISSING_MSG.key in _message_box_dict:
|
||
|
remove_message(ENTRY_STATE_MISSING_MSG.key)
|
||
|
|
||
|
# Check if current editing StateMachine is nested and has exit, warns user if exit state missing
|
||
|
func check_has_exit():
|
||
|
if not current_layer.state_machine:
|
||
|
return
|
||
|
if not path_viewer.get_cwd() == "root": # Nested state
|
||
|
if not current_layer.state_machine.has_exit():
|
||
|
if not (EXIT_STATE_MISSING_MSG.key in _message_box_dict):
|
||
|
add_message(EXIT_STATE_MISSING_MSG.key, EXIT_STATE_MISSING_MSG.text)
|
||
|
return
|
||
|
if EXIT_STATE_MISSING_MSG.key in _message_box_dict:
|
||
|
remove_message(EXIT_STATE_MISSING_MSG.key)
|
||
|
|
||
|
func _on_layer_selected(layer):
|
||
|
if layer:
|
||
|
layer.show_content()
|
||
|
check_has_entry()
|
||
|
check_has_exit()
|
||
|
|
||
|
func _on_layer_deselected(layer):
|
||
|
if layer:
|
||
|
layer.hide_content()
|
||
|
|
||
|
func _on_node_dragged(layer, node, dragged):
|
||
|
node.state.graph_offset = node.position
|
||
|
_on_edited()
|
||
|
|
||
|
func _on_node_added(layer, new_node):
|
||
|
# Godot 4 duplicates node with an internal @ name, which breaks everything
|
||
|
while String(new_node.name).begins_with("@"):
|
||
|
new_node.name = String(new_node.name).lstrip("@")
|
||
|
|
||
|
new_node.undo_redo = undo_redo
|
||
|
new_node.state.name = new_node.name
|
||
|
new_node.state.graph_offset = new_node.position
|
||
|
new_node.name_edit_entered.connect(_on_node_name_edit_entered.bind(new_node))
|
||
|
new_node.gui_input.connect(_on_state_node_gui_input.bind(new_node))
|
||
|
layer.state_machine.add_state(new_node.state)
|
||
|
check_has_entry()
|
||
|
check_has_exit()
|
||
|
_on_edited()
|
||
|
|
||
|
func _on_node_removed(layer, node_name):
|
||
|
var path = str(path_viewer.get_cwd(), "/", node_name)
|
||
|
var layer_to_remove = get_layer(path)
|
||
|
if layer_to_remove:
|
||
|
layer_to_remove.get_parent().remove_child(layer_to_remove)
|
||
|
layer_to_remove.queue_free()
|
||
|
var result = layer.state_machine.remove_state(node_name)
|
||
|
check_has_entry()
|
||
|
check_has_exit()
|
||
|
_on_edited()
|
||
|
return result
|
||
|
|
||
|
func _on_node_connected(layer, from, to):
|
||
|
if _reconnecting_connection:
|
||
|
# Reconnection will trigger _on_node_connected after _on_node_reconnect_end/_on_node_reconnect_failed
|
||
|
if is_instance_valid(_reconnecting_connection.from_node) and \
|
||
|
_reconnecting_connection.from_node.name == from and \
|
||
|
is_instance_valid(_reconnecting_connection.to_node) and \
|
||
|
_reconnecting_connection.to_node.name == to:
|
||
|
_reconnecting_connection = null
|
||
|
return
|
||
|
if layer.state_machine.transitions.has(from):
|
||
|
if layer.state_machine.transitions[from].has(to):
|
||
|
return # Already existed as it is loaded from file
|
||
|
|
||
|
var line = layer._connections[from][to].line
|
||
|
var new_transition = Transition.new(from, to)
|
||
|
line.transition = new_transition
|
||
|
layer.state_machine.add_transition(new_transition)
|
||
|
clear_selection()
|
||
|
select(line)
|
||
|
_on_edited()
|
||
|
|
||
|
func _on_node_disconnected(layer, from, to):
|
||
|
layer.state_machine.remove_transition(from, to)
|
||
|
_on_edited()
|
||
|
|
||
|
func _on_node_reconnect_begin(layer, from, to):
|
||
|
_reconnecting_connection = layer._connections[from][to]
|
||
|
layer.state_machine.remove_transition(from, to)
|
||
|
|
||
|
func _on_node_reconnect_end(layer, from, to):
|
||
|
var transition = _reconnecting_connection.line.transition
|
||
|
transition.to = to
|
||
|
layer.state_machine.add_transition(transition)
|
||
|
clear_selection()
|
||
|
select(_reconnecting_connection.line)
|
||
|
|
||
|
func _on_node_reconnect_failed(layer, from, to):
|
||
|
var transition = _reconnecting_connection.line.transition
|
||
|
layer.state_machine.add_transition(transition)
|
||
|
clear_selection()
|
||
|
select(_reconnecting_connection.line)
|
||
|
|
||
|
func _request_connect_from(layer, from):
|
||
|
if from == State.EXIT_STATE:
|
||
|
return false
|
||
|
return true
|
||
|
|
||
|
func _request_connect_to(layer, to):
|
||
|
if to == State.ENTRY_STATE:
|
||
|
return false
|
||
|
return true
|
||
|
|
||
|
func _on_duplicated(layer, old_nodes, new_nodes):
|
||
|
# Duplicate condition as well
|
||
|
for i in old_nodes.size():
|
||
|
var from_node = old_nodes[i]
|
||
|
for connection_pair in get_connection_list():
|
||
|
if from_node.name == connection_pair.from:
|
||
|
for j in old_nodes.size():
|
||
|
var to_node = old_nodes[j]
|
||
|
if to_node.name == connection_pair.to:
|
||
|
var old_connection = layer._connections[connection_pair.from][connection_pair.to]
|
||
|
var new_connection = layer._connections[new_nodes[i].name][new_nodes[j].name]
|
||
|
for condition in old_connection.line.transition.conditions.values():
|
||
|
new_connection.line.transition.add_condition(condition.duplicate())
|
||
|
_on_edited()
|
||
|
|
||
|
func _on_node_name_edit_entered(new_name, node):
|
||
|
var old = node.state.name
|
||
|
var new = new_name
|
||
|
if old == new:
|
||
|
return
|
||
|
if "/" in new or "\\" in new: # No back/forward-slash
|
||
|
push_warning("Illegal State Name: / and \\ are not allowed in State name(%s)" % new)
|
||
|
node.name_edit.text = old
|
||
|
return
|
||
|
|
||
|
if current_layer.state_machine.change_state_name(old, new):
|
||
|
rename_node(current_layer, node.name, new)
|
||
|
node.name = new
|
||
|
# Rename layer as well
|
||
|
var path = str(path_viewer.get_cwd(), "/", node.name)
|
||
|
var layer = get_layer(path)
|
||
|
if layer:
|
||
|
layer.name = new
|
||
|
for child in path_viewer.get_children():
|
||
|
if child.text == old:
|
||
|
child.text = new
|
||
|
break
|
||
|
_on_edited()
|
||
|
else:
|
||
|
node.name_edit.text = old
|
||
|
|
||
|
func _on_edited():
|
||
|
unsaved_indicator.text = "*"
|
||
|
|
||
|
func _on_remote_transited(from, to):
|
||
|
var from_dir = StateDirectory.new(from)
|
||
|
var to_dir = StateDirectory.new(to)
|
||
|
var focused_layer = get_focused_layer(from)
|
||
|
if from:
|
||
|
if focused_layer:
|
||
|
focused_layer.debug_transit_out(from, to)
|
||
|
if to:
|
||
|
if from_dir.is_nested() and from_dir.is_exit():
|
||
|
if focused_layer:
|
||
|
var path = path_viewer.back()
|
||
|
select_layer(get_layer(path))
|
||
|
elif to_dir.is_nested():
|
||
|
if to_dir.is_entry() and focused_layer:
|
||
|
# Open into next layer
|
||
|
to_dir.goto(to_dir.get_end_index())
|
||
|
to_dir.back()
|
||
|
var node = focused_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end()))
|
||
|
if node:
|
||
|
var layer = create_layer(node)
|
||
|
select_layer(layer)
|
||
|
# In case where, "from" state is nested yet not an exit state,
|
||
|
# while "to" state is on different level, then jump to destination layer directly.
|
||
|
# This happens when StateMachinePlayer transit to state that existing in the stack,
|
||
|
# which trigger StackPlayer.reset() and cause multiple states removed from stack within one frame
|
||
|
elif from_dir.is_nested() and not from_dir.is_exit():
|
||
|
if to_dir._dirs.size() != from_dir._dirs.size():
|
||
|
to_dir.goto(to_dir.get_end_index())
|
||
|
var n = to_dir.back()
|
||
|
if not n:
|
||
|
n = "root"
|
||
|
var layer = get_layer(n)
|
||
|
path_viewer.select_dir(layer.name)
|
||
|
select_layer(layer)
|
||
|
|
||
|
focused_layer = get_focused_layer(to)
|
||
|
if not focused_layer:
|
||
|
focused_layer = open_layer(to)
|
||
|
focused_layer.debug_transit_in(from, to)
|
||
|
|
||
|
# Return if current editing StateMachine can be saved, ignore built-in resource
|
||
|
func can_save():
|
||
|
if not state_machine:
|
||
|
return false
|
||
|
var resource_path = state_machine.resource_path
|
||
|
if resource_path.is_empty():
|
||
|
return false
|
||
|
if ".scn" in resource_path or ".tscn" in resource_path: # Built-in resource will be saved by scene
|
||
|
return false
|
||
|
return true
|
||
|
|
||
|
func set_debug_mode(v):
|
||
|
if debug_mode != v:
|
||
|
debug_mode = v
|
||
|
_on_debug_mode_changed(v)
|
||
|
emit_signal("debug_mode_changed", debug_mode)
|
||
|
|
||
|
func set_state_machine_player(smp):
|
||
|
if state_machine_player != smp:
|
||
|
state_machine_player = smp
|
||
|
_on_state_machine_player_changed(smp)
|
||
|
|
||
|
func set_state_machine(sm):
|
||
|
if state_machine != sm:
|
||
|
state_machine = sm
|
||
|
_on_state_machine_changed(sm)
|
||
|
|
||
|
func set_current_state(v):
|
||
|
if _current_state != v:
|
||
|
var from = _current_state
|
||
|
var to = v
|
||
|
_current_state = v
|
||
|
_on_remote_transited(from, to)
|