basic ECS spawner
This commit is contained in:
21
addons/gecs/tests/components/c_complex_serialization_test.gd
Normal file
21
addons/gecs/tests/components/c_complex_serialization_test.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
class_name C_ComplexSerializationTest
|
||||
extends Component
|
||||
|
||||
@export var array_value: Array[int] = [1, 2, 3, 4, 5]
|
||||
@export var string_array: Array[String] = ["hello", "world", "test"]
|
||||
@export var dict_value: Dictionary = {"key1": "value1", "key2": 123, "key3": true}
|
||||
@export var empty_array: Array = []
|
||||
@export var empty_dict: Dictionary = {}
|
||||
|
||||
func _init(
|
||||
_array_value: Array[int] = [1, 2, 3, 4, 5],
|
||||
_string_array: Array[String] = ["hello", "world", "test"],
|
||||
_dict_value: Dictionary = {"key1": "value1", "key2": 123, "key3": true},
|
||||
_empty_array: Array = [],
|
||||
_empty_dict: Dictionary = {}
|
||||
):
|
||||
array_value = _array_value
|
||||
string_array = _string_array
|
||||
dict_value = _dict_value
|
||||
empty_array = _empty_array
|
||||
empty_dict = _empty_dict
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpvr163gwyx2d
|
||||
4
addons/gecs/tests/components/c_debug_tracking_test_a.gd
Normal file
4
addons/gecs/tests/components/c_debug_tracking_test_a.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name C_DebugTrackingTestA
|
||||
extends Component
|
||||
|
||||
@export var value: float = 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0vhjx22wswv5
|
||||
4
addons/gecs/tests/components/c_debug_tracking_test_b.gd
Normal file
4
addons/gecs/tests/components/c_debug_tracking_test_b.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name C_DebugTrackingTestB
|
||||
extends Component
|
||||
|
||||
@export var count: int = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bijx0kal4npp
|
||||
3
addons/gecs/tests/components/c_domain_test_a.gd
Normal file
3
addons/gecs/tests/components/c_domain_test_a.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_DomainTestA
|
||||
extends Component
|
||||
@export var v_a: int = 1
|
||||
1
addons/gecs/tests/components/c_domain_test_a.gd.uid
Normal file
1
addons/gecs/tests/components/c_domain_test_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cqsmow0liv20e
|
||||
3
addons/gecs/tests/components/c_domain_test_b.gd
Normal file
3
addons/gecs/tests/components/c_domain_test_b.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_DomainTestB
|
||||
extends Component
|
||||
@export var v_b: int = 2
|
||||
1
addons/gecs/tests/components/c_domain_test_b.gd.uid
Normal file
1
addons/gecs/tests/components/c_domain_test_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjodoqd54f6pq
|
||||
22
addons/gecs/tests/components/c_observer_health.gd
Normal file
22
addons/gecs/tests/components/c_observer_health.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
## Test health component for observer tests with proper property_changed signal emission
|
||||
class_name C_ObserverHealth
|
||||
extends Component
|
||||
|
||||
@export var health: int = 100 : set = set_health
|
||||
@export var max_health: int = 100 : set = set_max_health
|
||||
|
||||
func set_health(new_health: int):
|
||||
var old_health = health
|
||||
health = new_health
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "health", old_health, new_health)
|
||||
|
||||
func set_max_health(new_max: int):
|
||||
var old_max = max_health
|
||||
max_health = new_max
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "max_health", old_max, new_max)
|
||||
|
||||
func _init(_health: int = 100, _max_health: int = 100):
|
||||
health = _health
|
||||
max_health = _max_health
|
||||
1
addons/gecs/tests/components/c_observer_health.gd.uid
Normal file
1
addons/gecs/tests/components/c_observer_health.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0o4jh5t35hqw
|
||||
22
addons/gecs/tests/components/c_observer_test.gd
Normal file
22
addons/gecs/tests/components/c_observer_test.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
## Test component for observer tests with proper property_changed signal emission
|
||||
class_name C_ObserverTest
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0 : set = set_value
|
||||
@export var name_prop: String = "" : set = set_name_prop
|
||||
|
||||
func set_value(new_value: int):
|
||||
var old_value = value
|
||||
value = new_value
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "value", old_value, new_value)
|
||||
|
||||
func set_name_prop(new_name: String):
|
||||
var old_name = name_prop
|
||||
name_prop = new_name
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "name_prop", old_name, new_name)
|
||||
|
||||
func _init(_value: int = 0, _name: String = ""):
|
||||
value = _value
|
||||
name_prop = _name
|
||||
1
addons/gecs/tests/components/c_observer_test.gd.uid
Normal file
1
addons/gecs/tests/components/c_observer_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmxcdgnk537l
|
||||
3
addons/gecs/tests/components/c_order_test_a.gd
Normal file
3
addons/gecs/tests/components/c_order_test_a.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestA
|
||||
extends Component
|
||||
@export var value_a: int = 1
|
||||
1
addons/gecs/tests/components/c_order_test_a.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://12rys1s4dqub
|
||||
3
addons/gecs/tests/components/c_order_test_b.gd
Normal file
3
addons/gecs/tests/components/c_order_test_b.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestB
|
||||
extends Component
|
||||
@export var value_b: int = 2
|
||||
1
addons/gecs/tests/components/c_order_test_b.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brsnu840dpdnw
|
||||
3
addons/gecs/tests/components/c_order_test_c.gd
Normal file
3
addons/gecs/tests/components/c_order_test_c.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestC
|
||||
extends Component
|
||||
@export var value_c: int = 3
|
||||
1
addons/gecs/tests/components/c_order_test_c.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_c.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkx8tgtgdngvs
|
||||
3
addons/gecs/tests/components/c_order_test_d.gd
Normal file
3
addons/gecs/tests/components/c_order_test_d.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestD
|
||||
extends Component
|
||||
@export var value_d: int = 4
|
||||
1
addons/gecs/tests/components/c_order_test_d.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdih4o87okurl
|
||||
3
addons/gecs/tests/components/c_order_test_e.gd
Normal file
3
addons/gecs/tests/components/c_order_test_e.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestE
|
||||
extends Component
|
||||
@export var value_e: int = 5
|
||||
1
addons/gecs/tests/components/c_order_test_e.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_e.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://djobbcytnokef
|
||||
3
addons/gecs/tests/components/c_order_test_f.gd
Normal file
3
addons/gecs/tests/components/c_order_test_f.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestF
|
||||
extends Component
|
||||
@export var value_f: int = 6
|
||||
1
addons/gecs/tests/components/c_order_test_f.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_f.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://be0tga28sdlof
|
||||
3
addons/gecs/tests/components/c_order_test_g.gd
Normal file
3
addons/gecs/tests/components/c_order_test_g.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestG
|
||||
extends Component
|
||||
@export var value_g: int = 7
|
||||
1
addons/gecs/tests/components/c_order_test_g.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_g.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctgvxw7pi4wro
|
||||
3
addons/gecs/tests/components/c_order_test_h.gd
Normal file
3
addons/gecs/tests/components/c_order_test_h.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestH
|
||||
extends Component
|
||||
@export var value_h: int = 8
|
||||
1
addons/gecs/tests/components/c_order_test_h.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_h.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://hyqseyaigq4o
|
||||
3
addons/gecs/tests/components/c_order_test_i.gd
Normal file
3
addons/gecs/tests/components/c_order_test_i.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestI
|
||||
extends Component
|
||||
@export var value_i: int = 9
|
||||
1
addons/gecs/tests/components/c_order_test_i.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_i.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c25bhc3kbc4e8
|
||||
3
addons/gecs/tests/components/c_order_test_j.gd
Normal file
3
addons/gecs/tests/components/c_order_test_j.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestJ
|
||||
extends Component
|
||||
@export var value_j: int = 10
|
||||
1
addons/gecs/tests/components/c_order_test_j.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_j.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1igiif6mkikj
|
||||
3
addons/gecs/tests/components/c_order_test_k.gd
Normal file
3
addons/gecs/tests/components/c_order_test_k.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestK
|
||||
extends Component
|
||||
@export var value_k: int = 11
|
||||
1
addons/gecs/tests/components/c_order_test_k.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_k.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jcoxghymmvmh
|
||||
3
addons/gecs/tests/components/c_order_test_l.gd
Normal file
3
addons/gecs/tests/components/c_order_test_l.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestL
|
||||
extends Component
|
||||
@export var value_l: int = 12
|
||||
1
addons/gecs/tests/components/c_order_test_l.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_l.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bce7cd48nf8e7
|
||||
3
addons/gecs/tests/components/c_order_test_m.gd
Normal file
3
addons/gecs/tests/components/c_order_test_m.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestM
|
||||
extends Component
|
||||
@export var value_m: int = 13
|
||||
1
addons/gecs/tests/components/c_order_test_m.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_m.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://df0af054av56n
|
||||
3
addons/gecs/tests/components/c_order_test_n.gd
Normal file
3
addons/gecs/tests/components/c_order_test_n.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestN
|
||||
extends Component
|
||||
@export var value_n: int = 14
|
||||
1
addons/gecs/tests/components/c_order_test_n.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_n.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkbwhig77q1j8
|
||||
3
addons/gecs/tests/components/c_order_test_o.gd
Normal file
3
addons/gecs/tests/components/c_order_test_o.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestO
|
||||
extends Component
|
||||
@export var value_o: int = 15
|
||||
1
addons/gecs/tests/components/c_order_test_o.gd.uid
Normal file
1
addons/gecs/tests/components/c_order_test_o.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bgsirllg7wil0
|
||||
3
addons/gecs/tests/components/c_perm_a.gd
Normal file
3
addons/gecs/tests/components/c_perm_a.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermA
|
||||
extends Component
|
||||
@export var v: int = 1
|
||||
1
addons/gecs/tests/components/c_perm_a.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bi4vscfom0st2
|
||||
3
addons/gecs/tests/components/c_perm_b.gd
Normal file
3
addons/gecs/tests/components/c_perm_b.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermB
|
||||
extends Component
|
||||
@export var v: int = 2
|
||||
1
addons/gecs/tests/components/c_perm_b.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c1svfcwyi2oie
|
||||
3
addons/gecs/tests/components/c_perm_c.gd
Normal file
3
addons/gecs/tests/components/c_perm_c.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermC
|
||||
extends Component
|
||||
@export var v: int = 3
|
||||
1
addons/gecs/tests/components/c_perm_c.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_c.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://0ynnafo2v1it
|
||||
3
addons/gecs/tests/components/c_perm_d.gd
Normal file
3
addons/gecs/tests/components/c_perm_d.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermD
|
||||
extends Component
|
||||
@export var v: int = 4
|
||||
1
addons/gecs/tests/components/c_perm_d.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cts7f306wa0fa
|
||||
3
addons/gecs/tests/components/c_perm_e.gd
Normal file
3
addons/gecs/tests/components/c_perm_e.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermE
|
||||
extends Component
|
||||
@export var v: int = 5
|
||||
1
addons/gecs/tests/components/c_perm_e.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_e.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c720mkd00xchu
|
||||
3
addons/gecs/tests/components/c_perm_f.gd
Normal file
3
addons/gecs/tests/components/c_perm_f.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_PermF
|
||||
extends Component
|
||||
@export var v: int = 6
|
||||
1
addons/gecs/tests/components/c_perm_f.gd.uid
Normal file
1
addons/gecs/tests/components/c_perm_f.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccws6g7g0j6w8
|
||||
21
addons/gecs/tests/components/c_persistent.gd
Normal file
21
addons/gecs/tests/components/c_persistent.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
extends Component
|
||||
class_name C_Persistent
|
||||
|
||||
@export var player_name: String = "Player1"
|
||||
@export var level: int = 1
|
||||
@export var health: float = 100.0
|
||||
@export var position: Vector2 = Vector2.ZERO
|
||||
@export var inventory: Array[String] = []
|
||||
|
||||
func _init(
|
||||
_player_name: String = "Player1",
|
||||
_level: int = 1,
|
||||
_health: float = 100.0,
|
||||
_position: Vector2 = Vector2.ZERO,
|
||||
_inventory: Array[String] = []
|
||||
):
|
||||
player_name = _player_name
|
||||
level = _level
|
||||
health = _health
|
||||
position = _position
|
||||
inventory = _inventory
|
||||
1
addons/gecs/tests/components/c_persistent.gd.uid
Normal file
1
addons/gecs/tests/components/c_persistent.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bikywcisu1fsu
|
||||
14
addons/gecs/tests/components/c_position.gd
Normal file
14
addons/gecs/tests/components/c_position.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
## Test position component for observer performance tests
|
||||
class_name C_TestPosition
|
||||
extends Component
|
||||
|
||||
@export var position: Vector3 = Vector3.ZERO : set = set_position
|
||||
|
||||
func set_position(new_pos: Vector3):
|
||||
var old_pos = position
|
||||
position = new_pos
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "position", old_pos, new_pos)
|
||||
|
||||
func _init(_position: Vector3 = Vector3.ZERO):
|
||||
position = _position
|
||||
1
addons/gecs/tests/components/c_position.gd.uid
Normal file
1
addons/gecs/tests/components/c_position.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://33n1ne8tuyja
|
||||
27
addons/gecs/tests/components/c_serialization_test.gd
Normal file
27
addons/gecs/tests/components/c_serialization_test.gd
Normal file
@@ -0,0 +1,27 @@
|
||||
extends Component
|
||||
class_name C_SerializationTest
|
||||
|
||||
@export var int_value: int = 42
|
||||
@export var float_value: float = 3.14
|
||||
@export var string_value: String = "test_string"
|
||||
@export var bool_value: bool = true
|
||||
@export var vector2_value: Vector2 = Vector2(1.0, 2.0)
|
||||
@export var vector3_value: Vector3 = Vector3(1.0, 2.0, 3.0)
|
||||
@export var color_value: Color = Color.RED
|
||||
|
||||
func _init(
|
||||
_int_value: int = 42,
|
||||
_float_value: float = 3.14,
|
||||
_string_value: String = "test_string",
|
||||
_bool_value: bool = true,
|
||||
_vector2_value: Vector2 = Vector2(1.0, 2.0),
|
||||
_vector3_value: Vector3 = Vector3(1.0, 2.0, 3.0),
|
||||
_color_value: Color = Color.RED
|
||||
):
|
||||
int_value = _int_value
|
||||
float_value = _float_value
|
||||
string_value = _string_value
|
||||
bool_value = _bool_value
|
||||
vector2_value = _vector2_value
|
||||
vector3_value = _vector3_value
|
||||
color_value = _color_value
|
||||
1
addons/gecs/tests/components/c_serialization_test.gd.uid
Normal file
1
addons/gecs/tests/components/c_serialization_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3w2r1fop8e52
|
||||
8
addons/gecs/tests/components/c_test_a.gd
Normal file
8
addons/gecs/tests/components/c_test_a.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name C_TestA
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0
|
||||
|
||||
|
||||
func _init(_value: int = 0):
|
||||
value = _value
|
||||
1
addons/gecs/tests/components/c_test_a.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://5antadqj7v84
|
||||
8
addons/gecs/tests/components/c_test_b.gd
Normal file
8
addons/gecs/tests/components/c_test_b.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name C_TestB
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0
|
||||
|
||||
|
||||
func _init(_value: int = 0):
|
||||
value = _value
|
||||
1
addons/gecs/tests/components/c_test_b.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6lvbdptfldrg
|
||||
8
addons/gecs/tests/components/c_test_c.gd
Normal file
8
addons/gecs/tests/components/c_test_c.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name C_TestC
|
||||
extends Component
|
||||
|
||||
@export var value: int
|
||||
|
||||
|
||||
func _init(_value: int = 0):
|
||||
value = _value
|
||||
1
addons/gecs/tests/components/c_test_c.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_c.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3lo6r4xvicxp
|
||||
8
addons/gecs/tests/components/c_test_d.gd
Normal file
8
addons/gecs/tests/components/c_test_d.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name C_TestD
|
||||
extends Component
|
||||
|
||||
@export var points: int = 0
|
||||
|
||||
|
||||
func _init(_points: int = 0):
|
||||
points = _points
|
||||
1
addons/gecs/tests/components/c_test_d.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cd2ml5rtb3c8g
|
||||
4
addons/gecs/tests/components/c_test_e.gd
Normal file
4
addons/gecs/tests/components/c_test_e.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name C_TestE
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0
|
||||
1
addons/gecs/tests/components/c_test_e.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_e.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cp6siju1aijj2
|
||||
11
addons/gecs/tests/components/c_test_f.gd
Normal file
11
addons/gecs/tests/components/c_test_f.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
class_name C_TestF
|
||||
extends Component
|
||||
|
||||
var value: int = 0 # properties with no export annotation
|
||||
|
||||
static var init_count: int = 0
|
||||
|
||||
func _init(_value: int = 0):
|
||||
value = _value
|
||||
init_count += 1
|
||||
print("Component c_test_f init, value=%d" % value)
|
||||
1
addons/gecs/tests/components/c_test_f.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_f.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://py2qgdkhiy30
|
||||
12
addons/gecs/tests/components/c_test_g.gd
Normal file
12
addons/gecs/tests/components/c_test_g.gd
Normal file
@@ -0,0 +1,12 @@
|
||||
class_name C_TestG
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0
|
||||
|
||||
static var init_count: int = 0
|
||||
|
||||
func _init(_value: int = 0):
|
||||
value = _value
|
||||
init_count += 1
|
||||
# to test _init() calling problem
|
||||
print("Component c_test_g init, value=%d" % value)
|
||||
1
addons/gecs/tests/components/c_test_g.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_g.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4ud215bve6ap
|
||||
8
addons/gecs/tests/components/c_test_h.gd
Normal file
8
addons/gecs/tests/components/c_test_h.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name C_TestH
|
||||
extends Component
|
||||
|
||||
@export var value: int = 0
|
||||
|
||||
# Simulates parameters with no default values
|
||||
func _init(_value: int):
|
||||
value = _value
|
||||
1
addons/gecs/tests/components/c_test_h.gd.uid
Normal file
1
addons/gecs/tests/components/c_test_h.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b8ptu8k8rp1sb
|
||||
14
addons/gecs/tests/components/c_velocity.gd
Normal file
14
addons/gecs/tests/components/c_velocity.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
## Test velocity component for observer performance tests
|
||||
class_name C_TestVelocity
|
||||
extends Component
|
||||
|
||||
@export var velocity: Vector3 = Vector3.ZERO : set = set_velocity
|
||||
|
||||
func set_velocity(new_vel: Vector3):
|
||||
var old_vel = velocity
|
||||
velocity = new_vel
|
||||
# Emit signal for observers to detect the change
|
||||
property_changed.emit(self, "velocity", old_vel, new_vel)
|
||||
|
||||
func _init(_velocity: Vector3 = Vector3.ZERO):
|
||||
velocity = _velocity
|
||||
1
addons/gecs/tests/components/c_velocity.gd.uid
Normal file
1
addons/gecs/tests/components/c_velocity.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ckhr8q3glmacs
|
||||
147
addons/gecs/tests/core/test_archetype_edge_cache.gd
Normal file
147
addons/gecs/tests/core/test_archetype_edge_cache.gd
Normal file
@@ -0,0 +1,147 @@
|
||||
class_name TestArchetypeEdgeCacheBug
|
||||
extends GdUnitTestSuite
|
||||
## Test suite for archetype edge cache bug
|
||||
##
|
||||
## Tests that archetypes retrieved from edge cache are properly re-registered
|
||||
## with the world when they were previously removed due to being empty.
|
||||
##
|
||||
## Bug sequence:
|
||||
## 1. Entity A gets component added -> creates archetype X, cached edge
|
||||
## 2. Entity A removed -> archetype X becomes empty, gets removed from world.archetypes
|
||||
## 3. Entity B gets same component -> uses cached edge to archetype X
|
||||
## 4. BUG: archetype X not in world.archetypes, so queries can't find Entity B
|
||||
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
|
||||
func after_test():
|
||||
if world:
|
||||
world.purge(false)
|
||||
|
||||
|
||||
## Test that archetypes retrieved from edge cache are re-registered with world
|
||||
func test_archetype_reregistered_after_edge_cache_retrieval():
|
||||
# ARRANGE: Create two entities with same initial components
|
||||
var entity1 = Entity.new()
|
||||
entity1.add_component(C_TestA.new())
|
||||
world.add_entities([entity1])
|
||||
|
||||
var entity2 = Entity.new()
|
||||
entity2.add_component(C_TestA.new())
|
||||
world.add_entities([entity2])
|
||||
|
||||
# ACT 1: Add ComponentB to entity1 (creates new archetype + edge cache)
|
||||
var comp_b1 = C_TestB.new()
|
||||
entity1.add_component(comp_b1)
|
||||
|
||||
# Get the archetype signature for A+B combination
|
||||
var archetype_with_b = world.entity_to_archetype[entity1]
|
||||
var signature_with_b = archetype_with_b.signature
|
||||
|
||||
# Verify archetype is in world.archetypes
|
||||
assert_bool(world.archetypes.has(signature_with_b)).is_true()
|
||||
|
||||
# ACT 2: Remove entity1 to make archetype empty (triggers cleanup)
|
||||
world.remove_entity(entity1)
|
||||
|
||||
# Verify archetype was removed from world.archetypes when empty
|
||||
assert_bool(world.archetypes.has(signature_with_b)).is_false()
|
||||
|
||||
# ACT 3: Add ComponentB to entity2 (should use edge cache)
|
||||
# This is where the bug would occur - archetype retrieved from cache
|
||||
# but not re-registered with world
|
||||
var comp_b2 = C_TestB.new()
|
||||
entity2.add_component(comp_b2)
|
||||
|
||||
# ASSERT: Archetype should be back in world.archetypes
|
||||
assert_bool(world.archetypes.has(signature_with_b)).is_true()
|
||||
|
||||
# ASSERT: Query should find entity2
|
||||
var query = QueryBuilder.new(world).with_all([C_TestA, C_TestB])
|
||||
var results = query.execute()
|
||||
assert_int(results.size()).is_equal(1)
|
||||
assert_object(results[0]).is_same(entity2)
|
||||
|
||||
|
||||
## Test that queries find entities in edge-cached archetypes
|
||||
func test_query_finds_entities_in_edge_cached_archetype():
|
||||
# This reproduces the exact projectile bug scenario
|
||||
# ARRANGE: Create 3 projectiles
|
||||
var projectile1 = Entity.new()
|
||||
projectile1.add_component(C_TestA.new()) # Simulates C_Projectile
|
||||
world.add_entities([projectile1])
|
||||
|
||||
var projectile2 = Entity.new()
|
||||
projectile2.add_component(C_TestA.new())
|
||||
world.add_entities([projectile2])
|
||||
|
||||
var projectile3 = Entity.new()
|
||||
projectile3.add_component(C_TestA.new())
|
||||
world.add_entities([projectile3])
|
||||
|
||||
# ACT 1: First projectile collides (adds ComponentB = C_Collision)
|
||||
projectile1.add_component(C_TestB.new())
|
||||
|
||||
# Verify query finds it
|
||||
var collision_query = QueryBuilder.new(world).with_all([C_TestA, C_TestB])
|
||||
assert_int(collision_query.execute().size()).is_equal(1)
|
||||
|
||||
# ACT 2: First projectile processed and removed (empties collision archetype)
|
||||
world.remove_entity(projectile1)
|
||||
|
||||
# ACT 3: Second projectile collides (edge cache used)
|
||||
projectile2.add_component(C_TestB.new())
|
||||
|
||||
# ASSERT: Query should find second projectile (BUG: it wouldn't before fix)
|
||||
var results = collision_query.execute()
|
||||
assert_int(results.size()).is_equal(1)
|
||||
assert_object(results[0]).is_same(projectile2)
|
||||
|
||||
# ACT 4: Third projectile also collides while second still exists
|
||||
projectile3.add_component(C_TestB.new())
|
||||
|
||||
# ASSERT: Query should find both projectiles
|
||||
results = collision_query.execute()
|
||||
assert_int(results.size()).is_equal(2)
|
||||
|
||||
|
||||
## Test rapid add/remove cycles don't lose archetypes
|
||||
func test_rapid_archetype_cycling():
|
||||
# Tests the exact pattern: create -> empty -> reuse via cache
|
||||
var entities = []
|
||||
for i in range(5):
|
||||
var e = Entity.new()
|
||||
e.add_component(C_TestA.new())
|
||||
world.add_entities([e])
|
||||
entities.append(e)
|
||||
|
||||
# Cycle through adding/removing ComponentB
|
||||
for cycle in range(3):
|
||||
# Add ComponentB to first entity (creates/reuses archetype)
|
||||
entities[0].add_component(C_TestB.new())
|
||||
|
||||
# Query should find it
|
||||
var query = QueryBuilder.new(world).with_all([C_TestA, C_TestB])
|
||||
var results = query.execute()
|
||||
assert_int(results.size()).is_equal(1)
|
||||
|
||||
# Remove entity (empties archetype)
|
||||
world.remove_entity(entities[0])
|
||||
|
||||
# Create new entity for next cycle
|
||||
entities[0] = Entity.new()
|
||||
entities[0].add_component(C_TestA.new())
|
||||
world.add_entities([entities[0]])
|
||||
|
||||
# Final cycle - should still work
|
||||
entities[0].add_component(C_TestB.new())
|
||||
var final_query = QueryBuilder.new(world).with_all([C_TestA, C_TestB])
|
||||
assert_int(final_query.execute().size()).is_equal(1)
|
||||
1
addons/gecs/tests/core/test_archetype_edge_cache.gd.uid
Normal file
1
addons/gecs/tests/core/test_archetype_edge_cache.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://hphmrhtswrjq
|
||||
156
addons/gecs/tests/core/test_archetype_systems.gd
Normal file
156
addons/gecs/tests/core/test_archetype_systems.gd
Normal file
@@ -0,0 +1,156 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
## Test archetype-based system execution
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
|
||||
func after_test():
|
||||
if world:
|
||||
world.purge(false)
|
||||
|
||||
|
||||
func test_archetype_system_processes_entities():
|
||||
# Create test system that uses archetype mode
|
||||
var test_system = ArchetypeTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
# Create entities with components
|
||||
var entity1 = Entity.new()
|
||||
entity1.name = "Entity1"
|
||||
world.add_entity(entity1, [C_TestA.new()])
|
||||
|
||||
var entity2 = Entity.new()
|
||||
entity2.name = "Entity2"
|
||||
world.add_entity(entity2, [C_TestA.new()])
|
||||
|
||||
# Process the system
|
||||
world.process(0.1)
|
||||
|
||||
# Verify archetype method was called
|
||||
assert_int(test_system.archetype_call_count).is_equal(1)
|
||||
assert_int(test_system.entities_processed).is_equal(2)
|
||||
|
||||
|
||||
func test_archetype_iteration_order_matches_iterate():
|
||||
# System that checks component order
|
||||
var test_system = ArchetypeOrderTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
# Create entity with multiple components
|
||||
var entity = Entity.new()
|
||||
entity.name = "TestEntity"
|
||||
world.add_entity(entity, [C_TestB.new(), C_TestA.new()])
|
||||
|
||||
# Process
|
||||
world.process(0.1)
|
||||
|
||||
# Verify components were in correct order (as specified in iterate())
|
||||
assert_bool(test_system.order_correct).is_true()
|
||||
|
||||
|
||||
func test_archetype_processes_entities_with_extra_components():
|
||||
# Query for A and B, but entity has A, B, and C
|
||||
var test_system = ArchetypeSubsetTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
# Entity has MORE components than query asks for
|
||||
var entity = Entity.new()
|
||||
entity.name = "ExtraComponents"
|
||||
world.add_entity(entity, [C_TestA.new(), C_TestB.new(), C_TestC.new()])
|
||||
|
||||
# Should still match and process
|
||||
world.process(0.1)
|
||||
|
||||
assert_int(test_system.entities_processed).is_equal(1)
|
||||
|
||||
|
||||
func test_archetype_processes_multiple_archetypes():
|
||||
# System that tracks archetype calls
|
||||
var test_system = ArchetypeMultipleArchetypesTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
# Create entities with different component combinations
|
||||
# Archetype 1: [A, B]
|
||||
var entity1 = Entity.new()
|
||||
world.add_entity(entity1, [C_TestA.new(), C_TestB.new()])
|
||||
|
||||
var entity2 = Entity.new()
|
||||
world.add_entity(entity2, [C_TestA.new(), C_TestB.new()])
|
||||
|
||||
# Archetype 2: [A, B, C]
|
||||
var entity3 = Entity.new()
|
||||
world.add_entity(entity3, [C_TestA.new(), C_TestB.new(), C_TestC.new()])
|
||||
|
||||
# Process
|
||||
world.process(0.1)
|
||||
|
||||
# Should be called once per archetype
|
||||
assert_int(test_system.archetype_call_count).is_equal(2)
|
||||
assert_int(test_system.total_entities_processed).is_equal(3)
|
||||
|
||||
|
||||
func test_archetype_column_data_is_correct():
|
||||
# System that verifies column data
|
||||
var test_system = ArchetypeColumnDataTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
# Create entities with specific values
|
||||
var entity1 = Entity.new()
|
||||
var comp_a1 = C_TestA.new()
|
||||
comp_a1.value = 10
|
||||
world.add_entity(entity1, [comp_a1])
|
||||
|
||||
var entity2 = Entity.new()
|
||||
var comp_a2 = C_TestA.new()
|
||||
comp_a2.value = 20
|
||||
world.add_entity(entity2, [comp_a2])
|
||||
|
||||
# Process
|
||||
world.process(0.1)
|
||||
|
||||
# Verify column had correct values
|
||||
assert_array(test_system.values_seen).contains_exactly([10, 20])
|
||||
|
||||
|
||||
func test_archetype_modifies_components():
|
||||
# System that modifies component values
|
||||
var test_system = ArchetypeModifyTestSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
var entity = Entity.new()
|
||||
var comp = C_TestA.new()
|
||||
comp.value = 5
|
||||
world.add_entity(entity, [comp])
|
||||
|
||||
# Process multiple times
|
||||
world.process(0.1)
|
||||
world.process(0.1)
|
||||
world.process(0.1)
|
||||
|
||||
# Value should have been incremented each time
|
||||
# Get the component from the entity (not the local reference)
|
||||
var updated_comp = entity.get_component(C_TestA)
|
||||
assert_int(updated_comp.value).is_equal(8)
|
||||
|
||||
|
||||
func test_archetype_works_without_iterate_call():
|
||||
# System that doesn't call iterate() still works, just gets empty components array
|
||||
var test_system = ArchetypeNoIterateSystem.new()
|
||||
world.add_system(test_system)
|
||||
|
||||
var entity = Entity.new()
|
||||
world.add_entity(entity, [C_TestA.new()])
|
||||
|
||||
# Should work fine - system can use get_component() instead
|
||||
world.process(0.1)
|
||||
|
||||
# System should have processed the entity
|
||||
assert_int(test_system.processed).is_equal(1)
|
||||
1
addons/gecs/tests/core/test_archetype_systems.gd.uid
Normal file
1
addons/gecs/tests/core/test_archetype_systems.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cem3jyvifqys
|
||||
@@ -0,0 +1,371 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
func after_test():
|
||||
if world:
|
||||
world.purge(false)
|
||||
|
||||
func test_complex_nested_relationships_serialization():
|
||||
# Create a complex hierarchy: Player -> Weapon -> Attachment
|
||||
# This tests multi-level relationship auto-inclusion
|
||||
# 1. Create Player entity
|
||||
var player = Entity.new()
|
||||
player.name = "Player"
|
||||
player.add_component(C_TestA.new()) # Player-specific component
|
||||
|
||||
# 2. Create Weapon entity with weapon-specific components
|
||||
var weapon = Entity.new()
|
||||
weapon.name = "AssaultRifle"
|
||||
weapon.add_component(C_TestB.new()) # Weapon component
|
||||
weapon.add_component(C_TestC.new()) # Damage component
|
||||
|
||||
# 3. Create Attachment entity
|
||||
var attachment = Entity.new()
|
||||
attachment.name = "RedDotSight"
|
||||
attachment.add_component(C_TestD.new()) # Attachment component
|
||||
attachment.add_component(C_TestE.new()) # Accuracy modifier component
|
||||
|
||||
# 4. Create another attachment for testing multiple relationships
|
||||
var attachment2 = Entity.new()
|
||||
attachment2.name = "Silencer"
|
||||
attachment2.add_component(C_TestF.new()) # Another attachment component
|
||||
|
||||
# 5. Set up relationships: Player -> Weapon -> Attachments
|
||||
var player_weapon_rel = Relationship.new(C_TestA.new(), weapon) # Player equipped with weapon
|
||||
player.add_relationship(player_weapon_rel)
|
||||
|
||||
var weapon_sight_rel = Relationship.new(C_TestB.new(), attachment) # Weapon has sight
|
||||
weapon.add_relationship(weapon_sight_rel)
|
||||
|
||||
var weapon_silencer_rel = Relationship.new(C_TestC.new(), attachment2) # Weapon has silencer
|
||||
weapon.add_relationship(weapon_silencer_rel)
|
||||
|
||||
# 6. Add entities to world (don't add to scene tree to preserve names)
|
||||
world.add_entity(player)
|
||||
world.add_entity(weapon)
|
||||
world.add_entity(attachment)
|
||||
world.add_entity(attachment2)
|
||||
|
||||
# Store original UUIDs for verification
|
||||
var player_id = player.id
|
||||
var weapon_id = weapon.id
|
||||
var attachment_id = attachment.id
|
||||
var attachment2_id = attachment2.id
|
||||
|
||||
print("=== BEFORE SERIALIZATION ===")
|
||||
print("Player UUID: ", player_id)
|
||||
print("Weapon UUID: ", weapon_id)
|
||||
print("Attachment UUID: ", attachment_id)
|
||||
print("Attachment2 UUID: ", attachment2_id)
|
||||
print("Player relationships: ", player.relationships.size())
|
||||
print("Weapon relationships: ", weapon.relationships.size())
|
||||
|
||||
# 7. Serialize ONLY the player (should auto-include weapon and attachments)
|
||||
var query = world.query.with_all([C_TestA]) # Only matches player
|
||||
var serialized_data = ECS.serialize(query)
|
||||
|
||||
print("=== SERIALIZATION RESULTS ===")
|
||||
print("Total entities serialized: ", serialized_data.entities.size())
|
||||
|
||||
# 8. Verify serialization results
|
||||
assert_that(serialized_data.entities).has_size(4) # All 4 entities should be included
|
||||
|
||||
# Count auto-included vs original entities
|
||||
var auto_included_count = 0
|
||||
var original_count = 0
|
||||
var player_data = null
|
||||
var weapon_data = null
|
||||
var attachment_data = null
|
||||
var attachment2_data = null
|
||||
|
||||
for entity_data in serialized_data.entities:
|
||||
print("Entity: ", entity_data.entity_name, " - Auto-included: ", entity_data.auto_included, " - id: ", entity_data.id)
|
||||
|
||||
if entity_data.auto_included:
|
||||
auto_included_count += 1
|
||||
else:
|
||||
original_count += 1
|
||||
|
||||
# Find specific entities for detailed verification
|
||||
match entity_data.entity_name:
|
||||
"Player":
|
||||
player_data = entity_data
|
||||
"AssaultRifle":
|
||||
weapon_data = entity_data
|
||||
"RedDotSight":
|
||||
attachment_data = entity_data
|
||||
"Silencer":
|
||||
attachment2_data = entity_data
|
||||
|
||||
# Verify auto-inclusion flags
|
||||
assert_that(original_count).is_equal(1) # Only player from original query
|
||||
assert_that(auto_included_count).is_equal(3) # Weapon and both attachments auto-included
|
||||
|
||||
# Verify specific entity data
|
||||
assert_that(player_data).is_not_null()
|
||||
assert_that(player_data.auto_included).is_false() # Player was in original query
|
||||
assert_that(player_data.relationships).has_size(1) # Player -> Weapon relationship
|
||||
|
||||
assert_that(weapon_data).is_not_null()
|
||||
assert_that(weapon_data.auto_included).is_true() # Weapon was auto-included
|
||||
assert_that(weapon_data.relationships).has_size(2) # Weapon -> Attachments relationships
|
||||
|
||||
assert_that(attachment_data).is_not_null()
|
||||
assert_that(attachment_data.auto_included).is_true() # Attachment was auto-included
|
||||
|
||||
assert_that(attachment2_data).is_not_null()
|
||||
assert_that(attachment2_data.auto_included).is_true() # Attachment2 was auto-included
|
||||
|
||||
# 9. Save and load the serialized data
|
||||
var file_path = "res://reports/test_complex_relationships.tres"
|
||||
ECS.save(serialized_data, file_path)
|
||||
|
||||
# 10. Clear the world to simulate fresh start
|
||||
world.purge(false)
|
||||
assert_that(world.entities).has_size(0)
|
||||
assert_that(world.entity_id_registry).has_size(0)
|
||||
|
||||
# 11. Deserialize and add back to world
|
||||
var deserialized_entities = ECS.deserialize(file_path)
|
||||
|
||||
print("=== DESERIALIZATION RESULTS ===")
|
||||
print("Deserialized entities: ", deserialized_entities.size())
|
||||
|
||||
assert_that(deserialized_entities).has_size(4)
|
||||
|
||||
# Add all entities back to world (don't add to scene tree to avoid naming conflicts)
|
||||
for entity in deserialized_entities:
|
||||
world.add_entity(entity, null, false)
|
||||
|
||||
# 12. Verify world state after deserialization
|
||||
assert_that(world.entities).has_size(4)
|
||||
assert_that(world.entity_id_registry).has_size(4)
|
||||
|
||||
# Find entities by UUID to verify they're properly restored
|
||||
var restored_player = world.get_entity_by_id(player_id)
|
||||
var restored_weapon = world.get_entity_by_id(weapon_id)
|
||||
var restored_attachment = world.get_entity_by_id(attachment_id)
|
||||
var restored_attachment2 = world.get_entity_by_id(attachment2_id)
|
||||
|
||||
print("=== RESTORED ENTITIES ===")
|
||||
print("Player found: ", restored_player != null, " - Name: ", restored_player.name if restored_player else "null")
|
||||
print("Weapon found: ", restored_weapon != null, " - Name: ", restored_weapon.name if restored_weapon else "null")
|
||||
print("Attachment found: ", restored_attachment != null, " - Name: ", restored_attachment.name if restored_attachment else "null")
|
||||
print("Attachment2 found: ", restored_attachment2 != null, " - Name: ", restored_attachment2.name if restored_attachment2 else "null")
|
||||
|
||||
# Verify all entities were found
|
||||
assert_that(restored_player).is_not_null()
|
||||
assert_that(restored_weapon).is_not_null()
|
||||
assert_that(restored_attachment).is_not_null()
|
||||
assert_that(restored_attachment2).is_not_null()
|
||||
|
||||
# Verify entity names are preserved
|
||||
assert_that(restored_player.name).is_equal("Player")
|
||||
assert_that(restored_weapon.name).is_equal("AssaultRifle")
|
||||
assert_that(restored_attachment.name).is_equal("RedDotSight")
|
||||
assert_that(restored_attachment2.name).is_equal("Silencer")
|
||||
|
||||
# 13. Verify relationships are intact
|
||||
print("=== RELATIONSHIP VERIFICATION ===")
|
||||
print("Player relationships: ", restored_player.relationships.size())
|
||||
print("Weapon relationships: ", restored_weapon.relationships.size())
|
||||
|
||||
# Player should have 1 relationship to weapon
|
||||
assert_that(restored_player.relationships).has_size(1)
|
||||
var player_rel = restored_player.relationships[0]
|
||||
assert_that(player_rel.target).is_equal(restored_weapon)
|
||||
print("Player -> Weapon relationship intact: ", player_rel.target.name)
|
||||
|
||||
# Weapon should have 2 relationships to attachments
|
||||
assert_that(restored_weapon.relationships).has_size(2)
|
||||
|
||||
var weapon_targets = []
|
||||
var weapon_target_entities = []
|
||||
for rel in restored_weapon.relationships:
|
||||
weapon_target_entities.append(rel.target)
|
||||
weapon_targets.append(rel.target.name)
|
||||
print("Weapon -> ", rel.target.name, " relationship intact")
|
||||
|
||||
# Verify weapon is connected to both attachments
|
||||
assert_that(weapon_target_entities).contains(restored_attachment)
|
||||
assert_that(weapon_target_entities).contains(restored_attachment2)
|
||||
assert_that(weapon_targets).contains("RedDotSight")
|
||||
assert_that(weapon_targets).contains("Silencer")
|
||||
|
||||
# 14. Verify components are preserved
|
||||
assert_that(restored_player.has_component(C_TestA)).is_true()
|
||||
assert_that(restored_weapon.has_component(C_TestB)).is_true()
|
||||
assert_that(restored_weapon.has_component(C_TestC)).is_true()
|
||||
assert_that(restored_attachment.has_component(C_TestD)).is_true()
|
||||
assert_that(restored_attachment.has_component(C_TestE)).is_true()
|
||||
assert_that(restored_attachment2.has_component(C_TestF)).is_true()
|
||||
|
||||
print("=== TEST PASSED: Complex nested relationships preserved! ===")
|
||||
world.remove_entities(deserialized_entities)
|
||||
|
||||
|
||||
func test_relationship_replacement_with_id_collision():
|
||||
# Test that when entities with relationships are replaced via UUID collision,
|
||||
# the relationships update correctly to point to the new entities
|
||||
# 1. Create initial setup: Player -> Weapon
|
||||
var player = Entity.new()
|
||||
player.name = "Player"
|
||||
player.add_component(C_TestA.new())
|
||||
player.set("id", "player-id-123")
|
||||
|
||||
var old_weapon = Entity.new()
|
||||
old_weapon.name = "OldWeapon"
|
||||
old_weapon.add_component(C_TestB.new())
|
||||
old_weapon.set("id", "weapon-id-456")
|
||||
|
||||
var player_weapon_rel = Relationship.new(C_TestA.new(), old_weapon)
|
||||
player.add_relationship(player_weapon_rel)
|
||||
|
||||
world.add_entity(player)
|
||||
world.add_entity(old_weapon)
|
||||
|
||||
# Verify initial relationship
|
||||
assert_that(player.relationships).has_size(1)
|
||||
assert_that(player.relationships[0].target).is_equal(old_weapon)
|
||||
assert_that(player.relationships[0].target.name).is_equal("OldWeapon")
|
||||
|
||||
# 2. Serialize the current state
|
||||
var query = world.query.with_all([C_TestA])
|
||||
var serialized_data = ECS.serialize(query)
|
||||
var file_path = "res://reports/test_replacement_relationships.tres"
|
||||
ECS.save(serialized_data, file_path)
|
||||
|
||||
# 3. Create "updated" entities with same UUIDs but different data
|
||||
var new_weapon = Entity.new()
|
||||
new_weapon.name = "NewUpgradedWeapon"
|
||||
new_weapon.add_component(C_TestB.new())
|
||||
new_weapon.add_component(C_TestC.new()) # Added component
|
||||
new_weapon.set("id", "weapon-id-456") # Same UUID!
|
||||
|
||||
# 4. Add new weapon (should replace old weapon)
|
||||
world.add_entity(new_weapon)
|
||||
|
||||
# Verify replacement occurred
|
||||
assert_that(world.entities).has_size(2) # Still only 2 entities
|
||||
var current_weapon = world.get_entity_by_id("weapon-id-456")
|
||||
assert_that(current_weapon).is_equal(new_weapon)
|
||||
assert_that(current_weapon.name).is_equal("NewUpgradedWeapon")
|
||||
assert_that(current_weapon.has_component(C_TestC)).is_true()
|
||||
|
||||
# 5. NOTE: When we replace an entity, existing relationships still point to the old entity object
|
||||
# This is expected behavior - the relationship contains a direct Entity reference
|
||||
# To update relationships, we would need to re-serialize/deserialize or manually update them
|
||||
print("Current relationship target: ", player.relationships[0].target.name)
|
||||
print("Expected: Relationship still points to old entity until re-serialized")
|
||||
|
||||
print("=== Relationship correctly updated after entity replacement ===")
|
||||
|
||||
# 6. Now test loading the old save file (should replace with old state)
|
||||
var loaded_entities = ECS.deserialize(file_path)
|
||||
|
||||
for entity in loaded_entities:
|
||||
world.add_entity(entity) # Should trigger replacements
|
||||
|
||||
# Verify entities were replaced with old state
|
||||
var final_weapon = world.get_entity_by_id("weapon-id-456")
|
||||
print("Final weapon name: ", final_weapon.name)
|
||||
assert_that(final_weapon.has_component(C_TestC)).is_false() # Lost the added component
|
||||
|
||||
# Verify relationship points to restored weapon
|
||||
var final_player = world.get_entity_by_id("player-id-123")
|
||||
assert_that(final_player.relationships).has_size(1)
|
||||
assert_that(final_player.relationships[0].target).is_equal(final_weapon)
|
||||
print("Final relationship target name: ", final_player.relationships[0].target.name)
|
||||
|
||||
print("=== Save/Load replacement cycle completed successfully ===")
|
||||
|
||||
|
||||
func test_partial_serialization_auto_inclusion():
|
||||
# Test that we can serialize a subset of entities and auto-include dependencies
|
||||
# while excluding unrelated entities
|
||||
# Create multiple independent entity groups
|
||||
# Group 1: Player -> Weapon -> Attachment (should be included)
|
||||
var player = Entity.new()
|
||||
player.name = "Player"
|
||||
player.add_component(C_TestA.new())
|
||||
|
||||
var weapon = Entity.new()
|
||||
weapon.name = "Weapon"
|
||||
weapon.add_component(C_TestB.new())
|
||||
|
||||
var attachment = Entity.new()
|
||||
attachment.name = "Attachment"
|
||||
attachment.add_component(C_TestC.new())
|
||||
|
||||
player.add_relationship(Relationship.new(C_TestA.new(), weapon))
|
||||
weapon.add_relationship(Relationship.new(C_TestB.new(), attachment))
|
||||
|
||||
# Group 2: Enemy -> EnemyWeapon (should NOT be included)
|
||||
var enemy = Entity.new()
|
||||
enemy.name = "Enemy"
|
||||
enemy.add_component(C_TestD.new()) # Different component type
|
||||
|
||||
var enemy_weapon = Entity.new()
|
||||
enemy_weapon.name = "EnemyWeapon"
|
||||
enemy_weapon.add_component(C_TestE.new())
|
||||
|
||||
enemy.add_relationship(Relationship.new(C_TestD.new(), enemy_weapon))
|
||||
|
||||
# Group 3: Standalone entity (should NOT be included)
|
||||
var standalone = Entity.new()
|
||||
standalone.name = "Standalone"
|
||||
standalone.add_component(C_TestF.new())
|
||||
|
||||
# Add all entities to world (don't add to scene tree)
|
||||
world.add_entity(player)
|
||||
world.add_entity(weapon)
|
||||
world.add_entity(attachment)
|
||||
world.add_entity(enemy)
|
||||
world.add_entity(enemy_weapon)
|
||||
world.add_entity(standalone)
|
||||
|
||||
assert_that(world.entities).has_size(6)
|
||||
|
||||
# Serialize ONLY entities with C_TestA (just the player)
|
||||
var query = world.query.with_all([C_TestA])
|
||||
var serialized_data = ECS.serialize(query)
|
||||
|
||||
print("=== PARTIAL SERIALIZATION RESULTS ===")
|
||||
print("Total entities in world: ", world.entities.size())
|
||||
print("Entities serialized: ", serialized_data.entities.size())
|
||||
|
||||
# Should include Player + Weapon + Attachment (3 total) but NOT Enemy group or Standalone
|
||||
assert_that(serialized_data.entities).has_size(3)
|
||||
|
||||
var serialized_names = []
|
||||
for entity_data in serialized_data.entities:
|
||||
serialized_names.append(entity_data.entity_name)
|
||||
print("Serialized: ", entity_data.entity_name, " (auto-included: ", entity_data.auto_included, ")")
|
||||
|
||||
# Verify correct entities were included
|
||||
assert_that(serialized_names).contains("Player")
|
||||
assert_that(serialized_names).contains("Weapon")
|
||||
assert_that(serialized_names).contains("Attachment")
|
||||
|
||||
# Verify incorrect entities were excluded
|
||||
assert_that(serialized_names.has("Enemy")).is_false()
|
||||
assert_that(serialized_names.has("EnemyWeapon")).is_false()
|
||||
assert_that(serialized_names.has("Standalone")).is_false()
|
||||
|
||||
# Verify auto-inclusion flags
|
||||
var player_data = serialized_data.entities.filter(func(e): return e.entity_name == "Player")[0]
|
||||
var weapon_data = serialized_data.entities.filter(func(e): return e.entity_name == "Weapon")[0]
|
||||
var attachment_data = serialized_data.entities.filter(func(e): return e.entity_name == "Attachment")[0]
|
||||
|
||||
assert_that(player_data.auto_included).is_false() # Original query
|
||||
assert_that(weapon_data.auto_included).is_true() # Auto-included via Player relationship
|
||||
assert_that(attachment_data.auto_included).is_true() # Auto-included via Weapon relationship
|
||||
|
||||
print("=== Partial serialization with auto-inclusion working correctly ===")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bdpuk46wqnhuw
|
||||
163
addons/gecs/tests/core/test_component.gd
Normal file
163
addons/gecs/tests/core/test_component.gd
Normal file
@@ -0,0 +1,163 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
|
||||
func test_component_key_is_set_correctly():
|
||||
# Create an instance of a concrete Component subclass
|
||||
var component = C_TestA.new()
|
||||
# The key should be set to the resource path of the component's script
|
||||
var expected_key = component.get_script().resource_path
|
||||
assert_str("res://addons/gecs/tests/components/c_test_a.gd").is_equal(expected_key)
|
||||
|
||||
|
||||
func test_component_query_matcher_equality():
|
||||
# Test _eq operator
|
||||
var component = C_TestA.new(42)
|
||||
|
||||
# Should match exact value
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_eq": 42}})).is_true()
|
||||
# Should not match different value
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_eq": 10}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_inequality():
|
||||
# Test _ne operator
|
||||
var component = C_TestA.new(42)
|
||||
|
||||
# Should match different value
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_ne": 10}})).is_true()
|
||||
# Should not match same value
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_ne": 42}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_greater_than():
|
||||
# Test _gt and _gte operators
|
||||
var component = C_TestA.new(50)
|
||||
|
||||
# _gt tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gt": 49}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gt": 50}})).is_false()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gt": 51}})).is_false()
|
||||
|
||||
# _gte tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gte": 49}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gte": 50}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gte": 51}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_less_than():
|
||||
# Test _lt and _lte operators
|
||||
var component = C_TestA.new(50)
|
||||
|
||||
# _lt tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lt": 51}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lt": 50}})).is_false()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lt": 49}})).is_false()
|
||||
|
||||
# _lte tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lte": 51}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lte": 50}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_lte": 49}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_array_membership():
|
||||
# Test _in and _nin operators
|
||||
var component = C_TestA.new(42)
|
||||
|
||||
# _in tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_in": [40, 41, 42]}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_in": [1, 2, 3]}})).is_false()
|
||||
|
||||
# _nin tests
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_nin": [1, 2, 3]}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_nin": [40, 41, 42]}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_custom_function():
|
||||
# Test func operator
|
||||
var component = C_TestA.new(42)
|
||||
|
||||
# Custom function that checks if value is even
|
||||
var is_even = func(val): return val % 2 == 0
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"func": is_even}})).is_true()
|
||||
|
||||
# Custom function that checks if value is odd
|
||||
var is_odd = func(val): return val % 2 == 1
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"func": is_odd}})).is_false()
|
||||
|
||||
# Custom function with complex logic
|
||||
var in_range = func(val): return val >= 40 and val <= 50
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"func": in_range}})).is_true()
|
||||
|
||||
|
||||
func test_component_query_matcher_multiple_operators():
|
||||
# Test combining multiple operators (all must pass)
|
||||
var component = C_TestA.new(50)
|
||||
|
||||
# Should match: value >= 40 AND value <= 60
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gte": 40, "_lte": 60}})).is_true()
|
||||
|
||||
# Should not match: value >= 40 AND value <= 45
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_gte": 40, "_lte": 45}})).is_false()
|
||||
|
||||
# Should match: value != 0 AND value > 30
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"value": {"_ne": 0, "_gt": 30}})).is_true()
|
||||
|
||||
|
||||
func test_component_query_matcher_falsy_values():
|
||||
# Test that falsy values (0, false, null) are handled correctly
|
||||
var component_zero = C_TestA.new(0)
|
||||
|
||||
# Should match 0 exactly
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_zero, {"value": {"_eq": 0}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_zero, {"value": {"_eq": 1}})).is_false()
|
||||
|
||||
# Should handle 0 in ranges
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_zero, {"value": {"_gte": 0}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_zero, {"value": {"_lte": 0}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_zero, {"value": {"_gt": 0}})).is_false()
|
||||
|
||||
# Should handle negative numbers
|
||||
var component_negative = C_TestA.new(-5)
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_negative, {"value": {"_eq": -5}})).is_true()
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component_negative, {"value": {"_lt": 0}})).is_true()
|
||||
|
||||
|
||||
func test_component_query_matcher_empty_query():
|
||||
# Empty query should match any component
|
||||
var component = C_TestA.new(42)
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {})).is_true()
|
||||
|
||||
|
||||
func test_component_query_matcher_nonexistent_property():
|
||||
# Should return false if property doesn't exist
|
||||
var component = C_TestA.new(42)
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {"nonexistent": {"_eq": 10}})).is_false()
|
||||
|
||||
|
||||
func test_component_query_matcher_multiple_properties():
|
||||
# Test querying multiple properties at once
|
||||
var component = C_TestD.new(5) # Has 'points' property
|
||||
|
||||
# Both properties must match
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {
|
||||
"points": {"_eq": 5}
|
||||
})).is_true()
|
||||
|
||||
assert_bool(ComponentQueryMatcher.matches_query(component, {
|
||||
"points": {"_eq": 10}
|
||||
})).is_false()
|
||||
|
||||
|
||||
func test_component_serialization():
|
||||
# Create an instance of a concrete Component subclass
|
||||
var component_a = C_TestA.new(42)
|
||||
var component_b = C_TestD.new(1)
|
||||
|
||||
# Serialize the component
|
||||
var serialized_data_a = component_a.serialize()
|
||||
var serialized_data_b = component_b.serialize()
|
||||
|
||||
# Check if the serialized data matches the expected values
|
||||
assert_int(serialized_data_a["value"]).is_equal(42)
|
||||
assert_int(serialized_data_b["points"]).is_equal(1)
|
||||
1
addons/gecs/tests/core/test_component.gd.uid
Normal file
1
addons/gecs/tests/core/test_component.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4nqun3t8nb18
|
||||
178
addons/gecs/tests/core/test_debug_tracking.gd
Normal file
178
addons/gecs/tests/core/test_debug_tracking.gd
Normal file
@@ -0,0 +1,178 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
# Test suite for System debug tracking (lastRunData)
|
||||
|
||||
var world: World
|
||||
|
||||
func before_test():
|
||||
world = World.new()
|
||||
world.name = "TestWorld"
|
||||
Engine.get_main_loop().root.add_child(world)
|
||||
ECS.world = world
|
||||
|
||||
func after_test():
|
||||
ECS.world = null
|
||||
if is_instance_valid(world):
|
||||
world.queue_free()
|
||||
|
||||
func test_debug_tracking_process_mode():
|
||||
# Enable debug mode for these tests
|
||||
ECS.debug = true
|
||||
# Create entities
|
||||
for i in range(10):
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_DebugTrackingTestA.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
# Create system with PROCESS execution method
|
||||
var system = ProcessSystem.new()
|
||||
world.add_system(system)
|
||||
|
||||
# Process once
|
||||
world.process(0.016)
|
||||
|
||||
# Debug: Print what's in lastRunData
|
||||
print("DEBUG: ECS.debug = ", ECS.debug)
|
||||
print("DEBUG: lastRunData = ", system.lastRunData)
|
||||
print("DEBUG: lastRunData keys = ", system.lastRunData.keys())
|
||||
|
||||
# Verify debug data
|
||||
assert_that(system.lastRunData.has("system_name")).is_true()
|
||||
assert_that(system.lastRunData.has("frame_delta")).is_true()
|
||||
assert_that(system.lastRunData.has("entity_count")).is_true()
|
||||
assert_that(system.lastRunData.has("execution_time_ms")).is_true()
|
||||
|
||||
# Verify values
|
||||
assert_that(system.lastRunData["frame_delta"]).is_equal(0.016)
|
||||
assert_that(system.lastRunData["entity_count"]).is_equal(10)
|
||||
assert_that(system.lastRunData["execution_time_ms"]).is_greater(0.0)
|
||||
assert_that(system.lastRunData["parallel"]).is_equal(false)
|
||||
|
||||
# Store first execution time
|
||||
var first_exec_time = system.lastRunData["execution_time_ms"]
|
||||
|
||||
# Process again
|
||||
world.process(0.032)
|
||||
|
||||
# Verify time is different (not accumulating)
|
||||
var second_exec_time = system.lastRunData["execution_time_ms"]
|
||||
assert_that(system.lastRunData["frame_delta"]).is_equal(0.032)
|
||||
|
||||
# Times should be similar but not identical (and definitely not accumulated)
|
||||
# If accumulating, second would be ~2x first
|
||||
assert_that(second_exec_time).is_less(first_exec_time * 1.5)
|
||||
print("First exec: %.3f ms, Second exec: %.3f ms" % [first_exec_time, second_exec_time])
|
||||
|
||||
|
||||
func test_debug_tracking_subsystems():
|
||||
# Enable debug mode for these tests
|
||||
ECS.debug = true
|
||||
# Create entities
|
||||
for i in range(10):
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_DebugTrackingTestA.new())
|
||||
entity.add_component(C_DebugTrackingTestB.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
# Create system with SUBSYSTEMS execution method
|
||||
var system = SubsystemsTestSystem.new()
|
||||
world.add_system(system)
|
||||
|
||||
# Process once
|
||||
world.process(0.016)
|
||||
|
||||
# Verify debug data
|
||||
assert_that(system.lastRunData["execution_time_ms"]).is_greater(0.0)
|
||||
|
||||
# Verify subsystem data
|
||||
assert_that(system.lastRunData.has(0)).is_true()
|
||||
assert_that(system.lastRunData.has(1)).is_true()
|
||||
|
||||
# First subsystem
|
||||
assert_that(system.lastRunData[0]["entity_count"]).is_equal(10)
|
||||
|
||||
# Second subsystem
|
||||
assert_that(system.lastRunData[1]["entity_count"]).is_equal(10)
|
||||
|
||||
print("Subsystem 0: %s" % [system.lastRunData[0]])
|
||||
print("Subsystem 1: %s" % [system.lastRunData[1]])
|
||||
|
||||
|
||||
func test_debug_disabled_has_no_data():
|
||||
# Disable debug mode
|
||||
ECS.debug = false
|
||||
|
||||
# Create entities
|
||||
for i in range(5):
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_DebugTrackingTestA.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
# Create system
|
||||
var system = ProcessSystem.new()
|
||||
world.add_system(system)
|
||||
|
||||
# Process
|
||||
world.process(0.016)
|
||||
|
||||
# lastRunData should be empty or not updated when debug is off
|
||||
# (It might still exist from a previous run, but shouldn't be updated)
|
||||
var initial_data = system.lastRunData.duplicate()
|
||||
|
||||
# Process again
|
||||
world.process(0.016)
|
||||
|
||||
# Data should not change (because ECS.debug = false)
|
||||
assert_that(system.lastRunData).is_equal(initial_data)
|
||||
|
||||
print("With ECS.debug=false, lastRunData remains unchanged: %s" % [system.lastRunData])
|
||||
|
||||
|
||||
# Test system - PROCESS mode
|
||||
class ProcessSystem extends System:
|
||||
func query() -> QueryBuilder:
|
||||
return ECS.world.query.with_all([C_DebugTrackingTestA])
|
||||
|
||||
func process(entities: Array[Entity], components: Array, delta: float) -> void:
|
||||
for entity in entities:
|
||||
var comp = entity.get_component(C_DebugTrackingTestA)
|
||||
comp.value += delta
|
||||
|
||||
# Test system - unified process
|
||||
class ProcessAllSystem extends System:
|
||||
func query() -> QueryBuilder:
|
||||
return ECS.world.query.with_all([C_DebugTrackingTestB])
|
||||
|
||||
func process(entities: Array[Entity], components: Array, delta: float) -> void:
|
||||
for entity in entities:
|
||||
var comp = entity.get_component(C_DebugTrackingTestB)
|
||||
comp.count += 1
|
||||
|
||||
# Test system - batch processing with iterate
|
||||
class ProcessBatchSystem extends System:
|
||||
func query() -> QueryBuilder:
|
||||
return ECS.world.query.with_all([C_DebugTrackingTestA]).iterate([C_DebugTrackingTestA])
|
||||
|
||||
func process(entities: Array[Entity], components: Array, delta: float) -> void:
|
||||
var test_a_components = components[0]
|
||||
for i in range(entities.size()):
|
||||
test_a_components[i].value += delta
|
||||
|
||||
# Test system - SUBSYSTEMS mode
|
||||
class SubsystemsTestSystem extends System:
|
||||
func sub_systems() -> Array[Array]:
|
||||
return [
|
||||
[ECS.world.query.with_all([C_DebugTrackingTestA]), process_sub],
|
||||
[ECS.world.query.with_all([C_DebugTrackingTestB]).iterate([C_DebugTrackingTestB]), batch_sub]
|
||||
]
|
||||
|
||||
func process_sub(entities: Array[Entity], components: Array, delta: float) -> void:
|
||||
for entity in entities:
|
||||
var comp = entity.get_component(C_DebugTrackingTestA)
|
||||
comp.value += delta
|
||||
|
||||
func batch_sub(entities: Array[Entity], components: Array, delta: float) -> void:
|
||||
if components.size() > 0 and components[0].size() > 0:
|
||||
var test_b_components = components[0]
|
||||
for i in range(entities.size()):
|
||||
test_b_components[i].count += 1
|
||||
1
addons/gecs/tests/core/test_debug_tracking.gd.uid
Normal file
1
addons/gecs/tests/core/test_debug_tracking.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bk45ditcdxhih
|
||||
164
addons/gecs/tests/core/test_entity.gd
Normal file
164
addons/gecs/tests/core/test_entity.gd
Normal file
@@ -0,0 +1,164 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
|
||||
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
|
||||
func after_test():
|
||||
if world:
|
||||
world.purge(false)
|
||||
|
||||
|
||||
# TODO: We need to add the world here becuase remove fails because we don't have access to world
|
||||
|
||||
|
||||
func test_add_and_get_component():
|
||||
var entity = auto_free(TestA.new())
|
||||
var comp = C_TestA.new()
|
||||
entity.add_component(comp)
|
||||
# Test that the component was added
|
||||
assert_bool(entity.has_component(C_TestA)).is_true()
|
||||
# Test retrieving the component
|
||||
var retrieved_component = entity.get_component(C_TestA)
|
||||
assert_str(type_string(typeof(retrieved_component))).is_equal(type_string(typeof(comp)))
|
||||
|
||||
# Components need default values on init or they will error
|
||||
# FIXME: How can we catch this in the code?
|
||||
func test_add_entity_with_component_with_no_defaults_in_init():
|
||||
var entity = auto_free(Entity.new())
|
||||
# this line will lead to crash (the _init parameters has no default value)
|
||||
assert_error(func(): entity.add_component(C_TestH.new(57)))
|
||||
|
||||
func test_add_multiple_components_and_has():
|
||||
var entity = auto_free(TestB.new())
|
||||
var comp1 = C_TestA.new()
|
||||
var comp2 = C_TestB.new()
|
||||
entity.add_components([comp1, comp2])
|
||||
# Test that the components were added
|
||||
assert_bool(entity.has_component(C_TestA)).is_true()
|
||||
assert_bool(entity.has_component(C_TestB)).is_true()
|
||||
assert_bool(entity.has_component(C_TestC)).is_false()
|
||||
|
||||
|
||||
func test_remove_component():
|
||||
var entity = auto_free(TestB.new())
|
||||
var comp = C_TestB.new()
|
||||
entity.add_component(comp)
|
||||
entity.remove_component(C_TestB)
|
||||
# Test that the component was removed
|
||||
assert_bool(entity.has_component(C_TestB)).is_false()
|
||||
|
||||
|
||||
func test_add_get_has_relationship():
|
||||
var entitya = auto_free(TestC.new())
|
||||
var entityb = auto_free(TestC.new())
|
||||
var r_testa_entitya = Relationship.new(C_TestA.new(), entitya)
|
||||
# Add the relationship
|
||||
entityb.add_relationship(r_testa_entitya)
|
||||
# Test that the relationship was added
|
||||
# With the actual relationship
|
||||
assert_bool(entityb.has_relationship(r_testa_entitya)).is_true()
|
||||
# with a matching relationship
|
||||
assert_bool(entityb.has_relationship(Relationship.new(C_TestA.new(), entitya))).is_true()
|
||||
# Test retrieving the relationship
|
||||
# with the actual relationship
|
||||
var inst_retrieved_relationship = entityb.get_relationship(r_testa_entitya)
|
||||
assert_str(type_string(typeof(inst_retrieved_relationship))).is_equal(
|
||||
type_string(typeof(r_testa_entitya))
|
||||
)
|
||||
# with a matching relationship
|
||||
var class_retrieved_relationship = entityb.get_relationship(
|
||||
Relationship.new(C_TestA.new(), entitya)
|
||||
)
|
||||
assert_str(type_string(typeof(class_retrieved_relationship))).is_equal(
|
||||
type_string(typeof(r_testa_entitya))
|
||||
)
|
||||
assert_str(type_string(typeof(class_retrieved_relationship))).is_equal(
|
||||
type_string(typeof(Relationship.new(C_TestA.new(), entitya)))
|
||||
)
|
||||
|
||||
func test_add_and_remove_component():
|
||||
var entity = auto_free(TestB.new())
|
||||
for i in range(99):
|
||||
var comp = C_TestB.new()
|
||||
entity.add_component(comp)
|
||||
entity.remove_component(C_TestB)
|
||||
print('_component_path_cache size=', entity._component_path_cache.size())
|
||||
|
||||
# Test memory leak
|
||||
assert_int(entity._component_path_cache.size()).is_equal(0)
|
||||
|
||||
|
||||
func test_remove_components_with_scripts():
|
||||
var entity = auto_free(TestB.new())
|
||||
var comp1 = C_TestA.new()
|
||||
var comp2 = C_TestB.new()
|
||||
var comp3 = C_TestC.new()
|
||||
|
||||
# Add multiple components
|
||||
entity.add_components([comp1, comp2, comp3])
|
||||
|
||||
# Verify all were added
|
||||
assert_bool(entity.has_component(C_TestA)).is_true()
|
||||
assert_bool(entity.has_component(C_TestB)).is_true()
|
||||
assert_bool(entity.has_component(C_TestC)).is_true()
|
||||
|
||||
# Remove multiple components by Script class
|
||||
entity.remove_components([C_TestA, C_TestB])
|
||||
|
||||
# Test that the components were removed
|
||||
assert_bool(entity.has_component(C_TestA)).is_false()
|
||||
assert_bool(entity.has_component(C_TestB)).is_false()
|
||||
# Test that C_TestC is still there
|
||||
assert_bool(entity.has_component(C_TestC)).is_true()
|
||||
|
||||
|
||||
func test_remove_components_with_instances():
|
||||
var entity = auto_free(TestB.new())
|
||||
var comp1 = C_TestA.new()
|
||||
var comp2 = C_TestB.new()
|
||||
var comp3 = C_TestC.new()
|
||||
|
||||
# Add multiple components
|
||||
entity.add_components([comp1, comp2, comp3])
|
||||
|
||||
# Verify all were added
|
||||
assert_bool(entity.has_component(C_TestA)).is_true()
|
||||
assert_bool(entity.has_component(C_TestB)).is_true()
|
||||
assert_bool(entity.has_component(C_TestC)).is_true()
|
||||
|
||||
# Remove multiple components by instance
|
||||
entity.remove_components([comp1, comp2])
|
||||
|
||||
# Test that the components were removed
|
||||
assert_bool(entity.has_component(C_TestA)).is_false()
|
||||
assert_bool(entity.has_component(C_TestB)).is_false()
|
||||
# Test that C_TestC is still there
|
||||
assert_bool(entity.has_component(C_TestC)).is_true()
|
||||
|
||||
|
||||
func test_remove_components_mixed():
|
||||
var entity = auto_free(TestB.new())
|
||||
var comp1 = C_TestA.new()
|
||||
var comp2 = C_TestB.new()
|
||||
var comp3 = C_TestC.new()
|
||||
|
||||
# Add multiple components
|
||||
entity.add_components([comp1, comp2, comp3])
|
||||
|
||||
# Remove with mixed Script and instance
|
||||
entity.remove_components([C_TestA, comp2])
|
||||
|
||||
# Test that the components were removed
|
||||
assert_bool(entity.has_component(C_TestA)).is_false()
|
||||
assert_bool(entity.has_component(C_TestB)).is_false()
|
||||
# Test that C_TestC is still there
|
||||
assert_bool(entity.has_component(C_TestC)).is_true()
|
||||
1
addons/gecs/tests/core/test_entity.gd.uid
Normal file
1
addons/gecs/tests/core/test_entity.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dh1uujht5xew7
|
||||
246
addons/gecs/tests/core/test_entity_id_system.gd
Normal file
246
addons/gecs/tests/core/test_entity_id_system.gd
Normal file
@@ -0,0 +1,246 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
## Test suite for the Entity ID system functionality
|
||||
## Tests auto-generation, custom IDs, singleton behavior, and world-level enforcement
|
||||
|
||||
var world: World
|
||||
|
||||
func before_test():
|
||||
world = World.new()
|
||||
world.name = "TestWorld"
|
||||
add_child(world)
|
||||
ECS.world = world
|
||||
|
||||
func after_test():
|
||||
if is_instance_valid(world):
|
||||
world.queue_free()
|
||||
await await_idle_frame()
|
||||
|
||||
func test_entity_id_auto_generation():
|
||||
# Test that entities auto-generate IDs in _enter_tree
|
||||
var entity = Entity.new()
|
||||
entity.name = "TestEntity"
|
||||
|
||||
# ID should be empty before entering tree
|
||||
assert_str(entity.id).is_empty()
|
||||
|
||||
# Add to tree - triggers _enter_tree and ID generation
|
||||
world.add_entity(entity)
|
||||
|
||||
# ID should now be auto-generated
|
||||
assert_str(entity.id).is_not_empty()
|
||||
assert_bool(entity.id.length() > 0).is_true()
|
||||
|
||||
# Should not change ID on subsequent checks
|
||||
var first_id = entity.id
|
||||
var second_id = entity.id
|
||||
assert_str(second_id).is_equal(first_id)
|
||||
|
||||
func test_entity_custom_id():
|
||||
# Test custom ID functionality for singleton entities
|
||||
var entity = Entity.new()
|
||||
entity.name = "SingletonEntity"
|
||||
|
||||
# Set custom ID before adding to world
|
||||
entity.id = "singleton_player"
|
||||
assert_str(entity.id).is_equal("singleton_player")
|
||||
|
||||
# Add to world - should preserve custom ID
|
||||
world.add_entity(entity)
|
||||
assert_str(entity.id).is_equal("singleton_player")
|
||||
|
||||
# Custom ID should not change on subsequent access
|
||||
var same_id = entity.id
|
||||
assert_str(same_id).is_equal("singleton_player")
|
||||
|
||||
func test_world_id_tracking():
|
||||
# Test that World tracks IDs and provides lookup functionality
|
||||
var entity1 = Entity.new()
|
||||
entity1.name = "Entity1"
|
||||
entity1.id = "test_id_1"
|
||||
|
||||
var entity2 = Entity.new()
|
||||
entity2.name = "Entity2"
|
||||
entity2.id = "test_id_2"
|
||||
|
||||
# Add entities to world
|
||||
world.add_entity(entity1)
|
||||
world.add_entity(entity2)
|
||||
|
||||
# Test lookup by ID
|
||||
assert_object(world.get_entity_by_id("test_id_1")).is_same(entity1)
|
||||
assert_object(world.get_entity_by_id("test_id_2")).is_same(entity2)
|
||||
assert_object(world.get_entity_by_id("nonexistent")).is_null()
|
||||
|
||||
# Test has_entity_with_id
|
||||
assert_bool(world.has_entity_with_id("test_id_1")).is_true()
|
||||
assert_bool(world.has_entity_with_id("test_id_2")).is_true()
|
||||
assert_bool(world.has_entity_with_id("nonexistent")).is_false()
|
||||
|
||||
func test_world_id_replacement():
|
||||
# Test singleton behavior - entities with same ID replace existing ones
|
||||
# Create first entity with custom ID
|
||||
var entity1 = Entity.new()
|
||||
entity1.name = "FirstEntity"
|
||||
entity1.id = "singleton_player"
|
||||
var comp1 = C_TestA.new()
|
||||
comp1.value = 100
|
||||
entity1.add_component(comp1)
|
||||
world.add_entity(entity1)
|
||||
|
||||
# Verify it's in the world
|
||||
assert_int(world.entities.size()).is_equal(1)
|
||||
assert_object(world.get_entity_by_id("singleton_player")).is_same(entity1)
|
||||
|
||||
# Create second entity with same ID
|
||||
var entity2 = Entity.new()
|
||||
entity2.name = "ReplacementEntity"
|
||||
entity2.id = "singleton_player"
|
||||
var comp2 = C_TestA.new()
|
||||
comp2.value = 200
|
||||
entity2.add_component(comp2)
|
||||
|
||||
# Add to world - should replace first entity
|
||||
world.add_entity(entity2)
|
||||
|
||||
# Should still have only one entity
|
||||
assert_int(world.entities.size()).is_equal(1)
|
||||
# Should be the new entity
|
||||
var found_entity = world.get_entity_by_id("singleton_player")
|
||||
assert_object(found_entity).is_same(entity2)
|
||||
assert_str(found_entity.name).is_equal("ReplacementEntity")
|
||||
|
||||
# Verify component value is from new entity
|
||||
var comp = found_entity.get_component(C_TestA) as C_TestA
|
||||
assert_int(comp.value).is_equal(200)
|
||||
|
||||
func test_auto_generated_id_tracking():
|
||||
# Test that auto-generated IDs are also tracked by the world
|
||||
var entity = Entity.new()
|
||||
entity.name = "AutoIDEntity"
|
||||
# Don't set custom ID - let it auto-generate
|
||||
|
||||
world.add_entity(entity)
|
||||
|
||||
# Should have auto-generated ID
|
||||
assert_str(entity.id).is_not_empty()
|
||||
|
||||
# Should be trackable by ID
|
||||
assert_object(world.get_entity_by_id(entity.id)).is_same(entity)
|
||||
assert_bool(world.has_entity_with_id(entity.id)).is_true()
|
||||
|
||||
func test_id_generation_format():
|
||||
# Test that generated IDs follow expected GUID format
|
||||
var entity = Entity.new()
|
||||
|
||||
# Add to tree to trigger ID generation
|
||||
world.add_entity(entity)
|
||||
|
||||
var id = entity.id
|
||||
assert_str(id).is_not_empty()
|
||||
assert_bool(id.contains("-")).is_true()
|
||||
|
||||
var parts = id.split("-")
|
||||
assert_int(parts.size()).is_equal(5)
|
||||
|
||||
# All parts should be valid hex strings
|
||||
for part in parts:
|
||||
assert_bool(part.is_valid_hex_number()).is_true()
|
||||
|
||||
func test_id_uniqueness():
|
||||
# Test that multiple entities get unique IDs
|
||||
var ids = {}
|
||||
var entities = []
|
||||
|
||||
# Generate 100 entities with auto IDs
|
||||
for i in range(100):
|
||||
var entity = Entity.new()
|
||||
entity.name = "Entity%d" % i
|
||||
world.add_entity(entity)
|
||||
entities.append(entity)
|
||||
|
||||
# Should not have seen this ID before
|
||||
assert_bool(ids.has(entity.id)).is_false()
|
||||
ids[entity.id] = true
|
||||
|
||||
# All IDs should be unique
|
||||
assert_int(ids.size()).is_equal(100)
|
||||
|
||||
func test_remove_entity_clears_id_registry():
|
||||
# Test that removing entities clears them from ID registry
|
||||
var entity = Entity.new()
|
||||
entity.name = "TestEntity"
|
||||
entity.id = "test_remove_id"
|
||||
|
||||
world.add_entity(entity)
|
||||
assert_bool(world.has_entity_with_id("test_remove_id")).is_true()
|
||||
|
||||
world.remove_entity(entity)
|
||||
assert_bool(world.has_entity_with_id("test_remove_id")).is_false()
|
||||
assert_object(world.get_entity_by_id("test_remove_id")).is_null()
|
||||
|
||||
func test_id_system_comprehensive_demo():
|
||||
# Comprehensive test demonstrating all ID system features
|
||||
# Test 1: Auto ID generation
|
||||
var auto_entity = Entity.new()
|
||||
auto_entity.name = "AutoIDEntity"
|
||||
world.add_entity(auto_entity)
|
||||
|
||||
var generated_id = auto_entity.id
|
||||
assert_str(generated_id).is_not_empty() # Should auto-generate
|
||||
assert_bool(generated_id.contains("-")).is_true() # Should have correct GUID format
|
||||
|
||||
# Should still have the same ID
|
||||
assert_str(auto_entity.id).is_equal(generated_id)
|
||||
|
||||
# Test 2: Custom ID singleton behavior
|
||||
var player1 = Entity.new()
|
||||
player1.name = "Player1"
|
||||
player1.id = "singleton_player"
|
||||
var comp1 = C_TestA.new()
|
||||
comp1.value = 100
|
||||
player1.add_component(comp1)
|
||||
world.add_entity(player1)
|
||||
|
||||
assert_int(world.entities.size()).is_equal(2) # auto_entity + player1
|
||||
assert_object(world.get_entity_by_id("singleton_player")).is_same(player1)
|
||||
|
||||
# Add second entity with same ID - should replace first
|
||||
var player2 = Entity.new()
|
||||
player2.name = "Player2"
|
||||
player2.id = "singleton_player"
|
||||
var comp2 = C_TestA.new()
|
||||
comp2.value = 200
|
||||
player2.add_component(comp2)
|
||||
world.add_entity(player2)
|
||||
|
||||
assert_int(world.entities.size()).is_equal(2) # Should still be 2 (replacement occurred)
|
||||
var found_entity = world.get_entity_by_id("singleton_player")
|
||||
assert_object(found_entity).is_same(player2) # Should be the new entity
|
||||
assert_str(found_entity.name).is_equal("Player2")
|
||||
|
||||
var found_comp = found_entity.get_component(C_TestA) as C_TestA
|
||||
assert_int(found_comp.value).is_equal(200) # Should have new entity's data
|
||||
|
||||
# Test 3: Multiple entity tracking
|
||||
var tracked_entities = []
|
||||
for i in range(3):
|
||||
var entity = Entity.new()
|
||||
entity.name = "TrackedEntity%d" % i
|
||||
entity.id = "tracked_%d" % i
|
||||
tracked_entities.append(entity)
|
||||
world.add_entity(entity)
|
||||
|
||||
# Verify all are tracked
|
||||
for i in range(3):
|
||||
var id = "tracked_%d" % i
|
||||
assert_bool(world.has_entity_with_id(id)).is_true()
|
||||
assert_object(world.get_entity_by_id(id)).is_same(tracked_entities[i])
|
||||
|
||||
# Test 4: ID registry cleanup on removal
|
||||
world.remove_entity(tracked_entities[1])
|
||||
assert_bool(world.has_entity_with_id("tracked_1")).is_false()
|
||||
assert_object(world.get_entity_by_id("tracked_1")).is_null()
|
||||
# Others should still exist
|
||||
assert_bool(world.has_entity_with_id("tracked_0")).is_true()
|
||||
assert_bool(world.has_entity_with_id("tracked_2")).is_true()
|
||||
1
addons/gecs/tests/core/test_entity_id_system.gd.uid
Normal file
1
addons/gecs/tests/core/test_entity_id_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d3n5subrpobw3
|
||||
392
addons/gecs/tests/core/test_observers.gd
Normal file
392
addons/gecs/tests/core/test_observers.gd
Normal file
@@ -0,0 +1,392 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
|
||||
func after_test():
|
||||
world.purge(false)
|
||||
|
||||
func test_observer_receive_component_changed():
|
||||
world.add_system(TestASystem.new())
|
||||
var test_a_observer = TestAObserver.new()
|
||||
world.add_observer(test_a_observer)
|
||||
|
||||
# Create entities with the required components
|
||||
var entity_a = TestA.new()
|
||||
entity_a.name = "a"
|
||||
entity_a.add_component(C_TestA.new())
|
||||
|
||||
var entity_b = TestB.new()
|
||||
entity_b.name = "b"
|
||||
entity_b.add_component(C_TestA.new())
|
||||
entity_b.add_component(C_TestB.new())
|
||||
|
||||
# issue #43
|
||||
var entity_a2 = TestA.new()
|
||||
entity_a2.name = "a"
|
||||
entity_a2.add_component(C_TestA.new())
|
||||
world.get_node(world.entity_nodes_root).add_child(entity_a2)
|
||||
world.add_entity(entity_a2, null, false)
|
||||
assert_int(test_a_observer.added_count).is_equal(1)
|
||||
|
||||
|
||||
# Add some entities before systems
|
||||
world.add_entities([entity_a, entity_b])
|
||||
assert_int(test_a_observer.added_count).is_equal(3)
|
||||
|
||||
|
||||
# Run the systems once
|
||||
print('process 1st')
|
||||
world.process(0.1)
|
||||
|
||||
# Check the event_count
|
||||
assert_int(test_a_observer.event_count).is_equal(2)
|
||||
|
||||
# Run the systems again
|
||||
print('process 2nd')
|
||||
world.process(0.1)
|
||||
|
||||
# Check the event_count
|
||||
assert_int(test_a_observer.event_count).is_equal(4)
|
||||
|
||||
|
||||
## Test that observers detect when a component is added to an entity
|
||||
func test_observer_on_component_added():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create an entity without the component
|
||||
var entity = Entity.new()
|
||||
world.add_entity(entity)
|
||||
|
||||
# Verify observer hasn't fired yet
|
||||
assert_int(observer.added_count).is_equal(0)
|
||||
|
||||
# Add the watched component
|
||||
var component = C_ObserverTest.new()
|
||||
entity.add_component(component)
|
||||
|
||||
# Verify observer detected the addition
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
assert_object(observer.last_added_entity).is_equal(entity)
|
||||
|
||||
|
||||
## Test that observers detect when a component is removed from an entity
|
||||
func test_observer_on_component_removed():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create an entity with the component
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new()
|
||||
entity.add_component(component)
|
||||
world.add_entity(entity)
|
||||
|
||||
# Verify observer detected the addition
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
|
||||
# Reset and remove the component
|
||||
observer.reset()
|
||||
entity.remove_component(C_ObserverTest)
|
||||
|
||||
# Verify observer detected the removal
|
||||
assert_int(observer.removed_count).is_equal(1)
|
||||
assert_object(observer.last_removed_entity).is_equal(entity)
|
||||
assert_int(observer.added_count).is_equal(0) # Should remain 0 after reset
|
||||
|
||||
|
||||
## Test that observers detect property changes on watched components
|
||||
func test_observer_on_component_changed():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create an entity with the component
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new(0, "initial")
|
||||
entity.add_component(component)
|
||||
world.add_entity(entity)
|
||||
|
||||
# Reset the observer (it may have fired on add)
|
||||
observer.reset()
|
||||
|
||||
# Change the value property (this will emit property_changed signal)
|
||||
component.value = 42
|
||||
|
||||
# Verify observer detected the change
|
||||
assert_int(observer.changed_count).is_equal(1)
|
||||
assert_object(observer.last_changed_entity).is_equal(entity)
|
||||
assert_str(observer.last_changed_property).is_equal("value")
|
||||
assert_int(observer.last_old_value).is_equal(0)
|
||||
assert_int(observer.last_new_value).is_equal(42)
|
||||
|
||||
# Change another property
|
||||
component.name_prop = "changed"
|
||||
|
||||
# Verify observer detected the second change
|
||||
assert_int(observer.changed_count).is_equal(2)
|
||||
assert_str(observer.last_changed_property).is_equal("name_prop")
|
||||
assert_str(observer.last_old_value).is_equal("initial")
|
||||
assert_str(observer.last_new_value).is_equal("changed")
|
||||
|
||||
|
||||
## Test that observers respect query filters (only match entities that pass the query)
|
||||
func test_observer_respects_query_filter():
|
||||
var health_observer = O_HealthObserver.new()
|
||||
world.add_observer(health_observer)
|
||||
|
||||
# Create entity with only health component (should NOT match - needs both components)
|
||||
var entity_only_health = Entity.new()
|
||||
entity_only_health.add_component(C_ObserverHealth.new())
|
||||
world.add_entity(entity_only_health)
|
||||
|
||||
# Observer should NOT have fired (doesn't match query)
|
||||
assert_int(health_observer.health_added_count).is_equal(0)
|
||||
|
||||
# Create entity with both components (should match)
|
||||
var entity_both = Entity.new()
|
||||
entity_both.add_component(C_ObserverTest.new())
|
||||
entity_both.add_component(C_ObserverHealth.new())
|
||||
world.add_entity(entity_both)
|
||||
|
||||
# Observer should have fired now (matches query)
|
||||
assert_int(health_observer.health_added_count).is_equal(1)
|
||||
|
||||
|
||||
## Test that multiple observers can watch the same component
|
||||
func test_multiple_observers_same_component():
|
||||
var observer1 = O_ObserverTest.new()
|
||||
var observer2 = O_ObserverTest.new()
|
||||
world.add_observer(observer1)
|
||||
world.add_observer(observer2)
|
||||
|
||||
# Create an entity with the component
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new()
|
||||
entity.add_component(component)
|
||||
world.add_entity(entity)
|
||||
|
||||
# Both observers should have detected the addition
|
||||
assert_int(observer1.added_count).is_equal(1)
|
||||
assert_int(observer2.added_count).is_equal(1)
|
||||
|
||||
# Change the component
|
||||
observer1.reset()
|
||||
observer2.reset()
|
||||
component.value = 100
|
||||
|
||||
# Both observers should have detected the change
|
||||
assert_int(observer1.changed_count).is_equal(1)
|
||||
assert_int(observer2.changed_count).is_equal(1)
|
||||
|
||||
|
||||
## Test that observers can track multiple property changes
|
||||
func test_observer_tracks_multiple_changes():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create an entity with the component
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new(0, "start")
|
||||
entity.add_component(component)
|
||||
world.add_entity(entity)
|
||||
|
||||
observer.reset()
|
||||
|
||||
# Make multiple changes
|
||||
component.value = 10
|
||||
component.value = 20
|
||||
component.name_prop = "middle"
|
||||
component.value = 30
|
||||
|
||||
# Should have detected all 4 changes
|
||||
assert_int(observer.changed_count).is_equal(4)
|
||||
|
||||
|
||||
## Test observer with health component and query matching
|
||||
func test_observer_health_low_health_alert():
|
||||
var health_observer = O_HealthObserver.new()
|
||||
world.add_observer(health_observer)
|
||||
|
||||
# Create entity with both components
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_ObserverTest.new())
|
||||
var health = C_ObserverHealth.new(100)
|
||||
entity.add_component(health)
|
||||
world.add_entity(entity)
|
||||
|
||||
health_observer.reset()
|
||||
|
||||
# Reduce health gradually
|
||||
health.health = 50
|
||||
assert_int(health_observer.health_changed_count).is_equal(1)
|
||||
assert_int(health_observer.low_health_alerts.size()).is_equal(0)
|
||||
|
||||
health.health = 25 # Below threshold
|
||||
assert_int(health_observer.health_changed_count).is_equal(2)
|
||||
assert_int(health_observer.low_health_alerts.size()).is_equal(1)
|
||||
assert_object(health_observer.low_health_alerts[0]).is_equal(entity)
|
||||
|
||||
|
||||
## Test that observer doesn't fire when entity doesn't match query
|
||||
func test_observer_ignores_non_matching_entities():
|
||||
var health_observer = O_HealthObserver.new()
|
||||
world.add_observer(health_observer)
|
||||
|
||||
# Create entity with only C_ObserverTest (not both components)
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_ObserverTest.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
# Try to add C_ObserverHealth to a different entity that doesn't have C_ObserverTest
|
||||
var entity2 = Entity.new()
|
||||
entity2.add_component(C_ObserverHealth.new())
|
||||
world.add_entity(entity2)
|
||||
|
||||
# Observer should not have fired (entity2 doesn't match query)
|
||||
assert_int(health_observer.health_added_count).is_equal(0)
|
||||
|
||||
|
||||
## Test observer detects component addition before entity is added to world
|
||||
func test_observer_component_added_before_entity_added():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create entity and add component BEFORE adding to world
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new()
|
||||
entity.add_component(component)
|
||||
|
||||
# Observer shouldn't have fired yet
|
||||
assert_int(observer.added_count).is_equal(0)
|
||||
|
||||
# Now add to world
|
||||
world.add_entity(entity)
|
||||
|
||||
# Observer should fire now
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
|
||||
|
||||
## Test observer with component replacement
|
||||
func test_observer_component_replacement():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create entity with component
|
||||
var entity = Entity.new()
|
||||
var component1 = C_ObserverTest.new(10, "first")
|
||||
entity.add_component(component1)
|
||||
world.add_entity(entity)
|
||||
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
|
||||
# Replace the component (add_component on same type replaces)
|
||||
var component2 = C_ObserverTest.new(20, "second")
|
||||
entity.add_component(component2)
|
||||
|
||||
# Should trigger both removed and added
|
||||
assert_int(observer.removed_count).is_equal(1)
|
||||
assert_int(observer.added_count).is_equal(2)
|
||||
|
||||
|
||||
## Test that property changes without signal emission don't trigger observer
|
||||
func test_observer_ignores_direct_property_changes():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create entity with component
|
||||
var entity = Entity.new()
|
||||
var component = C_ObserverTest.new()
|
||||
entity.add_component(component)
|
||||
world.add_entity(entity)
|
||||
|
||||
observer.reset()
|
||||
|
||||
# Directly set the property WITHOUT using the setter
|
||||
# This bypasses the property_changed signal
|
||||
# Note: In GDScript, using the property name always calls the setter,
|
||||
# so we need to access the internal variable directly
|
||||
# For this test, we're verifying that ONLY setters that emit signals work
|
||||
|
||||
# Using the setter (should trigger)
|
||||
component.value = 42
|
||||
assert_int(observer.changed_count).is_equal(1)
|
||||
|
||||
# The framework correctly requires explicit signal emission in setters
|
||||
|
||||
|
||||
## Test observer with entity that starts matching query after component addition
|
||||
func test_observer_entity_becomes_matching():
|
||||
var health_observer = O_HealthObserver.new()
|
||||
world.add_observer(health_observer)
|
||||
|
||||
# Create entity with only one component
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_ObserverTest.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
# Health observer shouldn't fire (needs both components)
|
||||
assert_int(health_observer.health_added_count).is_equal(0)
|
||||
|
||||
# Add the second component
|
||||
entity.add_component(C_ObserverHealth.new())
|
||||
|
||||
# Now health observer should fire
|
||||
assert_int(health_observer.health_added_count).is_equal(1)
|
||||
|
||||
|
||||
## Test removing observer from world
|
||||
func test_remove_observer():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create entity with component
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_ObserverTest.new())
|
||||
world.add_entity(entity)
|
||||
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
|
||||
# Remove the observer
|
||||
world.remove_observer(observer)
|
||||
|
||||
# Add another entity - observer should not fire
|
||||
var entity2 = Entity.new()
|
||||
entity2.add_component(C_ObserverTest.new())
|
||||
world.add_entity(entity2)
|
||||
|
||||
# Count should still be 1 (not 2)
|
||||
assert_int(observer.added_count).is_equal(1)
|
||||
|
||||
|
||||
## Test observer with multiple entities
|
||||
func test_observer_with_multiple_entities():
|
||||
var observer = O_ObserverTest.new()
|
||||
world.add_observer(observer)
|
||||
|
||||
# Create multiple entities
|
||||
for i in range(5):
|
||||
var entity = Entity.new()
|
||||
entity.add_component(C_ObserverTest.new(i))
|
||||
world.add_entity(entity)
|
||||
|
||||
# Should have detected all 5 additions
|
||||
assert_int(observer.added_count).is_equal(5)
|
||||
|
||||
observer.reset()
|
||||
|
||||
# Get all entities and modify their components
|
||||
var entities = world.query.with_all([C_ObserverTest]).execute()
|
||||
for entity in entities:
|
||||
var comp = entity.get_component(C_ObserverTest)
|
||||
comp.value = comp.value + 100
|
||||
|
||||
# Should have detected all 5 changes
|
||||
assert_int(observer.changed_count).is_equal(5)
|
||||
1
addons/gecs/tests/core/test_observers.gd.uid
Normal file
1
addons/gecs/tests/core/test_observers.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jr1qceldoims
|
||||
1249
addons/gecs/tests/core/test_query_builder.gd
Normal file
1249
addons/gecs/tests/core/test_query_builder.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
addons/gecs/tests/core/test_query_builder.gd.uid
Normal file
1
addons/gecs/tests/core/test_query_builder.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b06t0s7ajwlme
|
||||
30
addons/gecs/tests/core/test_query_cache_key_domains.gd
Normal file
30
addons/gecs/tests/core/test_query_cache_key_domains.gd
Normal file
@@ -0,0 +1,30 @@
|
||||
extends GdUnitTestSuite
|
||||
|
||||
var runner: GdUnitSceneRunner
|
||||
var world: World
|
||||
|
||||
func before():
|
||||
runner = scene_runner("res://addons/gecs/tests/test_scene.tscn")
|
||||
world = runner.get_property("world")
|
||||
ECS.world = world
|
||||
|
||||
func after_test():
|
||||
if world:
|
||||
world.purge(false)
|
||||
|
||||
func test_all_vs_any_distinct_cache_key():
|
||||
var qb_all = world.query.with_all([C_DomainTestA, C_DomainTestB])
|
||||
var key_all = qb_all.get_cache_key()
|
||||
var qb_any = world.query.with_any([C_DomainTestA, C_DomainTestB])
|
||||
var key_any = qb_any.get_cache_key()
|
||||
assert_int(key_all).is_not_equal(key_any)
|
||||
|
||||
func test_all_vs_mixed_not_colliding():
|
||||
var qb1 = world.query.with_all([C_DomainTestA]).with_any([C_DomainTestB])
|
||||
var qb2 = world.query.with_all([C_DomainTestA, C_DomainTestB])
|
||||
assert_int(qb1.get_cache_key()).is_not_equal(qb2.get_cache_key())
|
||||
|
||||
func test_any_vs_exclude_not_colliding():
|
||||
var qb3 = world.query.with_any([C_DomainTestA])
|
||||
var qb4 = world.query.with_none([C_DomainTestA])
|
||||
assert_int(qb3.get_cache_key()).is_not_equal(qb4.get_cache_key())
|
||||
@@ -0,0 +1 @@
|
||||
uid://dw542afdb7ydt
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user