diff options
Diffstat (limited to 'kennel')
| -rw-r--r-- | kennel/engine/components/component.py | 2 | ||||
| -rw-r--r-- | kennel/engine/components/controllable.py | 2 | ||||
| -rw-r--r-- | kennel/engine/components/position.py | 2 | ||||
| -rw-r--r-- | kennel/engine/entities/entity.py | 2 | ||||
| -rw-r--r-- | kennel/engine/systems/network.py | 104 | ||||
| -rw-r--r-- | kennel/kennel.py | 50 | ||||
| -rw-r--r-- | kennel/main.py | 42 |
7 files changed, 131 insertions, 73 deletions
diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py index c8ef2d0..da55138 100644 --- a/kennel/engine/components/component.py +++ b/kennel/engine/components/component.py @@ -12,5 +12,5 @@ class Component: self.component_type = component_type @abstractmethod - def dict(self) -> dict: + def to_dict(self) -> dict: pass diff --git a/kennel/engine/components/controllable.py b/kennel/engine/components/controllable.py index 25ec835..a9173af 100644 --- a/kennel/engine/components/controllable.py +++ b/kennel/engine/components/controllable.py @@ -9,6 +9,6 @@ class Controllable(Component): def __repr__(self) -> str: return f"Controllable(by={self.by})" - def dict(self) -> dict: + def to_dict(self) -> dict: # don't serialize who owns this return {} diff --git a/kennel/engine/components/position.py b/kennel/engine/components/position.py index 2a4da4a..5c5b372 100644 --- a/kennel/engine/components/position.py +++ b/kennel/engine/components/position.py @@ -10,5 +10,5 @@ class Position(Component): def __repr__(self) -> str: return f"Position(x={self.x}, y={self.y})" - def dict(self) -> dict: + def to_dict(self) -> dict: return {"x": self.x, "y": self.y} diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py index 6623512..1d8c34d 100644 --- a/kennel/engine/entities/entity.py +++ b/kennel/engine/entities/entity.py @@ -27,7 +27,7 @@ class Entity: return { "entity_type": self.entity_type, "id": self.id, - "components": {k: v.dict() for k, v in self.components.items()}, + "components": {k: v.to_dict() for k, v in self.components.items()}, } diff --git a/kennel/engine/systems/network.py b/kennel/engine/systems/network.py index c9696c8..8dbf4cf 100644 --- a/kennel/engine/systems/network.py +++ b/kennel/engine/systems/network.py @@ -1,15 +1,17 @@ from enum import Enum -from kennel.engine.entities.entity import EntityManager +from kennel.engine.entities.entity import Entity, EntityType, EntityManager from .system import System, SystemType from abc import abstractmethod +from typing import Optional, List import asyncio class EventType(str, Enum): INITIAL_STATE = "INITIAL_STATE" + SET_CONTROLLABLE = "SET_CONTROLLABLE" ENTITY_BORN = "ENTITY_BORN" - ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" ENTITY_DEATH = "ENTITY_DEATH" + ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" class Event: @@ -17,26 +19,83 @@ class Event: self.event_type = event_type self.data = data - def __str__(self): + def __str__(self) -> str: return f"Event({self.event_type}, {self.data})" - def dict(self): + def to_dict(self) -> dict: return {"event_type": self.event_type, "data": self.data} @staticmethod - def from_dict(data: dict): + 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 Publishable: - @abstractmethod - async def publish(self, event: Event): - pass +class InitialStateEvent(Event): + def __init__(self, world_width: int, world_height: int, entities: List[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": [entity.to_dict() for entity in 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 ClientEventTransformer: +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 - def apply(self, event: Event) -> Event: + async def publish(self, event: Event): pass @@ -47,37 +106,32 @@ class EventProcessor: class NetworkSystem(System): - def __init__( - self, - event_processor: EventProcessor, - client_event_transformer: ClientEventTransformer, - ): + def __init__(self, event_processor: EventProcessor): super().__init__(SystemType.NETWORK) self.event_processor = event_processor - self.client_event_transformer = client_event_transformer self.events = [] + self.client_events = [] self.clients = {} async def update(self, entity_manager: EntityManager, delta_time: float) -> None: - for event in self.events: + if len(self.events) + len(self.client_events) == 0: + return + for event in self.events + self.client_events: self.event_processor.accept(entity_manager, event) - client_events = [ - self.client_event_transformer.apply(entity_manager, event) - for event in self.events - ] + client_sendable = self.events + [event for event, _ in self.client_events] await asyncio.gather( *[ client.publish(event) for client in self.clients.values() - for event in client_events + for event in client_sendable ] ) self.events = [] + self.client_events = [] def client_event(self, client_id: str, event: Event) -> None: - event.data["client_id"] = client_id - self.events.append(event) + self.client_events.append((event, client_id)) def add_event(self, event: Event) -> None: self.events.append(event) diff --git a/kennel/kennel.py b/kennel/kennel.py index 61847a5..bb0438c 100644 --- a/kennel/kennel.py +++ b/kennel/kennel.py @@ -6,7 +6,6 @@ from kennel.engine.systems.system import SystemManager from kennel.engine.systems.network import ( NetworkSystem, EventProcessor, - ClientEventTransformer, Event, EventType, ) @@ -21,28 +20,33 @@ system_manager = SystemManager() class KennelEventProcessor(EventProcessor): - def accept(self, entity_manager: EntityManager, event: Event) -> None: + def accept( + self, entity_manager: EntityManager, event: type[Event | tuple[Event, str]] + ) -> None: + if isinstance(event, tuple): + client_event, client_id = event + self._process_client_event(entity_manager, client_event, client_id) + return + + def _process_client_event( + self, entity_manager: EntityManager, event: Event, client_id: str + ) -> None: if event.event_type == EventType.ENTITY_POSITION_UPDATE: - self._process_entity_position_update(entity_manager, event) + self._process_entity_position_update(entity_manager, event, client_id) def _process_entity_position_update( - self, entity_manager: EntityManager, event: Event + self, entity_manager: EntityManager, event: Event, client_id: str ) -> None: - entity = entity_manager.get_entity(event.data["entity_id"]) + entity = entity_manager.get_entity(event.data["id"]) if entity is None: - logger.error(f"Entity {event.data['entity_id']} does not exist") - return - if event.data["client_id"] is None: - logger.error("Client ID is required for position updates") + logger.error(f"Entity(id={event.data['id']}) does not exist") return controllable = entity.get_component(ComponentType.CONTROLLABLE) if controllable is None: logger.error(f"Entity {entity} is not controllable") return - if controllable.by != event.data["client_id"]: - logger.error( - f"Entity {entity} is not controllable by client {event.data['client_id']}" - ) + if controllable.by != client_id: + logger.error(f"Entity {entity} is not controllable by client {client_id}") return position = entity.get_component(ComponentType.POSITION) if position is None: @@ -51,30 +55,14 @@ class KennelEventProcessor(EventProcessor): entity.add_component(position) -class KennelClientEventTransformer(ClientEventTransformer): - def apply(self, entity_manager: EntityManager, event: Event) -> Event: - if event.event_type == EventType.ENTITY_BORN: - id = event.data["id"] - entity = entity_manager.get_entity(id) - if entity is None: - logger.error(f"Entity {id} does not exist") - return event - for component_type in entity.components: - component = entity.get_component(component_type) - if component is not None: - event.data[component_type] = component.dict() - - return event - - system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) system_manager.add_system( - NetworkSystem(EventProcessor(), KennelClientEventTransformer()), + NetworkSystem(KennelEventProcessor()), ) kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) def create_session_controllable_entities(session: str) -> List[Entity]: - laser = Laser(uuid.uuid4().hex, session) + laser = Laser(uuid.uuid4().hex[:10], session) return [laser] diff --git a/kennel/main.py b/kennel/main.py index 6f661cb..fc0fd0e 100644 --- a/kennel/main.py +++ b/kennel/main.py @@ -11,7 +11,14 @@ from fastapi import ( from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from kennel.engine.systems.system import SystemType -from kennel.engine.systems.network import Event, Publishable, EventType +from kennel.engine.systems.network import ( + Event, + Publishable, + EventType, + InitialStateEvent, + SetControllableEvent, + EntityBornEvent, +) from typing import Annotated, Optional from kennel.kennel import ( kennel, @@ -76,7 +83,7 @@ class WebSocketClient(Publishable): self.websocket = websocket async def publish(self, event: Event): - await self.websocket.send_json(event.dict()) + await self.websocket.send_json([event.event_type, event.data]) @app.websocket("/ws") @@ -85,19 +92,16 @@ async def websocket_endpoint( session: Annotated[str, Depends(get_cookie_or_token)], ): await websocket.accept() + client = WebSocketClient(websocket) logger.info(f"Websocket connection established for session {session}") - await websocket.send_json( - { - "event_type": EventType.INITIAL_STATE, - "data": { - "world": {"width": config.WORLD_WIDTH, "height": config.WORLD_HEIGHT}, - "entities": kennel.entity_manager.to_dict(), - }, - } + initial_state = InitialStateEvent( + config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict() ) + await client.publish(initial_state) session_entities = create_session_controllable_entities(session) + logger.info(f"Creating {len(session_entities)} entities for session {session}") try: network_system = system_manager.get_system(SystemType.NETWORK) if network_system is None: @@ -106,14 +110,26 @@ async def websocket_endpoint( for entity in session_entities: logger.info(f"Adding entity {entity.id} for session {session}") entity_manager.add_entity(entity) - network_system.add_event(Event(EventType.ENTITY_BORN, {"id": entity.id})) + network_system.add_event(EntityBornEvent(entity)) + + set_controllable_event = SetControllableEvent(entity.id, session) + await client.publish(set_controllable_event) network_system.add_client(session, WebSocketClient(websocket)) while True: message = await websocket.receive_json() - network_system.client_event(session, Event.from_dict(message)) + if type(message) is not list: + message = [message] + events = [Event.from_dict(event) for event in message] + if not all([event is not None for event in events]): + logger.info(f"Invalid events in: {message}"[:100]) + continue + for event in events: + network_system.client_event(session, event) + except WebSocketDisconnect as e: + logger.info(f"Websocket connection closed by client: {session}") except Exception as e: - logger.error(f"WebSocket exception {e}") + logger.error("Exception occurred", exc_info=e) finally: logger.info("Websocket connection closed") for entity in session_entities: |
