init commit with base ECS and Network System
This commit is contained in:
parent
6f223d2462
commit
b414046001
|
@ -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 uuid
|
||||||
|
|
||||||
import structlog
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
from fastapi import FastAPI, Request, Response
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
app = FastAPI(
|
loop = asyncio.get_event_loop()
|
||||||
servers = [
|
|
||||||
{"url": "https://kennel.hatecomputers.club", "description": "prod"}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.on_event("startup")
|
||||||
async def logger_middleware(request: Request, call_next):
|
async def startup_event():
|
||||||
structlog.contextvars.clear_contextvars()
|
logger.info("Starting Kennel...")
|
||||||
structlog.contextvars.bind_contextvars(
|
loop.create_task(kennel.run())
|
||||||
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
|
@app.on_event("shutdown")
|
||||||
if request.url.path != "/healthcheck":
|
async def shutdown_event():
|
||||||
if 400 <= response.status_code < 500:
|
logger.info("Stopping Kennel...")
|
||||||
logger.warn("Client error")
|
kennel.stop()
|
||||||
elif response.status_code >= 500:
|
loop.stop()
|
||||||
logger.error("Server error")
|
logger.info("Kennel stopped")
|
||||||
else:
|
|
||||||
logger.info("OK")
|
|
||||||
|
|
||||||
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()
|
||||||
|
logger.info(f"Websocket connection established for session {session}")
|
||||||
|
|
||||||
|
session_entities = create_session_controllable_entities(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:
|
||||||
|
message = await websocket.receive_json()
|
||||||
|
network_system.client_event(session, Event.from_dict(message))
|
||||||
|
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")
|
@app.get("/healthcheck")
|
||||||
async def healthcheck():
|
async def healthcheck():
|
||||||
return Response("hello")
|
return Response("healthy")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def read_main(request: Request):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request, name="index.html"
|
|
||||||
)
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory = "static"), name = "static")
|
|
||||||
|
|
||||||
|
|
|
@ -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"
|
structlog = "^23.2.0"
|
||||||
uvicorn = { extras = ["standard"], version = "^0.25.0" }
|
uvicorn = { extras = ["standard"], version = "^0.25.0" }
|
||||||
jinja2 = "^3.1.4"
|
jinja2 = "^3.1.4"
|
||||||
|
python-dotenv = "^1.0.1"
|
||||||
|
websockets = "^12.0"
|
||||||
|
requests = "^2.32.3"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "^0.1.9"
|
ruff = "^0.1.9"
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
console.log('from js');
|
const ws = new WebSocket("/ws");
|
||||||
const kennelWindowEle = document.querySelector('#kennel-window');
|
ws.onopen = () => {
|
||||||
kennelWindowEle.innerHTML = 'rendered from static/index.js';
|
console.log("connected");
|
||||||
}
|
};
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
console.log(e);
|
||||||
|
};
|
||||||
|
ws.onclose = (e) => {
|
||||||
|
console.log("disconnected", e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue