artifacts-dashboard/backend/app/websocket/client.py
Paweł Orzech 10781c7987
Some checks failed
Release / release (push) Has been cancelled
v0.2.0: Rich interactive map, automation gallery, auth & UX improvements
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
2026-03-01 20:18:29 +01:00

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)