Compare commits
	
		
			1 Commits
		
	
	
		
			6f374aac7f
			...
			88be03b689
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 88be03b689 | 
|  | @ -1,2 +0,0 @@ | |||
| *.sqlite | ||||
| *.db | ||||
|  | @ -0,0 +1,13 @@ | |||
| import structlog | ||||
| from fastapi import FastAPI | ||||
| from fastapi.templating import Jinja2Templates | ||||
| 
 | ||||
| app = FastAPI( | ||||
|     servers = [ | ||||
|         {"url": "https://kennel.hatecomputers.club", "description": "prod"} | ||||
|     ] | ||||
| ) | ||||
| 
 | ||||
| logger = structlog.get_logger() | ||||
| 
 | ||||
| templates = Jinja2Templates(directory="templates") | ||||
|  | @ -0,0 +1,20 @@ | |||
| from dotenv import load_dotenv | ||||
| import os | ||||
| 
 | ||||
| load_dotenv() | ||||
| 
 | ||||
| 
 | ||||
| class Config: | ||||
|     SIMULATION_WIDTH = int(os.getenv("SIMULATION_WIDTH", 1000)) | ||||
|     SIMULATION_HEIGHT = int(os.getenv("SIMULATION_HEIGHT", 1000)) | ||||
| 
 | ||||
|     HATECOMPUTERS_ENDPOINT = os.getenv( | ||||
|         "HATECOMPUTERS_ENDPOINT", "https://hatecomputers.club" | ||||
|     ) | ||||
| 
 | ||||
|     MIN_TIME_STEP = float(os.getenv("MIN_TIME_STEP", 0.05)) | ||||
| 
 | ||||
|     COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7))  # 1 week | ||||
| 
 | ||||
| 
 | ||||
| config = Config() | ||||
|  | @ -0,0 +1,16 @@ | |||
| from enum import Enum | ||||
| from abc import abstractmethod | ||||
| 
 | ||||
| 
 | ||||
| class ComponentType(str, Enum): | ||||
|     POSITION = "POSITION" | ||||
|     CONTROLLABLE = "CONTROLLABLE" | ||||
| 
 | ||||
| 
 | ||||
| class Component: | ||||
|     def __init__(self, component_type: ComponentType): | ||||
|         self.component_type = component_type | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def dict(self) -> dict: | ||||
|         pass | ||||
|  | @ -0,0 +1,13 @@ | |||
| from .component import Component, ComponentType | ||||
| 
 | ||||
| 
 | ||||
| class Controllable(Component): | ||||
|     def __init__(self, by: str): | ||||
|         super().__init__(ComponentType.CONTROLLABLE) | ||||
|         self.by = by | ||||
| 
 | ||||
|     def __repr__(self) -> str: | ||||
|         return f"Controllable(by={self.by})" | ||||
| 
 | ||||
|     def dict(self) -> dict: | ||||
|         return {"by": self.by} | ||||
|  | @ -0,0 +1,14 @@ | |||
| from .component import Component, ComponentType | ||||
| 
 | ||||
| 
 | ||||
| class Position(Component): | ||||
|     def __init__(self, x: float, y: float): | ||||
|         super().__init__(ComponentType.POSITION) | ||||
|         self.x = x | ||||
|         self.y = y | ||||
| 
 | ||||
|     def __repr__(self) -> str: | ||||
|         return f"Position(x={self.x}, y={self.y})" | ||||
| 
 | ||||
|     def dict(self) -> dict: | ||||
|         return {"x": self.x, "y": self.y} | ||||
|  | @ -0,0 +1,33 @@ | |||
| # | ||||
| # # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY | ||||
| # state_stochastic_matrix = [ | ||||
| #     # IDLE | ||||
| #     [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0], | ||||
| #     # FROLICKING | ||||
| #     [0.1, 0.5, 0.1, 0.1, 0.1, 0.05, 0.05, 0], | ||||
| #     # EEPY | ||||
| #     [0.1, 0.1, 0.5, 0.1, 0.1, 0.05, 0.05, 0], | ||||
| #     # ALERT | ||||
| #     [0.1, 0.1, 0.1, 0.5, 0.1, 0.05, 0.05, 0], | ||||
| #     # CHASING_CURSOR | ||||
| #     [0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0.05, 0], | ||||
| #     # CHASING_CAT | ||||
| #     [0.1, 0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0], | ||||
| #     # SCRATCHING | ||||
| #     [0.1, 0.1, 0.1, 0.1, 0.1, 0.05, 0.5, 0], | ||||
| #     # ITCHY | ||||
| #     [0, 0, 0, 0, 0, 0, 0, 1], | ||||
| # ] | ||||
| # | ||||
| # | ||||
| # class CatState(Enum): | ||||
| #     IDLE = 0 | ||||
| #     FROLICKING = 1 | ||||
| #     EEPY = 2 | ||||
| #     ALERT = 3 | ||||
| #     CHASING_CURSOR = 4 | ||||
| #     CHASING_CAT = 5 | ||||
| #     SCRATCHING = 6 | ||||
| #     ITCHY = 7 | ||||
| # | ||||
| # | ||||
|  | @ -0,0 +1,60 @@ | |||
| from enum import Enum | ||||
| from kennel.engine.components.component import Component, ComponentType | ||||
| from typing import List, Optional | ||||
| 
 | ||||
| 
 | ||||
| class EntityType(str, Enum): | ||||
|     LASER = "LASER" | ||||
| 
 | ||||
| 
 | ||||
| class Entity: | ||||
|     def __init__( | ||||
|         self, entity_type: EntityType, id: str, component_list: List[Component] | ||||
|     ): | ||||
|         self.entity_type = entity_type | ||||
|         self.id = id | ||||
|         self.components = {} | ||||
|         for component in component_list: | ||||
|             self.add_component(component) | ||||
| 
 | ||||
|     def get_component(self, component_type: ComponentType) -> Optional[Component]: | ||||
|         return self.components[component_type] | ||||
| 
 | ||||
|     def add_component(self, component: Component) -> None: | ||||
|         self.components[component.component_type] = component | ||||
| 
 | ||||
| 
 | ||||
| class EntityManager: | ||||
|     def __init__(self): | ||||
|         self.entities = {} | ||||
|         self.component_entities = {} | ||||
| 
 | ||||
|     def update(self) -> None: | ||||
|         self.component_entities = {} | ||||
|         for entity in self.entities.values(): | ||||
|             for component in entity.components.values(): | ||||
|                 if component.component_type not in self.component_entities: | ||||
|                     self.component_entities[component.component_type] = set() | ||||
|                 self.component_entities[component.component_type].add(entity) | ||||
| 
 | ||||
|     def get_entities_with_component( | ||||
|         self, component_type: ComponentType | ||||
|     ) -> List[Entity]: | ||||
|         return self.component_entities.get(component_type, []) | ||||
| 
 | ||||
|     def add_entity(self, entity: Entity) -> None: | ||||
|         self.entities[entity.id] = entity | ||||
|         for component in entity.components.values(): | ||||
|             if component.component_type not in self.component_entities: | ||||
|                 self.component_entities[component.component_type] = set() | ||||
|             self.component_entities[component.component_type].add(entity) | ||||
| 
 | ||||
|     def remove_entity(self, entity_id: str) -> None: | ||||
|         if entity_id in self.entities: | ||||
|             entity = self.entities[entity_id] | ||||
|             for component in entity.components.values(): | ||||
|                 if component.component_type in self.component_entities: | ||||
|                     self.component_entities[component.component_type].remove(entity) | ||||
| 
 | ||||
|     def get_entity(self, entity_id: str) -> Optional[Entity]: | ||||
|         return self.entities.get(entity_id) | ||||
|  | @ -0,0 +1,13 @@ | |||
| from .entity import Entity, EntityType | ||||
| from kennel.engine.components.position import Position | ||||
| from kennel.engine.components.controllable import Controllable | ||||
| 
 | ||||
| 
 | ||||
| class Laser(Entity): | ||||
|     def __init__(self, id: str, controllable_by: str): | ||||
|         components = [ | ||||
|             Position(0, 0), | ||||
|             Controllable(controllable_by), | ||||
|         ] | ||||
| 
 | ||||
|         super().__init__(EntityType.LASER, id, components) | ||||
|  | @ -0,0 +1,53 @@ | |||
| from kennel.engine.entities.entity import EntityManager | ||||
| from kennel.engine.systems.system import SystemManager | ||||
| from kennel.app import logger | ||||
| from typing import List, Optional | ||||
| import time | ||||
| import asyncio | ||||
| 
 | ||||
| 
 | ||||
| class Game: | ||||
|     def __init__( | ||||
|         self, | ||||
|         entity_manager: EntityManager, | ||||
|         system_manager: SystemManager, | ||||
|         min_time_step: float, | ||||
|     ): | ||||
|         self.entity_manager = entity_manager | ||||
|         self.system_manager = system_manager | ||||
|         self.min_time_step = min_time_step | ||||
| 
 | ||||
|         self.last_time = time.time() | ||||
|         self.running = False | ||||
| 
 | ||||
|     async def update(self, delta_time: float) -> None: | ||||
|         self.entity_manager.update() | ||||
|         await self.system_manager.update(self.entity_manager, delta_time) | ||||
| 
 | ||||
|     async def run(self) -> None: | ||||
|         logger.info("Game started") | ||||
|         self.running = True | ||||
|         self.last_time = time.time() | ||||
|         steps_since_log = 0 | ||||
| 
 | ||||
|         while self.running: | ||||
|             current_time = time.time() | ||||
|             if (int(current_time) - int(self.last_time)) == 1 and int( | ||||
|                 current_time | ||||
|             ) % 10 == 0: | ||||
|                 logger.info(f"Game loop: {steps_since_log} steps since last log time") | ||||
|                 steps_since_log = 0 | ||||
| 
 | ||||
|             delta_time = current_time - self.last_time | ||||
|             self.last_time = current_time | ||||
| 
 | ||||
|             await self.update(delta_time) | ||||
|             await asyncio.sleep( | ||||
|                 max(self.min_time_step - (time.time() - current_time), 0) | ||||
|             ) | ||||
| 
 | ||||
|             steps_since_log += 1 | ||||
| 
 | ||||
|     def stop(self) -> None: | ||||
|         self.running = False | ||||
|         logger.info("Game stopped") | ||||
|  | @ -0,0 +1,88 @@ | |||
| from enum import Enum | ||||
| from kennel.engine.entities.entity import EntityManager | ||||
| from .system import System, SystemType | ||||
| from abc import abstractmethod | ||||
| import asyncio | ||||
| 
 | ||||
| 
 | ||||
| class EventType(str, Enum): | ||||
|     ENTITY_BORN = "ENTITY_BORN" | ||||
|     ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" | ||||
|     ENTITY_DEATH = "ENTITY_DEATH" | ||||
| 
 | ||||
| 
 | ||||
| class Event: | ||||
|     def __init__(self, event_type: EventType, data: dict): | ||||
|         self.event_type = event_type | ||||
|         self.data = data | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"Event({self.event_type}, {self.data})" | ||||
| 
 | ||||
|     def dict(self): | ||||
|         return {"event_type": self.event_type, "data": self.data} | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def from_dict(data: dict): | ||||
|         return Event(EventType(data["event_type"]), data["data"]) | ||||
| 
 | ||||
| 
 | ||||
| class Publishable: | ||||
|     @abstractmethod | ||||
|     async def publish(self, event: Event): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class ClientEventTransformer: | ||||
|     @abstractmethod | ||||
|     def apply(self, event: Event) -> Event: | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class EventProcessor: | ||||
|     @abstractmethod | ||||
|     def accept(self, entity_manager: EntityManager, event: Event) -> None: | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class NetworkSystem(System): | ||||
|     def __init__( | ||||
|         self, | ||||
|         event_processor: EventProcessor, | ||||
|         client_event_transformer: ClientEventTransformer, | ||||
|     ): | ||||
|         super().__init__(SystemType.NETWORK) | ||||
|         self.event_processor = event_processor | ||||
|         self.client_event_transformer = client_event_transformer | ||||
| 
 | ||||
|         self.events = [] | ||||
|         self.clients = {} | ||||
| 
 | ||||
|     async def update(self, entity_manager: EntityManager, delta_time: float) -> None: | ||||
|         for event in self.events: | ||||
|             self.event_processor.accept(entity_manager, event) | ||||
|         client_events = [ | ||||
|             self.client_event_transformer.apply(entity_manager, event) | ||||
|             for event in self.events | ||||
|         ] | ||||
|         await asyncio.gather( | ||||
|             *[ | ||||
|                 client.publish(event) | ||||
|                 for client in self.clients.values() | ||||
|                 for event in client_events | ||||
|             ] | ||||
|         ) | ||||
|         self.events = [] | ||||
| 
 | ||||
|     def client_event(self, client_id: str, event: Event) -> None: | ||||
|         event.data["client_id"] = client_id | ||||
|         self.events.append(event) | ||||
| 
 | ||||
|     def add_event(self, event: Event) -> None: | ||||
|         self.events.append(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] | ||||
|  | @ -0,0 +1,31 @@ | |||
| from enum import Enum | ||||
| from kennel.engine.entities.entity import EntityManager | ||||
| from abc import abstractmethod | ||||
| 
 | ||||
| 
 | ||||
| class SystemType(str, Enum): | ||||
|     NETWORK = "NETWORK" | ||||
| 
 | ||||
| 
 | ||||
| class System: | ||||
|     def __init__(self, system_type: SystemType): | ||||
|         self.system_type = system_type | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     async def update(self, entity_manager: EntityManager, delta_time: float): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class SystemManager: | ||||
|     def __init__(self): | ||||
|         self.systems = {} | ||||
| 
 | ||||
|     def add_system(self, system: System) -> None: | ||||
|         self.systems[system.system_type] = system | ||||
| 
 | ||||
|     def get_system(self, system_type: SystemType) -> System: | ||||
|         return self.systems.get(system_type) | ||||
| 
 | ||||
|     async def update(self, entity_manager: EntityManager, delta_time: float) -> None: | ||||
|         for system in self.systems.values(): | ||||
|             await system.update(entity_manager, delta_time) | ||||
|  | @ -0,0 +1,81 @@ | |||
| from kennel.engine.game import Game | ||||
| from kennel.engine.components.component import ComponentType | ||||
| from kennel.engine.entities.entity import EntityManager, Entity | ||||
| from kennel.engine.entities.laser import Laser | ||||
| from kennel.engine.systems.system import SystemManager | ||||
| from kennel.engine.systems.network import ( | ||||
|     NetworkSystem, | ||||
|     EventProcessor, | ||||
|     ClientEventTransformer, | ||||
|     Event, | ||||
|     EventType, | ||||
| ) | ||||
| from kennel.config import config | ||||
| from .app import logger | ||||
| from typing import List | ||||
| import uuid | ||||
| 
 | ||||
| entity_manager = EntityManager() | ||||
| system_manager = SystemManager() | ||||
| 
 | ||||
| 
 | ||||
| class KennelEventProcessor(EventProcessor): | ||||
|     def accept(self, entity_manager: EntityManager, event: Event) -> None: | ||||
|         if event.event_type == EventType.ENTITY_POSITION_UPDATE: | ||||
|             self._process_entity_position_update(entity_manager, event) | ||||
| 
 | ||||
|     def _process_entity_position_update( | ||||
|         self, entity_manager: EntityManager, event: Event | ||||
|     ) -> None: | ||||
|         entity = entity_manager.get_entity(event.data["entity_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") | ||||
|             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']}" | ||||
|             ) | ||||
|             return | ||||
|         position = entity.get_component(ComponentType.POSITION) | ||||
|         if position is None: | ||||
|             logger.error(f"Entity {entity} has no position") | ||||
|             return | ||||
|         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: | ||||
|                 if component_type == ComponentType.CONTROLLABLE: | ||||
|                     # Do not send controllable components to clients | ||||
|                     continue | ||||
|                 component = entity.get_component(component_type) | ||||
|                 if component is not None: | ||||
|                     event.data[component_type] = component.dict() | ||||
| 
 | ||||
|         return event | ||||
| 
 | ||||
| 
 | ||||
| system_manager.add_system( | ||||
|     NetworkSystem(EventProcessor(), KennelClientEventTransformer()) | ||||
| ) | ||||
| 
 | ||||
| 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) | ||||
|     return [laser] | ||||
|  | @ -0,0 +1,45 @@ | |||
| from datetime import datetime | ||||
| from typing import List | ||||
| import requests | ||||
| 
 | ||||
| 
 | ||||
| class KennelCat: | ||||
|     def __init__( | ||||
|         self, | ||||
|         id: str, | ||||
|         user_id: str, | ||||
|         name: str, | ||||
|         link: str, | ||||
|         description: str, | ||||
|         spritesheet: str, | ||||
|         created_at: datetime, | ||||
|     ): | ||||
|         self.id = id | ||||
|         self.user_id = user_id | ||||
|         self.name = name | ||||
|         self.link = link | ||||
|         self.description = description | ||||
|         self.spritesheet = spritesheet | ||||
|         self.created_at = created_at | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def from_dict(dict: dict) -> "KennelCat": | ||||
|         return KennelCat( | ||||
|             id=dict["id"], | ||||
|             user_id=dict["user_id"], | ||||
|             name=dict["name"], | ||||
|             link=dict["link"], | ||||
|             description=dict["description"], | ||||
|             spritesheet=dict["spritesheet"], | ||||
|             created_at=datetime.fromisoformat(dict["created_at"]), | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class KennelCatService: | ||||
|     def __init__(self, endpoint: str): | ||||
|         self.endpoint = endpoint | ||||
| 
 | ||||
|     def get_kennel(self) -> List[KennelCat]: | ||||
|         response = requests.get(f"{self.endpoint}/kennel") | ||||
|         response.raise_for_status() | ||||
|         return [KennelCat.from_dict(cat) for cat in response.json()] | ||||
							
								
								
									
										149
									
								
								kennel/main.py
								
								
								
								
							
							
						
						
									
										149
									
								
								kennel/main.py
								
								
								
								
							|  | @ -1,57 +1,118 @@ | |||
| from fastapi import ( | ||||
|     FastAPI, | ||||
|     Request, | ||||
|     Response, | ||||
|     WebSocket, | ||||
|     WebSocketException, | ||||
|     status, | ||||
|     Cookie, | ||||
|     Depends, | ||||
| ) | ||||
| from fastapi.staticfiles import StaticFiles | ||||
| from kennel.engine.systems.system import SystemType | ||||
| from kennel.engine.systems.network import Event, Publishable, EventType | ||||
| from typing import Annotated, Optional | ||||
| from .kennel import ( | ||||
|     kennel, | ||||
|     system_manager, | ||||
|     entity_manager, | ||||
|     create_session_controllable_entities, | ||||
| ) | ||||
| from .app import app, templates, logger | ||||
| from .kennelcats import KennelCatService | ||||
| from .middleware import logger_middleware | ||||
| from .config import config | ||||
| import asyncio | ||||
| import uuid | ||||
| 
 | ||||
| import structlog | ||||
| from fastapi import FastAPI, Request, Response | ||||
| from fastapi.staticfiles import StaticFiles | ||||
| from fastapi.templating import Jinja2Templates | ||||
| app.mount("/static", StaticFiles(directory="static"), name="static") | ||||
| 
 | ||||
| app = FastAPI( | ||||
|     servers = [ | ||||
|         {"url": "https://kennel.hatecomputers.club", "description": "prod"} | ||||
|     ] | ||||
| ) | ||||
| logger = structlog.get_logger() | ||||
| loop = asyncio.get_event_loop() | ||||
| 
 | ||||
| 
 | ||||
| @app.middleware("http") | ||||
| async def logger_middleware(request: Request, call_next): | ||||
|     structlog.contextvars.clear_contextvars() | ||||
|     structlog.contextvars.bind_contextvars( | ||||
|         path=request.url.path, | ||||
|         method=request.method, | ||||
|         client_host=request.client.host, | ||||
|         request_id=str(uuid.uuid4()), | ||||
|     ) | ||||
|     response = await call_next(request) | ||||
| @app.on_event("startup") | ||||
| async def startup_event(): | ||||
|     logger.info("Starting Kennel...") | ||||
|     loop.create_task(kennel.run()) | ||||
| 
 | ||||
|     structlog.contextvars.bind_contextvars( | ||||
|         status_code=response.status_code, | ||||
|     ) | ||||
| 
 | ||||
|     # Exclude /healthcheck endpoint from producing logs | ||||
|     if request.url.path != "/healthcheck": | ||||
|         if 400 <= response.status_code < 500: | ||||
|             logger.warn("Client error") | ||||
|         elif response.status_code >= 500: | ||||
|             logger.error("Server error") | ||||
|         else: | ||||
|             logger.info("OK") | ||||
| @app.on_event("shutdown") | ||||
| async def shutdown_event(): | ||||
|     logger.info("Stopping Kennel...") | ||||
|     kennel.stop() | ||||
|     loop.stop() | ||||
|     logger.info("Kennel stopped") | ||||
| 
 | ||||
|     return response | ||||
| 
 | ||||
| templates = Jinja2Templates(directory="templates") | ||||
| @app.get("/") | ||||
| async def index(request: Request): | ||||
|     return templates.TemplateResponse(request=request, name="index.html") | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/assign") | ||||
| async def assign( | ||||
|     response: Response, | ||||
|     session: Annotated[Optional[str], Cookie()] = None, | ||||
| ): | ||||
|     if session is None: | ||||
|         session = str(uuid.uuid4().hex) | ||||
|         response.set_cookie(key="session", value=session, max_age=config.COOKIE_MAX_AGE) | ||||
|     return {"session": session} | ||||
| 
 | ||||
| 
 | ||||
| async def get_cookie_or_token( | ||||
|     websocket: WebSocket, | ||||
|     session: Annotated[str | None, Cookie()] = None, | ||||
| ): | ||||
|     if session is None: | ||||
|         raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) | ||||
|     return session | ||||
| 
 | ||||
| 
 | ||||
| class WebSocketClient(Publishable): | ||||
|     def __init__(self, websocket: WebSocket): | ||||
|         self.websocket = websocket | ||||
| 
 | ||||
|     async def publish(self, event: Event): | ||||
|         await self.websocket.send_json(event.dict()) | ||||
| 
 | ||||
| 
 | ||||
| @app.websocket("/ws") | ||||
| async def websocket_endpoint( | ||||
|     websocket: WebSocket, | ||||
|     session: Annotated[str, Depends(get_cookie_or_token)], | ||||
| ): | ||||
|     await websocket.accept() | ||||
|     session_entities = create_session_controllable_entities(session) | ||||
|     logger.info(f"Websocket connection established for session {session}") | ||||
|     try: | ||||
|         network_system = system_manager.get_system(SystemType.NETWORK) | ||||
|         if network_system is None: | ||||
|             raise "Network system not found" | ||||
| 
 | ||||
|         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_client(session, WebSocketClient(websocket)) | ||||
|         while True: | ||||
|             network_system.client_event( | ||||
|                 session, Event.from_dict(await websocket.receive_json()) | ||||
|             ) | ||||
|     except Exception as e: | ||||
|         logger.error(f"WebSocket exception {e}") | ||||
|     finally: | ||||
|         logger.info("Websocket connection closed") | ||||
|         for entity in session_entities: | ||||
|             logger.info(f"Removing entity {entity.id} for session {session}") | ||||
|             entity_manager.remove_entity(entity.id) | ||||
|             network_system.add_event(Event(EventType.ENTITY_DEATH, {"id": entity.id})) | ||||
| 
 | ||||
|         network_system.remove_client(session) | ||||
|         await websocket.close() | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/healthcheck") | ||||
| async def healthcheck(): | ||||
|     return Response("hello") | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/") | ||||
| async def read_main(request: Request): | ||||
|     return templates.TemplateResponse( | ||||
|         request=request, name="index.html" | ||||
|     ) | ||||
| 
 | ||||
| app.mount("/static", StaticFiles(directory = "static"), name = "static") | ||||
| 
 | ||||
|     return Response("healthy") | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| import uuid | ||||
| import structlog | ||||
| from fastapi import Request, Response | ||||
| from .app import app, logger | ||||
| 
 | ||||
| @app.middleware("http") | ||||
| async def logger_middleware(request: Request, call_next): | ||||
|     structlog.contextvars.clear_contextvars() | ||||
|     structlog.contextvars.bind_contextvars( | ||||
|         path=request.url.path, | ||||
|         method=request.method, | ||||
|         client_host=request.client.host, | ||||
|         request_id=str(uuid.uuid4()), | ||||
|     ) | ||||
|     response = await call_next(request) | ||||
| 
 | ||||
|     structlog.contextvars.bind_contextvars( | ||||
|         status_code=response.status_code, | ||||
|     ) | ||||
| 
 | ||||
|     # Exclude /healthcheck endpoint from producing logs | ||||
|     if request.url.path != "/healthcheck": | ||||
|         if 400 <= response.status_code < 500: | ||||
|             logger.warn("Client error") | ||||
|         elif response.status_code >= 500: | ||||
|             logger.error("Server error") | ||||
|         else: | ||||
|             logger.info("OK") | ||||
| 
 | ||||
|     return response | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -12,6 +12,9 @@ fastapi = "^0.108.0" | |||
| structlog = "^23.2.0" | ||||
| uvicorn = { extras = ["standard"], version = "^0.25.0" } | ||||
| jinja2 = "^3.1.4" | ||||
| python-dotenv = "^1.0.1" | ||||
| websockets = "^12.0" | ||||
| requests = "^2.32.3" | ||||
| 
 | ||||
| [tool.poetry.group.dev.dependencies] | ||||
| ruff = "^0.1.9" | ||||
|  |  | |||
|  | @ -1,5 +1,12 @@ | |||
| window.onload = () => { | ||||
|     console.log('from js'); | ||||
|     const kennelWindowEle = document.querySelector('#kennel-window'); | ||||
|     kennelWindowEle.innerHTML = 'rendered from static/index.js'; | ||||
| } | ||||
|   const ws = new WebSocket("/ws"); | ||||
|   ws.onopen = () => { | ||||
|     console.log("connected"); | ||||
|   }; | ||||
|   ws.onmessage = (e) => { | ||||
|     console.log(e); | ||||
|   }; | ||||
|   ws.onclose = (e) => { | ||||
|     console.log("disconnected", e); | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue