From fb34db6cc8132178c7e4aec0d8c356cdd81fed17 Mon Sep 17 00:00:00 2001 From: Axy Date: Fri, 25 Sep 2026 14:12:19 +0200 Subject: [PATCH] Possibly working hierarchy hooks --- src/pacman/ecs/__init__.py | 17 ++- src/pacman/ecs/hierarchy.py | 214 +++++++++++++++---------------- src/pacman/ecs/world.py | 56 +++++++- src/pacman/utils/type_resolve.py | 2 +- 4 files changed, 162 insertions(+), 127 deletions(-) diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py index 7b72260..c9e9bce 100644 --- a/src/pacman/ecs/__init__.py +++ b/src/pacman/ecs/__init__.py @@ -6,9 +6,8 @@ import pygame from pygame import Vector3 from pacman.ecs.hierarchy import ( - Parent, + ChildOf, hierachy_plugins, - traverse_hierarchy, ) from pacman.ecs.schedule import ( MainSchedule, @@ -71,7 +70,7 @@ def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None: abs_coord._vec = coord.xyz -def update_coord_hierarchy(world: World) -> None: +def update_coord_hierarchy() -> None: return for parent, child in traverse_hierarchy(world, tuple[CoordAbs2D]): if parent is None: @@ -94,9 +93,7 @@ def coord_update_plugins(world: World) -> None: def minimal_plugins(world: World) -> None: world.with_plugins( - schedule_plugins, - coord_update_plugins, - # schedule_plugins, hierachy_plugins, coord_update_plugins + schedule_plugins, hierachy_plugins, coord_update_plugins ) @@ -189,6 +186,7 @@ def move_root(root: Query[Coord2D, WiggleRoot]) -> None: def sprite_despawn_hook() -> RawComponentHook[WiggleSprite]: def inner(arg: RawComponentArg[WiggleSprite]) -> None: for ent, _ in arg: + continue print("despawn", ent) return inner @@ -197,6 +195,7 @@ def sprite_despawn_hook() -> RawComponentHook[WiggleSprite]: def sprite_spawn_hook() -> RawComponentHook[WiggleSprite]: def inner(arg: RawComponentArg[WiggleSprite]) -> None: for ent, _ in arg: + continue print("spawn", ent) return inner @@ -244,7 +243,7 @@ if __name__ == "__main__": Sprite2D, ShouldRender, WiggleSprite, - Parent, + ChildOf, ], [ ( @@ -257,9 +256,9 @@ if __name__ == "__main__": Sprite2D(pygame.image.load(asset)), ShouldRender(), WiggleSprite(), - Parent(root), + ChildOf(root), ) - for j in range(15) + for j in range(3) for i, asset in enumerate(assets) ], ) diff --git a/src/pacman/ecs/hierarchy.py b/src/pacman/ecs/hierarchy.py index 30ca1fd..de8697a 100644 --- a/src/pacman/ecs/hierarchy.py +++ b/src/pacman/ecs/hierarchy.py @@ -1,7 +1,18 @@ from collections.abc import Callable, Generator, Iterable -from typing import Self, cast, get_args, override - -from pacman.ecs.world import Component, Entity, FromWorld, World +from typing import get_args, override + +from pacman.ecs.world import ( + CommandQueue, + Component, + Entity, + FromWorld, + Not, + Query, + RawComponentArg, + RawComponentHook, + World, +) +from pacman.utils.type_resolve import resolve_type_aliases class Children(Component): @@ -13,60 +24,7 @@ class Children(Component): yield from self._children -__no_hook = False - - -class Hierarchy(FromWorld): - @override - @classmethod - def extract_from_world( - cls, ty: type, world: "World" - ) -> Callable[[], Self | None]: - pass - - def cleanup(self) -> None: - pass - - -def get_parents(world: World, entity: Entity) -> Generator[Entity]: - """Generate parents of entity including self""" - yield entity - while Parent in world[entity]: - nxt: Entity = world[entity][Parent]._parent - entity = nxt - yield entity - - -def children_insert_hook( - world: World, entity: Entity, component: Children -) -> None: - global __no_hook - if __no_hook: - return - invalid_children = set(get_parents(world, entity)) - if any(child in invalid_children for child in component.children): - raise Exception("Cyclic child hierarchy") - for child in map(lambda e: world[e], component.children): - if Parent in child: - del child[Parent] - __no_hook = True - child[Parent] = Parent(entity) - __no_hook = False - - -def children_remove_hook( - world: World, _entity: Entity, component: Children -) -> None: - global __no_hook - if __no_hook: - return - __no_hook = True - for child in component.children: - del world[child][Parent] - __no_hook = False - - -class Parent(Component): +class ChildOf(Component): def __init__(self, parent: Entity) -> None: self._parent = parent @@ -75,65 +33,99 @@ class Parent(Component): return self._parent -def parent_insert_hook( - world: World, entity: Entity, component: Parent -) -> None: - global __no_hook - if __no_hook: - return - parent = world[component.parent] - if Children not in parent: - __no_hook = True - parent[Children] = Children((entity,)) - __no_hook = False - else: - parent[Children]._children.add(entity) +def parent_insert_hook(queue: CommandQueue) -> RawComponentHook[Children]: + def inner(itr: RawComponentArg[Children]) -> None: + inserts = [ + (child, ChildOf(ent)) + for ent, inserted in itr + for child in inserted.children + ] + queue.queue(lambda world: world.insert_many(ChildOf, inserts)) + + return inner + + +def parent_remove_hook(queue: CommandQueue) -> RawComponentHook[Children]: + def inner(itr: RawComponentArg[Children]) -> None: + removes = [e for _, children in itr for e in children.children] + queue.queue(lambda world: world.remove_many(Children, removes)) + + return inner + + +def child_insert_hook( + queue: CommandQueue, + parents: Query[Children], +) -> RawComponentHook[ChildOf]: + def inner(itr: RawComponentArg[ChildOf]) -> None: + inserts = {} + for ent, inserted in itr: + parent = parents.get(inserted.parent) + if parent is None: + if inserted.parent not in inserts: + inserts[inserted.parent] = Children([]) + inserts[inserted.parent]._children.add(ent) + else: + parent._children.add(ent) + queue.queue(lambda world: world.insert_many(Children, inserts.items())) -def parent_remove_hook( - world: World, entity: Entity, component: Parent -) -> None: - if __no_hook: - return - world[component.parent][Children]._children.remove(entity) + return inner + + +def child_remove_hook(parents: Query[Children]) -> RawComponentHook[ChildOf]: + def inner(itr: RawComponentArg[ChildOf]) -> None: + for ent, removed in itr: + parent = parents.get(removed.parent) + if parent is not None: + parent._children.remove(ent) + + return inner def hierachy_plugins(world: World) -> None: - world.with_hook("insert", Children, children_insert_hook).with_hook( - "remove", Children, children_remove_hook - ).with_hook("insert", Parent, parent_insert_hook).with_hook( - "remove", Parent, parent_remove_hook + world.with_hook("insert", Children, parent_insert_hook).with_hook( + "remove", Children, parent_remove_hook + ).with_hook("insert", ChildOf, child_insert_hook).with_hook( + "remove", ChildOf, child_remove_hook ) -def traverse_hierarchy[*T]( - world: World, query: type[tuple[*T]] -) -> Generator[tuple[tuple[*T] | None, tuple[*T]]]: - params = sorted( - get_args(query), key=lambda e: len(world._components.get(e, {})) - ) - work: list[Entity] = [] - for e in world.query(query, without=tuple[Parent, Children]): - yield (None, e) - for entity, children in world.query( - tuple[Entity, Children], without=tuple[Parent] - ): - work.extend(children.children) - ent = world[entity] - if all(param in ent for param in params): - yield (None, cast(tuple[*T], (*(ent[param] for param in params),))) - while work: - curr = work.pop() - child = world[curr] - parent = world[world[curr][Parent].parent] - if all(param in child for param in params): - parent_res = None - if all(param in parent for param in params): - parent_res = cast( - tuple[*T], (*(parent[param] for param in params),) - ) - child_res = cast(tuple[*T], (*(child[param] for param in params),)) - yield (parent_res, child_res) - - if Children in child: - work.extend(child[Children].children) +type RootsQuery[Items, Filters] = Query[ + tuple[Items, Children], tuple[Filters, Not[ChildOf]] +] +type NodesQuery[Items, Filters] = Query[ + tuple[Items, Children | None, ChildOf], Filters +] + + +class Hierarchy[Items, Filters = tuple[()]](FromWorld): + def __init__( + self, + roots: RootsQuery[Items, Filters], + nodes: NodesQuery[Items, Filters], + ) -> None: + self._roots = roots + self._nodes = nodes + + @override + @classmethod + def extract_from_world( + cls, ty: type, world: "World" + ) -> Callable[[], "Hierarchy[Items, Filters]"]: + items, filters = get_args(ty) + roots_ty: type[RootsQuery[Items, Filters]] = resolve_type_aliases( + RootsQuery[items, filters] + ) + nodes_ty: type[NodesQuery[Items, Filters]] = resolve_type_aliases( + NodesQuery[items, filters] + ) + roots = roots_ty.extract_from_world(roots_ty, world) + nodes = nodes_ty.extract_from_world(nodes_ty, world) + + return lambda: Hierarchy(roots(), nodes()) + + @override + def cleanup(self) -> None: + self._roots.cleanup() + self._nodes.cleanup() diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index 59d552a..0443821 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -285,7 +285,7 @@ class QueryItem(QueryFilter): return lambda archetype: ( False if (filter_fetch := preprocessed(archetype)) is None - else lambda i: filter_fetch(i) is not None + else lambda i: filter_fetch(i) is not _NoFetch ) @classmethod @@ -341,7 +341,23 @@ class Entity(QueryItem): return lambda archetype: lambda i: archetype._entities[i] -class Not[T: FromWorld | Any](FromWorld, QueryFilter): +class Not[T: FromWorld | Any](FromWorld, QueryItem): + @override + @classmethod + def filter_fetch( + cls, ty: type + ) -> Callable[[Archetype], None | Callable[[int], Self | _NoFetchCls]]: + preprocessed = cls.filter(ty) + return lambda archetype: ( + (lambda _: Not()) + if (filt := preprocessed(archetype)) == False + else ( + None + if filt == True + else (lambda i: _NoFetch if filt(i) else Not()) + ) + ) + @override @classmethod def filter( @@ -573,7 +589,35 @@ class Query[Items, Filters = tuple[()]]( if (arch_idx := self._archetypes_idx.get(arch)) is None: return False components_idx = arch._positions[entity] - return self._filters[arch_idx](components_idx) + if not self._filters[arch_idx](components_idx): + return False + return self._fetchers[arch_idx](components_idx) is not _NoFetch + + def get(self, entity: Entity) -> Items | None: + arch: Archetype | None = self._world._entities.get(entity) + if arch is None: + return None + if (arch_idx := self._archetypes_idx.get(arch)) is None: + return None + components_idx = arch._positions[entity] + if not self._filters[arch_idx](components_idx): + return None + if (res := self._fetchers[arch_idx](components_idx)) is _NoFetch: + return None + return cast(Items, res) + + def __getitem__(self, entity: Entity) -> Items: + arch: Archetype | None = self._world._entities.get(entity) + if arch is None: + raise KeyError("Entity not in world", entity) + if (arch_idx := self._archetypes_idx.get(arch)) is None: + raise KeyError("Entity not in query", entity) + components_idx = arch._positions[entity] + if not self._filters[arch_idx](components_idx): + raise KeyError("Entity does not pass filters", entity) + if (res := self._fetchers[arch_idx](components_idx)) is _NoFetch: + raise KeyError("Entity does not pass fetchers", entity) + return cast(Items, res) def single(self) -> Items | None: itr: Iterator[Items] = self.__iter__() @@ -833,12 +877,12 @@ class World(FromWorld): ) -> "World": comp_id = self.component_id(resolve_type_aliases(ty)) hooks = self._components_cache[comp_id][1] - sys = self.add_raw_system(hook) + sys = self.add_system(hook, default=lambda _: None) match cycle: case "insert": - hooks.add_insert(lambda e: sys()(e)) + hooks.add_insert(lambda e: sys(e)) case "remove": - hooks.add_remove(lambda e: sys()(e)) + hooks.add_remove(lambda e: sys(e)) return self def with_plugins(self, *plugins: Plugin) -> "World": diff --git a/src/pacman/utils/type_resolve.py b/src/pacman/utils/type_resolve.py index 7b9d8b3..d49d848 100644 --- a/src/pacman/utils/type_resolve.py +++ b/src/pacman/utils/type_resolve.py @@ -24,7 +24,7 @@ def map_type_generics(ty: Any, mapping: dict[Any, type]) -> Any: return orig[args] -def resolve_type_aliases(ty: Any) -> Any: +def resolve_type_aliases[T](ty: Any) -> Any: """Resolve any type aliases for a given type, recursively.""" while hasattr(ty, "__value__"): if not ( -- 2.53.0