mirror of
https://github.com/Bratah123/Spirit-PTCGO.git
synced 2026-09-08 20:55:40 -05:00
FEAT: Add quest handling
This commit is contained in:
7
spirit/database/json_data/daily_challenges.json
Normal file
7
spirit/database/json_data/daily_challenges.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"key": "first_win", "title": "Victory Lap", "description": "Win 1 multiplayer match.", "stat": "wins", "target": 1, "coins": 50, "xp": 1},
|
||||
{"key": "damage", "title": "On the Offensive", "description": "Deal 500 damage in multiplayer matches.", "stat": "damagedealt", "target": 500, "coins": 50, "xp": 1},
|
||||
{"key": "energy", "title": "Power Up", "description": "Attach 5 Energy cards from your hand during multiplayer matches.", "stat": "energyplayed", "target": 5, "coins": 40, "xp": 1},
|
||||
{"key": "trainers", "title": "Trainer's Toolkit", "description": "Play 10 Trainer cards in multiplayer matches.", "stat": "trainersplayed", "target": 10, "coins": 40, "xp": 1},
|
||||
{"key": "prizes", "title": "Eyes on the Prize", "description": "Take 6 Prize cards in multiplayer matches.", "stat": "prizecardstaken", "target": 6, "coins": 50, "xp": 1}
|
||||
]
|
||||
@@ -2,6 +2,7 @@ from spirit.database.base import Base
|
||||
from spirit.database.models.account import Account
|
||||
from spirit.database.models.inventory import Wallet, Deck, Collection, ArchetypeFlag
|
||||
from spirit.database.models.social import Friendship
|
||||
from spirit.database.models.quests import QuestAccount, DailyQuest, QuestMatchCredit
|
||||
from spirit.database.models.economy import (
|
||||
RedemptionCode, CodeRedemptionEntry, ShopItem, TradeOffer, DynamicPage, VersusProgress, DailyLoginProgress
|
||||
)
|
||||
@@ -13,5 +14,6 @@ __all__ = [
|
||||
"Base", "Account", "Wallet", "Deck", "Collection", "ArchetypeFlag", "Friendship",
|
||||
"RedemptionCode", "CodeRedemptionEntry", "ShopItem", "TradeOffer", "DynamicPage",
|
||||
"VersusProgress", "DailyLoginProgress",
|
||||
"QuestAccount", "DailyQuest", "QuestMatchCredit",
|
||||
"AsyncTournament", "TournamentEntry", "TournamentLeaderboardClaim"
|
||||
]
|
||||
|
||||
35
spirit/database/models/quests.py
Normal file
35
spirit/database/models/quests.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from spirit.database.base import Base
|
||||
from spirit.database.models.inventory import JSONEncodedDict
|
||||
|
||||
|
||||
class QuestAccount(Base):
|
||||
__tablename__ = "quest_accounts"
|
||||
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.account_id"), primary_key=True)
|
||||
enabled: Mapped[bool] = mapped_column(default=True)
|
||||
selected_date: Mapped[datetime.date | None] = mapped_column(nullable=True)
|
||||
affinity_xp: Mapped[dict] = mapped_column(JSONEncodedDict, default=dict)
|
||||
|
||||
|
||||
class DailyQuest(Base):
|
||||
__tablename__ = "daily_quests"
|
||||
|
||||
quest_id: Mapped[str] = mapped_column(primary_key=True)
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.account_id"), index=True)
|
||||
offered_date: Mapped[datetime.date] = mapped_column()
|
||||
status: Mapped[str] = mapped_column(default="available")
|
||||
definition: Mapped[dict] = mapped_column(JSONEncodedDict)
|
||||
activations: Mapped[int] = mapped_column(default=0)
|
||||
accepted_at: Mapped[float | None] = mapped_column(nullable=True)
|
||||
|
||||
|
||||
class QuestMatchCredit(Base):
|
||||
__tablename__ = "quest_match_credits"
|
||||
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.account_id"), primary_key=True)
|
||||
game_id: Mapped[str] = mapped_column(primary_key=True)
|
||||
160
spirit/database/quests.py
Normal file
160
spirit/database/quests.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Transactional daily challenges and exactly-once match credit."""
|
||||
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from spirit.database import db_session
|
||||
from spirit.database.models import Account, Wallet, QuestAccount, DailyQuest, QuestMatchCredit
|
||||
from spirit.game.quests import AFFINITIES, MAX_ACTIVE, XP_LEVELS, coin_reward, configuration, load_catalog, quest_payload
|
||||
|
||||
|
||||
def utc_now():
|
||||
return dt.datetime.now(dt.timezone.utc)
|
||||
|
||||
|
||||
def _state(session, account_id):
|
||||
if session.query(Account).filter_by(account_id=account_id).first() is None:
|
||||
raise ValueError("Unknown account")
|
||||
state = session.get(QuestAccount, account_id)
|
||||
if state is None:
|
||||
state = QuestAccount(account_id=account_id, enabled=True, affinity_xp={})
|
||||
session.add(state)
|
||||
session.flush()
|
||||
return state
|
||||
|
||||
|
||||
def account_quest_attributes(account_id):
|
||||
with db_session() as session:
|
||||
state = session.get(QuestAccount, account_id)
|
||||
return {
|
||||
"enabled": state.enabled if state else True,
|
||||
"xp": {key: (state.affinity_xp or {}).get(key, 0) if state else 0 for key in AFFINITIES},
|
||||
}
|
||||
|
||||
|
||||
def _snapshot(session, state, now):
|
||||
today = now.date()
|
||||
rows = session.query(DailyQuest).filter_by(account_id=state.account_id).all()
|
||||
active = [row for row in rows if row.status == "active"]
|
||||
for row in rows:
|
||||
if row.status == "available" and row.offered_date != today:
|
||||
row.status = "expired"
|
||||
available = []
|
||||
if state.enabled and state.selected_date != today and len(active) < MAX_ACTIVE:
|
||||
available = [row for row in rows if row.status == "available"]
|
||||
if not available:
|
||||
active_keys = {row.definition["key"] for row in active}
|
||||
candidates = [row for row in load_catalog() if row["key"] not in active_keys]
|
||||
candidates.sort(key=lambda row: hashlib.sha256(
|
||||
f"{state.account_id}:{today}:{row['key']}".encode()).digest())
|
||||
for template in candidates[:2]:
|
||||
quest_id = str(uuid.uuid5(uuid.NAMESPACE_URL,
|
||||
f"spirit:daily:{state.account_id}:{today}:{template['key']}"))
|
||||
row = DailyQuest(quest_id=quest_id, account_id=state.account_id,
|
||||
offered_date=today, status="available",
|
||||
definition=dict(template), activations=0)
|
||||
session.add(row)
|
||||
available.append(row)
|
||||
tomorrow = dt.datetime.combine(today + dt.timedelta(days=1), dt.time.min, dt.timezone.utc)
|
||||
next_time = now if available else tomorrow
|
||||
return {
|
||||
"quests": {
|
||||
"currentQuests": [quest_payload(row) for row in active] if state.enabled else [],
|
||||
"availableQuests": [quest_payload(row) for row in available], "specialQuests": [],
|
||||
},
|
||||
"configuration": configuration(int(next_time.timestamp() * 1000)),
|
||||
}
|
||||
|
||||
|
||||
def get_quests(account_id, *, now=None):
|
||||
now = now or utc_now()
|
||||
with db_session() as session:
|
||||
session.execute(text("BEGIN IMMEDIATE"))
|
||||
return _snapshot(session, _state(session, account_id), now)
|
||||
|
||||
|
||||
def set_enabled(account_id, enabled, *, now=None):
|
||||
now = now or utc_now()
|
||||
with db_session() as session:
|
||||
session.execute(text("BEGIN IMMEDIATE"))
|
||||
state = _state(session, account_id)
|
||||
state.enabled = enabled
|
||||
return _snapshot(session, state, now)
|
||||
|
||||
|
||||
def select_quest(account_id, quest_ids, *, now=None):
|
||||
now = now or utc_now()
|
||||
with db_session() as session:
|
||||
session.execute(text("BEGIN IMMEDIATE"))
|
||||
state = _state(session, account_id)
|
||||
snapshot = _snapshot(session, state, now)
|
||||
valid = {q["questDefinition"]["questID"] for q in snapshot["quests"]["availableQuests"]}
|
||||
if len(quest_ids) != 1 or quest_ids[0] not in valid:
|
||||
return snapshot
|
||||
session.flush()
|
||||
row = session.get(DailyQuest, quest_ids[0])
|
||||
row.status = "active"
|
||||
row.accepted_at = now.timestamp()
|
||||
state.selected_date = now.date()
|
||||
session.flush()
|
||||
return _snapshot(session, state, now)
|
||||
|
||||
|
||||
def abandon_quests(account_id, quest_ids, *, now=None):
|
||||
now = now or utc_now()
|
||||
with db_session() as session:
|
||||
session.execute(text("BEGIN IMMEDIATE"))
|
||||
state = _state(session, account_id)
|
||||
for row in session.query(DailyQuest).filter_by(account_id=account_id, status="active"):
|
||||
if row.quest_id in quest_ids:
|
||||
row.status = "abandoned"
|
||||
session.flush()
|
||||
return _snapshot(session, state, now)
|
||||
|
||||
|
||||
def credit_match(account_id, game_id, stats, won, started_at):
|
||||
"""Applies server-owned counters and grants completed rewards atomically."""
|
||||
result = {"completedQuestsAndXPTotal": [], "progressedQuests": [], "rewards": []}
|
||||
with db_session() as session:
|
||||
session.execute(text("BEGIN IMMEDIATE"))
|
||||
state = _state(session, account_id)
|
||||
if session.get(QuestMatchCredit, (account_id, game_id)) is not None:
|
||||
return result
|
||||
session.add(QuestMatchCredit(account_id=account_id, game_id=game_id))
|
||||
if not state.enabled:
|
||||
return result
|
||||
xp = dict(state.affinity_xp or {})
|
||||
counters = dict(stats)
|
||||
counters["wins"] = int(won)
|
||||
for row in session.query(DailyQuest).filter_by(account_id=account_id, status="active"):
|
||||
if row.accepted_at is None or row.accepted_at > started_at:
|
||||
continue
|
||||
delta = max(0, int(counters.get(row.definition["stat"], 0)))
|
||||
if not delta:
|
||||
continue
|
||||
row.activations = min(row.definition["target"], row.activations + delta)
|
||||
payload = quest_payload(row)
|
||||
if row.activations < row.definition["target"]:
|
||||
result["progressedQuests"].append(payload)
|
||||
continue
|
||||
row.status = "completed"
|
||||
old_xp = xp.get("Colorless", 0)
|
||||
new_xp = min(max(XP_LEVELS.values()), old_xp + row.definition["xp"])
|
||||
xp["Colorless"] = new_xp
|
||||
result["completedQuestsAndXPTotal"].append([payload["questDefinition"], new_xp])
|
||||
result["rewards"].extend(payload["questDefinition"]["rewards"])
|
||||
for level, threshold in XP_LEVELS.items():
|
||||
if old_xp < threshold <= new_xp:
|
||||
result["rewards"].append(coin_reward(25, f"ColorlessLevel{level}"))
|
||||
state.affinity_xp = xp
|
||||
coins = sum(reward["rewardAmount"] for reward in result["rewards"])
|
||||
if coins:
|
||||
wallet = session.get(Wallet, account_id)
|
||||
if wallet is None:
|
||||
wallet = Wallet(account_id=account_id, coins=0, gems=0, tickets=0)
|
||||
session.add(wallet)
|
||||
wallet.coins += coins
|
||||
return result
|
||||
@@ -1,5 +1,6 @@
|
||||
from spirit.game.attributes import AttrID
|
||||
from spirit.database import versus_data
|
||||
from spirit.database.quests import account_quest_attributes
|
||||
from spirit.database.player_data import get_account_settings, get_screen_name, merge_account_settings
|
||||
from spirit.game.season_manager import VersusSeasonManager
|
||||
|
||||
@@ -23,6 +24,7 @@ def build_account_attributes(account_id):
|
||||
"""Full SerializableAccount attribute list (AccountUpdated ReplaceWith
|
||||
swaps the whole set, so every sender must include everything)."""
|
||||
|
||||
quest_state = account_quest_attributes(account_id)
|
||||
season = VersusSeasonManager().get_active_season()
|
||||
points, all_time = versus_data.get_progress(
|
||||
account_id, season.season_id if season else "")
|
||||
@@ -40,4 +42,6 @@ def build_account_attributes(account_id):
|
||||
{"name": AttrID.DECK_SHARE_MODE.value, "value": "Everybody"},
|
||||
{"name": AttrID.SEASON_POINTS.value, "value": points},
|
||||
{"name": AttrID.ALL_TIME_SEASON_POINTS.value, "value": all_time},
|
||||
{"name": AttrID.QUESTS_ENABLED.value, "value": quest_state["enabled"]},
|
||||
{"name": AttrID.QUEST_AFFINITY_XP.value, "value": quest_state["xp"]},
|
||||
]
|
||||
|
||||
@@ -140,6 +140,10 @@ class AttrID(IntEnum):
|
||||
SEASON_POINTS = 201810
|
||||
ALL_TIME_SEASON_POINTS = 201840
|
||||
|
||||
# Native daily challenges and affinity levels
|
||||
QUESTS_ENABLED = 201800
|
||||
QUEST_AFFINITY_XP = 201790
|
||||
|
||||
# Server-persisted client settings dict (Dictionary<int,int>; K.L.GetSetting(n))
|
||||
ACCOUNT_SETTINGS = 10230
|
||||
|
||||
|
||||
75
spirit/game/quests.py
Normal file
75
spirit/game/quests.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Daily challenge definitions and recovered client payloads."""
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
CATALOG_PATH = Path(__file__).resolve().parents[1] / "database/json_data/daily_challenges.json"
|
||||
MAX_ACTIVE = 3
|
||||
XP_LEVELS = {1: 5, 2: 10, 3: 20, 4: 35, 5: 50}
|
||||
AFFINITIES = "Colorless Darkness Dragon Fairy Fighting Fire Grass Lightning Metal Psychic Water".split()
|
||||
STATS = {"wins", "damagedealt", "energyplayed", "trainersplayed", "prizecardstaken"}
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_catalog():
|
||||
"""Loads validated challenge templates; restart the server after editing."""
|
||||
rows = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
|
||||
keys = set()
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or not isinstance(row.get("key"), str) or not row["key"]:
|
||||
raise ValueError("Each daily challenge needs a nonempty key")
|
||||
if row["key"] in keys or row.get("stat") not in STATS:
|
||||
raise ValueError(f"Duplicate key or unsupported stat: {row['key']}")
|
||||
keys.add(row["key"])
|
||||
for name in ("title", "description"):
|
||||
if not isinstance(row.get(name), str) or not row[name]:
|
||||
raise ValueError(f"Challenge {row['key']} needs {name}")
|
||||
for name, minimum in (("target", 1), ("coins", 1), ("xp", 0)):
|
||||
if type(row.get(name)) is not int or not minimum <= row[name] <= 100000:
|
||||
raise ValueError(f"Invalid {name} for challenge {row['key']}")
|
||||
if len(rows) < 2:
|
||||
raise ValueError("Daily challenges need at least two templates")
|
||||
return rows
|
||||
|
||||
|
||||
def coin_reward(amount, source=""):
|
||||
return {
|
||||
"name": "DailyChallengeReward", "rewardType": "Tokens", "rewardAmount": amount,
|
||||
"rewardProductID": None, "rewardCurrency": "Tokens",
|
||||
"rewardDescription": {"id": "QuestCompleted"}, "rewardReason": "QuestCompleted",
|
||||
"selectedFrom": [], "selectedIndex": 0, "rewardSource": source,
|
||||
"index": 0, "openedReward": None,
|
||||
}
|
||||
|
||||
|
||||
def quest_payload(row):
|
||||
template = row.definition
|
||||
return {
|
||||
"questDefinition": {
|
||||
"questID": row.quest_id, "name": f"SpiritDaily_{row.quest_id}",
|
||||
"title": {"id": template["title"]}, "description": {"id": template["description"]},
|
||||
"rewards": [coin_reward(template["coins"], row.quest_id)],
|
||||
"affinity": "Colorless", "xp": template["xp"], "tier": 1,
|
||||
"startTime": None, "endTime": None, "priority": "Bronze", "abandonable": True,
|
||||
"gameModes": ["PvPMatch", "TournamentMatch"],
|
||||
"questInformation": {"id": "Progress counts in multiplayer matches started after accepting. Unfinished challenges carry over; choose one new challenge per UTC day."},
|
||||
},
|
||||
"currentProgress": {
|
||||
"questID": row.quest_id, "activations": row.activations,
|
||||
"requiredActivations": template["target"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def configuration(next_available_ms):
|
||||
return {
|
||||
"xpLevelMap": {str(k): v for k, v in XP_LEVELS.items()},
|
||||
"levelTierMap": {"0": 1, "3": 1, "5": 1, "8": 1, "10": 1},
|
||||
"affinityToAffinityLevelRewardsMap": {
|
||||
affinity: {str(level): [coin_reward(25)] for level in XP_LEVELS}
|
||||
for affinity in AFFINITIES
|
||||
},
|
||||
"nextQuestAvailableTime": next_available_ms,
|
||||
"levelActiveQuestsMap": {"1": -1, "2": -1, "3": 2147483647},
|
||||
}
|
||||
@@ -2897,7 +2897,7 @@ class GameSession:
|
||||
profile = getattr(handler, "player", None) if handler else None
|
||||
if profile is None or getattr(profile, "wallet", None) is None:
|
||||
continue
|
||||
profile.wallet.refresh_wallet()
|
||||
await run_db(profile.wallet.refresh_wallet)
|
||||
await player.send_packet(
|
||||
OutboundMsg.CURRENT_WALLET.value, profile.get_wallet_data()
|
||||
)
|
||||
@@ -2947,6 +2947,7 @@ class GameSession:
|
||||
"index": 0,
|
||||
"openedReward": None,
|
||||
})
|
||||
reward_list.extend(await self._progress_daily_challenges(pid, player, winner_id, reason))
|
||||
envelope = self._sequence_envelope(
|
||||
EMPTY_SEQUENCE_ID,
|
||||
self._build_msg(
|
||||
@@ -2974,6 +2975,29 @@ class GameSession:
|
||||
self.declare_winner(winner_id, reason)
|
||||
raise GameOver()
|
||||
|
||||
async def _progress_daily_challenges(self, pid, player, winner_id, reason):
|
||||
"""Credits multiplayer results before the client's end-game quest animation."""
|
||||
if not isinstance(player, NetworkPlayer) or self.pairing.get("is_solo"):
|
||||
return []
|
||||
if len({p.account_id for p in self.players.values() if isinstance(p, NetworkPlayer)}) < 2:
|
||||
return []
|
||||
if reason == "A game error occurred." or self.turn_state.turn_number == 0:
|
||||
return []
|
||||
from spirit.database.quests import credit_match
|
||||
try:
|
||||
result = await run_db(credit_match, player.account_id, self.game_id,
|
||||
self.game_stats.get(pid, {}), pid == winner_id,
|
||||
self.match_started_at)
|
||||
if result["completedQuestsAndXPTotal"] or result["progressedQuests"]:
|
||||
payload = {key: result[key] for key in ("completedQuestsAndXPTotal", "progressedQuests")}
|
||||
envelope = self._sequence_envelope(EMPTY_SEQUENCE_ID, self._build_msg(
|
||||
OutboundMsg.QUESTS_PROGRESSED.value, payload))
|
||||
await player.send_packet(OutboundMsg.SEQUENCE_MESSAGE.value, envelope)
|
||||
return result["rewards"]
|
||||
except Exception:
|
||||
logging.exception("[Session %s] Daily challenge progress failed for %s", self.game_id, pid)
|
||||
return []
|
||||
|
||||
async def _record_legacy_tournament_result(self, winner_id: str):
|
||||
"""Advances the live Events-scene bracket this game belonged to."""
|
||||
ctx = self.pairing.get("legacy_tournament")
|
||||
|
||||
@@ -66,6 +66,8 @@ def main():
|
||||
# Prime the tournament cache off-loop (handlers only read it afterwards)
|
||||
from spirit.game.tournament_manager import TournamentManager
|
||||
TournamentManager()
|
||||
from spirit.game.quests import load_catalog
|
||||
load_catalog()
|
||||
|
||||
# 1. Ensure asset_map.json exists for first-time setup
|
||||
map_path = "spirit/server/asset_map.json"
|
||||
|
||||
@@ -173,6 +173,9 @@ class InboundMsg(str, Enum):
|
||||
|
||||
# Source: GetQuests
|
||||
GET_QUESTS = "GetQuests"
|
||||
GET_QUEST_CONFIGURATION_DATA = "GetQuestConfigurationData"
|
||||
QUESTS_SELECTED = "QuestsSelected"
|
||||
ABANDON_QUESTS = "AbandonQuests"
|
||||
|
||||
# Source: GetPokemonFamilyMap
|
||||
GET_POKEMON_FAMILY_MAP = "GetPokemonFamilyMap"
|
||||
@@ -537,8 +540,9 @@ class OutboundMsg(str, Enum):
|
||||
NOTIFY_JOIN = "NotifyJoin"
|
||||
NOTIFY_LEAVE = "NotifyLeave"
|
||||
|
||||
# Source: Quests.cs (Guessed)
|
||||
QUESTS = "Quests"
|
||||
# Source: pie/AllQuests.cs and pie/QuestsProgressed.cs.
|
||||
ALL_QUESTS = "AllQuests"
|
||||
QUESTS_PROGRESSED = "QuestsProgressed"
|
||||
|
||||
# Social Notifications
|
||||
NOTIFICATION = "Notification"
|
||||
|
||||
@@ -6,6 +6,7 @@ from spirit.database.accounts import get_account_by_username, create_account, ve
|
||||
from spirit.game.attributes import AttrID
|
||||
from spirit.game.season_manager import VersusSeasonManager
|
||||
from spirit.game.account_attributes import build_account_attributes, anchor_versus_animation
|
||||
from spirit.database.quests import get_quests
|
||||
from .base import BaseHandler, handle
|
||||
from spirit.server.state import consume_ticket, sweep_expired_tickets
|
||||
from .social import SocialHandler
|
||||
@@ -161,9 +162,10 @@ class AuthHandler(BaseHandler):
|
||||
anchor_versus_animation(account_id)
|
||||
attributes = build_account_attributes(account_id)
|
||||
daily_info = process_daily_login(account_id)
|
||||
return player, attributes, daily_info
|
||||
quest_snapshot = get_quests(account_id)
|
||||
return player, attributes, daily_info, quest_snapshot
|
||||
|
||||
player, account_attributes, daily_info = await run_db(_load_login_state)
|
||||
player, account_attributes, daily_info, quest_snapshot = await run_db(_load_login_state)
|
||||
self.client.player = player
|
||||
# Index the authenticated client so presence/challenge/login-guard are O(1).
|
||||
self.client.server.register_account(self.client)
|
||||
@@ -269,13 +271,7 @@ class AuthHandler(BaseHandler):
|
||||
# QuestConfigurationUpdated
|
||||
await self.client.send_packet({
|
||||
"messageName": OutboundMsg.QUEST_CONFIGURATION_UPDATED.value,
|
||||
"questConfiguration": {
|
||||
"xpLevelMap": {"1": 100, "2": 200},
|
||||
"levelTierMap": {"1": 1},
|
||||
"affinityToAffinityLevelRewardsMap": {},
|
||||
"nextQuestAvailableTime": 4102444800000,
|
||||
"levelActiveQuestsMap": {"1": 1}
|
||||
}
|
||||
"questConfiguration": quest_snapshot["configuration"]
|
||||
}, 0)
|
||||
|
||||
# DailyLogin — weeksRewards outer array = days; timestamp = next reward time in ms
|
||||
|
||||
70
spirit/packets/handlers/quests.py
Normal file
70
spirit/packets/handlers/quests.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Native daily challenge request handlers."""
|
||||
|
||||
from spirit.database import quests
|
||||
from spirit.database.async_utils import run_db
|
||||
from spirit.game.account_attributes import build_account_attributes
|
||||
from spirit.network.message_names import InboundMsg, OutboundMsg
|
||||
from .base import BaseHandler, handle
|
||||
|
||||
|
||||
class QuestHandler(BaseHandler):
|
||||
async def _send_snapshot(self, snapshot, request_id):
|
||||
await self.send({"messageName": OutboundMsg.QUEST_CONFIGURATION_UPDATED.value,
|
||||
"questConfiguration": snapshot["configuration"]})
|
||||
await self.send({"messageName": OutboundMsg.ALL_QUESTS.value,
|
||||
**snapshot["quests"]}, request_id)
|
||||
|
||||
@handle(InboundMsg.GET_QUESTS)
|
||||
async def handle_get_quests(self, message, request_id, flags):
|
||||
if self.client.player is None:
|
||||
return
|
||||
snapshot = await run_db(quests.get_quests, self.client.player.account_id)
|
||||
await self._send_snapshot(snapshot, request_id)
|
||||
|
||||
@handle(InboundMsg.GET_QUEST_CONFIGURATION_DATA)
|
||||
async def handle_get_configuration(self, message, request_id, flags):
|
||||
if self.client.player is None:
|
||||
return
|
||||
snapshot = await run_db(quests.get_quests, self.client.player.account_id)
|
||||
await self.send({"messageName": OutboundMsg.QUEST_CONFIGURATION_UPDATED.value,
|
||||
"questConfiguration": snapshot["configuration"]}, request_id)
|
||||
|
||||
@handle(InboundMsg.QUESTS_ENABLED)
|
||||
async def handle_quests_enabled(self, message, request_id, flags):
|
||||
if self.client.player is None or not isinstance(message, dict):
|
||||
return
|
||||
enabled = message.get("enabled")
|
||||
if type(enabled) is not bool:
|
||||
return await self.handle_get_quests(message, request_id, flags)
|
||||
player = self.client.player
|
||||
|
||||
def update_enabled():
|
||||
snapshot = quests.set_enabled(player.account_id, enabled)
|
||||
return snapshot, build_account_attributes(player.account_id)
|
||||
|
||||
snapshot, attributes = await run_db(update_enabled)
|
||||
await self.send({"messageName": OutboundMsg.ACCOUNT_UPDATED.value, "account": {
|
||||
"username": player.username, "accountID": player.account_id, "attributes": attributes,
|
||||
}})
|
||||
await self._send_snapshot(snapshot, request_id)
|
||||
|
||||
@staticmethod
|
||||
def _quest_ids(message):
|
||||
ids = message.get("quests") if isinstance(message, dict) else None
|
||||
return ids if isinstance(ids, list) and all(isinstance(i, str) for i in ids) else []
|
||||
|
||||
@handle(InboundMsg.QUESTS_SELECTED)
|
||||
async def handle_selected(self, message, request_id, flags):
|
||||
if self.client.player is None:
|
||||
return
|
||||
snapshot = await run_db(quests.select_quest, self.client.player.account_id,
|
||||
self._quest_ids(message))
|
||||
await self._send_snapshot(snapshot, request_id)
|
||||
|
||||
@handle(InboundMsg.ABANDON_QUESTS)
|
||||
async def handle_abandoned(self, message, request_id, flags):
|
||||
if self.client.player is None:
|
||||
return
|
||||
snapshot = await run_db(quests.abandon_quests, self.client.player.account_id,
|
||||
self._quest_ids(message))
|
||||
await self._send_snapshot(snapshot, request_id)
|
||||
@@ -135,11 +135,6 @@ class SocialHandler(BaseHandler):
|
||||
for member in self._room_member_clients(room_id):
|
||||
await member.send_packet(notify, 0)
|
||||
|
||||
@handle(InboundMsg.QUESTS_ENABLED)
|
||||
async def handle_quests_enabled(self, message, request_id, flags):
|
||||
logging.info(f"[TCP] [{self.client.addr}] Client checking if Quests are enabled.")
|
||||
pass
|
||||
|
||||
@handle(InboundMsg.GET_FRIEND_ROSTER)
|
||||
async def handle_get_friend_roster(self, message, request_id, flags):
|
||||
logging.info(f"[TCP] [{self.client.addr}] Client requested Friend Roster.")
|
||||
@@ -309,11 +304,3 @@ class SocialHandler(BaseHandler):
|
||||
"messageName": OutboundMsg.FRIEND_ERROR.value,
|
||||
"error": message
|
||||
}, request_id)
|
||||
|
||||
@handle(InboundMsg.GET_QUESTS)
|
||||
async def handle_get_quests(self, message, request_id, flags):
|
||||
logging.info(f"[TCP] [{self.client.addr}] Client requested Quests.")
|
||||
await self.send({
|
||||
"messageName": OutboundMsg.QUESTS.value,
|
||||
"quests": []
|
||||
}, request_id)
|
||||
|
||||
@@ -14,6 +14,7 @@ from .handlers.trade import TradeHandler
|
||||
from .handlers.matchmaking import MatchmakingHandler
|
||||
from .handlers.gameplay import GameplayHandler
|
||||
from .handlers.tournaments import TournamentHandler
|
||||
from .handlers.quests import QuestHandler
|
||||
|
||||
HANDLER_CLASSES = [
|
||||
HandshakeHandler,
|
||||
@@ -27,6 +28,7 @@ HANDLER_CLASSES = [
|
||||
MatchmakingHandler,
|
||||
GameplayHandler,
|
||||
TournamentHandler,
|
||||
QuestHandler,
|
||||
]
|
||||
|
||||
class PacketRouter:
|
||||
|
||||
Reference in New Issue
Block a user