artifacts-dashboard/backend/app/engine/cooldown.py
Paweł Orzech f845647934
Some checks failed
Release / release (push) Has been cancelled
Initial release: Artifacts MMO Dashboard & Automation Platform
Full-stack dashboard for controlling, automating, and analyzing
Artifacts MMO characters via the game's HTTP API.

Backend (FastAPI):
- Async Artifacts API client with rate limiting and retry
- 6 automation strategies (combat, gathering, crafting, trading, task, leveling)
- Automation engine with runner, manager, cooldown tracker, pathfinder
- WebSocket relay (game server -> frontend)
- Game data cache, character snapshots, price history, analytics
- 9 API routers, 7 database tables, 3 Alembic migrations
- 108 unit tests

Frontend (Next.js 15 + shadcn/ui):
- Live character dashboard with HP/XP bars and cooldowns
- Character detail with stats, equipment, inventory, skills, manual actions
- Automation management with live log streaming
- Interactive canvas map with content-type coloring and zoom/pan
- Bank management, Grand Exchange with price charts
- Events, logs, analytics pages with Recharts
- WebSocket auto-reconnect with query cache invalidation
- Settings page, error boundaries, dark theme

Infrastructure:
- Docker Compose (dev + prod)
- GitHub Actions CI/CD
- Documentation (Architecture, Automation, Deployment, API)
2026-03-01 19:46:45 +01:00

90 lines
3.2 KiB
Python

import asyncio
import logging
from datetime import datetime, timedelta, timezone
logger = logging.getLogger(__name__)
# Safety buffer added after every cooldown to avoid 499 "action already in progress" errors
_BUFFER_SECONDS: float = 0.1
class CooldownTracker:
"""Track per-character cooldowns with a safety buffer.
The Artifacts MMO API returns cooldown information after every action.
This tracker stores the expiry timestamp for each character and provides
an async ``wait`` method that sleeps until the cooldown has elapsed plus
a small buffer (100 ms) to prevent race-condition 499 errors.
"""
def __init__(self) -> None:
self._cooldowns: dict[str, datetime] = {}
def update(
self,
character_name: str,
cooldown_seconds: float,
cooldown_expiration: str | None = None,
) -> None:
"""Record the cooldown from an action response.
Parameters
----------
character_name:
The character whose cooldown is being updated.
cooldown_seconds:
Total cooldown duration in seconds (used as fallback).
cooldown_expiration:
ISO-8601 timestamp of when the cooldown expires (preferred).
"""
if cooldown_expiration:
try:
expiry = datetime.fromisoformat(cooldown_expiration)
# Ensure timezone-aware
if expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=timezone.utc)
except (ValueError, TypeError):
logger.warning(
"Failed to parse cooldown_expiration %r for %s, using duration fallback",
cooldown_expiration,
character_name,
)
expiry = datetime.now(timezone.utc) + timedelta(seconds=cooldown_seconds)
else:
expiry = datetime.now(timezone.utc) + timedelta(seconds=cooldown_seconds)
self._cooldowns[character_name] = expiry
logger.debug(
"Cooldown for %s set to %s (%.1fs)",
character_name,
expiry.isoformat(),
cooldown_seconds,
)
async def wait(self, character_name: str) -> None:
"""Sleep until the character's cooldown has expired plus a safety buffer."""
expiry = self._cooldowns.get(character_name)
if expiry is None:
return
now = datetime.now(timezone.utc)
remaining = (expiry - now).total_seconds() + _BUFFER_SECONDS
if remaining > 0:
logger.debug("Waiting %.2fs for %s cooldown", remaining, character_name)
await asyncio.sleep(remaining)
def is_ready(self, character_name: str) -> bool:
"""Return True if the character has no active cooldown."""
expiry = self._cooldowns.get(character_name)
if expiry is None:
return True
return datetime.now(timezone.utc) >= expiry
def remaining(self, character_name: str) -> float:
"""Return remaining cooldown seconds (0 if ready)."""
expiry = self._cooldowns.get(character_name)
if expiry is None:
return 0.0
delta = (expiry - datetime.now(timezone.utc)).total_seconds()
return max(delta, 0.0)