gacha patch 2
This commit is contained in:
1 parent
3d49c65de3
commit
b1ff45794a
5 files changed
+331
-2
No files matched your search
@@ -89,4 +89,6 @@ FILTER_LABELS: dict[str, int | None] = {"All": None, "3★": 3, "2★": 2, "1★
|
|||||||
FONT_PATHS = [
|
FONT_PATHS = [
|
||||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", # Debian (fonts-dejavu-core)
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
]
|
]
|
||||||
+18
-1
@@ -412,6 +412,22 @@ async def fetch_banners(session: Optional[aiohttp.ClientSession] = None) -> Opti
|
|||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def banner_unavailable_reason(banner: dict) -> Optional[str]:
|
||||||
|
"""Return why a banner can't be pulled right now, or None if it's live.
|
||||||
|
|
||||||
|
Banners without timestamps are treated as permanent.
|
||||||
|
"""
|
||||||
|
now_ms = datetime.now(timezone.utc).timestamp() * 1000
|
||||||
|
started = banner.get("startedAt", 0)
|
||||||
|
ended = banner.get("endedAt", 0)
|
||||||
|
if started and now_ms < started:
|
||||||
|
start_dt = datetime.fromtimestamp(started / 1000, tz=timezone.utc)
|
||||||
|
return f"This banner hasn't started yet (starts {start_dt.strftime('%b %d')})."
|
||||||
|
if ended and now_ms >= ended:
|
||||||
|
return "This banner has ended."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def format_banner_embed(banner: dict, index: int) -> str:
|
def format_banner_embed(banner: dict, index: int) -> str:
|
||||||
"""Format a single banner as an embed field value string."""
|
"""Format a single banner as an embed field value string."""
|
||||||
gtype = banner.get("gachaType", "PickupGacha")
|
gtype = banner.get("gachaType", "PickupGacha")
|
||||||
@@ -427,8 +443,9 @@ def format_banner_embed(banner: dict, index: int) -> str:
|
|||||||
dates = "Permanent"
|
dates = "Permanent"
|
||||||
|
|
||||||
is_fes = " [FES - 6% 3*!]" if gtype == "FesGacha" else ""
|
is_fes = " [FES - 6% 3*!]" if gtype == "FesGacha" else ""
|
||||||
|
upcoming = " [UPCOMING]" if started and started > datetime.now(timezone.utc).timestamp() * 1000 else ""
|
||||||
return (
|
return (
|
||||||
f"**#{index + 1} — {gtype}{is_fes}**\n"
|
f"**#{index + 1} — {gtype}{is_fes}{upcoming}**\n"
|
||||||
f"Rate-up: {rateups}\n"
|
f"Rate-up: {rateups}\n"
|
||||||
f"{dates}"
|
f"{dates}"
|
||||||
)
|
)
|
||||||
+51
-1
@@ -4,6 +4,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -15,10 +16,12 @@ from .data import (
|
|||||||
db,
|
db,
|
||||||
fetch_banners,
|
fetch_banners,
|
||||||
format_banner_embed,
|
format_banner_embed,
|
||||||
|
banner_unavailable_reason,
|
||||||
roll_rarity,
|
roll_rarity,
|
||||||
get_rates_for_banner,
|
get_rates_for_banner,
|
||||||
)
|
)
|
||||||
from .gacha_renderer import render_pull
|
from .gacha_renderer import render_pull
|
||||||
|
from .health import collect_health, OK, INFO
|
||||||
from .constants import SPARK_TARGET, BANNER_FILE, DEFAULT_RATES, PULL10_RATES, GACHA_ANIM_PATH, GACHA_ANIM_DURATION
|
from .constants import SPARK_TARGET, BANNER_FILE, DEFAULT_RATES, PULL10_RATES, GACHA_ANIM_PATH, GACHA_ANIM_DURATION
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -69,6 +72,10 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._banner_cache: dict = {"current": [], "upcoming": [], "ended": []}
|
self._banner_cache: dict = {"current": [], "upcoming": [], "ended": []}
|
||||||
self._cache_ready = asyncio.Event()
|
self._cache_ready = asyncio.Event()
|
||||||
|
# Refresh bookkeeping, reported by !gacha health
|
||||||
|
self._last_refresh_at: Optional[float] = None
|
||||||
|
self._last_refresh_ok: Optional[bool] = None
|
||||||
|
self._last_good_at: Optional[float] = None
|
||||||
|
|
||||||
# ── Lifecycle ───────────────────────────────────────────────────────
|
# ── Lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -87,14 +94,17 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
"""Refresh banners, then merge any new students they might feature."""
|
"""Refresh banners, then merge any new students they might feature."""
|
||||||
try:
|
try:
|
||||||
banners = await fetch_banners()
|
banners = await fetch_banners()
|
||||||
|
self._last_refresh_ok = banners is not None
|
||||||
if banners is None:
|
if banners is None:
|
||||||
log.warning("Banner refresh failed; keeping previous banner data")
|
log.warning("Banner refresh failed; keeping previous banner data")
|
||||||
else:
|
else:
|
||||||
self._banner_cache = banners
|
self._banner_cache = banners
|
||||||
|
self._last_good_at = time.time()
|
||||||
await db.merge_ennead(self._banner_cache)
|
await db.merge_ennead(self._banner_cache)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Banner refresh failed")
|
log.exception("Banner refresh failed")
|
||||||
finally:
|
finally:
|
||||||
|
self._last_refresh_at = time.time()
|
||||||
self._cache_ready.set()
|
self._cache_ready.set()
|
||||||
|
|
||||||
async def _periodic_refresh(self) -> None:
|
async def _periodic_refresh(self) -> None:
|
||||||
@@ -167,7 +177,8 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
"`!gacha` — list current banners\n"
|
"`!gacha` — list current banners\n"
|
||||||
"`!gacha pick <n>` — select banner #n\n"
|
"`!gacha pick <n>` — select banner #n\n"
|
||||||
"`!gacha pick regular` — permanent pool\n"
|
"`!gacha pick regular` — permanent pool\n"
|
||||||
"`!gacha info` — your status & spark"
|
"`!gacha info` — your status & spark\n"
|
||||||
|
"`!gacha health` — check everything is working"
|
||||||
),
|
),
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
@@ -229,6 +240,10 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
banner = banners[idx]
|
banner = banners[idx]
|
||||||
|
reason = banner_unavailable_reason(banner)
|
||||||
|
if reason:
|
||||||
|
await ctx.reply(reason)
|
||||||
|
return
|
||||||
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
||||||
gtype = banner.get("gachaType", "PickupGacha")
|
gtype = banner.get("gachaType", "PickupGacha")
|
||||||
|
|
||||||
@@ -280,6 +295,13 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
"Pick another with `!gacha pick <n>` (see `!gacha`) or `!gacha pick regular`."
|
"Pick another with `!gacha pick <n>` (see `!gacha`) or `!gacha pick regular`."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
reason = banner_unavailable_reason(banner)
|
||||||
|
if reason:
|
||||||
|
await ctx.reply(
|
||||||
|
f"{reason}\n"
|
||||||
|
"Pick another with `!gacha pick <n>` (see `!gacha`) or `!gacha pick regular`."
|
||||||
|
)
|
||||||
|
return
|
||||||
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
||||||
gtype = banner.get("gachaType", "PickupGacha")
|
gtype = banner.get("gachaType", "PickupGacha")
|
||||||
banner_name = f"{gtype} — {rateups}"
|
banner_name = f"{gtype} — {rateups}"
|
||||||
@@ -420,6 +442,10 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
"Your selected banner has ended or is unavailable right now, so it can't be sparked."
|
"Your selected banner has ended or is unavailable right now, so it can't be sparked."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
reason = banner_unavailable_reason(banner)
|
||||||
|
if reason:
|
||||||
|
await ctx.reply(reason)
|
||||||
|
return
|
||||||
rateups = [n.lower() for n in banner.get("rateups", [])]
|
rateups = [n.lower() for n in banner.get("rateups", [])]
|
||||||
if student["Name"].lower() not in db.resolve_rateups(rateups):
|
if student["Name"].lower() not in db.resolve_rateups(rateups):
|
||||||
rateup_str = ", ".join(banner.get("rateups", []))
|
rateup_str = ", ".join(banner.get("rateups", []))
|
||||||
@@ -445,6 +471,30 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
f"Remaining Recruitment Points: {remaining}"
|
f"Remaining Recruitment Points: {remaining}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@commands.cooldown(1, 30, commands.BucketType.user)
|
||||||
|
@gacha.command(name="health")
|
||||||
|
async def gacha_health(self, ctx: commands.Context) -> None:
|
||||||
|
"""Run live checks on everything a pull depends on."""
|
||||||
|
async with ctx.typing():
|
||||||
|
rows = await collect_health(self, ctx.author.id, await _get_banner_id(ctx.author.id))
|
||||||
|
|
||||||
|
bad = [r for r in rows if r[0] not in (OK, INFO)]
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Gacha Health — all checks passed" if not bad else f"Gacha Health — {len(bad)} issue(s)",
|
||||||
|
color=0x57F287 if not bad else 0xED4245,
|
||||||
|
timestamp=discord.utils.utcnow(),
|
||||||
|
)
|
||||||
|
sections: dict[str, list[str]] = {}
|
||||||
|
for status, section, text in rows:
|
||||||
|
sections.setdefault(section, []).append(f"{status} {text}")
|
||||||
|
for section, lines in sections.items():
|
||||||
|
value = "\n".join(lines)
|
||||||
|
if len(value) > 1024:
|
||||||
|
value = value[:1020] + " …"
|
||||||
|
embed.add_field(name=section, value=value, inline=False)
|
||||||
|
embed.set_footer(text="Every line was checked just now")
|
||||||
|
await ctx.reply(embed=embed)
|
||||||
|
|
||||||
@gacha.command(name="info")
|
@gacha.command(name="info")
|
||||||
async def gacha_info(self, ctx: commands.Context) -> None:
|
async def gacha_info(self, ctx: commands.Context) -> None:
|
||||||
"""Show your active banner, spark count, and collection summary."""
|
"""Show your active banner, spark count, and collection summary."""
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#this runs live health checks for the gacha system, used by !gacha health##
|
||||||
|
"""Gacha health checks — every row is a check actually run just now.
|
||||||
|
|
||||||
|
Nothing here changes bot state: live data is fetched and compared, never
|
||||||
|
written into the banner cache or the roster.
|
||||||
|
"""
|
||||||
|
######################################################################
|
||||||
|
import asyncio
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from . import db as gacha_db
|
||||||
|
from .data import db, fetch_banners, banner_unavailable_reason
|
||||||
|
from .gacha_renderer import render_pull
|
||||||
|
from .constants import CHARACTER_API, BG_PATH, GACHA_ANIM_PATH, FONT_PATHS
|
||||||
|
|
||||||
|
OK, WARN, FAIL, INFO = "✅", "⚠️", "❌", "ℹ️"
|
||||||
|
REFRESH_INTERVAL = 6 * 3600 # must match BlueArchiveGacha._periodic_refresh
|
||||||
|
MAX_PORTRAIT_PROBES = 6
|
||||||
|
BANNER_TIMEOUT = 20
|
||||||
|
|
||||||
|
Row = tuple[str, str, str] # (status, section, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _ago(ts: Optional[float]) -> str:
|
||||||
|
return f"<t:{int(ts)}:R>" if ts else "never"
|
||||||
|
|
||||||
|
|
||||||
|
async def _probe_image(session: aiohttp.ClientSession, url: str) -> Optional[str]:
|
||||||
|
"""Fetch and decode an image exactly like the renderer. None = success."""
|
||||||
|
try:
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
return f"HTTP {resp.status}"
|
||||||
|
data = await resp.read()
|
||||||
|
Image.open(io.BytesIO(data)).convert("RGBA")
|
||||||
|
return None
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return "timed out after 5s"
|
||||||
|
except Exception as e:
|
||||||
|
return type(e).__name__
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_banners(cog) -> tuple[list[Row], list[dict]]:
|
||||||
|
"""Cached banner state (what pulls use) + a live fetch compared against it."""
|
||||||
|
rows: list[Row] = []
|
||||||
|
s = "Banners"
|
||||||
|
cache = cog._banner_cache
|
||||||
|
cached = cache.get("current", []) + cache.get("upcoming", [])
|
||||||
|
|
||||||
|
if cog._last_good_at is None:
|
||||||
|
rows.append((FAIL, s, "Banner data has never been fetched successfully — every pull falls back to no rate-up"))
|
||||||
|
elif cog._last_refresh_ok is False:
|
||||||
|
rows.append((WARN, s, f"Last refresh {_ago(cog._last_refresh_at)} FAILED; pulls use data from {_ago(cog._last_good_at)}"))
|
||||||
|
else:
|
||||||
|
rows.append((OK, s, f"Pull data refreshed {_ago(cog._last_good_at)} ({len(cached)} current/upcoming)"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
live = await asyncio.wait_for(fetch_banners(), timeout=BANNER_TIMEOUT)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
live = None
|
||||||
|
rows.append((FAIL, s, f"Banner API check now: timed out after {BANNER_TIMEOUT}s"))
|
||||||
|
else:
|
||||||
|
if live is None:
|
||||||
|
rows.append((FAIL, s, "Banner API check now: request failed (see bot log)"))
|
||||||
|
if live is not None:
|
||||||
|
live_list = live.get("current", []) + live.get("upcoming", [])
|
||||||
|
if {str(b.get("id")) for b in live_list} == {str(b.get("id")) for b in cached}:
|
||||||
|
rows.append((OK, s, "Banner API check now: reachable, matches pull data"))
|
||||||
|
else:
|
||||||
|
rows.append((WARN, s, f"Banner API check now: reachable, but has {len(live_list)} banners vs {len(cached)} "
|
||||||
|
"in pull data — pulls use stale data until the next refresh"))
|
||||||
|
|
||||||
|
now_ms = time.time() * 1000
|
||||||
|
live_banners = [b for b in cached if banner_unavailable_reason(b) is None]
|
||||||
|
upcoming = [b for b in cached if b.get("startedAt", 0) > now_ms]
|
||||||
|
ended = len(cached) - len(live_banners) - len(upcoming)
|
||||||
|
rows.append((INFO, s, f"{len(live_banners)} live now, {len(upcoming)} upcoming"
|
||||||
|
+ (f", {ended} ended but still listed (blocked from pulls)" if ended else "")))
|
||||||
|
return rows, live_banners
|
||||||
|
|
||||||
|
|
||||||
|
def _check_rateups(live_banners: list[dict]) -> tuple[list[Row], list[dict]]:
|
||||||
|
"""Run each live banner through the exact pool builder pulls use."""
|
||||||
|
rows: list[Row] = []
|
||||||
|
s = "Rate-ups (live banners)"
|
||||||
|
students: list[dict] = []
|
||||||
|
if not live_banners:
|
||||||
|
rows.append((INFO, s, "No live banners to check"))
|
||||||
|
return rows, students
|
||||||
|
|
||||||
|
for b in live_banners[:6]:
|
||||||
|
pool = db.build_pool(b)
|
||||||
|
pooled = {st["Name"].lower() for r in pool.values() for st in r}
|
||||||
|
label = f"#{b.get('id')} {b.get('gachaType', '?')}"
|
||||||
|
problems: list[tuple[str, str]] = [] # (status, text)
|
||||||
|
for name in b.get("rateups", []):
|
||||||
|
st = db.get_by_name(name, strict=True)
|
||||||
|
if not st:
|
||||||
|
problems.append((FAIL, f"{name}: not in roster, can't be pulled"))
|
||||||
|
elif st["Name"].lower() not in pooled:
|
||||||
|
problems.append((FAIL, f"{name}: marked welfare, excluded from pulls"))
|
||||||
|
else:
|
||||||
|
students.append(st)
|
||||||
|
if st["StarGrade"] < 3:
|
||||||
|
problems.append((WARN, f"{name}: {st['StarGrade']}★, rate-up boost only applies to 3★"))
|
||||||
|
if problems:
|
||||||
|
status = FAIL if any(p[0] == FAIL for p in problems) else WARN
|
||||||
|
rows.append((status, s, f"{label}: " + "; ".join(p[1] for p in problems)))
|
||||||
|
else:
|
||||||
|
names = ", ".join(b.get("rateups", [])) or "no rate-ups"
|
||||||
|
rows.append((OK, s, f"{label}: all pullable ({names})"))
|
||||||
|
if len(live_banners) > 6:
|
||||||
|
rows.append((INFO, s, f"{len(live_banners) - 6} more live banners not checked"))
|
||||||
|
return rows, students
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_portraits(students: list[dict]) -> list[Row]:
|
||||||
|
"""Probe wiki art and the sprite fallback separately for rate-up students."""
|
||||||
|
s = "Portraits"
|
||||||
|
if not students:
|
||||||
|
return [(INFO, s, "No live rate-up students to probe")]
|
||||||
|
seen, sample = set(), []
|
||||||
|
for st in students:
|
||||||
|
if st["Name"] not in seen:
|
||||||
|
seen.add(st["Name"])
|
||||||
|
sample.append(st)
|
||||||
|
sample = sample[:MAX_PORTRAIT_PROBES]
|
||||||
|
|
||||||
|
rows: list[Row] = []
|
||||||
|
timeout = aiohttp.ClientTimeout(total=30, connect=5) # same as render_pull
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
results = await asyncio.gather(*(
|
||||||
|
asyncio.gather(_probe_image(session, db.cdn_portrait(st)), _probe_image(session, db.cdn_sprite(st)))
|
||||||
|
for st in sample
|
||||||
|
))
|
||||||
|
for st, (wiki, sprite) in zip(sample, results):
|
||||||
|
if wiki is None:
|
||||||
|
rows.append((OK, s, f"{st['Name']}: wiki art OK"))
|
||||||
|
elif sprite is None:
|
||||||
|
rows.append((WARN, s, f"{st['Name']}: wiki art failed ({wiki}), sprite fallback OK"))
|
||||||
|
else:
|
||||||
|
rows.append((FAIL, s, f"{st['Name']}: no art (wiki {wiki}, sprite {sprite}) — card renders blank"))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_roster() -> list[Row]:
|
||||||
|
s = "Roster"
|
||||||
|
std = db.build_pool(None) # exactly what Regular Recruitment pulls from
|
||||||
|
welfare = sum(1 for st in db.students if (st.get("IsLimited") or 0) >= 2)
|
||||||
|
limited = sum(1 for st in db.students if (st.get("IsLimited") or 0) == 1)
|
||||||
|
rows: list[Row] = [(OK if db.students else FAIL, s,
|
||||||
|
f"{len(db.students)} students loaded — standard pool 3★ {len(std[3])}, "
|
||||||
|
f"2★ {len(std[2])}, 1★ {len(std[1])}; {limited} limited (Fes/own banner only); "
|
||||||
|
f"{welfare} welfare (never pulled)")]
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(CHARACTER_API, timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
rows.append((FAIL, s, f"Character API check now: HTTP {resp.status}"))
|
||||||
|
return rows
|
||||||
|
chars = await resp.json()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
rows.append((FAIL, s, "Character API check now: timed out after 15s"))
|
||||||
|
return rows
|
||||||
|
except Exception as e:
|
||||||
|
rows.append((FAIL, s, f"Character API check now: {type(e).__name__}"))
|
||||||
|
return rows
|
||||||
|
missing = [c.get("name", "?") for c in chars if c.get("id") not in db.by_id]
|
||||||
|
if missing:
|
||||||
|
shown = ", ".join(missing[:5]) + (f" +{len(missing) - 5} more" if len(missing) > 5 else "")
|
||||||
|
rows.append((WARN, s, f"Character API check now: {len(missing)} not loaded yet ({shown}); "
|
||||||
|
"added on next refresh"))
|
||||||
|
else:
|
||||||
|
rows.append((OK, s, f"Character API check now: reachable, all {len(chars)} loaded"))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _check_refresh_task(cog) -> list[Row]:
|
||||||
|
s = "Background refresh"
|
||||||
|
t = getattr(cog, "_refresh_task", None)
|
||||||
|
if t is None:
|
||||||
|
return [(FAIL, s, "Refresh task was never started — banners will never update")]
|
||||||
|
if t.cancelled():
|
||||||
|
return [(FAIL, s, "Refresh task was cancelled — banners will not update")]
|
||||||
|
if t.done():
|
||||||
|
return [(FAIL, s, f"Refresh task crashed: {t.exception()!r} — banners will not update")]
|
||||||
|
due = (cog._last_refresh_at or 0) + REFRESH_INTERVAL
|
||||||
|
return [(OK, s, f"Running, next refresh <t:{int(due)}:R>")]
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_storage(user_id: int) -> list[Row]:
|
||||||
|
s = "Storage & render"
|
||||||
|
rows: list[Row] = []
|
||||||
|
try:
|
||||||
|
await gacha_db.get_inventory_stats(user_id)
|
||||||
|
rows.append((OK, s, "Inventory database: read OK (writes not tested)"))
|
||||||
|
except Exception as e:
|
||||||
|
rows.append((FAIL, s, f"Inventory database read failed: {type(e).__name__}"))
|
||||||
|
|
||||||
|
if any(Path(fp).exists() for fp in FONT_PATHS):
|
||||||
|
rows.append((OK, s, "Font found"))
|
||||||
|
else:
|
||||||
|
rows.append((WARN, s, "No font found — render uses tiny default font"))
|
||||||
|
rows.append((OK, s, "Background image found") if BG_PATH.exists()
|
||||||
|
else (WARN, s, "Background image missing — plain colour used"))
|
||||||
|
rows.append((OK, s, "Pull animation found") if GACHA_ANIM_PATH.exists()
|
||||||
|
else (WARN, s, "Pull animation missing — text teaser used"))
|
||||||
|
|
||||||
|
sample = next(iter(db.by_rarity.get(3) or db.students), None)
|
||||||
|
if sample is None:
|
||||||
|
rows.append((FAIL, s, "Render test skipped: no students loaded"))
|
||||||
|
return rows
|
||||||
|
try:
|
||||||
|
start = time.monotonic()
|
||||||
|
await asyncio.wait_for(render_pull([sample], "Health check", 0), timeout=30)
|
||||||
|
rows.append((OK, s, f"Render test OK in {time.monotonic() - start:.1f}s (art checked under Portraits)"))
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
rows.append((FAIL, s, "Render test timed out after 30s"))
|
||||||
|
except Exception as e:
|
||||||
|
rows.append((FAIL, s, f"Render test failed: {type(e).__name__}: {e}"))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _check_user_banner(cog, banner_id: Optional[str]) -> list[Row]:
|
||||||
|
s = "Your banner"
|
||||||
|
if not banner_id or banner_id == "regular":
|
||||||
|
return [(OK, s, "Regular Recruitment")]
|
||||||
|
for b in cog._all_banners():
|
||||||
|
if str(b.get("id")) == banner_id:
|
||||||
|
reason = banner_unavailable_reason(b)
|
||||||
|
if reason:
|
||||||
|
return [(WARN, s, f"#{banner_id}: {reason} Pick another with `!gacha pick`")]
|
||||||
|
return [(OK, s, f"#{banner_id} {b.get('gachaType', '?')} is live")]
|
||||||
|
return [(WARN, s, f"#{banner_id} is no longer listed — `!pull` will ask you to pick another")]
|
||||||
|
|
||||||
|
|
||||||
|
async def collect_health(cog, user_id: int, banner_id: Optional[str]) -> list[Row]:
|
||||||
|
"""Run every check. Network checks run concurrently."""
|
||||||
|
(banner_rows, live_banners), roster_rows, storage_rows = await asyncio.gather(
|
||||||
|
_check_banners(cog), _check_roster(), _check_storage(user_id),
|
||||||
|
)
|
||||||
|
rateup_rows, students = _check_rateups(live_banners)
|
||||||
|
portrait_rows = await _check_portraits(students)
|
||||||
|
return (
|
||||||
|
banner_rows + rateup_rows + portrait_rows + roster_rows
|
||||||
|
+ _check_refresh_task(cog) + storage_rows + _check_user_banner(cog, banner_id)
|
||||||
|
)
|
||||||
@@ -251,6 +251,13 @@ class Inventory(commands.Cog):
|
|||||||
view = InventoryView(uid, ctx.author.id)
|
view = InventoryView(uid, ctx.author.id)
|
||||||
await ctx.reply(embed=await view._build_dashboard(), view=view)
|
await ctx.reply(embed=await view._build_dashboard(), view=view)
|
||||||
|
|
||||||
|
@commands.command(name="eligma")
|
||||||
|
async def eligma(self, ctx: commands.Context) -> None:
|
||||||
|
"""Show your Eligma balance (advertised in !gacha help)."""
|
||||||
|
view = InventoryView(ctx.author.id, ctx.author.id)
|
||||||
|
view.stop() # only used to build the embed, no buttons attached
|
||||||
|
await ctx.reply(embed=await view._build_eligma_page())
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: commands.Bot) -> None:
|
||||||
await bot.add_cog(Inventory(bot))
|
await bot.add_cog(Inventory(bot))
|
||||||
Reference in new issue
Block a user