diff --git a/examples/health_plugin.py b/examples/health_plugin.py deleted file mode 100644 index a795d33..0000000 --- a/examples/health_plugin.py +++ /dev/null @@ -1,88 +0,0 @@ -from typing import final - -from pydantic import Field - -from snakia.core.ecs import Component -from snakia.core.ecs.system import System -from snakia.core.engine import Engine -from snakia.core.es import Event -from snakia.core.loader import Meta, Plugin, PluginProcessor -from snakia.types import Version - - -class HealthComponent(Component): - max_value: int = Field(default=100, ge=0) - value: int = Field(default=100, ge=0) - - -class DamageComponent(Component): - damage: int = Field(ge=0) - ticks: int = Field(default=1, ge=0) - - -class HealComponent(Component): - heal: int = Field(ge=0) - ticks: int = Field(default=1, ge=0) - - -class DeathEvent(Event): - entity: int = Field() - - -class HealthProcessor(PluginProcessor): - def process(self, system: System) -> None: - for entity, (heal, health) in system.get_components( - HealComponent, HealthComponent - ): - health.value += heal.heal - heal.ticks -= 1 - if heal.ticks <= 0: - system.remove_component(entity, HealComponent) - for entity, (damage, health) in system.get_components( - DamageComponent, HealthComponent - ): - health.value -= damage.damage - damage.ticks -= 1 - if damage.ticks <= 0: - system.remove_component(entity, DamageComponent) - if health.value <= 0: - system.remove_component(entity, HealthComponent) - self.plugin.dispatcher.publish(DeathEvent(entity=entity)) - - -@final -class HealthPlugin( - Plugin, - meta=Meta( - name="health", - author="snakia", - version=Version.from_args(1, 0, 0), - subscribers=(), - processors=(HealthProcessor,), - ), -): - def on_load(self) -> None: - pass - - def on_unload(self) -> None: - pass - - -def main() -> None: - engine = Engine() - engine.loader.register(HealthPlugin) - engine.loader.load_all() - - @engine.dispatcher.on(DeathEvent) - def on_death(event: DeathEvent) -> None: - print(f"Entity: {event.entity} is death!") - - player = engine.system.create_entity() - engine.system.add_component(player, HealthComponent()) - engine.system.add_component(player, DamageComponent(damage=10, ticks=10)) - - engine.start() - - -if __name__ == "__main__": - main() diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index d9fa955..9d48e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "types-networkx>=3.5.0.20251106", "typing-extensions>=4.15.0", ] -license = "CC0-1.0" +license = "Unlicense" license-files = ["LICENSE"] [project.urls] diff --git a/src/snakia/core/engine.py b/src/snakia/core/engine.py index 047c26e..a4f47b2 100644 --- a/src/snakia/core/engine.py +++ b/src/snakia/core/engine.py @@ -3,14 +3,12 @@ from typing import Final from .ecs import System from .es import Dispatcher -from .loader.loader import Loader class Engine: __slots__ = ( "system", "dispatcher", - "loader", "__system_thread", "__dispatcher_thread", ) @@ -18,12 +16,13 @@ class Engine: def __init__(self) -> None: self.system: Final = System() self.dispatcher: Final = Dispatcher() - self.loader: Final = Loader(self) self.__system_thread: threading.Thread | None = None self.__dispatcher_thread: threading.Thread | None = None def start(self) -> None: - self.__system_thread = threading.Thread(target=self.system.start, daemon=False) + self.__system_thread = threading.Thread( + target=self.system.start, daemon=False + ) self.__dispatcher_thread = threading.Thread( target=self.dispatcher.start, daemon=False ) diff --git a/src/snakia/core/loader/__init__.py b/src/snakia/core/loader/__init__.py deleted file mode 100644 index 9e099c5..0000000 --- a/src/snakia/core/loader/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .loadable import Loadable -from .meta import Meta -from .plugin import Plugin -from .plugin_processor import PluginProcessor - -__all__ = ["Loadable", "Meta", "Plugin", "PluginProcessor"] diff --git a/src/snakia/core/loader/loadable.py b/src/snakia/core/loader/loadable.py deleted file mode 100644 index 51e75d0..0000000 --- a/src/snakia/core/loader/loadable.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from snakia.core.engine import Engine - - -class Loadable(ABC): - @abstractmethod - def __init__(self, engine: Engine) -> None: ... - - @abstractmethod - def load(self) -> None: ... - - @abstractmethod - def unload(self) -> None: ... diff --git a/src/snakia/core/loader/loader.py b/src/snakia/core/loader/loader.py deleted file mode 100644 index 0dcf807..0000000 --- a/src/snakia/core/loader/loader.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Callable, Final - -if TYPE_CHECKING: - from snakia.core.engine import Engine - from snakia.core.loader import Loadable - - -class Loader: - __slots__ = ("__engine", "__loadables") - - def __init__(self, engine: Engine) -> None: - self.__engine: Final = engine - self.__loadables: Final[list[Loadable]] = [] - - def register(self, loadable: Callable[[Engine], Loadable]) -> None: - self.__loadables.append(loadable(self.__engine)) - - def load_all(self) -> None: - for loadable in self.__loadables: - loadable.load() - - def unload_all(self) -> None: - for loadable in reversed(self.__loadables): - loadable.unload() diff --git a/src/snakia/core/loader/meta.py b/src/snakia/core/loader/meta.py deleted file mode 100644 index 429d4b4..0000000 --- a/src/snakia/core/loader/meta.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from snakia.core.es import Event, Subscriber -from snakia.types import Version - -from .plugin_processor import PluginProcessor - - -class Meta(BaseModel): - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - - name: str = Field( - default="unknown", - min_length=4, - max_length=32, - pattern="^[a-z0-9_]{4,32}$", - ) - author: str = Field( - default="unknown", - min_length=4, - max_length=32, - pattern="^[a-z0-9_]{4,32}$", - ) - version: Version = Field(default_factory=lambda: Version(major=1, minor=0, patch=0)) - - subscribers: tuple[tuple[type[Event], Subscriber[Event]], ...] = Field( - default_factory=tuple - ) - processors: tuple[type[PluginProcessor], ...] = Field(default_factory=tuple) - - @property - def id(self) -> str: - return f"{self.author}.{self.name}" diff --git a/src/snakia/core/loader/plugin.py b/src/snakia/core/loader/plugin.py deleted file mode 100644 index f8abfa5..0000000 --- a/src/snakia/core/loader/plugin.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from typing import TYPE_CHECKING, ClassVar, Final, final - -from snakia.core.ecs import System -from snakia.core.es import Dispatcher - -from .loadable import Loadable -from .meta import Meta - -if TYPE_CHECKING: - from snakia.core.engine import Engine - - -class Plugin(Loadable): - __meta: ClassVar[Meta] - - @final - def __init__(self, engine: Engine) -> None: - self.__engine: Final = engine - - @final - @property - def meta(self) -> Meta: - """The plugin's metadata.""" - return self.__meta - - @final - @property - def dispatcher(self) -> Dispatcher: - return self.__engine.dispatcher - - @final - @property - def system(self) -> System: - return self.__engine.system - - @final - def load(self) -> None: - for processor in self.meta.processors: - self.__engine.system.add_processor(processor(self)) - for event_type, subscriber in self.meta.subscribers: - self.__engine.dispatcher.subscribe(event_type, subscriber) - self.on_load() - - @final - def unload(self) -> None: - for processor in self.meta.processors: - self.__engine.system.remove_processor(processor) - for event_type, subscriber in self.meta.subscribers: - self.__engine.dispatcher.unsubscribe(event_type, subscriber) - self.on_unload() - - @abstractmethod - def on_load(self) -> None: - pass - - @abstractmethod - def on_unload(self) -> None: - pass - - if TYPE_CHECKING: - - @final - def __init_subclass__(cls, meta: Meta) -> None: - pass - - else: - - def __init_subclass__(cls, meta: Meta) -> None: - cls.meta = meta diff --git a/src/snakia/core/loader/plugin_processor.py b/src/snakia/core/loader/plugin_processor.py deleted file mode 100644 index f959327..0000000 --- a/src/snakia/core/loader/plugin_processor.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Final, final - -from snakia.core.ecs import Processor - -if TYPE_CHECKING: - from .plugin import Plugin - - -class PluginProcessor(Processor): - @final - def __init__(self, plugin: Plugin) -> None: - self.plugin: Final = plugin diff --git a/src/snakia/core/rx/base_bindable.py b/src/snakia/core/rx/base_bindable.py index b229821..bd8c73b 100644 --- a/src/snakia/core/rx/base_bindable.py +++ b/src/snakia/core/rx/base_bindable.py @@ -29,8 +29,7 @@ class BaseBindable(Generic[T]): def value(self) -> T: if self.has_value: return self.__value - else: - return self.default_value + return self.default_value @property def has_value(self) -> bool: diff --git a/src/snakia/core/rx/combines.py b/src/snakia/core/rx/combines.py index 345d0a7..23c6675 100644 --- a/src/snakia/core/rx/combines.py +++ b/src/snakia/core/rx/combines.py @@ -95,7 +95,7 @@ def combine( ) def subscriber(_: ValueChanged[Any]) -> None: - combined.set(combiner(*[*map(lambda s: s.value, sources)])) + combined.set(combiner(*map(lambda s: s.value, sources))) for source in sources: if isinstance(source, Bindable): @@ -185,7 +185,7 @@ def async_combine( ) async def subscriber(_: ValueChanged[Any]) -> None: - result = await combiner(*[*map(lambda s: s.value, sources)]) + result = await combiner(*map(lambda s: s.value, sources)) await combined.set(result) for source in sources: diff --git a/src/snakia/field/__init__.py b/src/snakia/field/__init__.py index 654fdd9..e95ce46 100644 --- a/src/snakia/field/__init__.py +++ b/src/snakia/field/__init__.py @@ -3,6 +3,8 @@ from .bool import BoolField from .field import Field from .float import FloatField from .int import IntField +from .list import ListField +from .optional import OptionalField from .str import StrField __all__ = [ @@ -11,5 +13,7 @@ __all__ = [ "BoolField", "FloatField", "IntField", + "ListField", + "OptionalField", "StrField", ] diff --git a/src/snakia/field/auto.py b/src/snakia/field/auto.py index 1216a30..f456a1d 100644 --- a/src/snakia/field/auto.py +++ b/src/snakia/field/auto.py @@ -1,5 +1,9 @@ import pickle -from typing import Final, Generic, TypeVar +from typing import Callable, Final, Generic, TypeVar, overload + +from typing_extensions import Self + +from snakia.types import Unset from .field import Field @@ -9,15 +13,40 @@ T = TypeVar("T") class AutoField(Field[T], Generic[T]): __slots__ = ("__target_type",) - def __init__(self, default_value: T, *, target_type: type[T] | None = None) -> None: - super().__init__(default_value) - self.__target_type: Final = target_type + @overload + def __init__( + self, default_value: T, *, target_type: type[T] | Unset = Unset() + ) -> None: ... + + @overload + def __init__( + self, + *, + default_factory: Callable[[Self], T], + target_type: type[T] | Unset = Unset(), + ) -> None: ... + + def __init__( + self, + default_value: T | Unset = Unset(), + *, + default_factory: Callable[[Self], T] | Unset = Unset(), + target_type: type[T] | Unset = Unset(), + ) -> None: + if not Unset.itis(default_factory): + super().__init__(default_factory=Unset.unwrap(default_factory)) + elif not Unset.itis(default_value): + super().__init__(Unset.unwrap(default_value)) + else: + super().__init__() + self.__target_type: Final[type] = Unset.unwrap_or(target_type, object) def serialize(self, value: T, /) -> bytes: return pickle.dumps(value) def deserialize(self, serialized: bytes, /) -> T: value = pickle.loads(serialized) - if not isinstance(value, self.__target_type or object): - return self.default_value + + if not isinstance(value, self.__target_type): + return self._get_default() return value # type: ignore diff --git a/src/snakia/field/field.py b/src/snakia/field/field.py index 628b7c7..57e9de6 100644 --- a/src/snakia/field/field.py +++ b/src/snakia/field/field.py @@ -1,7 +1,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Final, Generic, TypeVar, final +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + TypeVar, + final, +) from snakia.property.priv_property import PrivProperty from snakia.utils import inherit @@ -11,10 +18,6 @@ R = TypeVar("R") class Field(ABC, PrivProperty[T], Generic[T]): - def __init__(self, default_value: T) -> None: - self.default_value: Final[T] = default_value - super().__init__(default_value) - @abstractmethod def serialize(self, value: T, /) -> bytes: """Serialize a value @@ -42,14 +45,18 @@ class Field(ABC, PrivProperty[T], Generic[T]): serialize: Callable[[Field[R], R], bytes], deserialize: Callable[[Field[R], bytes], R], ) -> type[Field[R]]: - return inherit(cls, {"serialize": serialize, "deserialize": deserialize}) + return inherit( + cls, {"serialize": serialize, "deserialize": deserialize} + ) @final @staticmethod def get_fields(class_: type[Any] | Any, /) -> dict[str, Field[Any]]: if not isinstance(class_, type): class_ = class_.__class__ - return {k: v for k, v in class_.__dict__.items() if isinstance(v, Field)} + return { + k: v for k, v in class_.__dict__.items() if isinstance(v, Field) + } if TYPE_CHECKING: diff --git a/src/snakia/field/list.py b/src/snakia/field/list.py new file mode 100644 index 0000000..f2671da --- /dev/null +++ b/src/snakia/field/list.py @@ -0,0 +1,38 @@ +from typing import Callable, Final, Iterable, TypeVar + +from typing_extensions import Self + +from .field import Field + +T = TypeVar("T") + + +class ListField(Field[list[T]]): + def __init__( + self, + field: Field[T], + *, + length_size: int = 1, + default_factory: Callable[[Self], Iterable[T]] = lambda _: (), + ) -> None: + self.length_size: Final[int] = length_size + self.field: Final = field + super().__init__(default_factory=lambda s: [*default_factory(s)]) + + def serialize(self, items: list[T], /) -> bytes: + result = b"" + for item in items: + value = self.field.serialize(item) + length_prefix = len(value).to_bytes(self.length_size, "big") + result += length_prefix + value + return result + + def deserialize(self, serialized: bytes, /) -> list[T]: + result = [] + while serialized: + length = int.from_bytes(serialized[: self.length_size], "big") + serialized = serialized[self.length_size :] + item = self.field.deserialize(serialized[:length]) + serialized = serialized[length:] + result.append(item) + return result diff --git a/src/snakia/field/optional.py b/src/snakia/field/optional.py new file mode 100644 index 0000000..eb437c0 --- /dev/null +++ b/src/snakia/field/optional.py @@ -0,0 +1,28 @@ +from typing import Final, TypeVar + +from .field import Field + +T = TypeVar("T") + + +class OptionalField(Field[T | None]): + + def __init__( + self, + field: Field[T], + *, + none_value: bytes = b"", + ) -> None: + super().__init__(None) + self.none_value: Final = none_value + self.field: Final = field + + def serialize(self, value: T | None, /) -> bytes: + if value is None: + return self.none_value + return self.field.serialize(value) + + def deserialize(self, serialized: bytes, /) -> T | None: + if serialized == self.none_value: + return None + return self.field.deserialize(serialized) diff --git a/src/snakia/field/t.py b/src/snakia/field/t.py index f535738..abb404b 100644 --- a/src/snakia/field/t.py +++ b/src/snakia/field/t.py @@ -4,6 +4,8 @@ from .bool import BoolField as bool from .field import Field as field from .float import FloatField as float from .int import IntField as int +from .list import ListField as list +from .optional import OptionalField as optional from .str import StrField as str __all__ = [ @@ -12,5 +14,7 @@ __all__ = [ "field", "float", "int", + "list", + "optional", "str", ] diff --git a/src/snakia/property/priv_property.py b/src/snakia/property/priv_property.py index 1a85e09..fba63da 100644 --- a/src/snakia/property/priv_property.py +++ b/src/snakia/property/priv_property.py @@ -1,22 +1,44 @@ -from typing import Any, Generic, TypeVar +from typing import Any, Callable, Final, Generic, TypeVar, overload + +from typing_extensions import Self + +from snakia.types import Unset T = TypeVar("T") class PrivProperty(Generic[T]): - __slots__ = "__name", "__default_value" + __slots__ = "__name", "__default_value", "__default_factory" __name: str - def __init__(self, default_value: T | None = None) -> None: - self.__default_value: T | None = default_value + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, default_value: T) -> None: ... + @overload + def __init__(self, *, default_factory: Callable[[Self], T]) -> None: ... + def __init__( + self, + default_value: T | Unset = Unset(), + default_factory: Callable[[Self], T] | Unset = Unset(), + ) -> None: + self.__default_value: Final[T | Unset] = default_value + self.__default_factory: Final[Callable[[Self], T] | Unset] = default_factory + + def _get_default(self: Self) -> T: + return Unset.map( + self.__default_factory, + lambda f: f(self), + lambda _: Unset.unwrap(self.__default_value), + ) def __set_name__(self, owner: type, name: str) -> None: self.__name = f"_{owner.__name__}__{name}" def __get__(self, instance: Any, owner: type | None = None, /) -> T: - if self.__default_value: - return getattr(instance, self.__name, self.__default_value) + if not hasattr(instance, self.__name): + setattr(instance, self.__name, self._get_default()) return getattr(instance, self.__name) # type: ignore def __set__(self, instance: Any, value: T, /) -> None: diff --git a/src/snakia/types/marker.py b/src/snakia/types/marker.py index 002e2ec..3024fb9 100644 --- a/src/snakia/types/marker.py +++ b/src/snakia/types/marker.py @@ -12,7 +12,7 @@ MARKERS_ATTR = "__snakia_markers__" def _get_all_markers(obj: Any) -> dict[type["Marker"], "Marker"]: - return get_or_set_attr(obj, MARKERS_ATTR, dict[type[Marker], Marker]()) + return get_or_set_attr(obj, MARKERS_ATTR, dict()) class Marker: diff --git a/src/snakia/types/unique.py b/src/snakia/types/unique.py index 3c149fd..2f66e0f 100644 --- a/src/snakia/types/unique.py +++ b/src/snakia/types/unique.py @@ -49,24 +49,29 @@ class UniqueType(type): raise TypeError(f"{cls} not unwrapped") return value # type: ignore + def unwrap_or(cls: type[T], value: V | type[T] | T, default: R, /) -> V | R: + if value is cls or isinstance(value, cls): + return default + return value # type: ignore + def map( cls: type[T], value: V | type[T] | T, - and_then: Callable[[V], R], - or_else: Callable[[type[T]], R], + or_else: Callable[[V], R], + and_then: Callable[[type[T]], R], ) -> R: if value is cls or isinstance(value, cls): - return or_else(cls) - return and_then(value) # type: ignore + return and_then(cls) + return or_else(value) # type: ignore - def and_then( + def or_else( cls: type[T], value: V | type[T] | T, func: Callable[[V], R] ) -> type[T] | R: if value is cls or isinstance(value, cls): return cls return func(value) # type: ignore - def or_else( + def and_then( cls: type[T], value: V | type[T] | T, func: Callable[[type[T]], R] ) -> R | V: if value is cls or isinstance(value, cls): diff --git a/src/snakia/utils/__init__.py b/src/snakia/utils/__init__.py index 353803d..ad6100d 100644 --- a/src/snakia/utils/__init__.py +++ b/src/snakia/utils/__init__.py @@ -1,10 +1,9 @@ from .attrs import get_attrs, get_or_set_attr -from .calls import call, caller from .exceptions import catch, throw from .frames import frame +from .funcs import call, caller, ret, side, side_func from .gil import GIL_ENABLED, nolock from .inherit import inherit -from .side import side, side_func from .this import this from .to_async import to_async @@ -17,6 +16,7 @@ __all__ = [ "frame", "inherit", "nolock", + "ret", "side", "side_func", "this", diff --git a/src/snakia/utils/calls.py b/src/snakia/utils/calls.py deleted file mode 100644 index eeabd5f..0000000 --- a/src/snakia/utils/calls.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Callable, ParamSpec, TypeVar - -P = ParamSpec("P") -T = TypeVar("T") - - -def call(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: - return f(*args, **kwargs) - - -def caller(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[..., T]: - return lambda *_, **__: f(*args, **kwargs) diff --git a/src/snakia/utils/funcs.py b/src/snakia/utils/funcs.py new file mode 100644 index 0000000..94cdad4 --- /dev/null +++ b/src/snakia/utils/funcs.py @@ -0,0 +1,24 @@ +from typing import Any, Callable, ParamSpec, TypeVar + +P = ParamSpec("P") +T = TypeVar("T") + + +def call(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + return f(*args, **kwargs) + + +def caller(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[..., T]: + return lambda *_, **__: f(*args, **kwargs) + + +def side(value: T, *_: Any, **__: Any) -> T: + return value + + +def side_func(value: T, *_: Any, **__: Any) -> Callable[..., T]: + return lambda *_, **__: value + + +def ret() -> Callable[[T], T]: + return lambda x: x diff --git a/src/snakia/utils/side.py b/src/snakia/utils/side.py deleted file mode 100644 index 985e919..0000000 --- a/src/snakia/utils/side.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any, Callable, TypeVar - -T = TypeVar("T") - - -def side(value: T, *_: Any, **__: Any) -> T: - return value - - -def side_func(value: T, *_: Any, **__: Any) -> Callable[..., T]: - return lambda *_, **__: value