]> Untitled Git - axy/ft/pacman.git/commitdiff
Possibly working hierarchy hooks
authorAxy <gilliardmarthey.axel@gmail.com>
Fri, 25 Sep 2026 12:12:19 +0000 (14:12 +0200)
committerAxy <gilliardmarthey.axel@gmail.com>
Fri, 25 Sep 2026 12:12:19 +0000 (14:12 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/hierarchy.py
src/pacman/ecs/world.py
src/pacman/utils/type_resolve.py

index 7b72260adc629c882bb04d6479f2e3e188593fe5..c9e9bce5791daa4aa574d1e9936ae31314325962 100644 (file)
@@ -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)
         ],
     )
index 30ca1fd403adf623051336f7892b16d1e807a06f..de8697a9fa77996d5cb6e0b1e190ad933f934436 100644 (file)
@@ -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()
index 59d552a3e5735f878863b5b227d774c6098d550b..044382133d66352c0f22de7d70eebc8b0ebd221c 100644 (file)
@@ -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":
index 7b9d8b303332dfc2e9b72e8732425205dca867fd..d49d848d8c62ba1ab5b03b9c7f80cb435ff5bcf0 100644 (file)
@@ -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 (