-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathentity.py
More file actions
1090 lines (862 loc) · 41.6 KB
/
entity.py
File metadata and controls
1090 lines (862 loc) · 41.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Entity management and interface tools."""
from __future__ import annotations
from collections.abc import Iterable, Iterator, Mapping, MutableMapping, MutableSet
from typing import (
TYPE_CHECKING,
Any,
Final,
Generic,
TypeVar,
overload,
)
from weakref import WeakKeyDictionary, WeakValueDictionary
import attrs
from sentinel_value import sentinel
from typing_extensions import Self, TypeForm, deprecated
import tcod.ecs.callbacks
import tcod.ecs.query
from tcod.ecs.constants import IsA
from tcod.ecs.typing import ComponentKey
if TYPE_CHECKING:
from collections.abc import Set as AbstractSet
from _typeshed import SupportsKeysAndGetItem
from tcod.ecs.registry import Registry
T = TypeVar("T")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_raise: Final = sentinel("_raise")
_entity_table: WeakKeyDictionary[Registry, WeakValueDictionary[object, Entity]] = WeakKeyDictionary()
"""A weak table of registries and unique identifiers to entity objects.
This table is used to that non-unique Entity's won't create a new object and thus will always share identities.
_entity_table[registry][uid] = entity
"""
class Entity:
"""A unique entity in a registry.
Example::
>>> import tcod.ecs
>>> registry = tcod.ecs.Registry() # Create a new registry
>>> registry.new_entity() # Create a new entity
<Entity(uid=object at ...)>
>>> entity = registry["entity"] # Get an entity from a specific identifier
>>> other_entity = registry["other"]
""" # Changes here should be reflected in conftest.py
__slots__ = ("__weakref__", "registry", "uid")
registry: Final[Registry] # type:ignore[misc] # https://github.com/python/mypy/issues/5774
"""The :any:`Registry` this entity belongs to."""
uid: Final[object] # type:ignore[misc]
"""This entities unique identifier."""
@property
@deprecated("Use '.registry' instead of '.world'")
def world(self) -> Registry:
"""Deprecated alias for registry.
.. deprecated:: 5.1
Use :any:`registry` instead.
"""
return self.registry
def __new__(cls, registry: Registry, uid: object = object) -> Self | Entity:
"""Return a unique entity for the given `registry` and `uid`.
If an entity already exists with a matching `registry` and `uid` then that entity is returned.
The `uid` default of `object` will create an instance of :any:`object` as the `uid`.
An entity created this way will never match or collide with an existing entity.
Example::
>>> registry = tcod.ecs.Registry()
>>> Entity(registry, "foo")
<Entity(uid='foo')>
>>> Entity(registry, "foo") is Entity(registry, "foo")
True
>>> Entity(registry) is Entity(registry)
False
"""
if uid is object:
uid = object()
try:
table = _entity_table[registry]
except KeyError:
table = WeakValueDictionary()
_entity_table[registry] = table
try:
return table[uid]
except KeyError:
pass
self = super().__new__(cls)
self.registry = registry # type:ignore[misc] # https://github.com/python/mypy/issues/5774
self.uid = uid # type:ignore[misc]
_entity_table[registry][uid] = self
return self
def clear(self) -> None:
"""Deletes all of this entities components, tags, and relations.
Relations targeting this component are still kept.
.. versionadded:: 4.2.0
"""
self.components.clear()
self.tags.clear()
self.relation_tags_many.clear()
self.relation_components.clear()
def instantiate(self) -> Self:
"""Return a new entity which inherits the components, tags, and relations of this entity.
This creates a new unique entity and assigns an :any:`IsA` relationship with `self` to the new entity.
The :any:`IsA` relation is the only data this new entity directly holds.
Immutable components inherited from the parent are always copy-on-write for its child instances.
Keep in mind that components/tags/relations inherited from the parent are not removable from the child instance.
Example::
# 'child = entity.instantiate()' is equivalent to the following:
>>> from tcod.ecs import IsA
>>> child = registry[object()] # New unique entity
>>> child.relation_tag[IsA] = entity # Configure IsA relation
Example::
>>> parent = registry.new_entity()
>>> parent.components[str] = "baz"
>>> child = parent.instantiate()
>>> child.components[str] # Inherits components from parent
'baz'
>>> parent.components[str] = "foo"
>>> child.components[str] # Changes in parent are reflected in children
'foo'
>>> child.components[str] += "bar" # In-place assignment operators will copy-on-write immutable objects
>>> child.components[str]
'foobar'
>>> parent.components[str]
'foo'
>>> child.components.pop(str, None) # Revert the component to the inherited value
'foobar'
>>> child.components[str]
'foo'
>>> child.components.pop(str, None) # Safe to call .pop with default when the value isn't set on the child
>>> child.components[str]
'foo'
# Note: Mutable objects have the same gotchas as in other Python examples:
>>> from typing import List, Tuple
>>> parent.components[List[str]] = ["foo"]
>>> child.components[List[str]] += ["bar"] # Will modify list in-place then assign that same list to child
>>> parent.components[List[str]] # Parent references the same list as the child now
['foo', 'bar']
>>> child.components[List[str]]
['foo', 'bar']
>>> parent.components[List[str]] is child.components[List[str]]
True
>>> parent.components[Tuple[str, ...]] = ("foo",) # Prefer immutable types to avoid the above issue
>>> child.components[Tuple[str, ...]] += ("bar",)
>>> child.components[Tuple[str, ...]]
('foo', 'bar')
>>> parent.components[Tuple[str, ...]]
('foo',)
.. versionadded:: 5.0
"""
new_entity = self.__class__(self.registry, object())
new_entity.relation_tag[IsA] = self
return new_entity
@property
def components(self) -> EntityComponents:
"""Access an entities components.
Example::
>>> entity.components[str] = "foo" # Assign component
>>> entity.components[("name", str)] = "my_name" # Assign named component
>>> entity.components |= { # Update components in-place
... ("hp", int): 10,
... ("attack", int): 4,
... ("defense", int): 1,
... }
>>> ("name", str) in entity.components
True
>>> {str, ("name", str)}.issubset(entity.components.keys())
True
>>> list(registry.Q.all_of(components=[str])) # Query components
[<Entity(uid='entity')>]
>>> list(registry.Q[tcod.ecs.Entity, str, ("name", str)]) # Query zip components
[(<Entity(uid='entity')>, 'foo', 'my_name')]
"""
return EntityComponents(self, (IsA,))
@components.setter
def components(self, value: EntityComponents) -> None:
assert value.entity is self
@property
def tags(self) -> EntityTags:
"""Access an entities tags.
Example::
>>> entity.tags.add("tag") # Add tag
>>> "tag" in entity.tags # Check tag
True
>>> list(registry.Q.all_of(tags=["tag"])) # Query tags
[<Entity(uid='entity')>]
>>> entity.tags.discard("tag")
>>> entity.tags |= {"IsPortable", "CanBurn", "OnFire"} # Supports in-place syntax
>>> {"CanBurn", "OnFire"}.issubset(entity.tags)
True
>>> entity.tags -= {"OnFire"}
>>> {"CanBurn", "OnFire"}.issubset(entity.tags)
False
"""
return EntityTags(self, (IsA,))
@tags.setter
def tags(self, value: EntityTags) -> None:
assert value.entity is self
@property
def relation_components(self) -> EntityComponentRelations:
"""Access an entities relation components.
Example::
>>> entity.relation_components[str][other_entity] = "foo" # Assign component to relation
>>> entity.relation_components[("distance", int)][other_entity] = 42 # Also works for named components
>>> other_entity in entity.relation_components[str]
True
>>> list(registry.Q.all_of(relations=[(str, other_entity)]))
[<Entity(uid='entity')>]
>>> list(registry.Q.all_of(relations=[(str, ...)]))
[<Entity(uid='entity')>]
>>> list(registry.Q.all_of(relations=[(entity, str, None)]))
[<Entity(uid='other')>]
>>> list(registry.Q.all_of(relations=[(..., str, None)]))
[<Entity(uid='other')>]
"""
return EntityComponentRelations(self, (IsA,))
@property
def relation_tag(self) -> EntityRelationsExclusive:
"""Access an entities exclusive relations.
Example::
>>> entity.relation_tag["ChildOf"] = other_entity # Assign relation
>>> list(registry.Q.all_of(relations=[("ChildOf", other_entity)])) # Get children of other_entity
[<Entity(uid='entity')>]
>>> list(registry.Q.all_of(relations=[(entity, "ChildOf", None)])) # Get parents of entity
[<Entity(uid='other')>]
>>> del entity.relation_tag["ChildOf"]
"""
return EntityRelationsExclusive(self, (IsA,))
@property
@deprecated("The '.relation_tags' attribute has been renamed to '.relation_tag'", category=FutureWarning)
def relation_tags(self) -> EntityRelationsExclusive:
"""Access an entities exclusive relations.
.. deprecated:: 3.2
This attribute was renamed to :any:`relation_tag`.
"""
return EntityRelationsExclusive(self, (IsA,))
@property
def relation_tags_many(self) -> EntityRelations:
"""Access an entities many-to-many relations.
Example::
>>> entity.relation_tags_many["KnownBy"].add(other_entity) # Assign relation
"""
return EntityRelations(self, (IsA,))
@deprecated("The name feature has been deprecated and will be removed.", category=FutureWarning)
def _set_name(self, value: object) -> None:
old_name = self.name
if old_name is not None: # Remove self from names
del self.registry._names_by_name[old_name]
del self.registry._names_by_entity[self]
if value is not None: # Add self to names
old_entity = self.registry._names_by_name.get(value)
if old_entity is not None: # Remove entity with old name, name will be overwritten
del self.registry._names_by_entity[old_entity]
self.registry._names_by_name[value] = self
self.registry._names_by_entity[self] = value
@property
def name(self) -> object:
"""The unique name of this entity or None.
You may assign a new name, but if an entity of the registry already has that name then it will lose it.
.. deprecated:: 3.1
This feature has been deprecated.
"""
return self.registry._names_by_entity.get(self)
@name.setter
@deprecated("The name feature has been deprecated and will be removed.", category=FutureWarning)
def name(self, value: object) -> None:
self._set_name(value)
def __repr__(self) -> str:
"""Return a representation of this entity.
Example::
>>> registry.new_entity()
<Entity(uid=object at ...)>
>>> registry["foo"]
<Entity(uid='foo')>
"""
uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__ is object else repr(self.uid)
items = [f"{self.__class__.__name__}(uid={uid_str})"]
name = self.name
if name is not None: # Switch to older style
items = [self.__class__.__name__, f"name={name!r}"]
return f"<{' '.join(items)}>"
def __reduce__(self) -> tuple[type[Entity], tuple[Registry, object]]:
"""Pickle this Entity.
Note that any pickled entity will include the registry it belongs to and all the entities of that registry.
"""
return self.__class__, (self.registry, self.uid)
def _force_remap(self, new_uid: object) -> None:
"""Remap this Entity to a new uid, both and old and new uid's will use this entity."""
_entity_table[self.registry][new_uid] = self
self.uid = new_uid # type: ignore[misc]
def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> Iterator[Entity]:
"""Iterate over all entities this one inherits from, including itself."""
if not traverse_parents:
yield start
return
traverse_parents = traverse_parents[::-1]
visited = {start}
stack = [start]
_relation_tags_by_entity = start.registry._relation_tags_by_entity
while stack:
entity = stack.pop()
yield entity
entity_relations = _relation_tags_by_entity.get(entity)
if entity_relations is None:
continue
for traverse_key in traverse_parents:
relations = entity_relations.get(traverse_key)
if relations is None:
continue
assert len(relations) == 1
next_entity = next(iter(relations))
if next_entity in visited:
continue
visited.add(next_entity)
stack.append(next_entity)
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any]], object]):
"""A proxy attribute to access an entities components like a dictionary.
See :any:`Entity.components`.
"""
entity: Entity
traverse: tuple[object, ...]
def __call__(self, *, traverse: Iterable[object]) -> Self:
"""Update this view with alternative parameters, such as a specific traversal relation.
.. versionadded:: 5.0
"""
return self.__class__(self.entity, tuple(traverse))
@deprecated("Setting values without an explicit key has been deprecated.", category=FutureWarning)
def set(self, value: object) -> None:
"""Assign or overwrite a component, automatically deriving the key.
.. deprecated:: 3.1
Setting values without an explicit key has been deprecated.
"""
key = value.__class__
self[key] = value
@staticmethod
def __assert_key(key: ComponentKey[Any]) -> bool:
"""Verify that abstract classes are accessed correctly."""
if isinstance(key, tuple):
key = key[1]
return True
def __getitem__(self, key: ComponentKey[T]) -> T:
"""Return a component belonging to this entity, or an indirect parent."""
assert self.__assert_key(key)
_components_by_entity = self.entity.registry._components_by_entity
for entity in _traverse_entities(self.entity, self.traverse):
try:
return _components_by_entity[entity][key] # type: ignore[no-any-return]
except KeyError: # noqa: PERF203
pass
raise KeyError(key)
def __setitem__(self, key: ComponentKey[T], value: T) -> None:
"""Assign a component directly to an entity."""
assert self.__assert_key(key)
old_value = self.entity.registry._components_by_entity[self.entity].get(key)
if old_value is None:
tcod.ecs.query._touch_component(self.entity.registry, key) # Component added
self.entity.registry._components_by_entity[self.entity][key] = value
self.entity.registry._components_by_type[key][self.entity] = value
tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, value)
def __delitem__(self, key: TypeForm[object] | tuple[object, TypeForm[object]]) -> None:
"""Delete a directly held component from an entity."""
assert self.__assert_key(key)
old_value = self.entity.registry._components_by_entity[self.entity].get(key)
del self.entity.registry._components_by_entity[self.entity][key]
if not self.entity.registry._components_by_entity[self.entity]:
del self.entity.registry._components_by_entity[self.entity]
del self.entity.registry._components_by_type[key][self.entity]
if not self.entity.registry._components_by_type[key]:
del self.entity.registry._components_by_type[key]
tcod.ecs.query._touch_component(self.entity.registry, key) # Component removed
tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, None)
def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
"""Return the components held by this entity, including inherited components."""
_components_by_entity = self.entity.registry._components_by_entity
if not self.traverse:
return _components_by_entity.get(self.entity, {}).keys()
set_: set[ComponentKey[object]] = set()
return set_.union(
*(_components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
)
def __contains__(self, key: object) -> bool:
"""Return True if this entity has the provided component key."""
_components_by_entity = self.entity.registry._components_by_entity
return any(
key in _components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)
)
def __iter__(self) -> Iterator[ComponentKey[Any]]:
"""Iterate over the component types belonging to this entity."""
return iter(self.keys())
def __len__(self) -> int:
"""Return the number of components belonging to this entity."""
return len(self.keys())
@deprecated("Setting values without an explicit key has been deprecated.", category=FutureWarning)
def update_values(self, values: Iterable[object]) -> None:
"""Add or overwrite multiple components inplace, deriving the keys from the values.
.. deprecated:: 3.1
Setting values without an explicit key has been deprecated.
"""
for value in values:
self.set(value)
@deprecated("This method has been deprecated. Iterate over items instead.", category=FutureWarning)
def by_name_type(self, name_type: type[_T1], component_type: TypeForm[_T2]) -> Iterator[tuple[_T1, TypeForm[_T2]]]:
"""Iterate over all of an entities component keys with a specific (name_type, component_type) combination.
.. versionadded:: 3.0
.. deprecated:: 3.1
This method has been deprecated. Iterate over items instead.
"""
for key in self:
if not isinstance(key, tuple):
continue
key_name, key_component = key
if key_component is component_type and isinstance(key_name, name_type):
yield key_name, key_component # type: ignore[unused-ignore] # Too complex for PyLance, deprecated anyways
@overload
def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: ...
@overload
def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self: ...
def __ior__(
self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any] | Iterable[tuple[ComponentKey[Any], Any]]
) -> Self:
"""Update components in-place.
.. versionadded:: 3.4
"""
self.update(value)
return self
@overload
def get(self, __key: ComponentKey[T], /) -> T | None: ...
@overload
def get(self, __key: ComponentKey[T], /, default: _T1) -> T | _T1: ...
def get(self, __key: ComponentKey[T], /, default: _T1 | None = None) -> T | _T1:
"""Return a component, returns None or a default value when the component is missing."""
try:
return self[__key]
except KeyError:
return default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737
def setdefault(self, __key: ComponentKey[T], __default: T, /) -> T: # type: ignore[override] # Disallows default None
"""Assign a default value if a component is missing, then returns the current value."""
try:
return self[__key]
except KeyError:
self[__key] = __default
return __default
@overload
def pop(self, __key: ComponentKey[T], /) -> T: ...
@overload
def pop(self, __key: ComponentKey[T], /, default: _T1) -> T | _T1: ...
def pop(
self,
__key: ComponentKey[T],
/,
default: _T1 = _raise, # type: ignore[assignment] # https://github.com/python/mypy/issues/3737
) -> T | _T1:
"""Remove a component directly from this entity.
Returns the removed value.
If the value is missing returns `default`.
If `default` is unset then raises :any:`KeyError` instead.
Operates directly on the entity without traversal.
>>> parent = registry[object()]
>>> parent.components[str] = "foo"
>>> child = parent.instantiate()
>>> child.components[str] = "bar"
>>> child.components.pop(str, None)
'bar'
>>> child.components.pop(str, None)
>>> child.components[str]
'foo'
>>> child.components.pop(str)
Traceback (most recent call last):
...
KeyError: <class 'str'>
"""
_components = self.entity.registry._components_by_entity.get(self.entity, {})
if __key not in _components:
if default is _raise:
raise KeyError(__key)
return default
value: T | _T1 = _components[__key]
del self[__key]
return value
def clear(self) -> None:
"""Remove any components stored directly in this entity."""
if self.traverse:
return self(traverse=()).clear()
return super().clear()
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityTags(MutableSet[Any]):
"""A proxy attribute to access an entities tags like a set.
See :any:`Entity.tags`.
"""
entity: Entity
traverse: tuple[object, ...]
def __call__(self, *, traverse: Iterable[object]) -> Self:
"""Update this view with alternative parameters, such as a specific traversal relation.
.. versionadded:: 5.0
"""
return self.__class__(self.entity, tuple(traverse))
def add(self, tag: object) -> None:
"""Add a tag to the entity."""
if tag in self.entity.registry._tags_by_entity[self.entity]:
return # Already has tag
tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag added
self.entity.registry._tags_by_entity[self.entity].add(tag)
self.entity.registry._tags_by_key[tag].add(self.entity)
def discard(self, tag: object) -> None:
"""Discard a tag directly held by an entity."""
if tag not in self.entity.registry._tags_by_entity[self.entity]:
return # Already doesn't have tag
tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag removed
self.entity.registry._tags_by_entity[self.entity].discard(tag)
if not self.entity.registry._tags_by_entity[self.entity]:
del self.entity.registry._tags_by_entity[self.entity]
self.entity.registry._tags_by_key[tag].discard(self.entity)
if not self.entity.registry._tags_by_key[tag]:
del self.entity.registry._tags_by_key[tag]
def remove(self, tag: object) -> None:
"""Remove a tag directly held by an entity."""
tags = self.entity.registry._tags_by_entity.get(self.entity)
if tags is None or tag not in tags:
raise KeyError(tag)
self.discard(tag)
def clear(self) -> None:
"""Remove all tags directly in this entity."""
_tags_by_entity = self.entity.registry._tags_by_entity
while _tags_by_entity[self.entity]:
self.discard(next(iter(_tags_by_entity[self.entity])))
def __contains__(self, x: object) -> bool:
"""Return True if this entity has the given tag."""
_tags_by_entity = self.entity.registry._tags_by_entity
return any(x in _tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
def _as_set(self) -> set[object]:
"""Return all tags inherited by traversal rules into a single set with no duplicates."""
_tags_by_entity = self.entity.registry._tags_by_entity
return set().union(
*(_tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
)
def __iter__(self) -> Iterator[Any]:
"""Iterate over this entities tags."""
return iter(self._as_set())
def __len__(self) -> int:
"""Return the number of tags this entity has."""
return len(self._as_set())
def __ior__(self, other: AbstractSet[object]) -> Self:
"""Add tags in-place.
.. versionadded:: 3.3
"""
for to_add in other:
self.add(to_add)
return self
def __isub__(self, other: AbstractSet[Any]) -> Self:
"""Remove tags in-place.
.. versionadded:: 3.3
"""
for to_discard in other:
self.discard(to_discard)
return self
def _relations_lookup_add(registry: Registry, origin: Entity, tag: object, target: Entity) -> None:
"""Add a relation tag/component to the lookup table and handle side effects."""
registry._relations_lookup[(tag, target)].add(origin)
registry._relations_lookup[(tag, ...)].add(origin)
registry._relations_lookup[(origin, tag, None)].add(target)
registry._relations_lookup[(..., tag, None)].add(target)
tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None)))
def _relations_lookup_discard(registry: Registry, origin: Entity, tag: object, target: Entity) -> None:
"""Discard a relation tag/component from the lookup table and handle side effects."""
registry._relations_lookup[(tag, target)].discard(origin)
if not registry._relations_lookup[(tag, target)]:
del registry._relations_lookup[(tag, target)]
registry._relations_lookup[(..., tag, None)].discard(target)
if not registry._relations_lookup[(..., tag, None)]:
del registry._relations_lookup[(..., tag, None)]
registry._relations_lookup[(origin, tag, None)].discard(target)
if not registry._relations_lookup[(origin, tag, None)]:
del registry._relations_lookup[(origin, tag, None)]
registry._relations_lookup[(tag, ...)].discard(origin)
if not registry._relations_lookup[(tag, ...)]:
del registry._relations_lookup[(tag, ...)]
tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None)))
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityRelationsMapping(MutableSet[Entity]):
"""A proxy attribute to access entity relation targets like a set.
See :any:`Entity.relation_tags_many`.
"""
entity: Entity
key: object
traverse: tuple[object, ...]
if __debug__:
def __attrs_post_init__(self) -> None:
"""Validate attributes."""
assert self.key not in {None, Ellipsis}
def add(self, target: Entity) -> None:
"""Add a relation target to this tag."""
registry = self.entity.registry
registry._relation_tags_by_entity[self.entity][self.key].add(target)
_relations_lookup_add(registry, self.entity, self.key, target)
def discard(self, target: Entity) -> None:
"""Discard a directly held relation target from this tag."""
registry = self.entity.registry
registry._relation_tags_by_entity[self.entity][self.key].discard(target)
if not registry._relation_tags_by_entity[self.entity][self.key]:
del registry._relation_tags_by_entity[self.entity][self.key]
if not registry._relation_tags_by_entity[self.entity]:
del registry._relation_tags_by_entity[self.entity]
_relations_lookup_discard(registry, self.entity, self.key, target)
def remove(self, target: Entity) -> None:
"""Remove a directly held relation target from this tag.
This will raise KeyError of only an indirect relation target exists.
"""
relations = self.entity.registry._relation_tags_by_entity.get(self.entity)
if relations is None:
raise KeyError(target)
targets = relations.get(self.key)
if targets is None or target not in targets:
raise KeyError(target)
self.discard(target)
def __contains__(self, target: Entity) -> bool: # type: ignore[override]
"""Return True if this relation contains the given value."""
_relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
for entity in _traverse_entities(self.entity, self.traverse):
by_entity = _relation_tags_by_entity.get(entity)
if by_entity is None:
continue
if target in by_entity.get(self.key, ()):
return True
return False
def _as_set(self) -> set[Entity]:
"""Return the combined targets of this mapping via traversal with duplicates removed."""
_relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
results: set[Entity] = set()
for entity in _traverse_entities(self.entity, self.traverse):
by_entity = _relation_tags_by_entity.get(entity)
if by_entity is None:
continue
results.update(by_entity.get(self.key, ()))
return results
def __iter__(self) -> Iterator[Entity]:
"""Iterate over this relation tags targets."""
yield from self._as_set()
def __len__(self) -> int:
"""Return the number of targets for this relation tag."""
return len(self._as_set())
def clear(self) -> None:
"""Discard all targets for this tag relation."""
by_entity = self.entity.registry._relation_tags_by_entity.get(self.entity)
if by_entity is None:
return
for key in list(by_entity.get(self.key, ())):
self.discard(key)
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityRelations(MutableMapping[object, EntityRelationsMapping]):
"""A proxy attribute to access entity relations like a dict of sets.
See :any:`Entity.relation_tags_many`.
"""
entity: Entity
traverse: tuple[object, ...]
def __call__(self, *, traverse: Iterable[object]) -> Self:
"""Update this view with alternative parameters, such as a specific traversal relation.
.. versionadded:: 5.0
"""
return self.__class__(self.entity, tuple(traverse))
def __getitem__(self, key: object) -> EntityRelationsMapping:
"""Return the relation mapping for a tag."""
return EntityRelationsMapping(self.entity, key, self.traverse)
def __setitem__(self, key: object, values: Iterable[Entity]) -> None:
"""Overwrite the targets of a relation tag with the new values."""
assert not isinstance(values, Entity), "Did you mean `entity.relations[key] = (target,)`?"
mapping = EntityRelationsMapping(self.entity, key, self.traverse)
mapping.clear()
for v in values:
mapping.add(v)
def __delitem__(self, key: object) -> None:
"""Clear the relation tags of an entity.
This does not remove relation tags towards this entity.
"""
self[key].clear()
def __iter__(self) -> Iterator[Any]:
"""Iterate over the unique relation tags of this entity."""
_relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
empty_dict: dict[object, set[Entity]] = {}
yield from set().union(
*(
_relation_tags_by_entity.get(entity, empty_dict).keys()
for entity in _traverse_entities(self.entity, self.traverse)
)
)
def __len__(self) -> int:
"""Return the number of unique relation tags this entity has."""
return len(list(self.__iter__()))
def clear(self) -> None:
"""Discard all tag relations from an entity."""
for key in list(self.entity.registry._relation_tags_by_entity.get(self.entity, ())):
del self[key]
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityRelationsExclusive(MutableMapping[object, Entity]):
"""A proxy attribute to access entity relations exclusively.
See :any:`Entity.relation_tag`.
"""
entity: Entity
traverse: tuple[object, ...]
def __call__(self, *, traverse: Iterable[object]) -> Self:
"""Update this view with alternative parameters, such as a specific traversal relation.
.. versionadded:: 5.0
"""
return self.__class__(self.entity, tuple(traverse))
def __getitem__(self, key: object) -> Entity:
"""Return the relation target for a key.
If the relation has no target then raises KeyError.
If the relation is not exclusive then raises ValueError.
"""
_relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
for entity in _traverse_entities(self.entity, self.traverse):
by_entity = _relation_tags_by_entity.get(entity)
if by_entity is None:
continue
values = by_entity.get(key)
if not values:
continue
try:
(target,) = values
except ValueError:
msg = "Entity relation has multiple targets but an exclusive value was expected."
raise ValueError(msg) from None
return target
raise KeyError(key)
def __setitem__(self, key: object, target: Entity) -> None:
"""Set a relation exclusively to a new target."""
mapping = EntityRelationsMapping(self.entity, key, self.traverse)
mapping.clear()
mapping.add(target)
def __delitem__(self, key: object) -> None:
"""Clear the relation targets of a relation key."""
EntityRelationsMapping(self.entity, key, self.traverse).clear()
def __iter__(self) -> Iterator[Any]:
"""Iterate over the keys of this entities relations."""
return EntityRelations(self.entity, self.traverse).__iter__()
def __len__(self) -> int:
"""Return the number of relations this entity has."""
return EntityRelations(self.entity, self.traverse).__len__()
def clear(self) -> None:
"""Discard all tag relations from an entity."""
EntityRelations(self.entity, self.traverse).clear()
@attrs.define(eq=False, frozen=True, weakref_slot=False)
class EntityComponentRelationMapping(MutableMapping[Entity, T], Generic[T]):
"""An entity-component mapping to access the relation target component objects.
See :any:`Entity.relation_components`.
"""
entity: Entity
key: ComponentKey[T]
traverse: tuple[object, ...]
if __debug__:
def __attrs_post_init__(self) -> None:
"""Validate attributes."""
assert isinstance(self.entity, Entity), self.entity
def __call__(self, *, traverse: Iterable[object]) -> Self:
"""Update this view with alternative parameters, such as a specific traversal relation.
.. versionadded:: 5.4
"""
return self.__class__(self.entity, self.key, tuple(traverse))
def __getitem__(self, target: Entity) -> T:
"""Return the component related to a target entity."""
_relation_components_by_entity = self.entity.registry._relation_components_by_entity
for entity in _traverse_entities(self.entity, self.traverse):
by_entity = _relation_components_by_entity.get(entity)
if by_entity is None:
continue
by_key = by_entity.get(self.key)
if by_key is None or target not in by_key:
continue
return by_key[target] # type: ignore[no-any-return]
raise KeyError(target)
def __setitem__(self, target: Entity, component: T) -> None:
"""Assign a component to the target entity."""
registry = self.entity.registry
old_value = registry._relation_components_by_entity[self.entity][self.key].get(target)
if old_value is None: # Relation added
tcod.ecs.query._touch_relations(
registry, ((self.key, target), (self.key, ...), (self.entity, self.key, None), (..., self.key, None))
)
registry._relation_components_by_entity[self.entity][self.key][target] = component
_relations_lookup_add(registry, self.entity, self.key, target)
def __delitem__(self, target: Entity) -> None:
"""Delete a component assigned to the target entity."""
registry = self.entity.registry
del registry._relation_components_by_entity[self.entity][self.key][target]
if not registry._relation_components_by_entity[self.entity][self.key]:
del registry._relation_components_by_entity[self.entity][self.key]
if not registry._relation_components_by_entity[self.entity]:
del registry._relation_components_by_entity[self.entity]
_relations_lookup_discard(registry, self.entity, self.key, target)
def keys(self) -> AbstractSet[Entity]: # type: ignore[override]
"""Return all entities with an associated component value."""
_relation_components_by_entity = self.entity.registry._relation_components_by_entity
result: set[Entity] = set()
for entity in _traverse_entities(self.entity, self.traverse):
by_entity = _relation_components_by_entity.get(entity)
if by_entity is None:
continue
result.update(by_entity.get(self.key, ()))
return result