77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from dataclasses import dataclass
|
|
|
|
from engine.network.protocol import DEFAULT_PORT, encode_message
|
|
|
|
|
|
@dataclass
|
|
class ConnectedClient:
|
|
entity_id: str
|
|
writer: asyncio.StreamWriter
|
|
|
|
|
|
class GameServer:
|
|
"""Minimal authoritative-relay TCP server: broadcasts join/move/leave to every other client.
|
|
|
|
Wire format is JSON-lines (see engine/network/protocol.py). This is a foundation, not a
|
|
full sync engine - it relays messages verbatim rather than validating/simulating world
|
|
state server-side.
|
|
"""
|
|
|
|
def __init__(self, host: str = "0.0.0.0", port: int = DEFAULT_PORT):
|
|
self.host = host
|
|
self.port = port
|
|
self.clients: dict[str, ConnectedClient] = {}
|
|
self._server: asyncio.AbstractServer | None = None
|
|
|
|
async def start(self) -> None:
|
|
self._server = await asyncio.start_server(self._handle_client, self.host, self.port)
|
|
self.port = self._server.sockets[0].getsockname()[1]
|
|
|
|
async def serve_forever(self) -> None:
|
|
assert self._server is not None
|
|
async with self._server:
|
|
await self._server.serve_forever()
|
|
|
|
async def stop(self) -> None:
|
|
if self._server is not None:
|
|
self._server.close()
|
|
await self._server.wait_closed()
|
|
self._server = None
|
|
|
|
async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
entity_id: str | None = None
|
|
try:
|
|
while True:
|
|
line = await reader.readline()
|
|
if not line:
|
|
break
|
|
message = json.loads(line)
|
|
if message.get("type") == "hello":
|
|
entity_id = str(message["entity_id"])
|
|
self.clients[entity_id] = ConnectedClient(entity_id, writer)
|
|
await self._broadcast(
|
|
{"type": "join", "entity_id": entity_id, "name": message.get("name", "")}, exclude=entity_id
|
|
)
|
|
else:
|
|
await self._broadcast(message, exclude=entity_id)
|
|
finally:
|
|
if entity_id is not None:
|
|
self.clients.pop(entity_id, None)
|
|
await self._broadcast({"type": "leave", "entity_id": entity_id})
|
|
writer.close()
|
|
|
|
async def _broadcast(self, message: dict, exclude: str | None = None) -> None:
|
|
data = encode_message(message)
|
|
for client_id, client in list(self.clients.items()):
|
|
if client_id == exclude:
|
|
continue
|
|
try:
|
|
client.writer.write(data)
|
|
await client.writer.drain()
|
|
except ConnectionError:
|
|
self.clients.pop(client_id, None)
|