diff --git a/addons/godot_state_charts/LICENSE b/addons/godot_state_charts/LICENSE
new file mode 100644
index 0000000..4542e9f
--- /dev/null
+++ b/addons/godot_state_charts/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Jan Thomä
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/addons/godot_state_charts/all_of_guard.gd b/addons/godot_state_charts/all_of_guard.gd
new file mode 100644
index 0000000..9765122
--- /dev/null
+++ b/addons/godot_state_charts/all_of_guard.gd
@@ -0,0 +1,15 @@
+@tool
+@icon("all_of_guard.svg")
+
+## A composite guard that is satisfied when all of its guards are satisfied.
+class_name AllOfGuard
+extends Guard
+
+## The guards that need to be satisified. When empty, returns true.
+@export var guards:Array[Guard] = []
+
+func is_satisfied(context_transition:Transition, context_state:State) -> bool:
+ for guard in guards:
+ if not guard.is_satisfied(context_transition, context_state):
+ return false
+ return true
diff --git a/addons/godot_state_charts/all_of_guard.svg b/addons/godot_state_charts/all_of_guard.svg
new file mode 100644
index 0000000..76b10c4
--- /dev/null
+++ b/addons/godot_state_charts/all_of_guard.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/addons/godot_state_charts/all_of_guard.svg.import b/addons/godot_state_charts/all_of_guard.svg.import
new file mode 100644
index 0000000..15a2f94
--- /dev/null
+++ b/addons/godot_state_charts/all_of_guard.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ux4ia8xhhjrx"
+path="res://.godot/imported/all_of_guard.svg-49642db22a4a20844b2d39e67c930c8b.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/all_of_guard.svg"
+dest_files=["res://.godot/imported/all_of_guard.svg-49642db22a4a20844b2d39e67c930c8b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/animation_player_state.gd b/addons/godot_state_charts/animation_player_state.gd
new file mode 100644
index 0000000..a5f5120
--- /dev/null
+++ b/addons/godot_state_charts/animation_player_state.gd
@@ -0,0 +1,62 @@
+@tool
+@icon("animation_player_state.svg")
+class_name AnimationPlayerState
+extends AtomicState
+
+## Animation player that this state will use.
+@export_node_path("AnimationPlayer") var animation_player: NodePath:
+ set(value):
+ animation_player = value
+ update_configuration_warnings()
+
+## The name of the animation that should be played when this state is entered.
+## When this is empty, the name of this state will be used.
+@export var animation_name: StringName = ""
+
+## A custom blend time for the animation. The default value of -1.0 will use the
+## default blend time of the animation player.
+@export var custom_blend: float = -1.0
+
+## A custom speed for the animation. Use negative values to play the animation
+## backwards.
+@export var custom_speed: float = 1.0
+
+## Whether the animation should be played from the end.
+@export var from_end: bool = false
+
+var _animation_player: AnimationPlayer
+
+func _ready():
+ if Engine.is_editor_hint():
+ return
+
+ super._ready()
+ _animation_player = get_node_or_null(animation_player)
+
+ if not is_instance_valid(_animation_player):
+ push_error("The animation player is invalid. This node will not work.")
+
+func _state_enter(expect_transition: bool = false):
+ super._state_enter()
+
+ if not is_instance_valid(_animation_player):
+ return
+
+ var target_animation = animation_name
+ if target_animation == "":
+ target_animation = get_name()
+
+ if _animation_player.current_animation == target_animation and _animation_player.is_playing():
+ return
+
+ _animation_player.play(target_animation, custom_blend, custom_speed, from_end)
+
+func _get_configuration_warnings():
+ var warnings = super._get_configuration_warnings()
+
+ if animation_player.is_empty():
+ warnings.append("No animation player is set.")
+ elif get_node_or_null(animation_player) == null:
+ warnings.append("The animation player path is invalid.")
+
+ return warnings
diff --git a/addons/godot_state_charts/animation_player_state.svg b/addons/godot_state_charts/animation_player_state.svg
new file mode 100644
index 0000000..79b16db
--- /dev/null
+++ b/addons/godot_state_charts/animation_player_state.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/addons/godot_state_charts/animation_player_state.svg.import b/addons/godot_state_charts/animation_player_state.svg.import
new file mode 100644
index 0000000..dbe9b93
--- /dev/null
+++ b/addons/godot_state_charts/animation_player_state.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b3m20gsesp4i0"
+path="res://.godot/imported/animation_player_state.svg-1acd03c414690dd7446458c5293935cb.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/animation_player_state.svg"
+dest_files=["res://.godot/imported/animation_player_state.svg-1acd03c414690dd7446458c5293935cb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/animation_tree_state.gd b/addons/godot_state_charts/animation_tree_state.gd
new file mode 100644
index 0000000..8af7186
--- /dev/null
+++ b/addons/godot_state_charts/animation_tree_state.gd
@@ -0,0 +1,62 @@
+@tool
+@icon("animation_tree_state.svg")
+class_name AnimationTreeState
+extends AtomicState
+
+
+## Animation tree that this state will use.
+@export_node_path("AnimationTree") var animation_tree:NodePath:
+ set(value):
+ animation_tree = value
+ update_configuration_warnings()
+
+## The name of the state that should be activated in the animation tree
+## when this state is entered. If this is empty, the name of this state
+## will be used.
+@export var state_name:StringName = ""
+
+
+var _animation_tree_state_machine:AnimationNodeStateMachinePlayback
+
+func _ready():
+ if Engine.is_editor_hint():
+ return
+
+ super._ready()
+
+ _animation_tree_state_machine = null
+ var the_tree = get_node_or_null(animation_tree)
+
+ if is_instance_valid(the_tree):
+ var state_machine = the_tree.get("parameters/playback")
+ if state_machine is AnimationNodeStateMachinePlayback:
+ _animation_tree_state_machine = state_machine
+ else:
+ push_error("The animation tree does not have a state machine as root node. This node will not work.")
+ else:
+ push_error("The animation tree is invalid. This node will not work.")
+
+
+func _state_enter(expect_transition:bool = false):
+ super._state_enter()
+
+ if not is_instance_valid(_animation_tree_state_machine):
+ return
+
+ var target_state = state_name
+ if target_state == "":
+ target_state = get_name()
+
+ # mirror this state to the animation tree
+ _animation_tree_state_machine.travel(target_state)
+
+
+func _get_configuration_warnings():
+ var warnings = super._get_configuration_warnings()
+
+ if animation_tree.is_empty():
+ warnings.append("No animation tree is set.")
+ elif get_node_or_null(animation_tree) == null:
+ warnings.append("The animation tree path is invalid.")
+
+ return warnings
diff --git a/addons/godot_state_charts/animation_tree_state.svg b/addons/godot_state_charts/animation_tree_state.svg
new file mode 100644
index 0000000..432dac9
--- /dev/null
+++ b/addons/godot_state_charts/animation_tree_state.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/addons/godot_state_charts/animation_tree_state.svg.import b/addons/godot_state_charts/animation_tree_state.svg.import
new file mode 100644
index 0000000..920e561
--- /dev/null
+++ b/addons/godot_state_charts/animation_tree_state.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://3wqyduuj0fq"
+path="res://.godot/imported/animation_tree_state.svg-b99077fc178cfa1e23b2c854c7735c4a.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/animation_tree_state.svg"
+dest_files=["res://.godot/imported/animation_tree_state.svg-b99077fc178cfa1e23b2c854c7735c4a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/any_of_guard.gd b/addons/godot_state_charts/any_of_guard.gd
new file mode 100644
index 0000000..c560824
--- /dev/null
+++ b/addons/godot_state_charts/any_of_guard.gd
@@ -0,0 +1,15 @@
+@tool
+@icon("any_of_guard.svg")
+
+## A composite guard, that is satisfied if any of the guards are satisfied.
+class_name AnyOfGuard
+extends Guard
+
+## The guards of which at least one must be satisfied. If empty, this guard is not satisfied.
+@export var guards: Array[Guard] = []
+
+func is_satisfied(context_transition:Transition, context_state:State) -> bool:
+ for guard in guards:
+ if guard.is_satisfied(context_transition, context_state):
+ return true
+ return false
diff --git a/addons/godot_state_charts/any_of_guard.svg b/addons/godot_state_charts/any_of_guard.svg
new file mode 100644
index 0000000..2bb1cfc
--- /dev/null
+++ b/addons/godot_state_charts/any_of_guard.svg
@@ -0,0 +1,23 @@
+
+
+
diff --git a/addons/godot_state_charts/any_of_guard.svg.import b/addons/godot_state_charts/any_of_guard.svg.import
new file mode 100644
index 0000000..61a320c
--- /dev/null
+++ b/addons/godot_state_charts/any_of_guard.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbf5ogymlonu4"
+path="res://.godot/imported/any_of_guard.svg-3b1aa026a997dbfebde2cc5993b5c820.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/any_of_guard.svg"
+dest_files=["res://.godot/imported/any_of_guard.svg-3b1aa026a997dbfebde2cc5993b5c820.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/atomic_state.gd b/addons/godot_state_charts/atomic_state.gd
new file mode 100644
index 0000000..fb8fa16
--- /dev/null
+++ b/addons/godot_state_charts/atomic_state.gd
@@ -0,0 +1,25 @@
+@tool
+@icon("atomic_state.svg")
+## This is a state that has no sub-states.
+class_name AtomicState
+extends State
+
+func _handle_transition(transition:Transition, source:State):
+ # resolve the target state
+ var target = transition.resolve_target()
+ if not target is State:
+ push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
+ return
+ # atomic states cannot transition, so we need to ask the parent
+ # ask the parent
+ get_parent()._handle_transition(transition, source)
+
+
+func _get_configuration_warnings() -> PackedStringArray :
+ var warnings = super._get_configuration_warnings()
+ # check if we have any child nodes which are not transitions
+ for child in get_children():
+ if child is State:
+ warnings.append("Atomic states cannot have child states. These will be ignored.")
+ break
+ return warnings
diff --git a/addons/godot_state_charts/atomic_state.svg b/addons/godot_state_charts/atomic_state.svg
new file mode 100644
index 0000000..85aba01
--- /dev/null
+++ b/addons/godot_state_charts/atomic_state.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/addons/godot_state_charts/atomic_state.svg.import b/addons/godot_state_charts/atomic_state.svg.import
new file mode 100644
index 0000000..9c1d9bc
--- /dev/null
+++ b/addons/godot_state_charts/atomic_state.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c4ojtah20jtxc"
+path="res://.godot/imported/atomic_state.svg-5ab16e5747cef5b5980c4bf84ef9b1af.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/atomic_state.svg"
+dest_files=["res://.godot/imported/atomic_state.svg-5ab16e5747cef5b5980c4bf84ef9b1af.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/compound_state.gd b/addons/godot_state_charts/compound_state.gd
new file mode 100644
index 0000000..2b713fd
--- /dev/null
+++ b/addons/godot_state_charts/compound_state.gd
@@ -0,0 +1,245 @@
+@tool
+@icon("compound_state.svg")
+## A compound state is a state that has multiple sub-states of which exactly one can
+## be active at any given time.
+class_name CompoundState
+extends State
+
+## Called when a child state is entered.
+signal child_state_entered()
+
+## Called when a child state is exited.
+signal child_state_exited()
+
+## The initial state which should be activated when this state is activated.
+@export_node_path("State") var initial_state:NodePath:
+ get:
+ return initial_state
+ set(value):
+ initial_state = value
+ update_configuration_warnings()
+
+
+## The currently active substate.
+var _active_state:State = null
+
+## The initial state
+@onready var _initial_state:State = get_node_or_null(initial_state)
+
+## The history states of this compound state.
+var _history_states:Array[HistoryState] = []
+## Whether any of the history states needs a deep history.
+var _needs_deep_history = false
+
+func _state_init():
+ super._state_init()
+
+ # check if we have any history states
+ for child in get_children():
+ if child is HistoryState:
+ var child_as_history_state:HistoryState = child as HistoryState
+ _history_states.append(child_as_history_state)
+ # remember if any of the history states needs a deep history
+ _needs_deep_history = _needs_deep_history or child_as_history_state.deep
+
+ # initialize all substates. find all children of type State and call _state_init on them.
+ for child in get_children():
+ if child is State:
+ var child_as_state:State = child as State
+ child_as_state._state_init()
+ child_as_state.state_entered.connect(func(): child_state_entered.emit())
+ child_as_state.state_exited.connect(func(): child_state_exited.emit())
+
+func _state_enter(expect_transition:bool = false):
+ super._state_enter()
+ # activate the initial state unless we expect a transition
+ if not expect_transition:
+ if _initial_state != null:
+ _active_state = _initial_state
+ _active_state._state_enter()
+ else:
+ push_error("No initial state set for state '" + name + "'.")
+
+func _state_step():
+ super._state_step()
+ if _active_state != null:
+ _active_state._state_step()
+
+func _state_save(saved_state:SavedState, child_levels:int = -1):
+ super._state_save(saved_state, child_levels)
+
+ # in addition save all history states, as they are never active and normally would not be saved
+ var parent = saved_state.get_substate_or_null(self)
+ if parent == null:
+ push_error("Probably a bug: The state of '" + name + "' was not saved.")
+ return
+
+ for history_state in _history_states:
+ history_state._state_save(parent, child_levels)
+
+func _state_restore(saved_state:SavedState, child_levels:int = -1):
+ super._state_restore(saved_state, child_levels)
+
+ # in addition check if we are now active and if so determine the current active state
+ if active:
+ # find the currently active child
+ for child in get_children():
+ if child is State and child.active:
+ _active_state = child
+ break
+
+func _state_exit():
+ # if we have any history states, we need to save the current active state
+ if _history_states.size() > 0:
+ var saved_state = SavedState.new()
+ # we save the entire hierarchy if any of the history states needs a deep history
+ # otherwise we only save this level. This way we can save memory and processing time
+ _state_save(saved_state, -1 if _needs_deep_history else 1)
+
+ # now save the saved state in all history states
+ for history_state in _history_states:
+ # when saving history it's ok when we save deep history in a history state that doesn't need it
+ # because at restore time we will use the state's deep flag to determine if we need to restore
+ # the entire hierarchy or just this level. This way we don't need multiple copies of the same
+ # state hierarchy.
+ history_state.history = saved_state
+
+ # deactivate the current state
+ if _active_state != null:
+ _active_state._state_exit()
+ _active_state = null
+ super._state_exit()
+
+
+func _process_transitions(event:StringName, property_change:bool = false) -> bool:
+ if not active:
+ return false
+
+ # forward to the active state
+ if is_instance_valid(_active_state):
+ if _active_state._process_transitions(event, property_change):
+ # emit the event_received signal, unless this is a property change
+ if not property_change:
+ self.event_received.emit(event)
+ return true
+
+ # if the event was not handled by the active state, we handle it here
+ # base class will also emit the event_received signal
+ return super._process_transitions(event, property_change)
+
+
+func _handle_transition(transition:Transition, source:State):
+ # print("CompoundState._handle_transition: " + name + " from " + source.name + " to " + str(transition.to))
+ # resolve the target state
+ var target = transition.resolve_target()
+ if not target is State:
+ push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
+ return
+
+ # the target state can be
+ # 0. this state. in this case exit this state and re-enter it. This can happen when
+ # a child state transfers to its parent state.
+ # 1. a direct child of this state. this is the easy case in which
+ # we will deactivate the current _active_state and activate the target
+ # 2. a descendant of this state. in this case we find the direct child which
+ # is the ancestor of the target state, activate it and then ask it to perform
+ # the transition.
+ # 3. no descendant of this state. in this case, we ask our parent state to
+ # perform the transition
+
+ if target == self:
+ # exit this state and re-enter it
+ _state_exit()
+ _state_enter(false)
+ return
+
+ if target in get_children():
+ # all good, now first deactivate the current state
+ if is_instance_valid(_active_state):
+ _active_state._state_exit()
+
+ # now check if the target is a history state, if this is the
+ # case, we need to restore the saved state
+ if target is HistoryState:
+ # print("Target is history state, restoring saved state.")
+ var saved_state = target.history
+ if saved_state != null:
+ # restore the saved state
+ _state_restore(saved_state, -1 if target.deep else 1)
+ return
+ # print("No history saved so far, activating default state.")
+ # if we don't have history, we just activate the default state
+ var default_state = target.get_node_or_null(target.default_state)
+ if is_instance_valid(default_state):
+ _active_state = default_state
+ _active_state._state_enter()
+ return
+ else:
+ push_error("The default state '" + target.default_state + "' of the history state '" + target.name + "' cannot be found.")
+ return
+
+ # else, just activate the target state
+ _active_state = target
+ _active_state._state_enter()
+ return
+
+ if self.is_ancestor_of(target):
+ # find the child which is the ancestor of the new target.
+ for child in get_children():
+ if child is State and child.is_ancestor_of(target):
+ # found it.
+ # change active state if necessary
+ if _active_state != child:
+ if is_instance_valid(_active_state):
+ _active_state._state_exit()
+
+ _active_state = child
+ # set the "expect_transition" flag to true because we will send
+ # the transition to the child state right after we activate it.
+ # this avoids the child needlessly entering the initial state
+ _active_state._state_enter(true)
+
+ # ask child to handle the transition
+ child._handle_transition(transition, source)
+ return
+ return
+
+ # ask the parent
+ get_parent()._handle_transition(transition, source)
+
+
+func add_child(node:Node, force_readable_name:bool = false, internal:InternalMode = INTERNAL_MODE_DISABLED) -> void:
+ super.add_child(node, force_readable_name, internal)
+ # when a child is added in the editor and the child is a state
+ # and we don't have an initial state yet, set the initial state
+ # to the newly added child
+ if Engine.is_editor_hint() and node is State:
+ if initial_state.is_empty():
+ # the newly added node may have a random name now,
+ # so we need to defer the call to build a node path
+ # to the next frame, so the editor has time to rename
+ # the node to its final name
+ (func(): initial_state = get_path_to(node)).call_deferred()
+
+
+func _get_configuration_warnings() -> PackedStringArray:
+ var warnings = super._get_configuration_warnings()
+
+ # count the amount of child states
+ var child_count = 0
+ for child in get_children():
+ if child is State:
+ child_count += 1
+
+ if child_count < 2:
+ warnings.append("Compound states should have at two child states.")
+
+ var the_initial_state = get_node_or_null(initial_state)
+
+ if not is_instance_valid(the_initial_state):
+ warnings.append("Initial state could not be resolved, is the path correct?")
+
+ elif the_initial_state.get_parent() != self:
+ warnings.append("Initial state must be a direct child of this compound state.")
+
+ return warnings
diff --git a/addons/godot_state_charts/compound_state.svg b/addons/godot_state_charts/compound_state.svg
new file mode 100644
index 0000000..5a3c507
--- /dev/null
+++ b/addons/godot_state_charts/compound_state.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/addons/godot_state_charts/compound_state.svg.import b/addons/godot_state_charts/compound_state.svg.import
new file mode 100644
index 0000000..3e5d01f
--- /dev/null
+++ b/addons/godot_state_charts/compound_state.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bbudjoa3ds4qj"
+path="res://.godot/imported/compound_state.svg-84780d78ec1f15e1cbb9d20f4df031a7.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot_state_charts/compound_state.svg"
+dest_files=["res://.godot/imported/compound_state.svg-84780d78ec1f15e1cbb9d20f4df031a7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/godot_state_charts/csharp/CompoundState.cs b/addons/godot_state_charts/csharp/CompoundState.cs
new file mode 100644
index 0000000..45aa034
--- /dev/null
+++ b/addons/godot_state_charts/csharp/CompoundState.cs
@@ -0,0 +1,52 @@
+
+
+// ReSharper disable once CheckNamespace
+namespace GodotStateCharts
+{
+ using System;
+ using Godot;
+
+ ///
+ /// Wrapper around the compound state node.
+ ///
+ public class CompoundState : State
+ {
+
+ private CompoundState(Node wrapped) : base(wrapped)
+ {
+ }
+
+ ///
+ /// Creates a wrapper object around the given node and verifies that the node
+ /// is actually a compound state. The wrapper object can then be used to interact
+ /// with the compound state chart from C#.
+ ///
+ /// the node that is the state
+ /// a State wrapper.
+ /// ArgumentException if the node is not a state.
+ public new static CompoundState Of(Node state)
+ {
+ if (state.GetScript().As