From 9e26ebad6063bed31ae6fb3fe69aa9a2d5386b87 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 23 Sep 2026 15:13:12 +0200 Subject: [PATCH] Partial hook impl --- src/pacman/ecs/__init__.py | 9 +- src/pacman/ecs/world.py | 154 ++++++++++++++++----------- src/pacman/utils/variadics_please.py | 10 ++ 3 files changed, 109 insertions(+), 64 deletions(-) diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py index 67c5ba7..522a3af 100644 --- a/src/pacman/ecs/__init__.py +++ b/src/pacman/ecs/__init__.py @@ -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) diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index 9327c9c..1c787b4 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -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": diff --git a/src/pacman/utils/variadics_please.py b/src/pacman/utils/variadics_please.py index 93ecbf1..1ad884f 100644 --- a/src/pacman/utils/variadics_please.py +++ b/src/pacman/utils/variadics_please.py @@ -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)) -- 2.53.0