diff --git a/CHANGES.md b/CHANGES.md
index 4b64fd2..1f02013 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,10 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.17.1] - 2024-11-04
+### Added
+- The state chart will now issue a warning in debug builds when trying to send an event that is not defined in any transition of the state chart. This can help to catch typos in event names early on ([#150](https://github.com/derkork/godot-statecharts/issues/150)). This warning is now enabled by default but can be disabled per state chart in state chart settings.
+
## [0.17.0] - 2024-08-16
### Added
- The C# wrappers now provide type-safe events for all signals that the underlying nodes emit. This way you can simply subscribe to a signal using the familiar `+=` notation, e.g. `stateChart.StateEntered += OnStateEntered`. This makes it easier to work with the state chart from C# code. A big thanks goes out to [Marques Lévy](https://github.com/Prakkkmak) for suggesting this feature and providing a POC PR for it ([#126](https://github.com/derkork/godot-statecharts/pull/126)). Note that the usual rules for signals in C# apply, e.g. signal connections will not automatically be disconnected when the receiver is freed.
--
+
### Fixed
- The library now handles cases better where code tries to access a state chart that has been removed from the tree. This may happen when using Godot's `change_scene_to_file` or `change_scene_to_packed` functions. Debug output in these cases will no longer try to get full path names of nodes that have been removed from the tree. This should prevent errors and crashes in these cases ([#129](https://github.com/derkork/godot-statecharts/issues/129)).
- The error messages for evaluating expressions have been improved. They now show the expression that was evaluated and the result of the evaluation ([#138](https://github.com/derkork/godot-statecharts/issues/138))
diff --git a/addons/godot_state_charts/plugin.cfg b/addons/godot_state_charts/plugin.cfg
index bc76779..0c2afbe 100644
--- a/addons/godot_state_charts/plugin.cfg
+++ b/addons/godot_state_charts/plugin.cfg
@@ -3,5 +3,5 @@
name="Godot State Charts"
description="A simple, yet powerful state charts library for Godot"
author="Jan Thomä & Contributors"
-version="0.17.0"
+version="0.17.1"
script="godot_state_charts.gd"
diff --git a/addons/godot_state_charts/state_chart.gd b/addons/godot_state_charts/state_chart.gd
index f080ea3..7a70ee7 100644
--- a/addons/godot_state_charts/state_chart.gd
+++ b/addons/godot_state_charts/state_chart.gd
@@ -8,6 +8,9 @@ extends Node
## The the remote debugger
const DebuggerRemote = preload("utilities/editor_debugger/editor_debugger_remote.gd")
+## The state chart utility class.
+const StateChartUtil = preload("utilities/state_chart_util.gd")
+
## Emitted when the state chart receives an event. This will be
## emitted no matter which state is currently active and can be
## useful to trigger additional logic elsewhere in the game
@@ -20,10 +23,19 @@ const DebuggerRemote = preload("utilities/editor_debugger/editor_debugger_remote
## while another is still processing, it will be enqueued.
signal event_received(event:StringName)
+@export_group("Debugging")
## Flag indicating if this state chart should be tracked by the
## state chart debugger in the editor.
@export var track_in_editor:bool = false
+## If set, the state chart will issue a warning when trying to
+## send an event that is not configured for any transition of
+## the state chart. It is usually a good idea to leave this
+## enabled, but in certain cases this may get in the way so
+## you can disable it here.
+@export var warn_on_sending_unknown_events:bool = true
+
+@export_group("")
## Initial values for the expression properties. These properties can be used in expressions, e.g
## for guards or transition delays. It is recommended to set an initial value for each property
## you use in an expression to ensure that this expression is always valid. If you don't set
@@ -54,6 +66,7 @@ var _queued_transitions:Array[Dictionary] = []
var _transitions_processing_active:bool = false
var _debugger_remote:DebuggerRemote = null
+var _valid_event_names:Array[StringName] = []
func _ready() -> void:
@@ -70,6 +83,12 @@ func _ready() -> void:
if not child is StateChartState:
push_error("StateMachine's child must be a State")
return
+
+ # in debug builds, collect a list of valid event names
+ # to warn the developer when using an event that doesn't
+ # exist.
+ if OS.is_debug_build():
+ _valid_event_names = StateChartUtil.events_of(self)
# set the initial expression properties
if initial_expression_properties != null:
@@ -105,6 +124,9 @@ func send_event(event:StringName) -> void:
if not is_instance_valid(_state):
push_error("State chart has no root state. Ignoring call to `send_event`.")
return
+
+ if warn_on_sending_unknown_events and event != "" and OS.is_debug_build() and not _valid_event_names.has(event):
+ push_warning("State chart does not have an event '", event , "' defined. Sending this event will do nothing.")
_queued_events.append(event)
_run_changes()
diff --git a/godot-state-charts.csproj b/godot-state-charts.csproj
index dfb41df..3603eeb 100644
--- a/godot-state-charts.csproj
+++ b/godot-state-charts.csproj
@@ -394,6 +394,7 @@
+
diff --git a/tests/framework/state_chart_test_base.gd b/tests/framework/state_chart_test_base.gd
index 5968a92..08a00b0 100644
--- a/tests/framework/state_chart_test_base.gd
+++ b/tests/framework/state_chart_test_base.gd
@@ -27,7 +27,9 @@ func before_each() -> void:
func after_each() -> void:
- remove_child(_chart)
+ var parent = _chart.get_parent()
+ if is_instance_valid(parent):
+ parent.remove_child(_chart)
_chart.free()
func assert_active(state: StateChartState) -> void:
diff --git a/tests/test_multiple_delayed_transition.gd b/tests/test_multiple_delayed_transition.gd
new file mode 100644
index 0000000..60b769c
--- /dev/null
+++ b/tests/test_multiple_delayed_transition.gd
@@ -0,0 +1,35 @@
+extends StateChartTestBase
+
+# Checks that multiple delayed transitions work.
+# https://github.com/derkork/godot-statecharts/issues/148
+func test_multiple_delayed_transitions_work():
+ var root := compound_state("root")
+
+ var think := atomic_state("think", root)
+ var explore := atomic_state("explore", root)
+ var inspect := atomic_state("inspect", root)
+
+ transition(think, explore, "pick_destination")
+ transition( explore, inspect, "target_reached", "1.0")
+ transition( explore, think, "", "2.0")
+
+ await finish_setup()
+
+ assert_active(think)
+
+ # when I pick a destination
+ send_event("pick_destination")
+
+ # then I should be exploring
+ assert_active(explore)
+
+ # when I now reach the target
+ await wait_seconds(1.1, "wait for target reached")
+ send_event("target_reached")
+
+ # then after 1 second I should be inspecting
+ await wait_seconds(1.1, "wait for inspect")
+ assert_active(inspect)
+
+
+
diff --git a/tests/test_parallel_state_initialization.gd b/tests/test_parallel_state_initialization.gd
new file mode 100644
index 0000000..6716f95
--- /dev/null
+++ b/tests/test_parallel_state_initialization.gd
@@ -0,0 +1,26 @@
+extends StateChartTestBase
+
+# https://github.com/derkork/godot-statecharts/issues/143
+# currently disabled because it doesn't work yet.
+func __test_parallel_state_initialization():
+ var root := parallel_state("root")
+ var a := atomic_state("a", root)
+ var b := compound_state("b", root)
+ var b1 := atomic_state("b1", b)
+ var b2 := atomic_state("b2", b)
+
+ transition(b1, b2, "some_event")
+ a.state_entered.connect(func():
+ send_event("some_event")
+ )
+
+
+ await finish_setup()
+
+ # a should be active right now..
+ assert_active(a)
+
+ # and b2 should be active
+ assert_active(b2)
+
+
diff --git a/tests/test_warning_on_nonexisting_event.gd b/tests/test_warning_on_nonexisting_event.gd
new file mode 100644
index 0000000..1c016b4
--- /dev/null
+++ b/tests/test_warning_on_nonexisting_event.gd
@@ -0,0 +1,22 @@
+extends StateChartTestBase
+
+func test_warning_on_nonexisting_event():
+ var root = compound_state("root")
+ var a = atomic_state("a", root)
+ var b = atomic_state("b", root)
+ transition(a, b, "some_event")
+ transition(b, a, "some_event")
+ await finish_setup()
+
+ assert_active(a)
+
+ # when i send the correct event, i move to state b
+ send_event("some_event")
+ assert_active(b)
+
+ # when i send a wrong event, nothing happens
+ send_event("narf")
+
+ assert_active(b)
+
+