Some checks failed
Release / release (push) Has been cancelled
Map overhaul: - Replace colored boxes with actual game tile images (skin textures from CDN) - Overlay content icons (monsters, resources, NPCs) on tiles - Add layer switching (Overworld/Underground/Interior) - Fix API schema to parse interactions.content and layer fields - Add hover tooltips, tile search with coordinate parsing, keyboard shortcuts - Add minimap with viewport rectangle, zoom-toward-cursor, loading progress - Show tile/content images in side panel, coordinate labels at high zoom Automation gallery: - 27+ pre-built automation templates (combat, gathering, crafting, trading, utility) - Multi-character selection for batch automation creation - Gallery component with activate dialog Auth & settings: - API key gate with auth provider for token management - Enhanced settings page with token configuration UI improvements: - Game icon component for item/monster/resource images - Character automations panel on character detail page - Equipment grid and inventory grid enhancements - Automations page layout refresh - Bank, exchange page minor fixes - README update with live demo link
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
"""Persistent WebSocket client for the Artifacts MMO game server.
|
|
|
|
Maintains a long-lived connection to ``wss://realtime.artifactsmmo.com``
|
|
and dispatches every incoming game event to the :class:`EventBus` so that
|
|
other components (event handlers, the frontend relay) can react in real
|
|
time.
|
|
|
|
Reconnection is handled automatically with exponential back-off.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
|
|
import websockets
|
|
|
|
from app.websocket.event_bus import EventBus
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GameWebSocketClient:
|
|
"""Persistent WebSocket connection to the Artifacts game server."""
|
|
|
|
WS_URL = "wss://realtime.artifactsmmo.com"
|
|
|
|
def __init__(self, token: str, event_bus: EventBus) -> None:
|
|
self._token = token
|
|
self._event_bus = event_bus
|
|
self._ws: websockets.WebSocketClientProtocol | None = None
|
|
self._task: asyncio.Task | None = None
|
|
self._reconnect_delay = 1.0
|
|
self._max_reconnect_delay = 60.0
|
|
self._running = False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Lifecycle
|
|
# ------------------------------------------------------------------
|
|
|
|
async def start(self) -> asyncio.Task:
|
|
"""Start the persistent WebSocket connection in a background task."""
|
|
self._running = True
|
|
self._task = asyncio.create_task(
|
|
self._connection_loop(),
|
|
name="game-ws-client",
|
|
)
|
|
logger.info("Game WebSocket client starting")
|
|
return self._task
|
|
|
|
async def stop(self) -> None:
|
|
"""Gracefully shut down the WebSocket connection."""
|
|
self._running = False
|
|
if self._ws is not None:
|
|
try:
|
|
await self._ws.close()
|
|
except Exception:
|
|
pass
|
|
if self._task is not None:
|
|
self._task.cancel()
|
|
try:
|
|
await self._task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
logger.info("Game WebSocket client stopped")
|
|
|
|
async def reconnect_with_token(self, token: str) -> None:
|
|
"""Update the token and reconnect."""
|
|
self._token = token
|
|
# Close current connection to trigger reconnect with new token
|
|
if self._ws is not None:
|
|
try:
|
|
await self._ws.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# ------------------------------------------------------------------
|
|
# Connection loop
|
|
# ------------------------------------------------------------------
|
|
|
|
async def _connection_loop(self) -> None:
|
|
"""Reconnect loop with exponential back-off."""
|
|
while self._running:
|
|
try:
|
|
async with websockets.connect(
|
|
self.WS_URL,
|
|
additional_headers={"Authorization": f"Bearer {self._token}"},
|
|
) as ws:
|
|
self._ws = ws
|
|
self._reconnect_delay = 1.0
|
|
logger.info("Game WebSocket connected")
|
|
await self._event_bus.publish("ws_status", {"connected": True})
|
|
|
|
async for message in ws:
|
|
try:
|
|
data = json.loads(message)
|
|
await self._handle_message(data)
|
|
except json.JSONDecodeError:
|
|
logger.warning(
|
|
"Invalid JSON from game WS: %s", message[:100]
|
|
)
|
|
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except websockets.ConnectionClosed:
|
|
logger.warning("Game WebSocket disconnected")
|
|
except Exception:
|
|
logger.exception("Game WebSocket error")
|
|
|
|
self._ws = None
|
|
|
|
if self._running:
|
|
await self._event_bus.publish("ws_status", {"connected": False})
|
|
logger.info("Reconnecting in %.1fs", self._reconnect_delay)
|
|
await asyncio.sleep(self._reconnect_delay)
|
|
self._reconnect_delay = min(
|
|
self._reconnect_delay * 2,
|
|
self._max_reconnect_delay,
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Message dispatch
|
|
# ------------------------------------------------------------------
|
|
|
|
async def _handle_message(self, data: dict) -> None:
|
|
"""Dispatch a game event to the event bus.
|
|
|
|
Game events are published under the key ``game_{type}`` where
|
|
*type* is the value of the ``"type"`` field in the incoming
|
|
message (defaults to ``"unknown"`` if absent).
|
|
"""
|
|
event_type = data.get("type", "unknown")
|
|
await self._event_bus.publish(f"game_{event_type}", data)
|