diff --git a/spirit/database/json_data/daily_challenges.json b/spirit/database/json_data/daily_challenges.json new file mode 100644 index 0000000..d1c0539 --- /dev/null +++ b/spirit/database/json_data/daily_challenges.json @@ -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} +] diff --git a/spirit/database/models/__init__.py b/spirit/database/models/__init__.py index c6887f0..fab032d 100644 --- a/spirit/database/models/__init__.py +++ b/spirit/database/models/__init__.py @@ -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" ] diff --git a/spirit/database/models/quests.py b/spirit/database/models/quests.py new file mode 100644 index 0000000..b995f2b --- /dev/null +++ b/spirit/database/models/quests.py @@ -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) diff --git a/spirit/database/quests.py b/spirit/database/quests.py new file mode 100644 index 0000000..e36c21c --- /dev/null +++ b/spirit/database/quests.py @@ -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 diff --git a/spirit/game/account_attributes.py b/spirit/game/account_attributes.py index e76f982..3e1a72a 100644 --- a/spirit/game/account_attributes.py +++ b/spirit/game/account_attributes.py @@ -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"]}, ] diff --git a/spirit/game/attributes.py b/spirit/game/attributes.py index 7672417..83f68e0 100644 --- a/spirit/game/attributes.py +++ b/spirit/game/attributes.py @@ -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; K.L.GetSetting(n)) ACCOUNT_SETTINGS = 10230 diff --git a/spirit/game/quests.py b/spirit/game/quests.py new file mode 100644 index 0000000..4dfae7c --- /dev/null +++ b/spirit/game/quests.py @@ -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}, + } diff --git a/spirit/game/session/game_session.py b/spirit/game/session/game_session.py index 10ffd29..24ad3e4 100644 --- a/spirit/game/session/game_session.py +++ b/spirit/game/session/game_session.py @@ -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") diff --git a/spirit/main.py b/spirit/main.py index 45d59e0..7365d17 100644 --- a/spirit/main.py +++ b/spirit/main.py @@ -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" diff --git a/spirit/network/message_names.py b/spirit/network/message_names.py index d73bf2d..39f5a50 100644 --- a/spirit/network/message_names.py +++ b/spirit/network/message_names.py @@ -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" diff --git a/spirit/packets/handlers/auth.py b/spirit/packets/handlers/auth.py index ebf7721..38f6184 100644 --- a/spirit/packets/handlers/auth.py +++ b/spirit/packets/handlers/auth.py @@ -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 diff --git a/spirit/packets/handlers/quests.py b/spirit/packets/handlers/quests.py new file mode 100644 index 0000000..ee07e2c --- /dev/null +++ b/spirit/packets/handlers/quests.py @@ -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) diff --git a/spirit/packets/handlers/social.py b/spirit/packets/handlers/social.py index 55a1335..0840f9f 100644 --- a/spirit/packets/handlers/social.py +++ b/spirit/packets/handlers/social.py @@ -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) diff --git a/spirit/packets/router.py b/spirit/packets/router.py index 6882b31..5ac0d81 100644 --- a/spirit/packets/router.py +++ b/spirit/packets/router.py @@ -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: