pokemon-base/addons/imjp94.yafsm/src/states/StateMachine.gd
2025-01-26 20:23:56 +01:00

230 lines
7.8 KiB
GDScript

@tool
@icon("../../assets/icons/state_machine_icon.png")
extends State
class_name StateMachine
signal transition_added(transition) # Transition added
signal transition_removed(to_state) # Transition removed
@export var states: Dictionary: # States within this StateMachine, keyed by State.name
get = get_states,
set = set_states
@export var transitions: Dictionary: # Transitions from this state, keyed by Transition.to
get = get_transitions,
set = set_transitions
var _states
var _transitions
func _init(p_name="", p_transitions={}, p_states={}):
super._init(p_name)
_transitions = p_transitions
_states = p_states
# Attempt to transit with global/local parameters, where local_params override params
func transit(current_state, params={}, local_params={}):
var nested_states = current_state.split("/")
var is_nested = nested_states.size() > 1
var end_state_machine = self
var base_path = ""
for i in nested_states.size() - 1: # Ignore last one, to get its parent StateMachine
var state = nested_states[i]
# Construct absolute base path
base_path = join_path(base_path, [state])
if end_state_machine != self:
end_state_machine = end_state_machine.states[state]
else:
end_state_machine = _states[state] # First level state
# Nested StateMachine in Exit state
if is_nested:
var is_nested_exit = nested_states[nested_states.size()-1] == State.EXIT_STATE
if is_nested_exit:
# Normalize path to transit again with parent of end_state_machine
var end_state_machine_parent_path = ""
for i in nested_states.size() - 2: # Ignore last two state(which is end_state_machine/end_state)
end_state_machine_parent_path = join_path(end_state_machine_parent_path, [nested_states[i]])
var end_state_machine_parent = get_state(end_state_machine_parent_path)
var normalized_current_state = end_state_machine.name
var next_state = end_state_machine_parent.transit(normalized_current_state, params)
if next_state:
# Construct next state into absolute path
next_state = join_path(end_state_machine_parent_path, [next_state])
return next_state
# Transit with current running nested state machine
var from_transitions = end_state_machine.transitions.get(nested_states[nested_states.size()-1])
if from_transitions:
var from_transitions_array = from_transitions.values()
from_transitions_array.sort_custom(func(a, b): Transition.sort(a, b))
for transition in from_transitions_array:
var next_state = transition.transit(params, local_params)
if next_state:
if "states" in end_state_machine.states[next_state]:
# Next state is a StateMachine, return entry state of the state machine in absolute path
next_state = join_path(base_path, [next_state, State.ENTRY_STATE])
else:
# Construct next state into absolute path
next_state = join_path(base_path, [next_state])
return next_state
return null
# Get state from absolute path, for exmaple, "path/to/state" (root == empty string)
# *It is impossible to get parent state machine with path like "../sibling", as StateMachine is not structed as a Tree
func get_state(path):
var state
if path.is_empty():
state = self
else:
var nested_states = path.split("/")
for i in nested_states.size():
var dir = nested_states[i]
if state:
state = state.states[dir]
else:
state = _states[dir] # First level state
return state
# Add state, state name must be unique within this StateMachine, return state added if succeed else return null
func add_state(state):
if not state:
return null
if state.name in _states:
return null
_states[state.name] = state
return state
# Remove state by its name
func remove_state(state):
return _states.erase(state)
# Change existing state key in states(Dictionary), return true if success
func change_state_name(from, to):
if not (from in _states) or to in _states:
return false
for state_key in _states.keys():
var state = _states[state_key]
var is_name_changing_state = state_key == from
if is_name_changing_state:
state.name = to
_states[to] = state
_states.erase(from)
for from_key in _transitions.keys():
var from_transitions = _transitions[from_key]
if from_key == from:
_transitions.erase(from)
_transitions[to] = from_transitions
for to_key in from_transitions.keys():
var transition = from_transitions[to_key]
if transition.from == from:
transition.from = to
elif transition.to == from:
transition.to = to
if not is_name_changing_state:
# Transitions to name changed state needs to be updated
from_transitions.erase(from)
from_transitions[to] = transition
return true
# Add transition, Transition.from must be equal to this state's name and Transition.to not added yet
func add_transition(transition):
if transition.from == "" or transition.to == "":
push_warning("Transition missing from/to (%s/%s)" % [transition.from, transition.to])
return
var from_transitions
if transition.from in _transitions:
from_transitions = _transitions[transition.from]
else:
from_transitions = {}
_transitions[transition.from] = from_transitions
from_transitions[transition.to] = transition
emit_signal("transition_added", transition)
# Remove transition with Transition.to(name of state transiting to)
func remove_transition(from_state, to_state):
var from_transitions = _transitions.get(from_state)
if from_transitions:
if to_state in from_transitions:
from_transitions.erase(to_state)
if from_transitions.is_empty():
_transitions.erase(from_state)
emit_signal("transition_removed", from_state, to_state)
func get_entries():
return _transitions[State.ENTRY_STATE].values()
func get_exits():
return _transitions[State.EXIT_STATE].values()
func has_entry():
return State.ENTRY_STATE in _states
func has_exit():
return State.EXIT_STATE in _states
# Get duplicate of states dictionary
func get_states():
return _states.duplicate()
func set_states(val):
_states = val
# Get duplicate of transitions dictionary
func get_transitions():
return _transitions.duplicate()
func set_transitions(val):
_transitions = val
static func join_path(base, dirs):
var path = base
for dir in dirs:
if path.is_empty():
path = dir
else:
path = str(path, "/", dir)
return path
# Validate state machine resource to identify and fix error
static func validate(state_machine):
var validated = false
for from_key in state_machine.transitions.keys():
# Non-existing state found in StateMachine.transitions
# See https://github.com/imjp94/gd-YAFSM/issues/6
if not (from_key in state_machine.states):
validated = true
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition" % from_key)
state_machine.transitions.erase(from_key)
continue
var from_transition = state_machine.transitions[from_key]
for to_key in from_transition.keys():
# Non-existing state found in StateMachine.transitions
# See https://github.com/imjp94/gd-YAFSM/issues/6
if not (to_key in state_machine.states):
validated = true
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition(%s -> %s)" % [to_key, from_key, to_key])
from_transition.erase(to_key)
continue
# Mismatch of StateMachine.transitions with Transition.to
# See https://github.com/imjp94/gd-YAFSM/issues/6
var to_transition = from_transition[to_key]
if to_key != to_transition.to:
validated = true
push_warning("gd-YAFSM ValidationError: Mismatch of StateMachine.transitions key(%s) with Transition.to(%s)" % [to_key, to_transition.to])
to_transition.to = to_key
# Self connecting transition
# See https://github.com/imjp94/gd-YAFSM/issues/5
if to_transition.from == to_transition.to:
validated = true
push_warning("gd-YAFSM ValidationError: Self connecting transition(%s -> %s)" % [to_transition.from, to_transition.to])
from_transition.erase(to_key)
return validated