tcod-ecs 5.1.0__tar.gz → 5.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/PKG-INFO +39 -21
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/README.md +38 -20
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/pyproject.toml +15 -46
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/__init__.py +1 -0
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/_converter.py +2 -2
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/_version.py +2 -2
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/callbacks.py +2 -2
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/constants.py +3 -0
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/entity.py +15 -15
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/query.py +30 -30
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/registry.py +11 -8
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/typing.py +2 -1
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/world.py +2 -0
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/LICENSE +0 -0
- {tcod_ecs-5.1.0 → tcod_ecs-5.2.0}/tcod/ecs/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tcod-ecs
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: A type-hinted Entity Component System based on Python dictionaries and sets.
|
|
5
5
|
Author-email: Kyle Benesch <4b796c65+github@gmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -236,13 +236,45 @@ True
|
|
|
236
236
|
|
|
237
237
|
## Relations
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
Use `Entity.relation_tag[tag] = target` to associate a tag exclusively with a target entity.
|
|
241
|
-
Use `Entity.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
|
|
239
|
+
Entity relations are unidirectional from an origin entity to possibly multiple target entities.
|
|
242
240
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
|
|
242
|
+
This uses standard assignment and is useful for tags which would not make sense with multiple targets.
|
|
243
|
+
Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
|
|
244
|
+
- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
|
|
245
|
+
This supports `set`-like syntax such as adding or removing multiple targets at once.
|
|
246
|
+
This allows for many-to-many relations.
|
|
247
|
+
- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
|
|
248
|
+
This allows storing data along with a relation.
|
|
249
|
+
This supports `dict`-like syntax.
|
|
250
|
+
The `component_key` can be queried like a normal tag.
|
|
251
|
+
|
|
252
|
+
### Relation queries
|
|
253
|
+
|
|
254
|
+
Relations are queried with `registry.Q.all_of(relations=[...])`.
|
|
255
|
+
This expects 2-item or 3-item tuples following these rules:
|
|
256
|
+
|
|
257
|
+
- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
|
|
258
|
+
- If `tag` is a component key then component relations are also matched.
|
|
259
|
+
This means you should be careful with tags which look like component keys.
|
|
260
|
+
- `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
|
|
261
|
+
- `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
|
|
262
|
+
- `target` can be `...` which means an entity with a relation to any entity is matched.
|
|
263
|
+
- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
|
|
264
|
+
|
|
265
|
+
Relations using sub-queries may be chained together.
|
|
266
|
+
See [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this.
|
|
267
|
+
|
|
268
|
+
You can use the following table to help with constructing relation queries:
|
|
269
|
+
|
|
270
|
+
| Matches | Syntax |
|
|
271
|
+
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
272
|
+
| Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
|
|
273
|
+
| Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
|
|
274
|
+
| Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
|
|
275
|
+
| Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
|
|
276
|
+
| Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
277
|
+
| Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
|
|
246
278
|
|
|
247
279
|
```py
|
|
248
280
|
>>> @attrs.define
|
|
@@ -273,17 +305,3 @@ True
|
|
|
273
305
|
|
|
274
306
|
```
|
|
275
307
|
|
|
276
|
-
### Relation queries
|
|
277
|
-
|
|
278
|
-
You can use the following table to help with constructing relation queries.
|
|
279
|
-
`tag` is a component key if you are querying for a component relation.
|
|
280
|
-
|
|
281
|
-
| Includes | Syntax |
|
|
282
|
-
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
283
|
-
| Entities with a relation tag to the given target | `(tag, target_entity)` |
|
|
284
|
-
| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
|
|
285
|
-
| Entities with a relation tag to the targets in the given query | `(tag, registry.Q.all_of(...))` |
|
|
286
|
-
| The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
|
|
287
|
-
| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
288
|
-
| The target entities of the queried entities with the given relation | `(tag, registry.Q.all_of(...))` |
|
|
289
|
-
|
|
@@ -207,13 +207,45 @@ True
|
|
|
207
207
|
|
|
208
208
|
## Relations
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Use `
|
|
210
|
+
Entity relations are unidirectional from an origin entity to possibly multiple target entities.
|
|
211
|
+
|
|
212
|
+
- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
|
|
213
|
+
This uses standard assignment and is useful for tags which would not make sense with multiple targets.
|
|
214
|
+
Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
|
|
215
|
+
- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
|
|
216
|
+
This supports `set`-like syntax such as adding or removing multiple targets at once.
|
|
217
|
+
This allows for many-to-many relations.
|
|
218
|
+
- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
|
|
219
|
+
This allows storing data along with a relation.
|
|
220
|
+
This supports `dict`-like syntax.
|
|
221
|
+
The `component_key` can be queried like a normal tag.
|
|
213
222
|
|
|
214
|
-
Relation queries
|
|
215
|
-
|
|
216
|
-
Relations are
|
|
223
|
+
### Relation queries
|
|
224
|
+
|
|
225
|
+
Relations are queried with `registry.Q.all_of(relations=[...])`.
|
|
226
|
+
This expects 2-item or 3-item tuples following these rules:
|
|
227
|
+
|
|
228
|
+
- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
|
|
229
|
+
- If `tag` is a component key then component relations are also matched.
|
|
230
|
+
This means you should be careful with tags which look like component keys.
|
|
231
|
+
- `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
|
|
232
|
+
- `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
|
|
233
|
+
- `target` can be `...` which means an entity with a relation to any entity is matched.
|
|
234
|
+
- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
|
|
235
|
+
|
|
236
|
+
Relations using sub-queries may be chained together.
|
|
237
|
+
See [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this.
|
|
238
|
+
|
|
239
|
+
You can use the following table to help with constructing relation queries:
|
|
240
|
+
|
|
241
|
+
| Matches | Syntax |
|
|
242
|
+
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
243
|
+
| Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
|
|
244
|
+
| Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
|
|
245
|
+
| Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
|
|
246
|
+
| Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
|
|
247
|
+
| Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
248
|
+
| Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
|
|
217
249
|
|
|
218
250
|
```py
|
|
219
251
|
>>> @attrs.define
|
|
@@ -243,17 +275,3 @@ True
|
|
|
243
275
|
True
|
|
244
276
|
|
|
245
277
|
```
|
|
246
|
-
|
|
247
|
-
### Relation queries
|
|
248
|
-
|
|
249
|
-
You can use the following table to help with constructing relation queries.
|
|
250
|
-
`tag` is a component key if you are querying for a component relation.
|
|
251
|
-
|
|
252
|
-
| Includes | Syntax |
|
|
253
|
-
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
254
|
-
| Entities with a relation tag to the given target | `(tag, target_entity)` |
|
|
255
|
-
| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
|
|
256
|
-
| Entities with a relation tag to the targets in the given query | `(tag, registry.Q.all_of(...))` |
|
|
257
|
-
| The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
|
|
258
|
-
| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
259
|
-
| The target entities of the queried entities with the given relation | `(tag, registry.Q.all_of(...))` |
|
|
@@ -46,16 +46,6 @@ Changelog = "https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.m
|
|
|
46
46
|
Documentation = "https://python-tcod-ecs.readthedocs.io"
|
|
47
47
|
Source = "https://github.com/HexDecimal/python-tcod-ecs"
|
|
48
48
|
|
|
49
|
-
[tool.black] # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file
|
|
50
|
-
target-version = ["py38"]
|
|
51
|
-
line-length = 120
|
|
52
|
-
|
|
53
|
-
[tool.isort] # https://pycqa.github.io/isort/docs/configuration/options.html
|
|
54
|
-
py_version = "38"
|
|
55
|
-
line_length = 120
|
|
56
|
-
profile = "black"
|
|
57
|
-
skip_gitignore = true
|
|
58
|
-
|
|
59
49
|
[tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html
|
|
60
50
|
files = "."
|
|
61
51
|
exclude = ['^build/', '^\.']
|
|
@@ -86,44 +76,23 @@ testpaths = ["."]
|
|
|
86
76
|
exclude_lines = ['^\s*\.\.\.', "if TYPE_CHECKING:", "# pragma: no cover"]
|
|
87
77
|
|
|
88
78
|
[tool.ruff]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"W", # pycodestyle
|
|
94
|
-
"F", # Pyflakes
|
|
95
|
-
"I", # isort
|
|
96
|
-
"UP", # pyupgrade
|
|
97
|
-
"YTT", # flake8-2020
|
|
98
|
-
"ANN", # flake8-annotations
|
|
99
|
-
"S", # flake8-bandit
|
|
100
|
-
"B", # flake8-bugbear
|
|
101
|
-
"C4", # flake8-comprehensions
|
|
102
|
-
"DTZ", # flake8-datetimez
|
|
103
|
-
"EM", # flake8-errmsg
|
|
104
|
-
"EXE", # flake8-executable
|
|
105
|
-
"RET", # flake8-return
|
|
106
|
-
"ICN", # flake8-import-conventions
|
|
107
|
-
"PIE", # flake8-pie
|
|
108
|
-
"PT", # flake8-pytest-style
|
|
109
|
-
"SIM", # flake8-simplify
|
|
110
|
-
"PTH", # flake8-use-pathlib
|
|
111
|
-
"PL", # Pylint
|
|
112
|
-
"TRY", # tryceratops
|
|
113
|
-
"RUF", # NumPy-specific rules
|
|
114
|
-
"G", # flake8-logging-format
|
|
115
|
-
"D", # pydocstyle
|
|
116
|
-
]
|
|
79
|
+
line-length = 120
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint] # https://docs.astral.sh/ruff/rules/
|
|
82
|
+
select = ["ALL"]
|
|
117
83
|
ignore = [
|
|
118
|
-
"E501", # line-too-long
|
|
119
|
-
"S101", # assert
|
|
120
84
|
"ANN101", # missing-type-self
|
|
121
85
|
"ANN102", # missing-type-cls
|
|
122
|
-
"
|
|
123
|
-
"
|
|
86
|
+
"COM", # flake8-commas, handled by formatter
|
|
87
|
+
"E501", # line-too-long
|
|
88
|
+
"S101", # assert
|
|
89
|
+
"SLF001", # private-member-access
|
|
90
|
+
"T10", # flake8-debugger
|
|
91
|
+
"T20", # flake8-print
|
|
124
92
|
]
|
|
125
|
-
line-length = 120
|
|
126
93
|
|
|
127
|
-
[tool.ruff.
|
|
128
|
-
|
|
129
|
-
|
|
94
|
+
[tool.ruff.lint.isort]
|
|
95
|
+
required-imports = ["from __future__ import annotations"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pydocstyle]
|
|
98
|
+
convention = "google" # Use Google-style docstrings.
|
|
@@ -12,9 +12,9 @@ def _is_defaultdict_type(type_hint: object) -> bool:
|
|
|
12
12
|
return get_origin(type_hint) is defaultdict
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any]
|
|
15
|
+
def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any] | object]) -> Callable[[], Any]:
|
|
16
16
|
"""Return the factory value for a defaultdict given its value type-hint."""
|
|
17
|
-
assert type_hint is not Any
|
|
17
|
+
assert type_hint is not Any # type: ignore[comparison-overlap]
|
|
18
18
|
if get_origin(type_hint) is not defaultdict:
|
|
19
19
|
return get_origin(type_hint) or type_hint
|
|
20
20
|
return functools.partial(defaultdict, _setup_defaultdict_factory(get_args(type_hint)[1]))
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""ECS callback management."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
|
5
6
|
|
|
6
7
|
from typing_extensions import TypeAlias
|
|
7
8
|
|
|
8
|
-
from tcod.ecs.typing import ComponentKey
|
|
9
|
-
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
10
|
from tcod.ecs.entity import Entity
|
|
11
|
+
from tcod.ecs.typing import ComponentKey
|
|
12
12
|
|
|
13
13
|
_T = TypeVar("_T")
|
|
14
14
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Entity management and interface tools."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import warnings
|
|
5
|
-
from collections.abc import Set
|
|
6
6
|
from typing import (
|
|
7
7
|
TYPE_CHECKING,
|
|
8
8
|
Any,
|
|
@@ -30,6 +30,8 @@ from tcod.ecs.constants import IsA
|
|
|
30
30
|
from tcod.ecs.typing import ComponentKey
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Set as AbstractSet
|
|
34
|
+
|
|
33
35
|
from _typeshed import SupportsKeysAndGetItem
|
|
34
36
|
|
|
35
37
|
from tcod.ecs.registry import Registry
|
|
@@ -79,7 +81,7 @@ class Entity:
|
|
|
79
81
|
warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2)
|
|
80
82
|
return self.registry
|
|
81
83
|
|
|
82
|
-
def __new__(cls, registry: Registry, uid: object = object) -> Entity:
|
|
84
|
+
def __new__(cls, registry: Registry, uid: object = object) -> Entity: # noqa: PYI034
|
|
83
85
|
"""Return a unique entity for the given `registry` and `uid`.
|
|
84
86
|
|
|
85
87
|
If an entity already exists with a matching `registry` and `uid` then that entity is returned.
|
|
@@ -333,7 +335,7 @@ class Entity:
|
|
|
333
335
|
>>> registry["foo"]
|
|
334
336
|
<Entity(uid='foo')>
|
|
335
337
|
"""
|
|
336
|
-
uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__
|
|
338
|
+
uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__ is object else repr(self.uid)
|
|
337
339
|
items = [f"{self.__class__.__name__}(uid={uid_str})"]
|
|
338
340
|
name = self.name
|
|
339
341
|
if name is not None: # Switch to older style
|
|
@@ -425,7 +427,7 @@ class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]]
|
|
|
425
427
|
for entity in _traverse_entities(self.entity, self.traverse):
|
|
426
428
|
try:
|
|
427
429
|
return _components_by_entity[entity][key] # type: ignore[no-any-return]
|
|
428
|
-
except KeyError:
|
|
430
|
+
except KeyError: # noqa: PERF203
|
|
429
431
|
pass
|
|
430
432
|
raise KeyError(key)
|
|
431
433
|
|
|
@@ -460,7 +462,7 @@ class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]]
|
|
|
460
462
|
tcod.ecs.query._touch_component(self.entity.registry, key) # Component removed
|
|
461
463
|
tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, None)
|
|
462
464
|
|
|
463
|
-
def keys(self) ->
|
|
465
|
+
def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
|
|
464
466
|
"""Return the components held by this entity, including inherited components."""
|
|
465
467
|
_components_by_entity = self.entity.registry._components_by_entity
|
|
466
468
|
if not self.traverse:
|
|
@@ -512,12 +514,10 @@ class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]]
|
|
|
512
514
|
yield key_name, key_component
|
|
513
515
|
|
|
514
516
|
@overload
|
|
515
|
-
def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self:
|
|
516
|
-
...
|
|
517
|
+
def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: ...
|
|
517
518
|
|
|
518
519
|
@overload
|
|
519
|
-
def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self:
|
|
520
|
-
...
|
|
520
|
+
def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self: ...
|
|
521
521
|
|
|
522
522
|
def __ior__(
|
|
523
523
|
self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any] | Iterable[tuple[ComponentKey[Any], Any]]
|
|
@@ -612,7 +612,7 @@ class EntityTags(MutableSet[Any]):
|
|
|
612
612
|
"""Return the number of tags this entity has."""
|
|
613
613
|
return len(self._as_set())
|
|
614
614
|
|
|
615
|
-
def __ior__(self, other:
|
|
615
|
+
def __ior__(self, other: AbstractSet[object]) -> Self:
|
|
616
616
|
"""Add tags in-place.
|
|
617
617
|
|
|
618
618
|
.. versionadded:: 3.3
|
|
@@ -621,7 +621,7 @@ class EntityTags(MutableSet[Any]):
|
|
|
621
621
|
self.add(to_add)
|
|
622
622
|
return self
|
|
623
623
|
|
|
624
|
-
def __isub__(self, other:
|
|
624
|
+
def __isub__(self, other: AbstractSet[Any]) -> Self:
|
|
625
625
|
"""Remove tags in-place.
|
|
626
626
|
|
|
627
627
|
.. versionadded:: 3.3
|
|
@@ -788,10 +788,10 @@ class EntityRelations(MutableMapping[object, EntityRelationsMapping]):
|
|
|
788
788
|
def __iter__(self) -> Iterator[Any]:
|
|
789
789
|
"""Iterate over the unique relation tags of this entity."""
|
|
790
790
|
_relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
|
|
791
|
-
|
|
791
|
+
empty_dict: dict[object, set[Entity]] = {}
|
|
792
792
|
yield from set().union(
|
|
793
793
|
*(
|
|
794
|
-
_relation_tags_by_entity.get(entity,
|
|
794
|
+
_relation_tags_by_entity.get(entity, empty_dict).keys()
|
|
795
795
|
for entity in _traverse_entities(self.entity, self.traverse)
|
|
796
796
|
)
|
|
797
797
|
)
|
|
@@ -926,7 +926,7 @@ class EntityComponentRelationMapping(Generic[T], MutableMapping[Entity, T]):
|
|
|
926
926
|
|
|
927
927
|
_relations_lookup_discard(registry, self.entity, self.key, target)
|
|
928
928
|
|
|
929
|
-
def keys(self) ->
|
|
929
|
+
def keys(self) -> AbstractSet[Entity]: # type: ignore[override]
|
|
930
930
|
"""Return all entities with an associated component value."""
|
|
931
931
|
_relation_components_by_entity = self.entity.registry._relation_components_by_entity
|
|
932
932
|
result: set[Entity] = set()
|
|
@@ -1004,7 +1004,7 @@ class EntityComponentRelations(MutableMapping[ComponentKey[Any], EntityComponent
|
|
|
1004
1004
|
for component_key in list(self.entity.registry._relation_components_by_entity.get(self.entity, ())):
|
|
1005
1005
|
self[component_key].clear()
|
|
1006
1006
|
|
|
1007
|
-
def keys(self) ->
|
|
1007
|
+
def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
|
|
1008
1008
|
"""Returns the components keys this entity has relations for."""
|
|
1009
1009
|
_relation_components_by_entity = self.entity.registry._relation_components_by_entity
|
|
1010
1010
|
return set().union(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Tools for querying Registry objects."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import itertools
|
|
5
6
|
import warnings
|
|
6
7
|
from collections import defaultdict
|
|
7
|
-
from collections.abc import Set
|
|
8
8
|
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Protocol, TypeVar, overload
|
|
9
9
|
from weakref import WeakKeyDictionary, WeakSet
|
|
10
10
|
|
|
@@ -13,11 +13,13 @@ from typing_extensions import Self
|
|
|
13
13
|
|
|
14
14
|
import tcod.ecs.entity
|
|
15
15
|
from tcod.ecs.constants import IsA
|
|
16
|
-
from tcod.ecs.typing import ComponentKey, _RelationQuery
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Set as AbstractSet
|
|
19
|
+
|
|
19
20
|
from tcod.ecs.entity import Entity
|
|
20
21
|
from tcod.ecs.registry import Registry
|
|
22
|
+
from tcod.ecs.typing import ComponentKey, _RelationQuery
|
|
21
23
|
|
|
22
24
|
_T1 = TypeVar("_T1")
|
|
23
25
|
_T2 = TypeVar("_T2")
|
|
@@ -34,7 +36,7 @@ _query_caches: WeakKeyDictionary[Registry, _QueryCache] = WeakKeyDictionary()
|
|
|
34
36
|
class _QueryCache:
|
|
35
37
|
"""Main data structure for the query cache."""
|
|
36
38
|
|
|
37
|
-
queries: dict[_Query,
|
|
39
|
+
queries: dict[_Query, AbstractSet[Entity]] = attrs.field(factory=dict)
|
|
38
40
|
"""Table of cached queries."""
|
|
39
41
|
by_components: defaultdict[ComponentKey[object], WeakSet[_Query]] = attrs.field(
|
|
40
42
|
factory=lambda: defaultdict(WeakSet)
|
|
@@ -100,7 +102,7 @@ def _check_suspicious_tags(tags: Iterable[object], stacklevel: int = 2) -> None:
|
|
|
100
102
|
)
|
|
101
103
|
|
|
102
104
|
|
|
103
|
-
def _fetch_relation_table(registry: Registry, relation: _RelationQuery) ->
|
|
105
|
+
def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> AbstractSet[Entity]:
|
|
104
106
|
"""Get the entity table for this relation.
|
|
105
107
|
|
|
106
108
|
For simple cases where target/origin is `Entity | ...` this returns the set directly from the lookup table.
|
|
@@ -130,7 +132,7 @@ def _get_query_cache(registry: Registry) -> _QueryCache:
|
|
|
130
132
|
return cache
|
|
131
133
|
|
|
132
134
|
|
|
133
|
-
def _get_query(registry: Registry, query: _Query) ->
|
|
135
|
+
def _get_query(registry: Registry, query: _Query) -> AbstractSet[Entity]:
|
|
134
136
|
"""Return the entities for the given query and registry."""
|
|
135
137
|
cache = _get_query_cache(registry)
|
|
136
138
|
if cache is not None:
|
|
@@ -167,7 +169,7 @@ class _Query(Protocol):
|
|
|
167
169
|
"""Add this query to the local cache."""
|
|
168
170
|
...
|
|
169
171
|
|
|
170
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
172
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]:
|
|
171
173
|
"""Compile the entities of this query, returning a set which must not be modified."""
|
|
172
174
|
...
|
|
173
175
|
|
|
@@ -178,10 +180,10 @@ class _QueryComponent:
|
|
|
178
180
|
|
|
179
181
|
_component: ComponentKey[object]
|
|
180
182
|
|
|
181
|
-
def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None:
|
|
183
|
+
def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002
|
|
182
184
|
cache.by_components[self._component].add(self)
|
|
183
185
|
|
|
184
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
186
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
185
187
|
return registry._components_by_type.get(self._component, {}).keys()
|
|
186
188
|
|
|
187
189
|
|
|
@@ -191,10 +193,10 @@ class _QueryTag:
|
|
|
191
193
|
|
|
192
194
|
_tag: object
|
|
193
195
|
|
|
194
|
-
def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None:
|
|
196
|
+
def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002
|
|
195
197
|
cache.by_tags[self._tag].add(self)
|
|
196
198
|
|
|
197
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
199
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
198
200
|
return registry._tags_by_key.get(self._tag, set())
|
|
199
201
|
|
|
200
202
|
|
|
@@ -220,7 +222,7 @@ class _QueryRelation:
|
|
|
220
222
|
if w_query is not None:
|
|
221
223
|
_get_query_cache(w_query.registry).dependencies[w_query._query].add((registry, self))
|
|
222
224
|
|
|
223
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
225
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
224
226
|
return _fetch_relation_table(registry, self._relation)
|
|
225
227
|
|
|
226
228
|
|
|
@@ -242,7 +244,7 @@ class _QueryLogicalAnd:
|
|
|
242
244
|
for dependency in itertools.chain(self._all_of, self._none_of):
|
|
243
245
|
cache.dependencies[dependency].add((registry, self))
|
|
244
246
|
|
|
245
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
247
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
246
248
|
if len(self._all_of) == 1 and not self._none_of: # Only one sub-query, simply return the results of it
|
|
247
249
|
return _get_query(registry, next(iter(self._all_of))) # Avoids an extra copy of a set
|
|
248
250
|
requires = sorted( # Place the smallest sets first to speed up intersections
|
|
@@ -271,7 +273,7 @@ class _QueryLogicalOr:
|
|
|
271
273
|
for dependency in self._any_of:
|
|
272
274
|
cache.dependencies[dependency].add((registry, self))
|
|
273
275
|
|
|
274
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
276
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
275
277
|
if len(self._any_of) == 1: # If there is only one sub-query then simply return the results of it
|
|
276
278
|
return _get_query(registry, next(iter(self._any_of))) # Avoids an extra copy of a set
|
|
277
279
|
entities: set[Entity] = set()
|
|
@@ -300,7 +302,7 @@ class _QueryTraversalPropagation:
|
|
|
300
302
|
cache.dependencies[self._sub_query].add((registry, self))
|
|
301
303
|
cache.dependencies[self._get_traverse_query()].add((registry, self))
|
|
302
304
|
|
|
303
|
-
def _compile(self, registry: Registry, cache: _QueryCache) ->
|
|
305
|
+
def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
|
|
304
306
|
cumulative_set = set(_get_query(registry, self._sub_query)) # All entities touched by this traversal
|
|
305
307
|
relations_set = _get_query(
|
|
306
308
|
registry, self._get_traverse_query()
|
|
@@ -341,7 +343,7 @@ class BoundQuery:
|
|
|
341
343
|
warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2)
|
|
342
344
|
return self.registry
|
|
343
345
|
|
|
344
|
-
def get_entities(self) ->
|
|
346
|
+
def get_entities(self) -> AbstractSet[Entity]:
|
|
345
347
|
"""Return entities matching the current query as a read-only set.
|
|
346
348
|
|
|
347
349
|
This is useful for post-processing the results of a query using set operations.
|
|
@@ -350,6 +352,10 @@ class BoundQuery:
|
|
|
350
352
|
"""
|
|
351
353
|
return _get_query(self.registry, self._query)
|
|
352
354
|
|
|
355
|
+
def __bool__(self) -> bool:
|
|
356
|
+
"""Return True if any entity matches this query."""
|
|
357
|
+
return bool(self.get_entities())
|
|
358
|
+
|
|
353
359
|
@staticmethod
|
|
354
360
|
def __as_queries(
|
|
355
361
|
components: Iterable[ComponentKey[object]] = (),
|
|
@@ -364,7 +370,7 @@ class BoundQuery:
|
|
|
364
370
|
yield from (_QueryTraversalPropagation(_QueryTag(tag), traverse, depth) for tag in tags)
|
|
365
371
|
yield from (_QueryTraversalPropagation(_QueryRelation(relations), traverse, depth) for relations in relations)
|
|
366
372
|
|
|
367
|
-
def all_of(
|
|
373
|
+
def all_of(
|
|
368
374
|
self,
|
|
369
375
|
components: Iterable[ComponentKey[object]] = (),
|
|
370
376
|
*,
|
|
@@ -381,7 +387,7 @@ class BoundQuery:
|
|
|
381
387
|
& self._query,
|
|
382
388
|
)
|
|
383
389
|
|
|
384
|
-
def none_of(
|
|
390
|
+
def none_of(
|
|
385
391
|
self,
|
|
386
392
|
components: Iterable[ComponentKey[object]] = (),
|
|
387
393
|
*,
|
|
@@ -403,42 +409,36 @@ class BoundQuery:
|
|
|
403
409
|
return iter(self.get_entities())
|
|
404
410
|
|
|
405
411
|
@overload
|
|
406
|
-
def __getitem__(self, key: tuple[ComponentKey[_T1]]) -> Iterable[tuple[_T1]]:
|
|
407
|
-
...
|
|
412
|
+
def __getitem__(self, key: tuple[ComponentKey[_T1]]) -> Iterable[tuple[_T1]]: ...
|
|
408
413
|
|
|
409
414
|
@overload
|
|
410
|
-
def __getitem__(self, key: tuple[ComponentKey[_T1], ComponentKey[_T2]]) -> Iterable[tuple[_T1, _T2]]:
|
|
411
|
-
...
|
|
415
|
+
def __getitem__(self, key: tuple[ComponentKey[_T1], ComponentKey[_T2]]) -> Iterable[tuple[_T1, _T2]]: ...
|
|
412
416
|
|
|
413
417
|
@overload
|
|
414
418
|
def __getitem__(
|
|
415
419
|
self, key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3]]
|
|
416
|
-
) -> Iterable[tuple[_T1, _T2, _T3]]:
|
|
417
|
-
...
|
|
420
|
+
) -> Iterable[tuple[_T1, _T2, _T3]]: ...
|
|
418
421
|
|
|
419
422
|
@overload
|
|
420
423
|
def __getitem__(
|
|
421
424
|
self, key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3], ComponentKey[_T4]]
|
|
422
|
-
) -> Iterable[tuple[_T1, _T2, _T3, _T4]]:
|
|
423
|
-
...
|
|
425
|
+
) -> Iterable[tuple[_T1, _T2, _T3, _T4]]: ...
|
|
424
426
|
|
|
425
427
|
@overload
|
|
426
428
|
def __getitem__(
|
|
427
429
|
self,
|
|
428
430
|
key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3], ComponentKey[_T4], ComponentKey[_T5]],
|
|
429
|
-
) -> Iterable[tuple[_T1, _T2, _T3, _T4, _T5]]:
|
|
430
|
-
...
|
|
431
|
+
) -> Iterable[tuple[_T1, _T2, _T3, _T4, _T5]]: ...
|
|
431
432
|
|
|
432
433
|
@overload
|
|
433
|
-
def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]:
|
|
434
|
-
...
|
|
434
|
+
def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]: ...
|
|
435
435
|
|
|
436
436
|
def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]:
|
|
437
437
|
"""Collect components from a query."""
|
|
438
438
|
assert key is not None
|
|
439
439
|
assert isinstance(key, tuple)
|
|
440
440
|
|
|
441
|
-
Entity = tcod.ecs.entity.Entity
|
|
441
|
+
Entity = tcod.ecs.entity.Entity # noqa: N806
|
|
442
442
|
|
|
443
443
|
entities = list(self.all_of(components=set(key) - {Entity}).get_entities())
|
|
444
444
|
entity_components = []
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"""Registry management tools."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import warnings
|
|
5
6
|
from collections import defaultdict
|
|
6
|
-
from typing import Any, DefaultDict, Dict, Iterable, Mapping, NoReturn, Set, TypeVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Final, Iterable, Mapping, NoReturn, Set, TypeVar
|
|
7
8
|
|
|
8
9
|
import attrs
|
|
9
10
|
|
|
10
11
|
import tcod.ecs._converter
|
|
11
12
|
import tcod.ecs.query
|
|
12
13
|
from tcod.ecs.entity import Entity
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from tcod.ecs.typing import ComponentKey, _RelationTargetLookup
|
|
14
17
|
|
|
15
18
|
_T1 = TypeVar("_T1")
|
|
16
19
|
_T2 = TypeVar("_T2")
|
|
@@ -110,9 +113,9 @@ class Registry:
|
|
|
110
113
|
|
|
111
114
|
dict[entity][tag] = {target_entities}
|
|
112
115
|
"""
|
|
113
|
-
_relation_components_by_entity: defaultdict[
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
_relation_components_by_entity: defaultdict[Entity, defaultdict[ComponentKey[object], dict[Entity, Any]]] = (
|
|
117
|
+
attrs.field(init=False, factory=lambda: defaultdict(_defaultdict_of_dict))
|
|
118
|
+
)
|
|
116
119
|
"""Random access relations owning components.
|
|
117
120
|
|
|
118
121
|
dict[entity][ComponentKey][target_entity] = component
|
|
@@ -177,7 +180,7 @@ class Registry:
|
|
|
177
180
|
global_: Entity | None = state.pop("global_", None) # Migrate from version <=1.2.0
|
|
178
181
|
|
|
179
182
|
# These attributes contain redundant data and will be removed
|
|
180
|
-
|
|
183
|
+
redundant_attributes: Final = frozenset(
|
|
181
184
|
{
|
|
182
185
|
"_components_by_entity", # <=3.4.0
|
|
183
186
|
"_tags_by_key", # <=3.4.0
|
|
@@ -185,7 +188,7 @@ class Registry:
|
|
|
185
188
|
"_names_by_entity", # <=3.4.0
|
|
186
189
|
}
|
|
187
190
|
)
|
|
188
|
-
for ignored in
|
|
191
|
+
for ignored in redundant_attributes:
|
|
189
192
|
state.pop(ignored, None)
|
|
190
193
|
|
|
191
194
|
converter = tcod.ecs._converter._get_converter()
|
|
@@ -306,7 +309,7 @@ class Registry:
|
|
|
306
309
|
return entity
|
|
307
310
|
|
|
308
311
|
@property
|
|
309
|
-
def Q(self) -> tcod.ecs.query.BoundQuery:
|
|
312
|
+
def Q(self) -> tcod.ecs.query.BoundQuery: # noqa: N802
|
|
310
313
|
"""Start a new Query for this registry.
|
|
311
314
|
|
|
312
315
|
Alias for ``tcod.ecs.Query(registry)``.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Common type-hints for tcod.ecs."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import sys
|
|
@@ -30,7 +31,7 @@ _RelationTargetLookup: TypeAlias = Union[Entity, EllipsisType]
|
|
|
30
31
|
_RelationQueryTarget: TypeAlias = Union[_RelationTargetLookup, BoundQuery]
|
|
31
32
|
"""Possible target for relation queries."""
|
|
32
33
|
|
|
33
|
-
_RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]]
|
|
34
|
+
_RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]] # noqa: PYI047
|
|
34
35
|
"""Query format for relations.
|
|
35
36
|
|
|
36
37
|
One of 4 formats:
|
|
File without changes
|
|
File without changes
|