]> Untitled Git - axy/ft/pacman.git/commitdiff
Partial hook impl
author= <=>
Wed, 23 Sep 2026 13:13:12 +0000 (15:13 +0200)
committer= <=>
Wed, 23 Sep 2026 13:13:12 +0000 (15:13 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/world.py
src/pacman/utils/variadics_please.py

index 67c5ba7448ce19a2eb1050bf32f118bb239264b6..522a3afe90a75562b643c8822bbb7ad60ad252dd 100644 (file)
@@ -81,11 +81,14 @@ def coord_update_plugins(world: World) -> None:
         .chain()
         .in_set(PostUpdate, PropagateTransform),
     )
+    world.with_systems(UpdateSchedule, update_coord)
 
 
 def minimal_plugins(world: World) -> None:
     world.with_plugins(
-        schedule_plugins, hierachy_plugins, coord_update_plugins
+        schedule_plugins,
+        coord_update_plugins,
+        # schedule_plugins, hierachy_plugins, coord_update_plugins
     )
 
 
@@ -134,9 +137,7 @@ def render_sprite2d(
         reverse=True,
     )
     sprites = [
-        (sprite.surface, coord._vec.xy)
-        for coord, sprite in sprites_raw
-        if sprite is not None
+        (sprite.surface, coord._vec.xy) for coord, sprite in sprites_raw
     ]
     window.surface.fill((0, 0, 0))
     window.surface.blits(sprites)
index 9327c9cd0e0c67b6913f315223607cd1691abcf9..1c787b4f6418795e9455a326091311821a81bc40 100644 (file)
@@ -1,9 +1,11 @@
 import inspect
 from abc import ABC, abstractmethod
 from collections.abc import Callable, Container, Iterable, Iterator
+from dataclasses import dataclass
 from typing import (
     Any,
     Literal,
+    NewType,
     Self,
     Union,
     cast,
@@ -21,6 +23,7 @@ from pacman.utils.panic import panic
 from pacman.utils.type_resolve import origin_or_cls, resolve_type_aliases
 from pacman.utils.variadics_please import (
     RecursiveTuple,
+    composed_by,
     fast_tuple_constructor,
 )
 
@@ -60,22 +63,23 @@ def components_type_iter(
         yield from cast(type[Bundle], components).components_type_iter()
 
 
+ComponentID = NewType("ComponentID", int)
+
+
 class Archetype:
-    def __init__(
-        self, world: "World", types: Iterable[type["Component"]]
-    ) -> None:
+    def __init__(self, world: "World", types: Iterable[ComponentID]) -> None:
         self._world = world
         self._types = frozenset(types)
         self._positions: dict[Entity, int] = {}
         self._entities: list[Entity] = []
-        self._components: dict[type[Component], list[Any]] = {
+        self._components: dict[ComponentID, list[Any]] = {
             ty: [] for ty in types
         }
         self._add_cache: dict[
-            int, tuple[Any, Archetype, list[type[Component]]]
+            int, tuple[Any, Archetype, list[ComponentID]]
         ] = {}
         self._remove_cache: dict[
-            int, tuple[Any, Archetype, list[type[Component]]]
+            int, tuple[Any, Archetype, list[ComponentID]]
         ] = {}
 
         self._world._archetypes.append(self)
@@ -83,11 +87,16 @@ class Archetype:
 
     def _compute_add(
         self, ty: type[Components]
-    ) -> tuple["Archetype", list[type["Component"]]]:
+    ) -> tuple["Archetype", list[ComponentID]]:
         ptr = id(ty)
         if res := self._add_cache.get(ptr):
             return (res[1], res[2])
-        comps = list(components_type_iter(resolve_type_aliases(ty)))
+        comps = list(
+            map(
+                self._world.component_id,
+                components_type_iter(resolve_type_aliases(ty)),
+            )
+        )
         res_arch: Archetype
         types = frozenset(comps) | self._types
         if prev := self._world._archetypes_lookup.get(types):
@@ -99,11 +108,16 @@ class Archetype:
 
     def _compute_remove(
         self, ty: type[Components]
-    ) -> tuple["Archetype", list[type["Component"]]]:
+    ) -> tuple["Archetype", list[ComponentID]]:
         ptr = id(ty)
         if res := self._remove_cache.get(ptr):
             return (res[1], res[2])
-        comps = list(components_type_iter(resolve_type_aliases(ty)))
+        comps = list(
+            map(
+                self._world.component_id,
+                components_type_iter(resolve_type_aliases(ty)),
+            )
+        )
         res_arch: Archetype
         types = self._types - frozenset(comps)
         if prev := self._world._archetypes_lookup.get(types):
@@ -123,8 +137,8 @@ class Archetype:
         for k in comps_ty:
             kv[k] = [next(e) for e in comps]
 
-        def cb(ty: type[Entity] | type[Component]) -> Iterable[Any]:
-            if issubclass(ty, Entity):
+        def cb(ty: ComponentID | None) -> Iterable[Any]:
+            if ty is None:
                 return (e for e, _ in components)
             if ty in kv:
                 return kv[ty]
@@ -139,8 +153,8 @@ class Archetype:
         dst, _ = self._compute_remove(ty)
         positions = [self._positions[e] for e in entities]
 
-        def cb(ty: type[Entity] | type[Component]) -> Iterable[Any]:
-            if issubclass(ty, Entity):
+        def cb(ty: ComponentID | None) -> Iterable[Any]:
+            if ty is None:
                 return entities
             return map(self._components[ty].__getitem__, positions)
 
@@ -152,9 +166,9 @@ class Archetype:
 
     def _extend(
         self,
-        cb: Callable[[type["Component"] | type["Entity"]], Iterable[Any]],
+        cb: Callable[[ComponentID | None], Iterable[Any]],
     ) -> None:
-        for entity in cb(Entity):
+        for entity in cb(None):
             self._positions[entity] = len(self._entities)
             self._entities.append(entity)
         for ty, comp in self._components.items():
@@ -191,7 +205,8 @@ type System[T] = Callable[..., T]
 
 type Plugin = RecursiveTuple[Callable[["World"], None]]
 
-type ComponentHook[T] = Callable[["World", "Entity", T], None]
+type RawComponentHook[T] = Callable[[list[tuple["Entity", T]]], None]
+type ComponentHook[T] = System[RawComponentHook[T]]
 
 
 class FromWorld(ABC):
@@ -236,6 +251,10 @@ class QueryItem(QueryFilter):
             else lambda i: filter_fetch(i) is not None
         )
 
+    @classmethod
+    def fetch_prio(cls, ty: type) -> int:
+        return 0
+
     @classmethod
     @abstractmethod
     def filter_fetch(
@@ -250,7 +269,9 @@ class Component(QueryItem):
     @override
     @classmethod
     def filter(cls, ty: type) -> Callable[[Archetype], bool]:
-        return lambda archetype: ty in archetype._components
+        return lambda archetype: (
+            archetype._world.component_id(ty) in archetype._components
+        )
 
     @override
     @classmethod
@@ -259,7 +280,12 @@ class Component(QueryItem):
     ) -> Callable[[Archetype], None | Callable[[int], Self | _NoFetchCls]]:
         return lambda archetype: (
             None
-            if (idx := archetype._components.get(ty)) is None
+            if (
+                idx := archetype._components.get(
+                    archetype._world.component_id(ty)
+                )
+            )
+            is None
             else cast(Callable[[int], Self], lambda i: idx[i])
         )
 
@@ -343,7 +369,7 @@ class Query[Items, Filters = tuple[()]](
         self._world: World = world
         self._fetchers_raw = cast(
             Callable[[Archetype], Callable[[int], Items | _NoFetchCls] | None],
-            Query.preprocess_fetchers(resolved_fetchers),
+            Query.preprocess_fetchers(resolved_fetchers)[1],
         )
         self._filters_raw = Query.preprocess_filters(resolved_filters)
         self._seen_archetypes: int = 0
@@ -356,8 +382,11 @@ class Query[Items, Filters = tuple[()]](
     @staticmethod
     def preprocess_fetchers(
         fetcher: type[QueryItems],
-    ) -> Callable[
-        [Archetype], Callable[[int], QueryItems | _NoFetchCls] | None
+    ) -> tuple[
+        int,
+        Callable[
+            [Archetype], Callable[[int], QueryItems | _NoFetchCls] | None
+        ],
     ]:
         args = list(map(Query.preprocess_fetchers, get_args(fetcher)))
 
@@ -365,7 +394,7 @@ class Query[Items, Filters = tuple[()]](
             archetype: Archetype,
         ) -> Callable[[int], QueryItems] | None:
             lst = []
-            for arg in args:
+            for _, arg in args:
                 curr = arg(archetype)
                 if curr is None:
                     return None
@@ -384,7 +413,7 @@ class Query[Items, Filters = tuple[()]](
             archetype: Archetype,
         ) -> Callable[[int], QueryItems | _NoFetchCls] | None:
             lst: list[Callable[[int], QueryItems | _NoFetchCls]] = []
-            for arg in args:
+            for _, arg in args:
                 curr = arg(archetype)
                 if curr is None:
                     continue
@@ -397,13 +426,18 @@ class Query[Items, Filters = tuple[()]](
 
         orig = origin_or_cls(fetcher)
         if orig is tuple:
-            return tuple_constructor
+            return (0, tuple_constructor)
         if orig is Union:
-            return union_constructor
+            args.sort(key=lambda e: e[0], reverse=True)
+            return (0, union_constructor)
         if orig is None or orig is type(None):
-            return lambda _: None
+            return (-1, lambda _: lambda _: None)
 
-        return cast(QueryItem, fetcher).filter_fetch(fetcher)
+        fetcher_cls = cast(QueryItem, fetcher)
+        return (
+            fetcher_cls.fetch_prio(fetcher),
+            fetcher_cls.filter_fetch(fetcher),
+        )
 
     @staticmethod
     def preprocess_filters(
@@ -411,15 +445,6 @@ class Query[Items, Filters = tuple[()]](
     ) -> Callable[[Archetype], Callable[[int], bool] | bool]:
         args = list(map(Query.preprocess_filters, get_args(filter)))
 
-        def composed_by(
-            a: Callable[[int], bool] | None,
-            b: Callable[[int], bool],
-            op: Callable[[bool, bool], bool],
-        ) -> Callable[[int], bool]:
-            if a is None:
-                return b
-            return lambda i: op(a(i), b(i))
-
         def tuple_constructor(
             archetype: Archetype,
         ) -> Callable[[int], bool] | bool:
@@ -557,20 +582,37 @@ def _system_failed[T]() -> T:
     raise ValueError("Failed to fetch arguments for system")
 
 
+@dataclass
+class ComponentHooks[T: Component]:
+    _insert: RawComponentHook[T] = lambda _: None
+    _remove: RawComponentHook[T] = lambda _: None
+
+    def add_insert(self, cb: RawComponentHook[T]) -> None:
+        self._insert = composed_by(self._insert, cb, lambda _1, _2: None)
+
+    def add_remove(self, cb: RawComponentHook[T]) -> None:
+        self._remove = composed_by(self._remove, cb, lambda _1, _2: None)
+
+
 class World(FromWorld):
     def __init__(self) -> None:
         self._entities: dict[Entity, Archetype] = {}
-        self._archetypes_lookup: dict[
-            frozenset[type[Component]], Archetype
-        ] = {}
+        self._archetypes_lookup: dict[frozenset[ComponentID], Archetype] = {}
         self._archetypes: list[Archetype] = []
+        self._components_cache: list[
+            tuple[type, ComponentHooks[Component]]
+        ] = []
+        self._components_lookup: dict[type, ComponentID] = {}
         self._schedules: dict[ScheduleLabel, Schedule[RawSystem]] = {}
-        self._insert_hooks: dict[type, ComponentHook[Any]] = {}
-        self._remove_hooks: dict[type, ComponentHook[Any]] = {}
         self._base_archetype: Archetype = Archetype(self, ())
 
-    def _run_cond(self, _: RawSystem | SystemSet) -> bool:
-        return True
+    def component_id(self, comp: type) -> ComponentID:
+        if (res := self._components_lookup.get(comp)) is None:
+            print(f"added comp {comp}")
+            res = ComponentID(len(self._components_cache))
+            self._components_cache.append((comp, ComponentHooks()))
+            self._components_lookup[comp] = res
+        return res
 
     def spawn_empty(self) -> Entity:
         return self.spawn_many_empty(1)[0]
@@ -580,7 +622,7 @@ class World(FromWorld):
         self._base_archetype._extend(
             lambda ty: (
                 res
-                if ty is Entity
+                if ty is None
                 else panic(Exception("Should be unreachable"))
             )
         )
@@ -644,7 +686,7 @@ class World(FromWorld):
     def tick(self, schedule: ScheduleLabel) -> None:
         if schedule not in self._schedules:
             return
-        for system in self._schedules[schedule].traverse(self._run_cond):
+        for system in self._schedules[schedule].traverse(lambda _: True):
             system()
 
     def run(
@@ -668,28 +710,20 @@ class World(FromWorld):
         Systems(*systems).in_set(*sets)._apply(self, self._schedules[schedule])
         return self
 
-    def with_hook[T](
+    def with_hook[T: Component](
         self,
         cycle: Literal["insert", "remove"],
         ty: type[T],
         hook: ComponentHook[T],
     ) -> "World":
+        comp_id = self.component_id(resolve_type_aliases(ty))
+        hooks = self._components_cache[comp_id][1]
+        sys = self.add_system_raw(hook)
         match cycle:
             case "insert":
-                d = self._insert_hooks
+                hooks.add_insert(lambda e: sys()(e))
             case "remove":
-                d = self._remove_hooks
-        if ty not in d:
-            d[ty] = hook
-        else:
-            old = d[ty]
-
-            def new(world: World, entity: Entity, component: T) -> None:
-                old(world, entity, component)
-                hook(world, entity, component)
-                pass
-
-            d[ty] = new
+                hooks.add_remove(lambda e: sys()(e))
         return self
 
     def with_plugins(self, *plugins: Plugin) -> "World":
index 93ecbf109ccc0ff4baa1729c51823090c3b47fec..1ad884f036f0e29a16312a40f5ed5ac8625381f8 100644 (file)
@@ -216,3 +216,13 @@ def fast_tuple_constructor[T, U](
             )
         case e:
             return lambda arg: tuple(cb(arg) for cb in e)
+
+
+def composed_by[T, U](
+    a: Callable[[T], U] | None,
+    b: Callable[[T], U],
+    op: Callable[[U, U], U],
+) -> Callable[[T], U]:
+    if a is None:
+        return b
+    return lambda i: op(a(i), b(i))