import asyncio from abc import abstractmethod from enum import Enum from typing import List, Optional, Dict from kennel.app import logger from kennel.engine.entities.entity import Entity, EntityManager from .system import System, SystemType class EventType(str, Enum): INITIAL_STATE = "INITIAL_STATE" SET_CONTROLLABLE = "SET_CONTROLLABLE" ENTITY_BORN = "ENTITY_BORN" ENTITY_DEATH = "ENTITY_DEATH" ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" class Event: def __init__(self, event_type: EventType, data: dict): self.event_type = event_type self.data = data def __str__(self) -> str: return f"Event({self.event_type}, {self.data})" def to_dict(self) -> dict: return {"event_type": self.event_type, "data": self.data} @staticmethod def from_dict(data: dict) -> Optional["Event"]: if "event_type" not in data or "data" not in data: return event_type = data["event_type"] if event_type == EventType.INITIAL_STATE: return InitialStateEvent( data["data"]["world"]["width"], data["data"]["world"]["height"], data["data"]["entities"], ) if event_type == EventType.SET_CONTROLLABLE: return SetControllableEvent(data["data"]["id"], data["data"]["client_id"]) if event_type == EventType.ENTITY_BORN: return EntityBornEvent(data["data"]) if event_type == EventType.ENTITY_POSITION_UPDATE: return EntityPositionUpdateEvent( data["data"]["id"], data["data"]["position"] ) if event_type == EventType.ENTITY_DEATH: return EntityDeathEvent(data["data"]["id"]) logger.warn(f"Unknown event type: {data['event_type']}") return Event(EventType(data["event_type"]), data["data"]) class InitialStateEvent(Event): def __init__( self, world_width: int, world_height: int, entities: Dict[str, Entity] ): self.world_width = world_width self.world_height = world_height self.entities = entities super().__init__( EventType.INITIAL_STATE, { "world": {"width": world_width, "height": world_height}, "entities": entities, }, ) class SetControllableEvent(Event): def __init__(self, entity_id: str, client_id: str): self.entity_id = entity_id self.client_id = client_id super().__init__( EventType.SET_CONTROLLABLE, {"id": entity_id, "client_id": client_id} ) class EntityBornEvent(Event): def __init__(self, entity: Entity): self.entity = entity super().__init__(EventType.ENTITY_BORN, {"entity": entity.to_dict()}) class EntityPositionUpdateEvent(Event): def __init__(self, entity_id: str, position: dict): super().__init__( EventType.ENTITY_POSITION_UPDATE, {"id": entity_id, "position": position}, ) class EntityDeathEvent(Event): def __init__(self, entity_id: str): super().__init__(EventType.ENTITY_DEATH, {"id": entity_id}) class Publishable: @abstractmethod async def publish(self, event: List[Event]): pass class UpstreamEventProcessor: @abstractmethod def accept( self, entity_manager: EntityManager, client_event: tuple[Event, str] ) -> Optional[Event]: pass class NetworkSystem(System): event_processor: UpstreamEventProcessor sever_events: List[Event] client_upstream_events: List[tuple[Event, str]] clients: Dict[str, tuple[Publishable, List[Event]]] def __init__(self, upstream_event_processor: UpstreamEventProcessor): super().__init__(SystemType.NETWORK) self.upstream_event_processor = upstream_event_processor self.server_events = [] # events to propogate to the entire network self.client_upstream_events = [] # events that come from the clients self.clients = {} async def update(self, entity_manager: EntityManager, delta_time: float) -> None: for event in self.client_upstream_events: produced_event = self.upstream_event_processor.accept(entity_manager, event) if produced_event is not None: self.server_events.append(produced_event) promises = [ client.publish(self.server_events + events) for client, events in self.clients.values() if len(self.server_events + events) > 0 ] await asyncio.gather(*promises) self.server_events = [] self.client_upstream_events = [] for client_id in self.clients.keys(): (client, _) = self.clients[client_id] self.clients[client_id] = (client, []) def server_global_event(self, event: Event) -> None: self.server_events.append(event) def client_upstream_event(self, client_id: str, event: Event) -> None: self.client_upstream_events.append((event, client_id)) def client_downstream_event(self, client_id: str, event: Event) -> None: if client_id not in self.clients: logger.info(f"client {client_id} not found") return (client, events) = self.clients[client_id] self.clients[client_id] = (client, events + [event]) def add_client(self, client_id: str, client: Publishable) -> None: self.clients[client_id] = (client, []) def remove_client(self, client_id: str) -> None: del self.clients[client_id]