Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions addons/flowkit/editor/editor_globals.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class_name FKEditorGlobals

const EVENT_ROW_SCENE := preload("res://addons/flowkit/ui/workspace/event_row_ui.tscn")
const COMMENT_SCENE := preload("res://addons/flowkit/ui/workspace/comment_ui.tscn")
const CONDITION_ITEM_SCENE := preload("res://addons/flowkit/ui/workspace/condition_item_ui.tscn")
const CONDITION_ITEM_SCENE := preload("res://addons/flowkit/ui/workspace/condition_unit_ui.tscn")
const ACTION_ITEM_SCENE := preload("res://addons/flowkit/ui/workspace/action_unit_ui.tscn")

const BRANCH_ITEM_SCENE_PATH := "res://addons/flowkit/ui/workspace/branch_item_ui.tscn"
const BRANCH_ITEM_SCENE := preload(BRANCH_ITEM_SCENE_PATH)
const BRANCH_ITEM_SCENE_PATH := "res://addons/flowkit/ui/workspace/branch_unit_ui.tscn"
const BRANCH_ITEM_SCENE := preload(BRANCH_ITEM_SCENE_PATH)
148 changes: 111 additions & 37 deletions addons/flowkit/runtime/expression_evaluator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ class_name FKExpressionEvaluator

## Evaluate a string expression and returns the result
## Tries to parse as literal first, then as GDScript expression
## Evaluation is syntax-driven: the expression is parsed/evaluated based on its syntax alone.
## After evaluation, the result type is validated against expected_type (if provided).
## context_node: the base instance for expression execution (determines where get_node() resolves from)
## scene_root: optional scene root node, exposed as 'scene_root' variable in expressions
## target_node: optional action target node, used for n_ variable lookups (falls back to context_node)
static func evaluate(expr_str: String, context_node: Node = null, scene_root: Node = null, target_node: Node = null) -> Variant:
## expected_type: optional Variant.Type to validate the result against (-1 = no validation)
static func evaluate(expr_str: String, context_node: Node = null, scene_root: Node = null, \
target_node: Node = null, expected_type: int = -1) -> Variant:
if expr_str.is_empty():
return ""
return _check_type("", expected_type, expr_str)

# Trim whitespace
expr_str = expr_str.strip_edges()
Expand All @@ -35,35 +39,35 @@ static func evaluate(expr_str: String, context_node: Node = null, scene_root: No
var var_name = expr_str.substr(2)
# Only treat as standalone n_ variable if it's a simple identifier (no operators/spaces)
if var_name.is_valid_identifier():
var result = _resolve_n_variable(n_var_node, var_name)
if result[0]: # Variable was found
return result[1]
var result := _resolve_n_variable(n_var_node, var_name)
if result.success:
return _check_type(result.value, expected_type, expr_str)
# Variable not found - still try as expression below

# Try to parse as a literal value first
var literal_result = _try_parse_literal(expr_str)
if literal_result != null:
return literal_result
var literal_result := _try_parse_literal(expr_str)
if literal_result.success:
return _check_type(literal_result.value, expected_type, expr_str)

# If not a literal, try to evaluate as a GDScript expression
var expr_result = _evaluate_expression(expr_str, context_node, scene_root, target_node)
if expr_result != null:
return expr_result
var expr_result := _evaluate_expression(expr_str, context_node, scene_root, target_node)
if expr_result.success:
return _check_type(expr_result.value, expected_type, expr_str)

# If all else fails, return as string
return expr_str
# Evaluation failed - do not fall back to raw string
push_error("FlowKit: Failed to evaluate expression: '%s'" % expr_str)
return null


## Resolve an n_ variable from a node. Checks FlowKitSystem node variables first,
## then node metadata (inspector-defined), then script properties.
## Returns a 2-element array [found: bool, value: Variant] to distinguish
## 'not found' from 'found with value null'.
static func _resolve_n_variable(node: Node, var_name: String) -> Array:
## Returns an FKEvalResult to distinguish 'not found' from 'found with value null'.
static func _resolve_n_variable(node: Node, var_name: String) -> FKEvalResult:
var system = node.get_tree().root.get_node_or_null("/root/FlowKitSystem")
if system and system.has_method("get_node_var"):
# Check if the variable exists before getting it
if system.has_method("has_node_var") and system.has_node_var(node, var_name):
return [true, system.get_node_var(node, var_name, null)]
return FKEvalResult.succeeded(system.get_node_var(node, var_name, null))

# Fallback: check node metadata directly (inspector-defined FlowKit variables)
if node.has_meta("flowkit_variables"):
Expand All @@ -85,45 +89,48 @@ static func _resolve_n_variable(node: Node, var_name: String) -> Array:
"bool":
if value is String:
value = value.to_lower() == "true"
return [true, value]
return FKEvalResult.succeeded(value)

# Fallback: check if the node itself has this property (script-exported variables)
if var_name in node:
return [true, node.get(var_name)]
return FKEvalResult.succeeded(node.get(var_name))

return [false, null]
return FKEvalResult.failed()


## Try to parse the string as a literal value (not an expression)
## Returns the parsed value, or null if it's not a literal
static func _try_parse_literal(expr: String) -> Variant:
## Returns an FKEvalResult to distinguish 'not a literal' from a literal null
static func _try_parse_literal(expr: String) -> FKEvalResult:
# Boolean literals
if expr.to_lower() == "true":
return true
return FKEvalResult.succeeded(true)
if expr.to_lower() == "false":
return false
return FKEvalResult.succeeded(false)

# Null literal
if expr.to_lower() == "null":
return null
return FKEvalResult.succeeded(null)

# String literals (quoted)
if _is_quoted_string(expr):
return _parse_quoted_string(expr)
return FKEvalResult.succeeded(_parse_quoted_string(expr))

# Numeric literals
if _is_numeric(expr):
if "." in expr or "e" in expr.to_lower():
return float(expr)
return FKEvalResult.succeeded(float(expr))
else:
return int(expr)
return FKEvalResult.succeeded(int(expr))

# Vector/Color literals (e.g., "Vector2(0,0)", "Color(1,0,0,1)")
if _is_constructor_literal(expr):
return _evaluate_expression(expr, null)
var result := _evaluate_expression(expr, null)
if result.success:
return result
return FKEvalResult.failed()

# Not a literal
return null
return FKEvalResult.failed()


## Check if string is a quoted string literal
Expand Down Expand Up @@ -198,7 +205,8 @@ static func _is_numeric(expr: String) -> bool:

## Check if string is a constructor literal like "Vector2(0,0)" or "Color(1,0,0)"
static func _is_constructor_literal(expr: String) -> bool:
var constructors = ["Vector2", "Vector3", "Vector4", "Color", "Rect2", "Transform2D", "Plane", "Quaternion", "AABB", "Basis", "Transform3D"]
var constructors = ["Vector2", "Vector3", "Vector4", "Color", "Rect2",
"Transform2D", "Plane", "Quaternion", "AABB", "Basis", "Transform3D"]

for constructor in constructors:
if expr.begins_with(constructor + "(") and expr.ends_with(")"):
Expand All @@ -211,7 +219,8 @@ static func _is_constructor_literal(expr: String) -> bool:
## context_node: used as the base instance for Expression.execute() (where get_node() resolves from)
## scene_root: optional scene root node, exposed as 'scene_root' in expressions
## target_node: optional action target node, exposed as 'node' in expressions (falls back to context_node)
static func _evaluate_expression(expr_str: String, context_node: Node, scene_root: Node = null, target_node: Node = null) -> Variant:
static func _evaluate_expression(expr_str: String, context_node: Node, scene_root: Node = null, \
target_node: Node = null) -> FKEvalResult:
var expression = Expression.new()

# Build input variables for the expression
Expand Down Expand Up @@ -310,33 +319,98 @@ static func _evaluate_expression(expr_str: String, context_node: Node, scene_roo
var parse_error = expression.parse(expr_str, input_names)
if parse_error != OK:
# Silently fail - not an expression
return null
return FKEvalResult.failed()

# Execute it
var result = expression.execute(input_values, context_node, false)

if expression.has_execute_failed():
# Silently fail - expression execution failed
return null
return FKEvalResult.failed()

return result
return FKEvalResult.succeeded(result)


## Convenience method to evaluate all inputs in a dictionary
## Returns a new dictionary with evaluated values
## context_node: the base instance for expression execution
## scene_root: optional scene root node, forwarded to evaluate()
## target_node: optional action target node for n_ variable lookups
static func evaluate_inputs(inputs: Dictionary, context_node: Node = null, scene_root: Node = null, target_node: Node = null) -> Dictionary:
## type_hints: optional dictionary mapping input names to Variant.Type int values for post-evaluation validation
static func evaluate_inputs(inputs: Dictionary, context_node: Node = null, \
scene_root: Node = null, target_node: Node = null, type_hints: Dictionary = {}) -> Dictionary:
var evaluated: Dictionary = {}

for key in inputs.keys():
var value = inputs[key]

# Only evaluate if the value is a string
if value is String:
evaluated[key] = evaluate(value, context_node, scene_root, target_node)
var expected_type: int = type_hints.get(key, -1)
evaluated[key] = evaluate(value, context_node, scene_root, target_node, expected_type)
else:
evaluated[key] = value

return evaluated


## Validate that a result matches the expected Variant.Type
## Returns the value as-is but pushes an error if type does not match
static func _check_type(value: Variant, expected_type: int, expr_str: String) -> Variant:
if expected_type < 0:
return value
if typeof(value) != expected_type:
push_error("FlowKit: Expression '%s' evaluated to %s (type %s) but expected type %s" % [
expr_str, str(value), type_string(typeof(value)), type_string(expected_type)])
return value


## Convert a type name string (as used in provider get_inputs()) to a Variant.Type int
## Returns -1 for unknown or "Variant" types (no validation)
static func type_name_to_variant_type(type_name: String) -> int:
match type_name:
"bool":
return TYPE_BOOL
"int":
return TYPE_INT
"float":
return TYPE_FLOAT
"String":
return TYPE_STRING
"Vector2":
return TYPE_VECTOR2
"Vector2i":
return TYPE_VECTOR2I
"Vector3":
return TYPE_VECTOR3
"Vector3i":
return TYPE_VECTOR3I
"Vector4":
return TYPE_VECTOR4
"Vector4i":
return TYPE_VECTOR4I
"Color":
return TYPE_COLOR
"Rect2":
return TYPE_RECT2
"Transform2D":
return TYPE_TRANSFORM2D
"Plane":
return TYPE_PLANE
"Quaternion":
return TYPE_QUATERNION
"AABB":
return TYPE_AABB
"Basis":
return TYPE_BASIS
"Transform3D":
return TYPE_TRANSFORM3D
"NodePath":
return TYPE_NODE_PATH
"Array":
return TYPE_ARRAY
"Dictionary":
return TYPE_DICTIONARY
"Variant", "":
return -1
return -1
24 changes: 24 additions & 0 deletions addons/flowkit/runtime/fk_eval_result.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extends RefCounted
class_name FKEvalResult

## Result of an expression evaluation attempt.
## Wraps a success flag and the resulting value to distinguish
## 'evaluation failed' from 'evaluated to null'.

var success: bool
var value: Variant

## Good for providing further details about the evaluation.
var message := ""


func _init(p_success: bool = false, p_value: Variant = null, p_message = "") -> void:
success = p_success
value = p_value
message = p_message

static func succeeded(p_value: Variant, p_message = "") -> FKEvalResult:
return FKEvalResult.new(true, p_value, p_message)

static func failed(p_message = "") -> FKEvalResult:
return FKEvalResult.new(false, null, p_message)
1 change: 1 addition & 0 deletions addons/flowkit/runtime/fk_eval_result.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cftd1n7sl41wv
2 changes: 1 addition & 1 deletion addons/flowkit/saved/event_sheet/1035986417700190942.tres
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ script = ExtResource("5_1wrk8")
condition_id = "compare_node_variable"
target_node = NodePath("CharacterBody2D")
inputs = {
"Comparison": ">",
"Comparison": "\">\"",
"Property": "node.position.y",
"Value": "n_lowest_y"
}
Expand Down
18 changes: 10 additions & 8 deletions addons/flowkit/saved/event_sheet/2450759864276140733.tres
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ script = ExtResource("3_7puo2")
action_id = "set_window_mode"
target_node = NodePath("System")
inputs = {
"Mode": "windowed"
"Mode": "\"windowed\""
}
block_type = "action"

Expand All @@ -31,7 +31,7 @@ script = ExtResource("3_7puo2")
action_id = "set_variable"
target_node = NodePath("System")
inputs = {
"Name": "times",
"Name": "\"times\"",
"Value": "1"
}
block_type = "action"
Expand All @@ -41,7 +41,9 @@ script = ExtResource("3_7puo2")
action_id = "Print Message"
target_node = NodePath(".")
inputs = {
"color": "red",
"Color": "\"red\"",
"Message": "\"Counting from 1 - 10\"",
"color": "\"red\"",
"message": "\"Counting from 1 to 10\""
}
block_type = "action"
Expand All @@ -51,7 +53,7 @@ script = ExtResource("3_7puo2")
action_id = "Print Message"
target_node = NodePath(".")
inputs = {
"color": "blue",
"color": "\"blue\"",
"message": "system.get_var(\"times\")"
}
block_type = "action"
Expand All @@ -61,7 +63,7 @@ script = ExtResource("3_7puo2")
action_id = "set_variable"
target_node = NodePath("System")
inputs = {
"Name": "times",
"Name": "\"times\"",
"Value": "system.get_var(\"times\")+1"
}
block_type = "action"
Expand All @@ -82,14 +84,14 @@ script = ExtResource("3_7puo2")
action_id = "Print Message"
target_node = NodePath(".")
inputs = {
"color": "red",
"message": "Done!"
"color": "\"red\"",
"message": "\"Done!\""
}
block_type = "action"

[sub_resource type="Resource" id="Resource_5nvf0"]
script = ExtResource("2_o0xcg")
block_id = "event_1775232400_3648767005"
block_id = "event_1775613196_2355312579"
event_id = "on_ready"
target_node = NodePath(".")
actions = Array[ExtResource("3_7puo2")]([SubResource("Resource_xv5k1"), SubResource("Resource_o0xcg"), SubResource("Resource_7puo2"), SubResource("Resource_ko5ym"), SubResource("Resource_v5tg8"), SubResource("Resource_l83bg")])
Expand Down
Loading