215 lines
6.2 KiB
GDScript
215 lines
6.2 KiB
GDScript
class_name EventCondition
|
|
extends RefCounted
|
|
|
|
## Side-effect-free, data-only event predicate.
|
|
|
|
enum Operator {
|
|
EQ,
|
|
NE,
|
|
LT,
|
|
LTE,
|
|
GT,
|
|
GTE,
|
|
HAS_FLAG,
|
|
NOT_HAS_FLAG,
|
|
IN_RANGE,
|
|
ALL,
|
|
ANY,
|
|
NOT,
|
|
ENTITY_EXISTS,
|
|
PATH_EXISTS,
|
|
TIME_IN_WINDOW,
|
|
}
|
|
|
|
var operator: Operator = Operator.EQ
|
|
var path: Array[StringName] = []
|
|
var expected: Variant = null
|
|
var children: Array[EventCondition] = []
|
|
|
|
|
|
func configure(
|
|
p_operator: Variant,
|
|
p_path: Variant = [],
|
|
p_expected: Variant = null,
|
|
p_children: Array = []
|
|
) -> EventCondition:
|
|
operator = _operator_from_variant(p_operator)
|
|
path = _normalize_path(p_path)
|
|
expected = _copy(p_expected)
|
|
children.clear()
|
|
for raw_child: Variant in p_children:
|
|
var child := from_variant(raw_child)
|
|
if child != null:
|
|
children.append(child)
|
|
return self
|
|
|
|
|
|
func is_valid() -> bool:
|
|
if operator < Operator.EQ or operator > Operator.TIME_IN_WINDOW:
|
|
return false
|
|
if operator in [Operator.ALL, Operator.ANY] and children.is_empty():
|
|
return false
|
|
if operator == Operator.NOT and children.size() != 1:
|
|
return false
|
|
if operator not in [Operator.ALL, Operator.ANY, Operator.NOT, Operator.TIME_IN_WINDOW] and path.is_empty():
|
|
return false
|
|
for child: EventCondition in children:
|
|
if child == null or not child.is_valid():
|
|
return false
|
|
return true
|
|
|
|
|
|
func evaluate(context: Dictionary, callbacks: Dictionary = {}) -> bool:
|
|
match operator:
|
|
Operator.ALL:
|
|
for child: EventCondition in children:
|
|
if not child.evaluate(context, callbacks):
|
|
return false
|
|
return true
|
|
Operator.ANY:
|
|
for child: EventCondition in children:
|
|
if child.evaluate(context, callbacks):
|
|
return true
|
|
return false
|
|
Operator.NOT:
|
|
return not children[0].evaluate(context, callbacks)
|
|
Operator.TIME_IN_WINDOW:
|
|
var minute := int(_resolve(context, path)) if not path.is_empty() else int(_dictionary_value(context, &"absolute_minute", 0))
|
|
return _time_in_window(minute, expected)
|
|
|
|
var actual: Variant = _resolve(context, path)
|
|
match operator:
|
|
Operator.EQ:
|
|
return actual == expected
|
|
Operator.NE:
|
|
return actual != expected
|
|
Operator.LT:
|
|
return _numeric(actual) < _numeric(expected)
|
|
Operator.LTE:
|
|
return _numeric(actual) <= _numeric(expected)
|
|
Operator.GT:
|
|
return _numeric(actual) > _numeric(expected)
|
|
Operator.GTE:
|
|
return _numeric(actual) >= _numeric(expected)
|
|
Operator.HAS_FLAG:
|
|
return _has_flag(actual, expected)
|
|
Operator.NOT_HAS_FLAG:
|
|
return not _has_flag(actual, expected)
|
|
Operator.IN_RANGE:
|
|
return _in_range(actual, expected)
|
|
Operator.ENTITY_EXISTS:
|
|
return _callback_predicate(callbacks, &"entity_exists", actual)
|
|
Operator.PATH_EXISTS:
|
|
return _callback_predicate(callbacks, &"path_exists", actual)
|
|
return false
|
|
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
var child_snapshots: Array[Dictionary] = []
|
|
for child: EventCondition in children:
|
|
child_snapshots.append(child.create_snapshot())
|
|
var path_strings: Array[String] = []
|
|
for part: StringName in path:
|
|
path_strings.append(String(part))
|
|
return {
|
|
&"operator": Operator.keys()[operator],
|
|
&"path": path_strings,
|
|
&"expected": _copy(expected),
|
|
&"children": child_snapshots,
|
|
}
|
|
|
|
|
|
static func from_variant(raw_value: Variant) -> EventCondition:
|
|
if raw_value is EventCondition:
|
|
return raw_value
|
|
if not raw_value is Dictionary:
|
|
return null
|
|
var raw_children: Variant = _dictionary_value(raw_value, &"children", [])
|
|
return EventCondition.new().configure(
|
|
_dictionary_value(raw_value, &"operator", "EQ"),
|
|
_dictionary_value(raw_value, &"path", []),
|
|
_dictionary_value(raw_value, &"expected", _dictionary_value(raw_value, &"value", null)),
|
|
raw_children if raw_children is Array else []
|
|
)
|
|
|
|
|
|
static func _operator_from_variant(raw_value: Variant) -> Operator:
|
|
if raw_value is int:
|
|
return clampi(int(raw_value), Operator.EQ, Operator.TIME_IN_WINDOW) as Operator
|
|
var index := Operator.keys().find(String(raw_value).to_upper())
|
|
return (index if index >= 0 else Operator.EQ) as Operator
|
|
|
|
|
|
static func _normalize_path(raw_value: Variant) -> Array[StringName]:
|
|
var result: Array[StringName] = []
|
|
if raw_value is String or raw_value is StringName:
|
|
for part: String in String(raw_value).split(".", false):
|
|
result.append(StringName(part))
|
|
elif raw_value is Array:
|
|
for raw_part: Variant in raw_value:
|
|
var part := StringName(raw_part)
|
|
if not part.is_empty():
|
|
result.append(part)
|
|
return result
|
|
|
|
|
|
static func _resolve(context: Dictionary, field_path: Array[StringName]) -> Variant:
|
|
var current: Variant = context
|
|
for part: StringName in field_path:
|
|
if not current is Dictionary:
|
|
return null
|
|
current = _dictionary_value(current, part, null)
|
|
return current
|
|
|
|
|
|
static func _numeric(value: Variant) -> float:
|
|
return float(value) if value is int or value is float else 0.0
|
|
|
|
|
|
static func _has_flag(actual: Variant, flag_value: Variant) -> bool:
|
|
var flag := StringName(flag_value)
|
|
if actual is Dictionary:
|
|
return bool(_dictionary_value(actual, flag, false))
|
|
if actual is Array:
|
|
for raw_entry: Variant in actual:
|
|
if StringName(raw_entry) == flag:
|
|
return true
|
|
return false
|
|
|
|
|
|
static func _in_range(actual: Variant, bounds: Variant) -> bool:
|
|
if not bounds is Array or bounds.size() < 2:
|
|
return false
|
|
var numeric := _numeric(actual)
|
|
return numeric >= _numeric(bounds[0]) and numeric <= _numeric(bounds[1])
|
|
|
|
|
|
static func _time_in_window(absolute_minute: int, raw_window: Variant) -> bool:
|
|
if not raw_window is Array or raw_window.size() < 2:
|
|
return false
|
|
var minute_of_day := posmod(absolute_minute, 1_440)
|
|
var start := posmod(int(raw_window[0]), 1_440)
|
|
var finish := posmod(int(raw_window[1]), 1_440)
|
|
if start == finish:
|
|
return true
|
|
if start < finish:
|
|
return minute_of_day >= start and minute_of_day < finish
|
|
return minute_of_day >= start or minute_of_day < finish
|
|
|
|
|
|
static func _callback_predicate(callbacks: Dictionary, callback_id: StringName, value: Variant) -> bool:
|
|
var callback: Variant = _dictionary_value(callbacks, callback_id, Callable())
|
|
if callback is Callable and callback.is_valid():
|
|
return bool(callback.call(value))
|
|
return false
|
|
|
|
|
|
static func _dictionary_value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
|
if source.has(key):
|
|
return source[key]
|
|
return source.get(String(key), fallback)
|
|
|
|
|
|
static func _copy(value: Variant) -> Variant:
|
|
return value.duplicate(true) if value is Dictionary or value is Array else value
|