diff --git a/docs/ext/dota2/api.rst b/docs/ext/dota2/api.rst new file mode 100644 index 00000000..18f0ce2d --- /dev/null +++ b/docs/ext/dota2/api.rst @@ -0,0 +1,28 @@ +.. currentmodule:: steam.ext.dota2 + +API Reference +=============== + +The following section outlines the API of steam.py's Dota 2 extension module. + + +Client +------ + +.. attributetable:: Client + +.. autoclass:: Client + :members: + :inherited-members: + +Bot +---- + +``ext.dota2`` also provides a :class:`Bot` class, which is a subclass of :class:`Client` and :class:`steam.ext.commands.Bot`. + +.. attributetable:: steam.ext.dota2.Bot + +.. autoclass:: steam.ext.dota2.Bot + :members: + :inherited-members: + diff --git a/docs/ext/dota2/index.md b/docs/ext/dota2/index.md new file mode 100644 index 00000000..6fbd5903 --- /dev/null +++ b/docs/ext/dota2/index.md @@ -0,0 +1,11 @@ +(steam-ext-dota2)= + +# `steam.ext.dota2` - A Dota 2 Game Coordinator client + +This extension offers the ability to interact with the Dota 2 Game Coordinator (Dota 2 GC) + +```{toctree} +:maxdepth: 2 + +api +``` diff --git a/docs/index.md b/docs/index.md index b72456e6..1824db82 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,6 +29,7 @@ steam API Reference steam.ext.commands API Reference steam.ext.csgo API Reference steam.ext.tf2 API Reference +steam.ext.dota2 API Reference ``` ## Extensions @@ -41,4 +42,5 @@ steam.py has extension modules to help with common tasks. ext/commands/index.rst ext/csgo/index.rst ext/tf2/index.rst +ext/dota2/index.rst ``` diff --git a/steam/ext/dota2/__init__.py b/steam/ext/dota2/__init__.py new file mode 100644 index 00000000..bbca0a53 --- /dev/null +++ b/steam/ext/dota2/__init__.py @@ -0,0 +1,12 @@ +""" +steam.ext.dota2 +~~~~~~~~~~~~~~ + +A library for interacting with the Dota 2 Game Coordinator. + +Licensed under The MIT License (MIT) - Copyright (c) 2020-present James H-B. See LICENSE +""" + +from .client import * +from .enums import * +from .models import * diff --git a/steam/ext/dota2/client.py b/steam/ext/dota2/client.py new file mode 100644 index 00000000..335133ea --- /dev/null +++ b/steam/ext/dota2/client.py @@ -0,0 +1,186 @@ +"""Licensed under The MIT License (MIT) - Copyright (c) 2020-present James H-B. See LICENSE""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final, overload + +from ..._const import DOCS_BUILDING +from ..._gc import Client as Client_ +from ...app import DOTA2 +from ...ext import commands +from ...utils import ( + MISSING, + cached_property, +) +from .models import ClientUser, LiveMatch, MatchMinimal, PartialMatch, PartialUser +from .state import GCState # noqa: TCH001 + +if TYPE_CHECKING: + from ...types.id import Intable + from .enums import Hero + from .models import User + +__all__ = ( + "Client", + "Bot", +) + + +class Client(Client_): + """Represents a client connection that connects to Steam. This class is used to interact with the Steam API, CMs + and the Dota 2 Game Coordinator. + + :class:`Client` is a subclass of :class:`steam.Client`, so whatever you can do with :class:`steam.Client` you can + do with :class:`Client`. + """ + + _APP: Final = DOTA2 + _ClientUserCls = ClientUser + _state: GCState # type: ignore # PEP 705 + + if TYPE_CHECKING: + + @cached_property + def user(self) -> ClientUser: ... + + # TODO: maybe this should exist as a part of the whole lib (?) + def instantiate_partial_user(self, id: Intable) -> PartialUser: + return self._state.get_partial_user(id) + + def instantiate_partial_match(self, id: int) -> PartialMatch: + """Instantiate partial match. + + Convenience method, allows using match related requests to gc like `match.details` + for any match. + """ + return PartialMatch(self._state, id) + + async def top_live_matches(self, *, hero: Hero = MISSING, limit: int = 100) -> list[LiveMatch]: + """Fetch top live matches. + + This is similar to game list in the Watch Tab of Dota 2 game app. + "Top matches" in this context means + * featured tournament matches + * highest average MMR matches + + Parameters + ---------- + hero + Filter matches by Hero. Note, in this case Game Coordinator still only uses current top100 live matches, + i.e. requesting "filter by Muerta" results only in subset of those matches in which + Muerta is currently being played. It does not look into lower MMR match than top100 to extend the return + list to number of games from `limit` argument. This behavior is consistent with how Watch Tab works. + limit + Maximum amount of matches to fetch. This works rather as a boundary limit than "number of matches" to + fetch, i.e. Dota 2 will sometimes give 90 matches when `limit` is 100. + Or even "successfully" return 0 matches. + + Returns + ------- + List of currently live top matches. + + Raises + ------ + ValueError + `limit` value should be between 1 and 100 inclusively. + asyncio.TimeoutError + Request time-outed. The reason is usually Dota 2 Game Coordinator lagging or being down. + """ + if limit < 1 or limit > 100: + raise ValueError("limit value should be between 1 and 100 inclusively.") + + protos = await self._state.fetch_top_source_tv_games( + start_game=(limit - 1) // 10 * 10, # mini-math: limit 100 -> start_game 90, 91 -> 90, 90 -> 80 + hero_id=hero.value if hero else 0, + ) + live_matches = [LiveMatch(self._state, match) for proto in protos for match in proto.game_list] + # still need to slice the list, i.e. limit = 85, but live_matches above will have 90 matches + return live_matches[:limit] + + async def tournament_live_matches(self, league_id: int) -> list[LiveMatch]: + """Fetch currently live tournament matches + + Parameters + ---------- + league_id + Tournament league_id + + Returns + ------- + List of currently live tournament matches. + + Raises + ------ + asyncio.TimeoutError + Request time-outed. The reason is usually Dota 2 Game Coordinator lagging or being down. + """ + + protos = await self._state.fetch_top_source_tv_games(league_id=league_id) + # TODO: ^ this will only fetch 10 games because of implementation... + # but there is no good way to know if there gonna be more than 10 games + # but does any tournament play more than 10 games at once? :x + return [LiveMatch(self._state, match) for proto in protos for match in proto.game_list] + + @overload + async def live_matches(self, *, lobby_id: int = ...) -> LiveMatch: ... + + @overload + async def live_matches(self, *, lobby_ids: list[int] = ...) -> list[LiveMatch]: ... + + async def live_matches(self, *, lobby_id: int = MISSING, lobby_ids: list[int] = MISSING): + """Fetch currently live matches by lobby_ids + + Parameters + ---------- + lobby_ids + Lobby IDs + + Returns + ------- + List of live matches. + + Raises + ------ + asyncio.TimeoutError + Request time-outed. The reason is usually Dota 2 Game Coordinator lagging or being down. + """ + if lobby_id is not MISSING and lobby_ids is not MISSING: + raise TypeError("Cannot mix lobby_id and lobby_ids keyword arguments.") + + lobby_ids = [lobby_id] if lobby_id else lobby_ids + + protos = await self._state.fetch_top_source_tv_games( + start_game=(len(lobby_ids) - 1) // 10 * 10, + lobby_ids=lobby_ids, + ) + live_matches = [LiveMatch(self._state, match) for proto in protos for match in proto.game_list] + if lobby_id: + try: + return live_matches[0] + except IndexError: + # can happen even with valid lobby_id if it's private + raise RuntimeError(f"Failed to fetch match with {lobby_id=}") + else: + return live_matches + + async def matches_minimal(self, match_id: int) -> list[MatchMinimal]: + proto = await self._state.fetch_matches_minimal(match_ids=[match_id]) + return [MatchMinimal(self._state, match) for match in proto.matches] + + async def matchmaking_stats(self): + proto = await self._state.fetch_matchmaking_stats() + return proto # TODO: Modelize (I never figured out what regions are which) + + if TYPE_CHECKING or DOCS_BUILDING: + + def get_user(self, id: Intable) -> User | None: ... + + async def fetch_user(self, id: Intable) -> User: ... + + +class Bot(commands.Bot, Client): + """Represents a Steam bot. + + :class:`Bot` is a subclass of :class:`~steam.ext.commands.Bot`, so whatever you can do with + :class:`~steam.ext.commands.Bot` you can do with :class:`Bot`. + """ diff --git a/steam/ext/dota2/enums.py b/steam/ext/dota2/enums.py new file mode 100644 index 00000000..1585d7dc --- /dev/null +++ b/steam/ext/dota2/enums.py @@ -0,0 +1,1388 @@ +"""Licensed under The MIT License (MIT) - Copyright (c) 2020-present James H-B. See LICENSE""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ...enums import IntEnum, classproperty + +if TYPE_CHECKING: + from collections.abc import Mapping + + from typing_extensions import Self + +__all__ = ( + "Hero", + "GameMode", + "LobbyType", + "MatchOutcome", + "RankTier", +) + + +# fmt: off +class Hero(IntEnum): + """Enum representing Dota 2 hero. + + Primarily, mapping hero_id to hero name. + """ + NONE = 0 + AntiMage = 1 + Axe = 2 + Bane = 3 + Bloodseeker = 4 + CrystalMaiden = 5 + DrowRanger = 6 + Earthshaker = 7 + Juggernaut = 8 + Mirana = 9 + Morphling = 10 + ShadowFiend = 11 + PhantomLancer = 12 + Puck = 13 + Pudge = 14 + Razor = 15 + SandKing = 16 + StormSpirit = 17 + Sven = 18 + Tiny = 19 + VengefulSpirit = 20 + Windranger = 21 + Zeus = 22 + Kunkka = 23 + Lina = 25 + Lion = 26 + ShadowShaman = 27 + Slardar = 28 + Tidehunter = 29 + WitchDoctor = 30 + Lich = 31 + Riki = 32 + Enigma = 33 + Tinker = 34 + Sniper = 35 + Necrophos = 36 + Warlock = 37 + Beastmaster = 38 + QueenOfPain = 39 + Venomancer = 40 + FacelessVoid = 41 + WraithKing = 42 + DeathProphet = 43 + PhantomAssassin = 44 + Pugna = 45 + TemplarAssassin = 46 + Viper = 47 + Luna = 48 + DragonKnight = 49 + Dazzle = 50 + Clockwerk = 51 + Leshrac = 52 + NaturesProphet = 53 + Lifestealer = 54 + DarkSeer = 55 + Clinkz = 56 + Omniknight = 57 + Enchantress = 58 + Huskar = 59 + NightStalker = 60 + Broodmother = 61 + BountyHunter = 62 + Weaver = 63 + Jakiro = 64 + Batrider = 65 + Chen = 66 + Spectre = 67 + AncientApparition = 68 + Doom = 69 + Ursa = 70 + SpiritBreaker = 71 + Gyrocopter = 72 + Alchemist = 73 + Invoker = 74 + Silencer = 75 + OutworldDestroyer = 76 + Lycan = 77 + Brewmaster = 78 + ShadowDemon = 79 + LoneDruid = 80 + ChaosKnight = 81 + Meepo = 82 + TreantProtector = 83 + OgreMagi = 84 + Undying = 85 + Rubick = 86 + Disruptor = 87 + NyxAssassin = 88 + NagaSiren = 89 + KeeperOfTheLight = 90 + Io = 91 + Visage = 92 + Slark = 93 + Medusa = 94 + TrollWarlord = 95 + CentaurWarrunner = 96 + Magnus = 97 + Timbersaw = 98 + Bristleback = 99 + Tusk = 100 + SkywrathMage = 101 + Abaddon = 102 + ElderTitan = 103 + LegionCommander = 104 + Techies = 105 + EmberSpirit = 106 + EarthSpirit = 107 + Underlord = 108 + Terrorblade = 109 + Phoenix = 110 + Oracle = 111 + WinterWyvern = 112 + ArcWarden = 113 + MonkeyKing = 114 + DarkWillow = 119 + Pangolier = 120 + Grimstroke = 121 + Hoodwink = 123 + VoidSpirit = 126 + Snapfire = 128 + Mars = 129 + Ringmaster = 131 + Dawnbreaker = 135 + Marci = 136 + PrimalBeast = 137 + Muerta = 138 + Kez = 145 + + @classproperty + def DISPLAY_NAMES(cls: type[Self]) -> Mapping[Hero, str]: # type: ignore + return { + cls.NONE : "None", # happens when player disconnects or hasn't picked yet. + cls.AntiMage : "Anti-Mage", + cls.Axe : "Axe", + cls.Bane : "Bane", + cls.Bloodseeker : "Bloodseeker", + cls.CrystalMaiden : "Crystal Maiden", + cls.DrowRanger : "Drow Ranger", + cls.Earthshaker : "Earthshaker", + cls.Juggernaut : "Juggernaut", + cls.Mirana : "Mirana", + cls.Morphling : "Morphling", + cls.ShadowFiend : "Shadow Fiend", + cls.PhantomLancer : "Phantom Lancer", + cls.Puck : "Puck", + cls.Pudge : "Pudge", + cls.Razor : "Razor", + cls.SandKing : "Sand King", + cls.StormSpirit : "Storm Spirit", + cls.Sven : "Sven", + cls.Tiny : "Tiny", + cls.VengefulSpirit : "Vengeful Spirit", + cls.Windranger : "Windranger", + cls.Zeus : "Zeus", + cls.Kunkka : "Kunkka", + cls.Lina : "Lina", + cls.Lion : "Lion", + cls.ShadowShaman : "Shadow Shaman", + cls.Slardar : "Slardar", + cls.Tidehunter : "Tidehunter", + cls.WitchDoctor : "Witch Doctor", + cls.Lich : "Lich", + cls.Riki : "Riki", + cls.Enigma : "Enigma", + cls.Tinker : "Tinker", + cls.Sniper : "Sniper", + cls.Necrophos : "Necrophos", + cls.Warlock : "Warlock", + cls.Beastmaster : "Beastmaster", + cls.QueenOfPain : "Queen of Pain", + cls.Venomancer : "Venomancer", + cls.FacelessVoid : "Faceless Void", + cls.WraithKing : "Wraith King", + cls.DeathProphet : "Death Prophet", + cls.PhantomAssassin : "Phantom Assassin", + cls.Pugna : "Pugna", + cls.TemplarAssassin : "Templar Assassin", + cls.Viper : "Viper", + cls.Luna : "Luna", + cls.DragonKnight : "Dragon Knight", + cls.Dazzle : "Dazzle", + cls.Clockwerk : "Clockwerk", + cls.Leshrac : "Leshrac", + cls.NaturesProphet : "Nature's Prophet", + cls.Lifestealer : "Lifestealer", + cls.DarkSeer : "Dark Seer", + cls.Clinkz : "Clinkz", + cls.Omniknight : "Omniknight", + cls.Enchantress : "Enchantress", + cls.Huskar : "Huskar", + cls.NightStalker : "Night Stalker", + cls.Broodmother : "Broodmother", + cls.BountyHunter : "Bounty Hunter", + cls.Weaver : "Weaver", + cls.Jakiro : "Jakiro", + cls.Batrider : "Batrider", + cls.Chen : "Chen", + cls.Spectre : "Spectre", + cls.AncientApparition: "Ancient Apparition", + cls.Doom : "Doom", + cls.Ursa : "Ursa", + cls.SpiritBreaker : "Spirit Breaker", + cls.Gyrocopter : "Gyrocopter", + cls.Alchemist : "Alchemist", + cls.Invoker : "Invoker", + cls.Silencer : "Silencer", + cls.OutworldDestroyer: "Outworld Destroyer", + cls.Lycan : "Lycan", + cls.Brewmaster : "Brewmaster", + cls.ShadowDemon : "Shadow Demon", + cls.LoneDruid : "Lone Druid", + cls.ChaosKnight : "Chaos Knight", + cls.Meepo : "Meepo", + cls.TreantProtector : "Treant Protector", + cls.OgreMagi : "Ogre Magi", + cls.Undying : "Undying", + cls.Rubick : "Rubick", + cls.Disruptor : "Disruptor", + cls.NyxAssassin : "Nyx Assassin", + cls.NagaSiren : "Naga Siren", + cls.KeeperOfTheLight : "Keeper of the Light", + cls.Io : "Io", + cls.Visage : "Visage", + cls.Slark : "Slark", + cls.Medusa : "Medusa", + cls.TrollWarlord : "Troll Warlord", + cls.CentaurWarrunner : "Centaur Warrunner", + cls.Magnus : "Magnus", + cls.Timbersaw : "Timbersaw", + cls.Bristleback : "Bristleback", + cls.Tusk : "Tusk", + cls.SkywrathMage : "Skywrath Mage", + cls.Abaddon : "Abaddon", + cls.ElderTitan : "Elder Titan", + cls.LegionCommander : "Legion Commander", + cls.Techies : "Techies", + cls.EmberSpirit : "Ember Spirit", + cls.EarthSpirit : "Earth Spirit", + cls.Underlord : "Underlord", + cls.Terrorblade : "Terrorblade", + cls.Phoenix : "Phoenix", + cls.Oracle : "Oracle", + cls.WinterWyvern : "Winter Wyvern", + cls.ArcWarden : "Arc Warden", + cls.MonkeyKing : "Monkey King", + cls.DarkWillow : "Dark Willow", + cls.Pangolier : "Pangolier", + cls.Grimstroke : "Grimstroke", + cls.Hoodwink : "Hoodwink", + cls.VoidSpirit : "Void Spirit", + cls.Snapfire : "Snapfire", + cls.Mars : "Mars", + cls.Ringmaster : "Ringmaster", + cls.Dawnbreaker : "Dawnbreaker", + cls.Marci : "Marci", + cls.PrimalBeast : "Primal Beast", + cls.Muerta : "Muerta", + cls.Kez : "Kez", + } + + @property + def display_name(self) -> str: + return self.DISPLAY_NAMES[self] + + @property + def id(self) -> int: + return self.value + + def __bool__(self) -> bool: # type: ignore # idk I need `Hero.NONE` to be `False` + return bool(self.value) + + +class GameMode(IntEnum): # source: dota_shared_enums.proto + NONE = 0 + AllPick = 1 + CaptainsMode = 2 + RandomDraft = 3 + SingleDraft = 4 + AllRandom = 5 + Intro = 6 + Diretide = 7 + ReverseCaptainsMode = 8 + Frostivus = 9 + Tutorial = 10 + MidOnly = 11 + LeastPlayed = 12 + NewPlayerMode = 13 + CompendiumMatch = 14 + Custom = 15 + CaptainsDraft = 16 + BalancedDraft = 17 + AbilityDraft = 18 + Event = 19 + AllRandomDeathMatch = 20 + Mid1v1 = 21 + AllDraft = 22 + Turbo = 23 + Mutation = 24 + CoachesChallenge = 25 + + @classproperty + def DISPLAY_NAMES(cls: type[Self]) -> Mapping[GameMode, str]: # type: ignore + return { + cls.NONE : "None", + cls.AllPick : "All Pick", + cls.CaptainsMode : "Captain's Mode", + cls.RandomDraft : "Random Draft", + cls.SingleDraft : "Single Draft", + cls.AllRandom : "All Random", + cls.Intro : "Intro", + cls.Diretide : "Diretide", + cls.ReverseCaptainsMode: "Reverse Captain's Mode", + cls.Frostivus : "Frostivus", # XMAS, The Greeviling + cls.Tutorial : "Tutorial", + cls.MidOnly : "Mid Only", + cls.LeastPlayed : "Least Played", + cls.NewPlayerMode : "New Player Mode", + cls.CompendiumMatch : "Compendium Match", + cls.Custom : "Custom Game", + cls.CaptainsDraft : "Captain's Draft", + cls.BalancedDraft : "Balanced Draft", + cls.AbilityDraft : "Ability Draft", + cls.Event : "Event Game", + cls.AllRandomDeathMatch: "All Random DeathMatch", + cls.Mid1v1 : "1v1 Mid Only", + cls.AllDraft : "All Draft", # Ranked Matchmaking + cls.Turbo : "Turbo", + cls.Mutation : "Mutation", + cls.CoachesChallenge : "Coaches Challenge", + } + + @property + def display_name(self) -> str: + return self.DISPLAY_NAMES[self] + + +class LobbyType(IntEnum): # source: dota_gcmessages_common_lobby.proto + Invalid = -1 + Unranked = 0 + Practice = 1 + CoopBotMatch = 4 + Ranked = 7 + BattleCup = 9 + LocalBotMatch = 10 + Spectator = 11 + EventGameMode = 12 + NewPlayerMode = 14 + FeaturedGameMode = 15 + + @classproperty + def DISPLAY_NAMES(cls: type[Self]) -> Mapping[LobbyType, str]: # type: ignore + return { + cls.Invalid : "Invalid", + cls.Unranked : "Unranked", + cls.Practice : "Practice", + cls.CoopBotMatch : "Coop Bots", + cls.Ranked : "Ranked", + cls.BattleCup : "Battle Cup", + cls.LocalBotMatch : "Local Bot Match", + cls.Spectator : "Spectator", + cls.EventGameMode : "Event", + cls.NewPlayerMode : "New Player Mode", + cls.FeaturedGameMode: "Featured Gamemode", + } + + @property + def display_name(self) -> str: + return self.DISPLAY_NAMES[self] + + +class RankTier(IntEnum): + """Enum representing Dota 2 Rank Tier. + + Commonly called "ranked medals". + """ + Uncalibrated = 0 + Herald1 = 11 + Herald2 = 12 + Herald3 = 13 + Herald4 = 14 + Herald5 = 15 + Guardian1 = 21 + Guardian2 = 22 + Guardian3 = 23 + Guardian4 = 24 + Guardian5 = 25 + Crusader1 = 31 + Crusader2 = 32 + Crusader3 = 33 + Crusader4 = 34 + Crusader5 = 35 + Archon1 = 41 + Archon2 = 42 + Archon3 = 43 + Archon4 = 44 + Archon5 = 45 + Legend1 = 51 + Legend2 = 52 + Legend3 = 53 + Legend4 = 54 + Legend5 = 55 + Ancient1 = 61 + Ancient2 = 62 + Ancient3 = 63 + Ancient4 = 64 + Ancient5 = 65 + Divine1 = 71 + Divine2 = 72 + Divine3 = 73 + Divine4 = 74 + Divine5 = 75 + Immortal = 80 + + @property + def division(self) -> str: + if self.value % 10 == 0: + return self.name + else: + return self.name[:-1] + + @property + def stars(self) -> str: + return self.value % 10 + + # do we need it as a factory helper method? + @property + def display_name(self) -> str: + suffix = f' {self.stars}' if self.stars else '' + return self.division + suffix + + +class MatchOutcome(IntEnum): # source: dota_shared_enums.proto + """Represents Match Outcome.""" + Unknown = 0 + RadiantVictory = 2 + DireVictory = 3 + NeutralVictory = 4 + NoTeamWinner = 5 + Custom1Victory = 6 + Custom2Victory = 7 + Custom3Victory = 8 + Custom4Victory = 9 + Custom5Victory = 10 + Custom6Victory = 11 + Custom7Victory = 12 + Custom8Victory = 13 + NotScoredPoorNetworkConditions = 64 + NotScoredLeaver = 65 + NotScoredServerCrash = 66 + NotScoredNeverStarted = 67 + NotScoredCanceled = 68 + NotScoredSuspicious = 69 + + +class EMsg(IntEnum): + # EGCBaseClientMsg - source: gcsystemmsgs.proto + PingRequest = 3001 + PingResponse = 3002 + GCToClientPollConvarRequest = 3003 + GCToClientPollConvarResponse = 3004 + CompressedMsgToClient = 3005 + CompressedMsgToClient_Legacy = 523 + GCToClientRequestDropped = 3006 + ClientWelcome = 4004 + ServerWelcome = 4005 + ClientHello = 4006 + ServerHello = 4007 + ClientConnectionStatus = 4009 + ServerConnectionStatus = 4010 + + # EDOTAGCMsg - source: dota_gcmessages_msgid.proto + DOTABase = 7000 + GameMatchSignOut = 7004 + GameMatchSignOutResponse = 7005 + JoinChatChannel = 7009 + JoinChatChannelResponse = 7010 + OtherJoinedChannel = 7013 + OtherLeftChannel = 7014 + ServerToGCRequestStatus = 7026 + StartFindingMatch = 7033 + ConnectedPlayers = 7034 + AbandonCurrentGame = 7035 + StopFindingMatch = 7036 + PracticeLobbyCreate = 7038 + PracticeLobbyLeave = 7040 + PracticeLobbyLaunch = 7041 + PracticeLobbyList = 7042 + PracticeLobbyListResponse = 7043 + PracticeLobbyJoin = 7044 + PracticeLobbySetDetails = 7046 + PracticeLobbySetTeamSlot = 7047 + InitialQuestionnaireResponse = 7049 + PracticeLobbyResponse = 7055 + BroadcastNotification = 7056 + LiveScoreboardUpdate = 7057 + RequestChatChannelList = 7060 + RequestChatChannelListResponse = 7061 + ReadyUp = 7070 + KickedFromMatchmakingQueue = 7071 + LeaverDetected = 7072 + SpectateFriendGame = 7073 + SpectateFriendGameResponse = 7074 + ReportsRemainingRequest = 7076 + ReportsRemainingResponse = 7077 + SubmitPlayerReport = 7078 + SubmitPlayerReportResponse = 7079 + PracticeLobbyKick = 7081 + SubmitPlayerReportV2 = 7082 + SubmitPlayerReportResponseV2 = 7083 + RequestSaveGames = 7084 + RequestSaveGamesServer = 7085 + RequestSaveGamesResponse = 7086 + LeaverDetectedResponse = 7087 + PlayerFailedToConnect = 7088 + GCToRelayConnect = 7089 + GCToRelayConnectresponse = 7090 + WatchGame = 7091 + WatchGameResponse = 7092 + BanStatusRequest = 7093 + BanStatusResponse = 7094 + MatchDetailsRequest = 7095 + MatchDetailsResponse = 7096 + CancelWatchGame = 7097 + Popup = 7102 + FriendPracticeLobbyListRequest = 7111 + FriendPracticeLobbyListResponse = 7112 + PracticeLobbyJoinResponse = 7113 + CreateTeam = 7115 + CreateTeamResponse = 7116 + TeamInviteInviterToGC = 7122 + TeamInviteImmediateResponseToInviter = 7123 + TeamInviteRequestToInvitee = 7124 + TeamInviteInviteeResponseToGC = 7125 + TeamInviteResponseToInviter = 7126 + TeamInviteResponseToInvitee = 7127 + KickTeamMember = 7128 + KickTeamMemberResponse = 7129 + LeaveTeam = 7130 + LeaveTeamResponse = 7131 + ApplyTeamToPracticeLobby = 7142 + TransferTeamAdmin = 7144 + PracticeLobbyJoinBroadcastChannel = 7149 + TournamentItemEvent = 7150 + TournamentItemEventResponse = 7151 + TeamFanfare = 7156 + ResponseTeamFanfare = 7157 + GameServerUploadSaveGame = 7158 + GameServerSaveGameResult = 7159 + GameServerGetLoadGame = 7160 + GameServerGetLoadGameResult = 7161 + EditTeamDetails = 7166 + EditTeamDetailsResponse = 7167 + ReadyUpStatus = 7170 + GCToGCMatchCompleted = 7186 + BalancedShuffleLobby = 7188 + MatchmakingStatsRequest = 7197 + MatchmakingStatsResponse = 7198 + BotGameCreate = 7199 + SetMatchHistoryAccess = 7200 + SetMatchHistoryAccessResponse = 7201 + UpgradeLeagueItem = 7203 + UpgradeLeagueItemResponse = 7204 + WatchDownloadedReplay = 7206 + ClientsRejoinChatChannels = 7217 + GCToGCGetUserChatInfo = 7218 + GCToGCGetUserChatInfoResponse = 7219 + GCToGCLeaveAllChatChannels = 7220 + GCToGCUpdateAccountChatBan = 7221 + GCToGCCanInviteUserToTeam = 7234 + GCToGCCanInviteUserToTeamResponse = 7235 + GCToGCGetUserRank = 7236 + GCToGCGetUserRankResponse = 7237 + GCToGCAdjustUserRank = 7238 + GCToGCAdjustUserRankResponse = 7239 + GCToGCUpdateTeamStats = 7240 + GCToGCValidateTeam = 7241 + GCToGCValidateTeamResponse = 7242 + GCToGCGetLeagueAdmin = 7255 + GCToGCGetLeagueAdminResponse = 7256 + LeaveChatChannel = 7272 + ChatMessage = 7273 + GetHeroStandings = 7274 + GetHeroStandingsResponse = 7275 + ItemEditorReservationsRequest = 7283 + ItemEditorReservationsResponse = 7284 + ItemEditorReserveItemDef = 7285 + ItemEditorReserveItemDefResponse = 7286 + ItemEditorReleaseReservation = 7287 + ItemEditorReleaseReservationResponse = 7288 + RewardTutorialPrizes = 7289 + FantasyLivePlayerStats = 7308 + FantasyFinalPlayerStats = 7309 + FlipLobbyTeams = 7320 + GCToGCEvaluateReportedPlayer = 7322 + GCToGCEvaluateReportedPlayerResponse = 7323 + GCToGCProcessPlayerReportForTarget = 7324 + GCToGCProcessReportSuccess = 7325 + NotifyAccountFlagsChange = 7326 + SetProfilePrivacy = 7327 + SetProfilePrivacyResponse = 7328 + ClientSuspended = 7342 + PartyMemberSetCoach = 7343 + PracticeLobbySetCoach = 7346 + ChatModeratorBan = 7359 + LobbyUpdateBroadcastChannelInfo = 7367 + GCToGCGrantTournamentItem = 7372 + GCToGCUpgradeTwitchViewerItems = 7375 + GCToGCGetLiveMatchAffiliates = 7376 + GCToGCGetLiveMatchAffiliatesResponse = 7377 + GCToGCUpdatePlayerPennantCounts = 7378 + GCToGCGetPlayerPennantCounts = 7379 + GCToGCGetPlayerPennantCountsResponse = 7380 + GameMatchSignOutPermissionRequest = 7381 + GameMatchSignOutPermissionResponse = 7382 + AwardEventPoints = 7384 + GetEventPoints = 7387 + GetEventPointsResponse = 7388 + PartyLeaderWatchGamePrompt = 7397 + CompendiumSetSelection = 7405 + CompendiumDataRequest = 7406 + CompendiumDataResponse = 7407 + GetPlayerMatchHistory = 7408 + GetPlayerMatchHistoryResponse = 7409 + GCToGCMatchmakingAddParty = 7410 + GCToGCMatchmakingRemoveParty = 7411 + GCToGCMatchmakingRemoveAllParties = 7412 + GCToGCMatchmakingMatchFound = 7413 + GCToGCUpdateMatchManagementStats = 7414 + GCToGCUpdateMatchmakingStats = 7415 + GCToServerPingRequest = 7416 + GCToServerPingResponse = 7417 + GCToServerEvaluateToxicChat = 7418 + ServerToGCEvaluateToxicChat = 7419 + ServerToGCEvaluateToxicChatResponse = 7420 + GCToGCProcessMatchLeaver = 7426 + NotificationsRequest = 7427 + NotificationsResponse = 7428 + GCToGCModifyNotification = 7429 + LeagueAdminList = 7434 + NotificationsMarkReadRequest = 7435 + ServerToGCRequestBatchPlayerResources = 7450 + ServerToGCRequestBatchPlayerResourcesResponse = 7451 + CompendiumSetSelectionResponse = 7453 + PlayerInfoSubmit = 7456 + PlayerInfoSubmitResponse = 7457 + GCToGCGetAccountLevel = 7458 + GCToGCGetAccountLevelResponse = 7459 + DOTAGetWeekendTourneySchedule = 7464 + DOTAWeekendTourneySchedule = 7465 + JoinableCustomGameModesRequest = 7466 + JoinableCustomGameModesResponse = 7467 + JoinableCustomLobbiesRequest = 7468 + JoinableCustomLobbiesResponse = 7469 + QuickJoinCustomLobby = 7470 + QuickJoinCustomLobbyResponse = 7471 + GCToGCGrantEventPointAction = 7472 + GCToGCSetCompendiumSelection = 7478 + HasItemQuery = 7484 + HasItemResponse = 7485 + GCToGCGrantEventPointActionMsg = 7488 + GCToGCGetCompendiumSelections = 7492 + GCToGCGetCompendiumSelectionsResponse = 7493 + ServerToGCMatchConnectionStats = 7494 + GCToClientTournamentItemDrop = 7495 + SQLDelayedGrantLeagueDrop = 7496 + ServerGCUpdateSpectatorCount = 7497 + GCToGCEmoticonUnlock = 7501 + SignOutDraftInfo = 7502 + ClientToGCEmoticonDataRequest = 7503 + GCToClientEmoticonData = 7504 + PracticeLobbyToggleBroadcastChannelCameramanStatus = 7505 + RedeemItem = 7518 + RedeemItemResponse = 7519 + ClientToGCGetAllHeroProgress = 7521 + ClientToGCGetAllHeroProgressResponse = 7522 + GCToGCGetServerForClient = 7523 + GCToGCGetServerForClientResponse = 7524 + SQLProcessTournamentGameOutcome = 7525 + SQLGrantTrophyToAccount = 7526 + ClientToGCGetTrophyList = 7527 + ClientToGCGetTrophyListResponse = 7528 + GCToClientTrophyAwarded = 7529 + GCGameBotMatchSignOut = 7530 + GCGameBotMatchSignOutPermissionRequest = 7531 + SignOutBotInfo = 7532 + GCToGCUpdateProfileCards = 7533 + ClientToGCGetProfileCard = 7534 + ClientToGCGetProfileCardResponse = 7535 + ClientToGCGetBattleReport = 7536 + ClientToGCGetBattleReportResponse = 7537 + ClientToGCSetProfileCardSlots = 7538 + GCToClientProfileCardUpdated = 7539 + ServerToGCVictoryPredictions = 7540 + ClientToGCGetBattleReportAggregateStats = 7541 + ClientToGCGetBattleReportAggregateStatsResponse = 7542 + ClientToGCGetBattleReportInfo = 7543 + ClientToGCGetBattleReportInfoResponse = 7544 + SignOutCommunicationSummary = 7545 + ServerToGCRequestStatus_Response = 7546 + ClientToGCCreateHeroStatue = 7547 + GCToClientHeroStatueCreateResult = 7548 + GCToLANServerRelayConnect = 7549 + ClientToGCAcknowledgeBattleReport = 7550 + ClientToGCAcknowledgeBattleReportResponse = 7551 + ClientToGCGetBattleReportMatchHistory = 7552 + ClientToGCGetBattleReportMatchHistoryResponse = 7553 + ServerToGCReportKillSummaries = 7554 + GCToGCUpdatePlayerPredictions = 7561 + GCToServerPredictionResult = 7562 + GCToGCReplayMonitorValidateReplay = 7569 + LobbyEventPoints = 7572 + GCToGCGetCustomGameTickets = 7573 + GCToGCGetCustomGameTicketsResponse = 7574 + GCToGCCustomGamePlayed = 7576 + GCToGCGrantEventPointsToUser = 7577 + GameserverCrashReport = 7579 + GameserverCrashReportResponse = 7580 + GCToClientSteamDatagramTicket = 7581 + GCToGCSendAccountsEventPoints = 7583 + ClientToGCRerollPlayerChallenge = 7584 + ServerToGCRerollPlayerChallenge = 7585 + RerollPlayerChallengeResponse = 7586 + SignOutUpdatePlayerChallenge = 7587 + ClientToGCSetPartyLeader = 7588 + ClientToGCCancelPartyInvites = 7589 + SQLGrantLeagueMatchToTicketHolders = 7592 + GCToGCEmoticonUnlockNoRollback = 7594 + ClientToGCApplyGemCombiner = 7603 + ClientToGCGetAllHeroOrder = 7606 + ClientToGCGetAllHeroOrderResponse = 7607 + SQLGCToGCGrantBadgePoints = 7608 + GCToGCCheckOwnsEntireEmoticonRange = 7611 + GCToGCCheckOwnsEntireEmoticonRangeResponse = 7612 + GCToClientRequestLaneSelection = 7623 + GCToClientRequestLaneSelectionResponse = 7624 + ServerToGCCavernCrawlIsHeroActive = 7625 + ServerToGCCavernCrawlIsHeroActiveResponse = 7626 + ClientToGCPlayerCardSpecificPurchaseRequest = 7627 + ClientToGCPlayerCardSpecificPurchaseResponse = 7628 + GCtoServerTensorflowInstance = 7629 + SQLSetIsLeagueAdmin = 7630 + GCToGCGetLiveLeagueMatches = 7631 + GCToGCGetLiveLeagueMatchesResponse = 7632 + LeagueInfoListAdminsRequest = 7633 + LeagueInfoListAdminsReponse = 7634 + GCToGCLeagueMatchStarted = 7645 + GCToGCLeagueMatchCompleted = 7646 + GCToGCLeagueMatchStartedResponse = 7647 + LeagueAvailableLobbyNodesRequest = 7650 + LeagueAvailableLobbyNodes = 7651 + GCToGCLeagueRequest = 7652 + GCToGCLeagueResponse = 7653 + GCToGCLeagueNodeGroupRequest = 7654 + GCToGCLeagueNodeGroupResponse = 7655 + GCToGCLeagueNodeRequest = 7656 + GCToGCLeagueNodeResponse = 7657 + GCToGCRealtimeStatsTerseRequest = 7658 + GCToGCRealtimeStatsTerseResponse = 7659 + GCToGCGetTopMatchesRequest = 7660 + GCToGCGetTopMatchesResponse = 7661 + ClientToGCGetFilteredPlayers = 7662 + GCToClientGetFilteredPlayersResponse = 7663 + ClientToGCRemoveFilteredPlayer = 7664 + GCToClientRemoveFilteredPlayerResponse = 7665 + GCToClientPlayerBeaconState = 7666 + GCToClientPartyBeaconUpdate = 7667 + GCToClientPartySearchInvite = 7668 + ClientToGCUpdatePartyBeacon = 7669 + ClientToGCRequestActiveBeaconParties = 7670 + GCToClientRequestActiveBeaconPartiesResponse = 7671 + ClientToGCManageFavorites = 7672 + GCToClientManageFavoritesResponse = 7673 + ClientToGCJoinPartyFromBeacon = 7674 + GCToClientJoinPartyFromBeaconResponse = 7675 + ClientToGCGetFavoritePlayers = 7676 + GCToClientGetFavoritePlayersResponse = 7677 + ClientToGCVerifyFavoritePlayers = 7678 + GCToClientVerifyFavoritePlayersResponse = 7679 + GCToClientPartySearchInvites = 7680 + GCToClientRequestMMInfo = 7681 + ClientToGCMMInfo = 7682 + SignOutTextMuteInfo = 7683 + ClientToGCPurchaseLabyrinthBlessings = 7684 + ClientToGCPurchaseLabyrinthBlessingsResponse = 7685 + ClientToGCPurchaseFilteredPlayerSlot = 7686 + GCToClientPurchaseFilteredPlayerSlotResponse = 7687 + ClientToGCUpdateFilteredPlayerNote = 7688 + GCToClientUpdateFilteredPlayerNoteResponse = 7689 + ClientToGCClaimSwag = 7690 + GCToClientClaimSwagResponse = 7691 + ServerToGCLockCharmTrading = 8004 + ClientToGCPlayerStatsRequest = 8006 + GCToClientPlayerStatsResponse = 8007 + ClearPracticeLobbyTeam = 8008 + ClientToGCFindTopSourceTVGames = 8009 + GCToClientFindTopSourceTVGamesResponse = 8010 + LobbyList = 8011 + LobbyListResponse = 8012 + PlayerStatsMatchSignOut = 8013 + ClientToGCSocialFeedPostCommentRequest = 8016 + GCToClientSocialFeedPostCommentResponse = 8017 + ClientToGCCustomGamesFriendsPlayedRequest = 8018 + GCToClientCustomGamesFriendsPlayedResponse = 8019 + ClientToGCFriendsPlayedCustomGameRequest = 8020 + GCToClientFriendsPlayedCustomGameResponse = 8021 + TopCustomGamesList = 8024 + ClientToGCSetPartyOpen = 8029 + ClientToGCMergePartyInvite = 8030 + GCToClientMergeGroupInviteReply = 8031 + ClientToGCMergePartyResponse = 8032 + GCToClientMergePartyResponseReply = 8033 + ClientToGCGetProfileCardStats = 8034 + ClientToGCGetProfileCardStatsResponse = 8035 + ClientToGCTopLeagueMatchesRequest = 8036 + ClientToGCTopFriendMatchesRequest = 8037 + GCToClientProfileCardStatsUpdated = 8040 + ServerToGCRealtimeStats = 8041 + GCToServerRealtimeStatsStartStop = 8042 + GCToGCGetServersForClients = 8045 + GCToGCGetServersForClientsResponse = 8046 + PracticeLobbyKickFromTeam = 8047 + ChatGetMemberCount = 8048 + ChatGetMemberCountResponse = 8049 + ClientToGCSocialFeedPostMessageRequest = 8050 + GCToClientSocialFeedPostMessageResponse = 8051 + CustomGameListenServerStartedLoading = 8052 + CustomGameClientFinishedLoading = 8053 + PracticeLobbyCloseBroadcastChannel = 8054 + StartFindingMatchResponse = 8055 + SQLGCToGCGrantAccountFlag = 8057 + GCToClientTopLeagueMatchesResponse = 8061 + GCToClientTopFriendMatchesResponse = 8062 + ClientToGCMatchesMinimalRequest = 8063 + ClientToGCMatchesMinimalResponse = 8064 + GCToClientChatRegionsEnabled = 8067 + ClientToGCPingData = 8068 + GCToGCEnsureAccountInParty = 8071 + GCToGCEnsureAccountInPartyResponse = 8072 + ClientToGCGetProfileTickets = 8073 + ClientToGCGetProfileTicketsResponse = 8074 + GCToClientMatchGroupsVersion = 8075 + ClientToGCH264Unsupported = 8076 + ClientToGCGetQuestProgress = 8078 + ClientToGCGetQuestProgressResponse = 8079 + SignOutXPCoins = 8080 + GCToClientMatchSignedOut = 8081 + GetHeroStatsHistory = 8082 + GetHeroStatsHistoryResponse = 8083 + ClientToGCPrivateChatInvite = 8084 + ClientToGCPrivateChatKick = 8088 + ClientToGCPrivateChatPromote = 8089 + ClientToGCPrivateChatDemote = 8090 + GCToClientPrivateChatResponse = 8091 + ClientToGCLatestConductScorecardRequest = 8095 + ClientToGCLatestConductScorecard = 8096 + ClientToGCWageringRequest = 8099 + GCToClientWageringResponse = 8100 + ClientToGCEventGoalsRequest = 8103 + ClientToGCEventGoalsResponse = 8104 + GCToGCLeaguePredictionsUpdate = 8108 + GCToGCAddUserToPostGameChat = 8110 + ClientToGCHasPlayerVotedForMVP = 8111 + ClientToGCHasPlayerVotedForMVPResponse = 8112 + ClientToGCVoteForMVP = 8113 + ClientToGCVoteForMVPResponse = 8114 + GCToGCGetEventOwnership = 8115 + GCToGCGetEventOwnershipResponse = 8116 + GCToClientAutomatedTournamentStateChange = 8117 + ClientToGCWeekendTourneyOpts = 8118 + ClientToGCWeekendTourneyOptsResponse = 8119 + ClientToGCWeekendTourneyLeave = 8120 + ClientToGCWeekendTourneyLeaveResponse = 8121 + ClientToGCTeammateStatsRequest = 8124 + ClientToGCTeammateStatsResponse = 8125 + ClientToGCGetGiftPermissions = 8126 + ClientToGCGetGiftPermissionsResponse = 8127 + ClientToGCVoteForArcana = 8128 + ClientToGCVoteForArcanaResponse = 8129 + ClientToGCRequestArcanaVotesRemaining = 8130 + ClientToGCRequestArcanaVotesRemainingResponse = 8131 + TransferTeamAdminResponse = 8132 + GCToClientTeamInfo = 8135 + GCToClientTeamsInfo = 8136 + ClientToGCMyTeamInfoRequest = 8137 + ClientToGCPublishUserStat = 8140 + GCToGCSignoutSpendWager = 8141 + SubmitLobbyMVPVote = 8144 + SubmitLobbyMVPVoteResponse = 8145 + SignOutCommunityGoalProgress = 8150 + GCToClientLobbyMVPAwarded = 8152 + GCToClientQuestProgressUpdated = 8153 + GCToClientWageringUpdate = 8154 + GCToClientArcanaVotesUpdate = 8155 + ClientToGCSetSpectatorLobbyDetails = 8157 + ClientToGCSetSpectatorLobbyDetailsResponse = 8158 + ClientToGCCreateSpectatorLobby = 8159 + ClientToGCCreateSpectatorLobbyResponse = 8160 + ClientToGCSpectatorLobbyList = 8161 + ClientToGCSpectatorLobbyListResponse = 8162 + SpectatorLobbyGameDetails = 8163 + ServerToGCCompendiumInGamePredictionResults = 8166 + ServerToGCCloseCompendiumInGamePredictionVoting = 8167 + ClientToGCOpenPlayerCardPack = 8168 + ClientToGCOpenPlayerCardPackResponse = 8169 + ClientToGCSelectCompendiumInGamePrediction = 8170 + ClientToGCSelectCompendiumInGamePredictionResponse = 8171 + ClientToGCWeekendTourneyGetPlayerStats = 8172 + ClientToGCWeekendTourneyGetPlayerStatsResponse = 8173 + ClientToGCRecyclePlayerCard = 8174 + ClientToGCRecyclePlayerCardResponse = 8175 + ClientToGCCreatePlayerCardPack = 8176 + ClientToGCCreatePlayerCardPackResponse = 8177 + ClientToGCGetPlayerCardRosterRequest = 8178 + ClientToGCGetPlayerCardRosterResponse = 8179 + ClientToGCSetPlayerCardRosterRequest = 8180 + ClientToGCSetPlayerCardRosterResponse = 8181 + ServerToGCCloseCompendiumInGamePredictionVotingResponse = 8183 + LobbyBattleCupVictory = 8186 + GetPlayerCardItemInfo = 8187 + GetPlayerCardItemInfoResponse = 8188 + ClientToGCRequestSteamDatagramTicket = 8189 + ClientToGCRequestSteamDatagramTicketResponse = 8190 + GCToClientBattlePassRollupRequest = 8191 + GCToClientBattlePassRollupResponse = 8192 + ClientToGCTransferSeasonalMMRRequest = 8193 + ClientToGCTransferSeasonalMMRResponse = 8194 + GCToGCPublicChatCommunicationBan = 8195 + GCToGCUpdateAccountInfo = 8196 + ChatReportPublicSpam = 8197 + ClientToGCSetPartyBuilderOptions = 8198 + ClientToGCSetPartyBuilderOptionsResponse = 8199 + GCToClientPlaytestStatus = 8200 + ClientToGCJoinPlaytest = 8201 + ClientToGCJoinPlaytestResponse = 8202 + LobbyPlaytestDetails = 8203 + SetFavoriteTeam = 8204 + GCToClientBattlePassRollupListRequest = 8205 + GCToClientBattlePassRollupListResponse = 8206 + ClaimEventAction = 8209 + ClaimEventActionResponse = 8210 + GetPeriodicResource = 8211 + GetPeriodicResourceResponse = 8212 + PeriodicResourceUpdated = 8213 + ServerToGCSpendWager = 8214 + GCToGCSignoutSpendWagerToken = 8215 + SubmitTriviaQuestionAnswer = 8216 + SubmitTriviaQuestionAnswerResponse = 8217 + ClientToGCGiveTip = 8218 + ClientToGCGiveTipResponse = 8219 + StartTriviaSession = 8220 + StartTriviaSessionResponse = 8221 + AnchorPhoneNumberRequest = 8222 + AnchorPhoneNumberResponse = 8223 + UnanchorPhoneNumberRequest = 8224 + UnanchorPhoneNumberResponse = 8225 + GCToGCSignoutSpendRankWager = 8229 + GCToGCGetFavoriteTeam = 8230 + GCToGCGetFavoriteTeamResponse = 8231 + SignOutEventGameData = 8232 + ClientToGCQuickStatsRequest = 8238 + ClientToGCQuickStatsResponse = 8239 + GCToGCSubtractEventPointsFromUser = 8240 + SelectionPriorityChoiceRequest = 8241 + SelectionPriorityChoiceResponse = 8242 + GCToGCCompendiumInGamePredictionResults = 8243 + GameAutographReward = 8244 + GameAutographRewardResponse = 8245 + DestroyLobbyRequest = 8246 + DestroyLobbyResponse = 8247 + PurchaseItemWithEventPoints = 8248 + PurchaseItemWithEventPointsResponse = 8249 + ServerToGCMatchPlayerItemPurchaseHistory = 8250 + GCToGCGrantPlusHeroMatchResults = 8251 + ServerToGCMatchStateHistory = 8255 + PurchaseHeroRandomRelic = 8258 + PurchaseHeroRandomRelicResponse = 8259 + ClientToGCClaimEventActionUsingItem = 8260 + ClientToGCClaimEventActionUsingItemResponse = 8261 + PartyReadyCheckRequest = 8262 + PartyReadyCheckResponse = 8263 + PartyReadyCheckAcknowledge = 8264 + GetRecentPlayTimeFriendsRequest = 8265 + GetRecentPlayTimeFriendsResponse = 8266 + GCToClientCommendNotification = 8267 + ProfileRequest = 8268 + ProfileResponse = 8269 + ProfileUpdate = 8270 + ProfileUpdateResponse = 8271 + HeroGlobalDataRequest = 8274 + HeroGlobalDataResponse = 8275 + ClientToGCRequestPlusWeeklyChallengeResult = 8276 + ClientToGCRequestPlusWeeklyChallengeResultResponse = 8277 + GCToGCGrantPlusPrepaidTime = 8278 + PrivateMetadataKeyRequest = 8279 + PrivateMetadataKeyResponse = 8280 + GCToGCReconcilePlusStatus = 8281 + GCToGCCheckPlusStatus = 8282 + GCToGCCheckPlusStatusResponse = 8283 + GCToGCReconcilePlusAutoGrantItems = 8284 + GCToGCReconcilePlusStatusUnreliable = 8285 + GCToClientCavernCrawlMapPathCompleted = 8288 + ClientToGCCavernCrawlClaimRoom = 8289 + ClientToGCCavernCrawlClaimRoomResponse = 8290 + ClientToGCCavernCrawlUseItemOnRoom = 8291 + ClientToGCCavernCrawlUseItemOnRoomResponse = 8292 + ClientToGCCavernCrawlUseItemOnPath = 8293 + ClientToGCCavernCrawlUseItemOnPathResponse = 8294 + ClientToGCCavernCrawlRequestMapState = 8295 + ClientToGCCavernCrawlRequestMapStateResponse = 8296 + SignOutTips = 8297 + ClientToGCRequestEventPointLogV2 = 8298 + ClientToGCRequestEventPointLogResponseV2 = 8299 + ClientToGCRequestEventTipsSummary = 8300 + ClientToGCRequestEventTipsSummaryResponse = 8301 + ClientToGCRequestSocialFeed = 8303 + ClientToGCRequestSocialFeedResponse = 8304 + ClientToGCRequestSocialFeedComments = 8305 + ClientToGCRequestSocialFeedCommentsResponse = 8306 + ClientToGCCavernCrawlGetClaimedRoomCount = 8308 + ClientToGCCavernCrawlGetClaimedRoomCountResponse = 8309 + GCToGCReconcilePlusAutoGrantItemsUnreliable = 8310 + ServerToGCAddBroadcastTimelineEvent = 8311 + GCToServerUpdateSteamBroadcasting = 8312 + ClientToGCRecordContestVote = 8313 + GCToClientRecordContestVoteResponse = 8314 + GCToGCGrantAutograph = 8315 + GCToGCGrantAutographResponse = 8316 + SignOutConsumableUsage = 8317 + LobbyEventGameDetails = 8318 + DevGrantEventPoints = 8319 + DevGrantEventPointsResponse = 8320 + DevGrantEventAction = 8321 + DevGrantEventActionResponse = 8322 + DevResetEventState = 8323 + DevResetEventStateResponse = 8324 + GCToGCReconcileEventOwnership = 8325 + ConsumeEventSupportGrantItem = 8326 + ConsumeEventSupportGrantItemResponse = 8327 + GCToClientClaimEventActionUsingItemCompleted = 8328 + GCToClientCavernCrawlMapUpdated = 8329 + ServerToGCRequestPlayerRecentAccomplishments = 8330 + ServerToGCRequestPlayerRecentAccomplishmentsResponse = 8331 + ClientToGCRequestPlayerRecentAccomplishments = 8332 + ClientToGCRequestPlayerRecentAccomplishmentsResponse = 8333 + ClientToGCRequestPlayerHeroRecentAccomplishments = 8334 + ClientToGCRequestPlayerHeroRecentAccomplishmentsResponse = 8335 + SignOutEventActionGrants = 8336 + ClientToGCRequestPlayerCoachMatches = 8337 + ClientToGCRequestPlayerCoachMatchesResponse = 8338 + ClientToGCSubmitCoachTeammateRating = 8341 + ClientToGCSubmitCoachTeammateRatingResponse = 8342 + GCToClientCoachTeammateRatingsChanged = 8343 + ClientToGCRequestPlayerCoachMatch = 8345 + ClientToGCRequestPlayerCoachMatchResponse = 8346 + ClientToGCRequestContestVotes = 8347 + ClientToGCRequestContestVotesResponse = 8348 + ClientToGCMVPVoteTimeout = 8349 + ClientToGCMVPVoteTimeoutResponse = 8350 + MatchMatchmakingStats = 8360 + ClientToGCSubmitPlayerMatchSurvey = 8361 + ClientToGCSubmitPlayerMatchSurveyResponse = 8362 + SQLGCToGCGrantAllHeroProgressAccount = 8363 + SQLGCToGCGrantAllHeroProgressVictory = 8364 + DevDeleteEventActions = 8365 + DevDeleteEventActionsResponse = 8366 + GCToGCGetAllHeroCurrent = 8635 + GCToGCGetAllHeroCurrentResponse = 8636 + SubmitPlayerAvoidRequest = 8637 + SubmitPlayerAvoidRequestResponse = 8638 + GCToClientNotificationsUpdated = 8639 + GCtoGCAssociatedExploiterAccountInfo = 8640 + GCtoGCAssociatedExploiterAccountInfoResponse = 8641 + GCtoGCRequestRecalibrationCheck = 8642 + GCToClientVACReminder = 8643 + ClientToGCUnderDraftBuy = 8644 + ClientToGCUnderDraftBuyResponse = 8645 + ClientToGCUnderDraftReroll = 8646 + ClientToGCUnderDraftRerollResponse = 8647 + NeutralItemStats = 8648 + ClientToGCCreateGuild = 8649 + ClientToGCCreateGuildResponse = 8650 + ClientToGCSetGuildInfo = 8651 + ClientToGCSetGuildInfoResponse = 8652 + ClientToGCAddGuildRole = 8653 + ClientToGCAddGuildRoleResponse = 8654 + ClientToGCModifyGuildRole = 8655 + ClientToGCModifyGuildRoleResponse = 8656 + ClientToGCRemoveGuildRole = 8657 + ClientToGCRemoveGuildRoleResponse = 8658 + ClientToGCJoinGuild = 8659 + ClientToGCJoinGuildResponse = 8660 + ClientToGCLeaveGuild = 8661 + ClientToGCLeaveGuildResponse = 8662 + ClientToGCInviteToGuild = 8663 + ClientToGCInviteToGuildResponse = 8664 + ClientToGCDeclineInviteToGuild = 8665 + ClientToGCDeclineInviteToGuildResponse = 8666 + ClientToGCCancelInviteToGuild = 8667 + ClientToGCCancelInviteToGuildResponse = 8668 + ClientToGCKickGuildMember = 8669 + ClientToGCKickGuildMemberResponse = 8670 + ClientToGCSetGuildMemberRole = 8671 + ClientToGCSetGuildMemberRoleResponse = 8672 + ClientToGCRequestGuildData = 8673 + ClientToGCRequestGuildDataResponse = 8674 + GCToClientGuildDataUpdated = 8675 + ClientToGCRequestGuildMembership = 8676 + ClientToGCRequestGuildMembershipResponse = 8677 + GCToClientGuildMembershipUpdated = 8678 + ClientToGCAcceptInviteToGuild = 8681 + ClientToGCAcceptInviteToGuildResponse = 8682 + ClientToGCSetGuildRoleOrder = 8683 + ClientToGCSetGuildRoleOrderResponse = 8684 + ClientToGCRequestGuildFeed = 8685 + ClientToGCRequestGuildFeedResponse = 8686 + ClientToGCRequestAccountGuildEventData = 8687 + ClientToGCRequestAccountGuildEventDataResponse = 8688 + GCToClientAccountGuildEventDataUpdated = 8689 + ClientToGCRequestActiveGuildContracts = 8690 + ClientToGCRequestActiveGuildContractsResponse = 8691 + GCToClientActiveGuildContractsUpdated = 8692 + GCToClientGuildFeedUpdated = 8693 + ClientToGCSelectGuildContract = 8694 + ClientToGCSelectGuildContractResponse = 8695 + GCToGCCompleteGuildContracts = 8696 + ClientToGCAddPlayerToGuildChat = 8698 + ClientToGCAddPlayerToGuildChatResponse = 8699 + ClientToGCUnderDraftSell = 8700 + ClientToGCUnderDraftSellResponse = 8701 + ClientToGCUnderDraftRequest = 8702 + ClientToGCUnderDraftResponse = 8703 + ClientToGCUnderDraftRedeemReward = 8704 + ClientToGCUnderDraftRedeemRewardResponse = 8705 + GCToServerLobbyHeroBanRates = 8708 + SignOutGuildContractProgress = 8711 + SignOutMVPStats = 8712 + ClientToGCRequestActiveGuildChallenge = 8713 + ClientToGCRequestActiveGuildChallengeResponse = 8714 + GCToClientActiveGuildChallengeUpdated = 8715 + ClientToGCRequestReporterUpdates = 8716 + ClientToGCRequestReporterUpdatesResponse = 8717 + ClientToGCAcknowledgeReporterUpdates = 8718 + SignOutGuildChallengeProgress = 8720 + ClientToGCRequestGuildEventMembers = 8721 + ClientToGCRequestGuildEventMembersResponse = 8722 + ClientToGCReportGuildContent = 8725 + ClientToGCReportGuildContentResponse = 8726 + ClientToGCRequestAccountGuildPersonaInfo = 8727 + ClientToGCRequestAccountGuildPersonaInfoResponse = 8728 + ClientToGCRequestAccountGuildPersonaInfoBatch = 8729 + ClientToGCRequestAccountGuildPersonaInfoBatchResponse = 8730 + GCToClientUnderDraftGoldUpdated = 8731 + GCToServerRecordTrainingData = 8732 + SignOutBounties = 8733 + LobbyFeaturedGamemodeProgress = 8734 + LobbyGauntletProgress = 8735 + ClientToGCSubmitDraftTriviaMatchAnswer = 8736 + ClientToGCSubmitDraftTriviaMatchAnswerResponse = 8737 + GCToGCSignoutSpendBounty = 8738 + ClientToGCApplyGauntletTicket = 8739 + ClientToGCUnderDraftRollBackBench = 8740 + ClientToGCUnderDraftRollBackBenchResponse = 8741 + GCToGCGetEventActionScore = 8742 + GCToGCGetEventActionScoreResponse = 8743 + ServerToGCGetGuildContracts = 8744 + ServerToGCGetGuildContractsResponse = 8745 + LobbyEventGameData = 8746 + GCToClientGuildMembersDataUpdated = 8747 + SignOutReportActivityMarkers = 8748 + SignOutDiretideCandy = 8749 + GCToClientPostGameItemAwardNotification = 8750 + ClientToGCGetOWMatchDetails = 8751 + ClientToGCGetOWMatchDetailsResponse = 8752 + ClientToGCSubmitOWConviction = 8753 + ClientToGCSubmitOWConvictionResponse = 8754 + GCToGCGetAccountSteamChina = 8755 + GCToGCGetAccountSteamChinaResponse = 8756 + ClientToGCClaimLeaderboardRewards = 8757 + ClientToGCClaimLeaderboardRewardsResponse = 8758 + ClientToGCRecalibrateMMR = 8759 + ClientToGCRecalibrateMMRResponse = 8760 + GCToGCGrantEventPointActionList = 8761 + ClientToGCChinaSSAURLRequest = 8764 + ClientToGCChinaSSAURLResponse = 8765 + ClientToGCChinaSSAAcceptedRequest = 8766 + ClientToGCChinaSSAAcceptedResponse = 8767 + SignOutOverwatchSuspicion = 8768 + ServerToGCGetSuspicionConfig = 8769 + ServerToGCGetSuspicionConfigResponse = 8770 + GCToGCGrantPlusHeroChallengeMatchResults = 8771 + GCToClientOverwatchCasesAvailable = 8772 + ServerToGCAccountCheck = 8773 + ClientToGCStartWatchingOverwatch = 8774 + ClientToGCStopWatchingOverwatch = 8775 + SignOutPerfData = 8776 + ClientToGCGetDPCFavorites = 8777 + ClientToGCGetDPCFavoritesResponse = 8778 + ClientToGCSetDPCFavoriteState = 8779 + ClientToGCSetDPCFavoriteStateResponse = 8780 + ClientToGCOverwatchReplayError = 8781 + ServerToGCPlayerChallengeHistory = 8782 + SignOutBanData = 8783 + WebapiDPCSeasonResults = 8784 + ClientToGCCoachFriend = 8785 + ClientToGCCoachFriendResponse = 8786 + ClientToGCRequestPrivateCoachingSession = 8787 + ClientToGCRequestPrivateCoachingSessionResponse = 8788 + ClientToGCAcceptPrivateCoachingSession = 8789 + ClientToGCAcceptPrivateCoachingSessionResponse = 8790 + ClientToGCLeavePrivateCoachingSession = 8791 + ClientToGCLeavePrivateCoachingSessionResponse = 8792 + ClientToGCGetCurrentPrivateCoachingSession = 8793 + ClientToGCGetCurrentPrivateCoachingSessionResponse = 8794 + GCToClientPrivateCoachingSessionUpdated = 8795 + ClientToGCSubmitPrivateCoachingSessionRating = 8796 + ClientToGCSubmitPrivateCoachingSessionRatingResponse = 8797 + ClientToGCGetAvailablePrivateCoachingSessions = 8798 + ClientToGCGetAvailablePrivateCoachingSessionsResponse = 8799 + ClientToGCGetAvailablePrivateCoachingSessionsSummary = 8800 + ClientToGCGetAvailablePrivateCoachingSessionsSummaryResponse = 8801 + ClientToGCJoinPrivateCoachingSessionLobby = 8802 + ClientToGCJoinPrivateCoachingSessionLobbyResponse = 8803 + ClientToGCRespondToCoachFriendRequest = 8804 + ClientToGCRespondToCoachFriendRequestResponse = 8805 + ClientToGCSetEventActiveSeasonID = 8806 + ClientToGCSetEventActiveSeasonIDResponse = 8807 + ServerToGCMatchPlayerNeutralItemEquipHistory = 8808 + ServerToGCCompendiumChosenInGamePredictions = 8809 + ClientToGCCreateTeamPlayerCardPack = 8810 + ClientToGCCreateTeamPlayerCardPackResponse = 8811 + GCToServerSubmitCheerData = 8812 + GCToServerCheerConfig = 8813 + ServerToGCGetCheerConfig = 8814 + ServerToGCGetCheerConfigResponse = 8815 + GCToGCGrantAutographByID = 8816 + GCToServerCheerScalesOverride = 8817 + GCToServerGetCheerState = 8818 + ServerToGCReportCheerState = 8819 + GCToServerScenarioSave = 8820 + GCToServerAbilityDraftLobbyData = 8821 + SignOutReportCommunications = 8822 + ClientToGCBatchGetPlayerCardRosterRequest = 8823 + ClientToGCBatchGetPlayerCardRosterResponse = 8824 + ClientToGCGetStickerbookRequest = 8825 + ClientToGCGetStickerbookResponse = 8826 + ClientToGCCreateStickerbookPageRequest = 8827 + ClientToGCCreateStickerbookPageResponse = 8828 + ClientToGCDeleteStickerbookPageRequest = 8829 + ClientToGCDeleteStickerbookPageResponse = 8830 + ClientToGCPlaceStickersRequest = 8831 + ClientToGCPlaceStickersResponse = 8832 + ClientToGCPlaceCollectionStickersRequest = 8833 + ClientToGCPlaceCollectionStickersResponse = 8834 + ClientToGCOrderStickerbookTeamPageRequest = 8835 + ClientToGCOrderStickerbookTeamPageResponse = 8836 + ServerToGCGetStickerHeroes = 8837 + ServerToGCGetStickerHeroesResponse = 8838 + ClientToGCCandyShopGetUserData = 8840 + ClientToGCCandyShopGetUserDataResponse = 8841 + GCToClientCandyShopUserDataUpdated = 8842 + ClientToGCCandyShopPurchaseReward = 8843 + ClientToGCCandyShopPurchaseRewardResponse = 8844 + ClientToGCCandyShopDoExchange = 8845 + ClientToGCCandyShopDoExchangeResponse = 8846 + ClientToGCCandyShopDoVariableExchange = 8847 + ClientToGCCandyShopDoVariableExchangeResponse = 8848 + ClientToGCCandyShopRerollRewards = 8849 + ClientToGCCandyShopRerollRewardsResponse = 8850 + ClientToGCSetHeroSticker = 8851 + ClientToGCSetHeroStickerResponse = 8852 + ClientToGCGetHeroStickers = 8853 + ClientToGCGetHeroStickersResponse = 8854 + ClientToGCSetFavoritePage = 8855 + ClientToGCSetFavoritePageResponse = 8856 + ClientToGCCandyShopDevGrantCandy = 8857 + ClientToGCCandyShopDevGrantCandyResponse = 8858 + ClientToGCCandyShopDevClearInventory = 8859 + ClientToGCCandyShopDevClearInventoryResponse = 8860 + ClientToGCCandyShopOpenBags = 8861 + ClientToGCCandyShopOpenBagsResponse = 8862 + ClientToGCCandyShopDevGrantCandyBags = 8863 + ClientToGCCandyShopDevGrantCandyBagsResponse = 8864 + ClientToGCCandyShopDevShuffleExchange = 8865 + ClientToGCCandyShopDevShuffleExchangeResponse = 8866 + ClientToGCCandyShopDevGrantRerollCharges = 8867 + ClientToGCCandyShopDevGrantRerollChargesResponse = 8868 + LobbyAdditionalAccountData = 8869 + ServerToGCLobbyInitialized = 8870 + ClientToGCCollectorsCacheAvailableDataRequest = 8871 + GCToClientCollectorsCacheAvailableDataResponse = 8872 + ClientToGCUploadMatchClip = 8873 + GCToClientUploadMatchClipResponse = 8874 + GCToServerSetSteamLearnKeysChanged = 8876 + SignOutMuertaMinigame = 8877 + GCToServerLobbyHeroRoleStats = 8878 + ClientToGCRankRequest = 8879 + GCToClientRankResponse = 8880 + GCToClientRankUpdate = 8881 + SignOutMapStats = 8882 + ClientToGCMapStatsRequest = 8883 + GCToClientMapStatsResponse = 8884 + GCToServerSetSteamLearnInferencing = 8885 + ClientToGCShowcaseGetUserData = 8886 + ClientToGCShowcaseGetUserDataResponse = 8887 + ClientToGCShowcaseSetUserData = 8888 + ClientToGCShowcaseSetUserDataResponse = 8889 + ClientToGCFantasyCraftingGetData = 8890 + ClientToGCFantasyCraftingGetDataResponse = 8891 + ClientToGCFantasyCraftingPerformOperation = 8892 + ClientToGCFantasyCraftingPerformOperationResponse = 8893 + GCToClientFantasyCraftingGetDataUpdated = 8894 + ClientToGCFantasyCraftingDevModifyTablet = 8895 + ClientToGCFantasyCraftingDevModifyTabletResponse = 8896 + ClientToGCRoadToTIGetQuests = 8897 + ClientToGCRoadToTIGetQuestsResponse = 8898 + ClientToGCRoadToTIGetActiveQuest = 8899 + ClientToGCRoadToTIGetActiveQuestResponse = 8900 + ClientToGCBingoGetUserData = 8901 + ClientToGCBingoGetUserDataResponse = 8902 + ClientToGCBingoClaimRow = 8903 + ClientToGCBingoClaimRowResponse = 8904 + ClientToGCBingoDevRerollCard = 8905 + ClientToGCBingoDevRerollCardResponse = 8906 + ClientToGCBingoGetStatsData = 8907 + ClientToGCBingoGetStatsDataResponse = 8908 + GCToClientBingoUserDataUpdated = 8909 + GCToClientRoadToTIQuestDataUpdated = 8910 + ClientToGCRoadToTIUseItem = 8911 + ClientToGCRoadToTIUseItemResponse = 8912 + ClientToGCShowcaseSubmitReport = 8913 + ClientToGCShowcaseSubmitReportResponse = 8914 + ClientToGCShowcaseAdminGetReportsRollupList = 8915 + ClientToGCShowcaseAdminGetReportsRollupListResponse = 8916 + ClientToGCShowcaseAdminGetReportsRollup = 8917 + ClientToGCShowcaseAdminGetReportsRollupResponse = 8918 + ClientToGCShowcaseAdminGetUserDetails = 8919 + ClientToGCShowcaseAdminGetUserDetailsResponse = 8920 + ClientToGCShowcaseAdminConvict = 8921 + ClientToGCShowcaseAdminConvictResponse = 8922 + ClientToGCShowcaseAdminExonerate = 8923 + ClientToGCShowcaseAdminExonerateResponse = 8924 + ClientToGCShowcaseAdminReset = 8925 + ClientToGCShowcaseAdminResetResponse = 8926 + ClientToGCShowcaseAdminLockAccount = 8927 + ClientToGCShowcaseAdminLockAccountResponse = 8928 + ClientToGCFantasyCraftingSelectPlayer = 8929 + ClientToGCFantasyCraftingSelectPlayerResponse = 8930 + ClientToGCFantasyCraftingGenerateTablets = 8931 + ClientToGCFantasyCraftingGenerateTabletsResponse = 8932 + ClientToGcFantasyCraftingUpgradeTablets = 8933 + ClientToGcFantasyCraftingUpgradeTabletsResponse = 8934 + ClientToGCFantasyCraftingRerollOptions = 8936 + ClientToGCFantasyCraftingRerollOptionsResponse = 8937 + ClientToGCRoadToTIDevForceQuest = 8935 + LobbyRoadToTIMatchQuestData = 8939 + ClientToGCShowcaseModerationGetQueue = 8940 + ClientToGCShowcaseModerationGetQueueResponse = 8941 + ClientToGCShowcaseModerationApplyModeration = 8942 + ClientToGCShowcaseModerationApplyModerationResponse = 8943 +# fmt: on diff --git a/steam/ext/dota2/models.py b/steam/ext/dota2/models.py new file mode 100644 index 00000000..2e70137c --- /dev/null +++ b/steam/ext/dota2/models.py @@ -0,0 +1,490 @@ +"""Licensed under The MIT License (MIT) - Copyright (c) 2020-present James H-B. See LICENSE""" + +from __future__ import annotations + +import datetime +from dataclasses import dataclass +from operator import attrgetter +from typing import TYPE_CHECKING, TypeVar + +from ... import abc, user +from ..._gc.client import ClientUser as ClientUser_ +from ...utils import MISSING, DateTime +from .enums import GameMode, Hero, LobbyType, MatchOutcome, RankTier +from .protobufs.client_messages import ERankType + +if TYPE_CHECKING: + from ...types.id import Intable + from .protobufs import client_messages, common, watch + from .state import GCState, MatchHistoryKwargs + +UserT = TypeVar("UserT", bound=abc.PartialUser) + +__all__ = ( + "ClientUser", + "LiveMatch", + "MatchMinimal", + "MatchHistoryMatch", + "PartialMatch", + "PartialUser", + "ProfileCard", + "User", +) + + +class PartialUser(abc.PartialUser): + __slots__ = () + _state: GCState + + async def dota2_profile(self): # TODO: Don't forget to include return type + """Fetch user's Dota 2 profile. + + Almost fully mirrors old profile page. + """ + proto = await self._state.fetch_dota2_profile(account_id=self.id) + return proto # TODO: Modelize (?) + + async def dota2_profile_card(self) -> ProfileCard: + """Fetch user's Dota 2 profile card. + + Contains basic information about the account. Mirrors some from old profile page. + """ + proto = await self._state.fetch_dota2_profile_card(self.id) + return ProfileCard(proto) + + async def match_history( + self, + *, + start_at_match_id: int = MISSING, + matches_requested: int = 20, + hero: Hero = MISSING, + include_practice_matches: bool = False, + include_custom_games: bool = False, + include_event_games: bool = False, + ) -> list[MatchHistoryMatch]: + """Fetch user's Dota 2 match history. + + Only works for steam friends. + """ + kwargs: MatchHistoryKwargs = { + "account_id": self.id, + "matches_requested": matches_requested, + "include_practice_matches": include_practice_matches, + "include_custom_games": include_custom_games, + "include_event_games": include_event_games, + } + if start_at_match_id: + kwargs["start_at_match_id"] = start_at_match_id + if hero: + kwargs["hero_id"] = hero.id + + proto = await self._state.fetch_match_history(**kwargs) + return [MatchHistoryMatch(self._state, match) for match in proto.matches] + + +class User(PartialUser, user.User): # type: ignore + __slots__ = () + + +class PartialMatch: + """Represents an already finished Dota 2 Match. + + This class allows using Dota 2 Coordinator requests related to matches. + """ + + def __init__(self, state: GCState, id: int): + self._state = state + self.id = id + + async def details(self) -> MatchDetails | None: + """Fetch Match Details. + + Contains most of the information that can be found in post-match stats in-game. + + Raises + ------ + asyncio.TimeoutError + Request time-outed. Potential reasons: + * Match ID is incorrect. + * This match is still live. + * Dota 2 Game Coordinator lagging or being down. + """ + proto = await self._state.fetch_match_details(match_id=self.id) + return MatchDetails(self._state, proto.match) + + async def minimal(self) -> MatchMinimal: + """Fetches basic "minimal" information about the match.""" + proto = await self._state.fetch_matches_minimal(match_ids=[self.id]) + match = next(iter(proto.matches), None) + if match is not None: + return MatchMinimal(self._state, match) + else: + msg = f"Failed to get match_minimal for {self.id}" + raise ValueError(msg) + + +class ClientUser(PartialUser, ClientUser_): # type: ignore + # TODO: if TYPE_CHECKING: for inventory + + async def glicko_rating(self) -> GlickoRating: + """Request Glicko Rank Information.""" + proto = await self._state.fetch_rank(rank_type=ERankType.RankedGlicko) + return GlickoRating( + mmr=proto.rank_value, deviation=proto.rank_data1, volatility=proto.rank_data2, const=proto.rank_data3 + ) + + async def behavior_summary(self) -> BehaviorSummary: + """Request Behavior Summary.""" + proto = await self._state.fetch_rank(rank_type=ERankType.BehaviorPublic) + return BehaviorSummary(behavior_score=proto.rank_value, communication_score=proto.rank_data1) + + async def post_social_message(self, message: str) -> None: + """Post message in social feed. + + Currently, messages sent with this are visible in "User Feed - Widget" of Profile Showcase. + This functionality was possible long ago naturally in the in-game client. + """ + await self._state.post_social_message(message=message) + + +class ProfileCard: + def __init__(self, proto: common.ProfileCard): + self.account_id = proto.account_id + self.badge_points = proto.badge_points + self.event_points = proto.event_points + self.event_id = proto.event_id + self.recent_battle_cup_victory = proto.recent_battle_cup_victory + self.rank_tier = RankTier.try_value(proto.rank_tier) + """Ranked medal like Herald-Immortal with a number of stars, i.e. Legend 5.""" + self.leaderboard_rank = proto.leaderboard_rank + """Leaderboard rank, i.e. found here https://www.dota2.com/leaderboards/#europe.""" + self.is_plus_subscriber = proto.is_plus_subscriber + """Is Dota Plus Subscriber.""" + self.plus_original_start_date = proto.plus_original_start_date + """When user subscribed to Dota Plus for their very first time.""" + self.favorite_team_packed = proto.favorite_team_packed + self.lifetime_games = proto.lifetime_games + """Amount of lifetime games, includes Turbo games as well.""" + + # (?) Unused/Deprecated by Valve + # self.slots = proto.slots # profile page was reworked + # self.title = proto.title + # self.rank_tier_score = proto.rank_tier_score # relic from time when support/core MMR were separated + # self.leaderboard_rank_core = proto.leaderboard_rank_core # relic from time when support/core MMR were separated + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} account_id={self.account_id}>" + + +class LiveMatchPlayer(PartialUser): + def __init__( + self, + state: GCState, + id: Intable, + hero: Hero, + team: int, + team_slot: int, + ) -> None: + super().__init__(state, id) + self.hero = hero + self.team = team + self.team_slot = team_slot + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} hero={self.hero!r}>" + + +class MatchMinimalPlayer: + def __init__(self, state: GCState, proto: common.MatchMinimalPlayer): + self._state = state + + self.id = proto.account_id + self.hero = Hero.try_value(proto.hero_id) + self.kills = proto.kills + self.deaths = proto.deaths + self.assists = proto.assists + self.items = proto.items + self.player_slot = proto.player_slot + self.pro_name = proto.pro_name + self.level = proto.level + self.team_number = proto.team_number + + +class MatchMinimal: + def __init__(self, state: GCState, proto: common.MatchMinimal) -> None: + self._state = state + + self.id = proto.match_id + self.start_time = DateTime.from_timestamp(proto.start_time) + self.duration = datetime.timedelta(seconds=proto.duration) + self.game_mode = GameMode.try_value(proto.game_mode) + self.players = [MatchMinimalPlayer(state, player) for player in proto.players] + self.tourney = proto.tourney # TODO: modelize further `common.MatchMinimalTourney` + self.outcome = MatchOutcome.try_value(proto.match_outcome) + self.radiant_score = proto.radiant_score + self.dire_score = proto.dire_score + self.lobby_type = LobbyType.try_value(proto.lobby_type) + + +class MatchDetails: + def __init__(self, state: GCState, proto: common.Match) -> None: + self._state = state + + self.id = proto.match_id + self.duration = datetime.timedelta(seconds=proto.duration) + self.start_time = DateTime.from_timestamp(proto.starttime) + self.human_players_amount = proto.human_players + self.players = proto.players # TODO: modelize + self.tower_status = proto.tower_status # TODO: decipher + self.barracks_status = proto.barracks_status # TODO: decipher + self.cluster = proto.cluster + self.first_blood_time = proto.first_blood_time + self.replay_salt = proto.replay_salt + self.server_port = proto.server_port + self.lobby_type = LobbyType.try_value(proto.lobby_type) + self.server_ip = proto.server_ip + self.average_skill = proto.average_skill + self.game_balance = proto.game_balance + self.radiant_team_id = proto.radiant_team_id + self.dire_team_id = proto.dire_team_id + self.league_id = proto.leagueid + self.radiant_team_name = proto.radiant_team_name + self.dire_team_name = proto.dire_team_name + self.radiant_team_logo = proto.radiant_team_logo + self.dire_team_logo = proto.dire_team_logo + self.radiant_team_logo_url = proto.radiant_team_logo_url + self.dire_team_logo_url = proto.dire_team_logo_url + self.radiant_team_complete = proto.radiant_team_complete + self.dire_team_complete = proto.dire_team_complete + self.game_mode = GameMode.try_value(proto.game_mode) + self.picks_bans = proto.picks_bans # TODO: modelize + self.match_seq_num = proto.match_seq_num + self.replay_state = proto.replay_state + self.radiant_guild_id = proto.radiant_guild_id + self.dire_guild_id = proto.dire_guild_id + self.radiant_team_tag = proto.radiant_team_tag + self.dire_team_tag: str = proto.dire_team_tag + self.series_id = proto.series_id + self.series_type = proto.series_type + self.broadcaster_channels = proto.broadcaster_channels # TODO: modelize + self.engine = proto.engine + self.custom_game_data = proto.custom_game_data # TODO: ??? + self.match_flags = proto.match_flags # TODO: ??? + self.private_metadata_key = proto.private_metadata_key # TODO: ??? + self.radiant_team_score = proto.radiant_team_score + self.dire_team_score = proto.dire_team_score + self.match_outcome = MatchOutcome.try_value(proto.match_outcome) + self.tournament_id = proto.tournament_id + self.tournament_round = proto.tournament_round + self.pre_game_duration = proto.pre_game_duration + self.coaches = proto.coaches # TODO: modelize + + @property + def replay_url(self) -> str: + return f"http://replay{self.cluster}.valve.net/570/{self.id}_{self.replay_salt}.dem.bz2" + + @property + def metadata_url(self) -> str: + return f"http://replay{self.cluster}.valve.net/570/{self.id}_{self.replay_salt}.meta.bz2" + + +class LiveMatch: + """Represents a live match of Dota 2 + + Attributes + ----------- + id + Match ID. + server_steam_id + Server Steam ID. + lobby_id + Lobby ID. + lobby_type + Lobby Type. i.e. Ranked, Unranked. + game_mode + Game mode, i.e. All Pick, Captains Mode. + players + List of players in this match. This is sorted with team slot order (or commonly referred as player's colour). + Radiant team players first, i.e. "blue", "teal", "purple", ... Then Dire team players. + average_mmr + Average MMR. + sort_score + Number for in-game's Watch Tab to sort the game list in descending order with. + spectators: + Amount of people currently watching this match live in the Dota 2 application. + start_time: + Datetime in UTC for when the match started, including pre-draft stage. + end_time: + Datetime in UTC for when the match is finished. 0 if match is currently live. + game_time: + Timedelta representing in-game timer. + Draft stage and time before runes (0:00 mark) have this as non-positive timedelta. + delay + Time delay for the match if watching in the Dota 2 application. + Similarly, the data in attributes of this class is also behind the present by this delay. + last_update_time: + Time the data was updated last by the Game Coordinator. + tournament + Tournament information, if the match is a tournament game. + Includes information about tournament teams. + battle_cup + Battle Cup information, if the match is a battle cup game. + Includes information about tournament teams. + radiant_lead + Amount of gold lead Radiant team has. Negative value in case Dire is leading. + radiant_score + Amount of kills Radiant team has + dire_score + Amount of kills Dire team has + building_state + Bitmask. An integer that represents a binary of which buildings are still standing. + custom_game_difficulty + Custom Game Difficulty + """ + + def __init__(self, state: GCState, proto: watch.SourceTVGameSmall) -> None: + self._state = state + + self.id = proto.match_id + self.server_steam_id = proto.server_steam_id + self.lobby_id = proto.lobby_id + + self.lobby_type = LobbyType.try_value(proto.lobby_type) + self.game_mode = GameMode.try_value(proto.game_mode) + self.average_mmr = proto.average_mmr + self.sort_score = proto.sort_score + self.spectators = proto.spectators + + self.start_time = DateTime.from_timestamp(proto.activate_time) + self.game_time = datetime.timedelta(seconds=proto.game_time) + self.end_time = datetime.timedelta(seconds=proto.deactivate_time) + self.delay = datetime.timedelta(seconds=proto.delay) + self.last_update_time = DateTime.from_timestamp(proto.last_update_time) + + self.tournament = ( + TournamentMatch( + proto.league_id, + proto.series_id, + ( + TournamentTeam(proto.team_id_radiant, proto.team_name_radiant, proto.team_logo_radiant), + TournamentTeam(proto.team_id_dire, proto.team_name_dire, proto.team_logo_dire), + ), + ) + if proto.league_id # if it is 0 then all tournament related fields are going to be 0 as well + else None + ) + self.battle_cup = ( + BattleCup( + proto.weekend_tourney_tournament_id, + proto.weekend_tourney_division, + proto.weekend_tourney_skill_level, + proto.weekend_tourney_bracket_round, + ( + TournamentTeam(proto.team_id_radiant, proto.team_name_radiant, proto.team_logo_radiant), + TournamentTeam(proto.team_id_dire, proto.team_name_dire, proto.team_logo_dire), + ), + ) + if proto.weekend_tourney_tournament_id # if it is 0 then all battle cup related fields are going to be 0 + else None + ) + + self.radiant_lead = proto.radiant_lead + self.radiant_score = proto.radiant_score + self.dire_score = proto.dire_score + self.building_state = proto.building_state # TODO: helper function to decode this into human-readable + + self.custom_game_difficulty = proto.custom_game_difficulty + + # Since Immortal Draft update, players come from the proto message in a wrong order + # which can be fixed back with extra fields that they introduced later: `team`, `team_slot` + sorted_players = sorted(proto.players, key=attrgetter("team", "team_slot")) + + self.players: list[LiveMatchPlayer] = [] + for player in sorted_players: + live_match_player = LiveMatchPlayer( + self._state, player.account_id, Hero.try_value(player.hero_id), player.team, player.team_slot + ) + self.players.append(live_match_player) + + @property + def heroes(self) -> list[Hero]: + """List of heroes in the match. The list is sorted by their player's colour.""" + return [p.hero for p in self.players] + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} server_steam_id={self.server_steam_id}>" + + +@dataclass(slots=True) +class TournamentMatch: # should this be named LiveTournamentMatch ? Idk how fast I gonna break all these namings, + league_id: int # TODO: can I get more info like name of the tournament + series_id: int + teams: tuple[TournamentTeam, TournamentTeam] + + +@dataclass(slots=True) +class TournamentTeam: + id: int + name: str + logo: float # TODO: can I get more info on logo than float nonsense ? + + +@dataclass(slots=True) +class BattleCup: + tournament_id: int + division: int + skill_level: int + bracket_round: int + teams: tuple[TournamentTeam, TournamentTeam] + + +# maybe name it Match History Record +class MatchHistoryMatch(PartialMatch): + def __init__(self, state: GCState, proto: client_messages.GetPlayerMatchHistoryResponseMatch) -> None: + super().__init__(state, proto.match_id) + + self.start_time = DateTime.from_timestamp(proto.start_time) + self.hero = Hero.try_value(proto.hero_id) + self.win = proto.winner + self.game_mode = GameMode.try_value(proto.game_mode) + self.lobby_type = LobbyType.try_value(proto.lobby_type) + self.abandon = proto.abandon + self.duration = datetime.timedelta(seconds=proto.duration) + self.active_plus_subscription = proto.active_plus_subscription + + self.tourney_id = proto.tourney_id + self.tourney_round = proto.tourney_round + self.tourney_tier = proto.tourney_tier + self.tourney_division = proto.tourney_division + self.team_id = proto.team_id + self.team_name = proto.team_name + self.ugc_team_ui_logo = proto.ugc_team_ui_logo + self.selected_facet = proto.selected_facet + + # Deprecated / Pointless (?) + # self.previous_rank = proto.previous_rank + # self.solo_rank = proto.solo_rank # always False + # self.rank_change = proto.rank_change + # self.seasonal_rank = proto.seasonal_rank # always False + # self.engine = proto.engine # always 1 + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} hero={self.hero} win={self.win}>" + + +@dataclass(slots=True) +class GlickoRating: + mmr: int + deviation: int + volatility: int + const: int # TODO: confirm all those names somehow or leave a note in doc that I'm clueless + + @property + def confidence(self): + return self.deviation / self.volatility # TODO: confirm this + + +@dataclass(slots=True) +class BehaviorSummary: + behavior_score: int + communication_score: int diff --git a/steam/ext/dota2/protobufs/__init__.py b/steam/ext/dota2/protobufs/__init__.py new file mode 100644 index 00000000..13e2ab08 --- /dev/null +++ b/steam/ext/dota2/protobufs/__init__.py @@ -0,0 +1,15 @@ +from typing import Final + +import betterproto + +APP_ID: Final = 570 + +from ....protobufs.msg import GCProtobufMessage +from . import ( + client_messages as client_messages, + common as common, + sdk as sdk, + watch as watch, +) + +[setattr(cls, "_betterproto", betterproto.ProtoClassMetadata(cls)) for cls in GCProtobufMessage.__subclasses__()] diff --git a/steam/ext/dota2/protobufs/base.py b/steam/ext/dota2/protobufs/base.py new file mode 100644 index 00000000..01448423 --- /dev/null +++ b/steam/ext/dota2/protobufs/base.py @@ -0,0 +1,42 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: base_gcmessages.proto +# plugin: python-betterproto + +from __future__ import annotations + +from dataclasses import dataclass + +import betterproto + +# PROFILE + + +@dataclass(eq=False, repr=False) +class SOEconItem(betterproto.Message): + id: int = betterproto.uint64_field(1) + account_id: int = betterproto.uint32_field(2) + inventory: int = betterproto.uint32_field(3) + def_index: int = betterproto.uint32_field(4) + quantity: int = betterproto.uint32_field(5) + level: int = betterproto.uint32_field(6) + quality: int = betterproto.uint32_field(7) + flags: int = betterproto.uint32_field(8) + origin: int = betterproto.uint32_field(9) + attribute: list[SOEconItemAttribute] = betterproto.message_field(12) + interior_item: SOEconItem = betterproto.message_field(13) + style: int = betterproto.uint32_field(15) + original_id: int = betterproto.uint64_field(16) + equipped_state: list[SOEconItemEquipped] = betterproto.message_field(18) + + +@dataclass(eq=False, repr=False) +class SOEconItemAttribute(betterproto.Message): + def_index: int = betterproto.uint32_field(1) + value: int = betterproto.uint32_field(2) + value_bytes: bytes = betterproto.bytes_field(3) + + +@dataclass(eq=False, repr=False) +class SOEconItemEquipped(betterproto.Message): + new_class: int = betterproto.uint32_field(1) + new_slot: int = betterproto.uint32_field(2) diff --git a/steam/ext/dota2/protobufs/client_messages.py b/steam/ext/dota2/protobufs/client_messages.py new file mode 100644 index 00000000..2421eecb --- /dev/null +++ b/steam/ext/dota2/protobufs/client_messages.py @@ -0,0 +1,190 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: dota_gcmessages_client.proto +# plugin: python-betterproto + +from __future__ import annotations + +from dataclasses import dataclass + +import betterproto + +from ....protobufs.msg import GCProtobufMessage +from ..enums import EMsg +from .base import SOEconItem # noqa: TCH001 +from .common import Match, RecentMatchInfo, StickerbookPage, SuccessfulHero # noqa: TCH001 +from .shared_enums import EMatchGroupServerStatus, MatchVote # noqa: TCH001 + +# PROFILE CARD + + +class ClientToGCGetProfileCard(GCProtobufMessage, msg=EMsg.ClientToGCGetProfileCard): + account_id: int = betterproto.uint32_field(1) + + +# RANK/BEHAVIOR + + +class ERankType(betterproto.Enum): + Invalid = 0 + Casual = 1 + Ranked = 2 + CasualLegacy = 3 + RankedLegacy = 4 + CasualGlicko = 5 + RankedGlicko = 6 + RankMax = 7 + BehaviorPrivate = 100 + BehaviorPublic = 101 + Max = 102 + + +class ClientToGCRankRequest(GCProtobufMessage, msg=EMsg.ClientToGCRankRequest): + rank_type: ERankType = betterproto.enum_field(1) + + +class GCToClientRankResponseEResultCode(betterproto.Enum): + k_Succeeded = 0 + k_Failed = 1 + k_InvalidRankType = 2 + + +class GCToClientRankResponse(GCProtobufMessage, msg=EMsg.GCToClientRankResponse): + result_enum: GCToClientRankResponseEResultCode = betterproto.enum_field(1) + rank_value: int = betterproto.uint32_field(2) + rank_data1: int = betterproto.uint32_field(3) + rank_data2: int = betterproto.uint32_field(4) + rank_data3: int = betterproto.uint32_field(5) + + +# MATCHMAKING STATS + + +class MatchmakingStatsRequest(GCProtobufMessage, msg=EMsg.MatchmakingStatsRequest): + pass + + +class MatchmakingStatsResponse(GCProtobufMessage, msg=EMsg.MatchmakingStatsResponse): + matchgroups_version: int = betterproto.uint32_field(1) + legacy_searching_players_by_group_source2: list[int] = betterproto.uint32_field(7) + match_groups: list[MatchmakingMatchGroupInfo] = betterproto.message_field(8) + + +@dataclass(eq=False, repr=False) +class MatchmakingMatchGroupInfo(betterproto.Message): + players_searching: int = betterproto.uint32_field(1) + auto_region_select_ping_penalty: int = betterproto.sint32_field(2) + auto_region_select_ping_penalty_custom: int = betterproto.sint32_field(4) + status: EMatchGroupServerStatus = betterproto.enum_field(3) + + +# MATCH DETAILS + + +class MatchDetailsRequest(GCProtobufMessage, msg=EMsg.MatchDetailsRequest): + match_id: int = betterproto.uint64_field(1) + + +class MatchDetailsResponse(GCProtobufMessage, msg=EMsg.MatchDetailsResponse): + eresult: int = betterproto.uint32_field(1) # originally called result + match: Match = betterproto.message_field(2) + vote: MatchVote = betterproto.enum_field(3) + + +# MATCH HISTORY + + +class GetPlayerMatchHistory(GCProtobufMessage, msg=EMsg.GetPlayerMatchHistory): + account_id: int = betterproto.uint32_field(1) + start_at_match_id: int = betterproto.uint64_field(2) + matches_requested: int = betterproto.uint32_field(3) + hero_id: int = betterproto.uint32_field(4) + request_id: int = betterproto.uint32_field(5) + include_practice_matches: bool = betterproto.bool_field(7) + include_custom_games: bool = betterproto.bool_field(8) + include_event_games: bool = betterproto.bool_field(9) + + +class GetPlayerMatchHistoryResponse(GCProtobufMessage, msg=EMsg.GetPlayerMatchHistoryResponse): + matches: list[GetPlayerMatchHistoryResponseMatch] = betterproto.message_field(1) + request_id: int = betterproto.uint32_field(2) + + +@dataclass(eq=False, repr=False) +class GetPlayerMatchHistoryResponseMatch(betterproto.Message): + match_id: int = betterproto.uint64_field(1) + start_time: int = betterproto.uint32_field(2) + hero_id: int = betterproto.uint32_field(3) + winner: bool = betterproto.bool_field(4) + game_mode: int = betterproto.uint32_field(5) + rank_change: int = betterproto.int32_field(6) + previous_rank: int = betterproto.uint32_field(7) + lobby_type: int = betterproto.uint32_field(8) + solo_rank: bool = betterproto.bool_field(9) + abandon: bool = betterproto.bool_field(10) + duration: int = betterproto.uint32_field(11) + engine: int = betterproto.uint32_field(12) + active_plus_subscription: bool = betterproto.bool_field(13) + seasonal_rank: bool = betterproto.bool_field(14) + tourney_id: int = betterproto.uint32_field(15) + tourney_round: int = betterproto.uint32_field(16) + tourney_tier: int = betterproto.uint32_field(17) + tourney_division: int = betterproto.uint32_field(18) + team_id: int = betterproto.uint32_field(19) + team_name: str = betterproto.string_field(20) + ugc_team_ui_logo: int = betterproto.uint64_field(21) + selected_facet: int = betterproto.uint32_field(22) + + +# SOCIAL FEED POST MESSAGE + + +class ClientToGCSocialFeedPostMessageRequest(GCProtobufMessage, msg=EMsg.ClientToGCSocialFeedPostMessageRequest): + message: str = betterproto.string_field(1) + match_id: int = betterproto.uint64_field(2) # doesn't seem like we can use it + match_timestamp: int = betterproto.uint32_field(3) # doesn't seem like we can use it + + +class GCToClientSocialFeedPostMessageResponse(GCProtobufMessage, msg=EMsg.GCToClientSocialFeedPostMessageResponse): + success: bool = betterproto.bool_field(1) + + +# PROFILE REQUEST + + +class ProfileRequest(GCProtobufMessage, msg=EMsg.ProfileRequest): + account_id: int = betterproto.uint32_field(1) + + +class ProfileResponseEResponse(betterproto.Enum): + InternalError = 0 + Success = 1 + TooBusy = 2 + Disabled = 3 + + +class ProfileResponse(GCProtobufMessage, msg=EMsg.ProfileResponse): + background_item: SOEconItem = betterproto.message_field(1) + featured_heroes: list[ProfileResponseFeaturedHero] = betterproto.message_field(2) + recent_matches: list[ProfileResponseMatchInfo] = betterproto.message_field(3) + successful_heroes: list[SuccessfulHero] = betterproto.message_field(4) + recent_match_details: RecentMatchInfo = betterproto.message_field(5) + eresult: ProfileResponseEResponse = betterproto.enum_field(6) + stickerbook_page: StickerbookPage = betterproto.message_field(7) + + +@dataclass +class ProfileResponseFeaturedHero(betterproto.Message): + hero_id: int = betterproto.uint32_field(1) + equipped_econ_items: list[SOEconItem] = betterproto.message_field(2) + manually_set: bool = betterproto.bool_field(3) + plus_hero_xp: int = betterproto.uint32_field(4) + plus_hero_relics_item: SOEconItem = betterproto.message_field(5) + + +@dataclass +class ProfileResponseMatchInfo(betterproto.Message): + match_id: int = betterproto.uint64_field(1) + match_timestamp: int = betterproto.uint32_field(2) + performance_rating: int = betterproto.sint32_field(3) + hero_id: int = betterproto.uint32_field(4) + won_match: bool = betterproto.bool_field(5) diff --git a/steam/ext/dota2/protobufs/common.py b/steam/ext/dota2/protobufs/common.py new file mode 100644 index 00000000..fdab2d9c --- /dev/null +++ b/steam/ext/dota2/protobufs/common.py @@ -0,0 +1,413 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: dota_gcmessages_common.proto +# plugin: python-betterproto + +from __future__ import annotations + +from dataclasses import dataclass + +import betterproto + +from ....protobufs.msg import GCProtobufMessage +from ..enums import EMsg +from .shared_enums import EEvent, EMatchOutcome, GameMode, Team # noqa: TCH001 + +# PROFILE CARD + + +class ProfileCard(GCProtobufMessage, msg=EMsg.ClientToGCGetProfileCardResponse): + account_id: int = betterproto.uint32_field(1) + slots: list[ProfileCardSlot] = betterproto.message_field(3) + badge_points: int = betterproto.uint32_field(4) + event_points: int = betterproto.uint32_field(5) + event_id: int = betterproto.uint32_field(6) + recent_battle_cup_victory: BattleCupVictory = betterproto.message_field(7) + rank_tier: int = betterproto.uint32_field(8) + leaderboard_rank: int = betterproto.uint32_field(9) + is_plus_subscriber: bool = betterproto.bool_field(10) + plus_original_start_date: int = betterproto.uint32_field(11) + rank_tier_score: int = betterproto.uint32_field(12) + leaderboard_rank_core: int = betterproto.uint32_field(17) + title: int = betterproto.uint32_field(23) + favorite_team_packed: int = betterproto.uint64_field(24) + lifetime_games: int = betterproto.uint32_field(25) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlot(betterproto.Message): + slot_id: int = betterproto.uint32_field(1) + trophy: ProfileCardSlotTrophy = betterproto.message_field(2) + stat: ProfileCardSlotStat = betterproto.message_field(3) + item: ProfileCardSlotItem = betterproto.message_field(4) + hero: ProfileCardSlotHero = betterproto.message_field(5) + emoticon: ProfileCardSlotEmoticon = betterproto.message_field(6) + team: ProfileCardSlotTeam = betterproto.message_field(7) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotTrophy(betterproto.Message): + trophy_id: int = betterproto.uint32_field(1) + trophy_score: int = betterproto.uint32_field(2) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotStat(betterproto.Message): + stat_id: ProfileCardEStatID = betterproto.enum_field(1) + stat_score: int = betterproto.uint32_field(2) + + +class ProfileCardEStatID(betterproto.Enum): + Wins = 3 + Commends = 4 + GamesPlayed = 5 + FirstMatchDate = 6 + PreviousSeasonRank = 7 + GamesMVP = 8 + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotItem(betterproto.Message): + serialized_item: bytes = betterproto.bytes_field(1) + item_id: int = betterproto.uint64_field(2) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotHero(betterproto.Message): + hero_id: int = betterproto.uint32_field(1) + hero_wins: int = betterproto.uint32_field(2) + hero_losses: int = betterproto.uint32_field(3) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotEmoticon(betterproto.Message): + emoticon_id: int = betterproto.uint32_field(1) + + +@dataclass(eq=False, repr=False) +class ProfileCardSlotTeam(betterproto.Message): + team_id: int = betterproto.uint32_field(1) + + +@dataclass(eq=False, repr=False) +class BattleCupVictory(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + win_date: int = betterproto.uint32_field(2) + valid_until: int = betterproto.uint32_field(3) + skill_level: int = betterproto.uint32_field(4) + tournament_id: int = betterproto.uint32_field(5) + division_id: int = betterproto.uint32_field(6) + team_id: int = betterproto.uint32_field(7) + streak: int = betterproto.uint32_field(8) + trophy_id: int = betterproto.uint32_field(9) + + +# MATCH DETAILS + + +@dataclass(eq=False, repr=False) +class Match(betterproto.Message): + duration: int = betterproto.uint32_field(3) + starttime: float = betterproto.fixed32_field(4) + players: list[Player] = betterproto.message_field(5) + match_id: int = betterproto.uint64_field(6) + tower_status: list[int] = betterproto.uint32_field(8) + barracks_status: list[int] = betterproto.uint32_field(9) + cluster: int = betterproto.uint32_field(10) + first_blood_time: int = betterproto.uint32_field(12) + replay_salt: float = betterproto.fixed32_field(13) + server_ip: float = betterproto.fixed32_field(14) + server_port: int = betterproto.uint32_field(15) + lobby_type: int = betterproto.uint32_field(16) + human_players: int = betterproto.uint32_field(17) + average_skill: int = betterproto.uint32_field(18) + game_balance: float = betterproto.float_field(19) + radiant_team_id: int = betterproto.uint32_field(20) + dire_team_id: int = betterproto.uint32_field(21) + leagueid: int = betterproto.uint32_field(22) + radiant_team_name: str = betterproto.string_field(23) + dire_team_name: str = betterproto.string_field(24) + radiant_team_logo: int = betterproto.uint64_field(25) + dire_team_logo: int = betterproto.uint64_field(26) + radiant_team_logo_url: str = betterproto.string_field(54) + dire_team_logo_url: str = betterproto.string_field(55) + radiant_team_complete: int = betterproto.uint32_field(27) + dire_team_complete: int = betterproto.uint32_field(28) + game_mode: GameMode = betterproto.enum_field(31) + picks_bans: list[HeroSelectEvent] = betterproto.message_field(32) + match_seq_num: int = betterproto.uint64_field(33) + replay_state: ReplayState = betterproto.enum_field(34) + radiant_guild_id: int = betterproto.uint32_field(35) + dire_guild_id: int = betterproto.uint32_field(36) + radiant_team_tag: str = betterproto.string_field(37) + dire_team_tag: str = betterproto.string_field(38) + series_id: int = betterproto.uint32_field(39) + series_type: int = betterproto.uint32_field(40) + broadcaster_channels: list[BroadcasterChannel] = betterproto.message_field(43) + engine: int = betterproto.uint32_field(44) + custom_game_data: CustomGameData = betterproto.message_field(45) + match_flags: int = betterproto.uint32_field(46) + private_metadata_key: float = betterproto.fixed32_field(47) + radiant_team_score: int = betterproto.uint32_field(48) + dire_team_score: int = betterproto.uint32_field(49) + match_outcome: EMatchOutcome = betterproto.enum_field(50) + tournament_id: int = betterproto.uint32_field(51) + tournament_round: int = betterproto.uint32_field(52) + pre_game_duration: int = betterproto.uint32_field(53) + coaches: list[Coach] = betterproto.message_field(57) + + +@dataclass(eq=False, repr=False) +class Player(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + player_slot: int = betterproto.uint32_field(2) + hero_id: int = betterproto.uint32_field(3) + item_0: int = betterproto.int32_field(4) + item_1: int = betterproto.int32_field(5) + item_2: int = betterproto.int32_field(6) + item_3: int = betterproto.int32_field(7) + item_4: int = betterproto.int32_field(8) + item_5: int = betterproto.int32_field(9) + item_6: int = betterproto.int32_field(59) + item_7: int = betterproto.int32_field(60) + item_8: int = betterproto.int32_field(61) + item_9: int = betterproto.int32_field(76) + expected_team_contribution: float = betterproto.float_field(10) + scaled_metric: float = betterproto.float_field(11) + previous_rank: int = betterproto.uint32_field(12) + rank_change: int = betterproto.sint32_field(13) + mmr_type: int = betterproto.uint32_field(74) + kills: int = betterproto.uint32_field(14) + deaths: int = betterproto.uint32_field(15) + assists: int = betterproto.uint32_field(16) + leaver_status: int = betterproto.uint32_field(17) + gold: int = betterproto.uint32_field(18) + last_hits: int = betterproto.uint32_field(19) + denies: int = betterproto.uint32_field(20) + gold_per_min: int = betterproto.uint32_field(21) + xp_per_min: int = betterproto.uint32_field(22) + gold_spent: int = betterproto.uint32_field(23) + hero_damage: int = betterproto.uint32_field(24) + tower_damage: int = betterproto.uint32_field(25) + hero_healing: int = betterproto.uint32_field(26) + level: int = betterproto.uint32_field(27) + time_last_seen: int = betterproto.uint32_field(28) + player_name: str = betterproto.string_field(29) + support_ability_value: int = betterproto.uint32_field(30) + feeding_detected: bool = betterproto.bool_field(32) + search_rank: int = betterproto.uint32_field(34) + search_rank_uncertainty: int = betterproto.uint32_field(35) + rank_uncertainty_change: int = betterproto.int32_field(36) + hero_play_count: int = betterproto.uint32_field(37) + party_id: float = betterproto.fixed64_field(38) + scaled_hero_damage: int = betterproto.uint32_field(54) + scaled_tower_damage: int = betterproto.uint32_field(55) + scaled_hero_healing: int = betterproto.uint32_field(56) + scaled_kills: float = betterproto.float_field(39) + scaled_deaths: float = betterproto.float_field(40) + scaled_assists: float = betterproto.float_field(41) + claimed_farm_gold: int = betterproto.uint32_field(42) + support_gold: int = betterproto.uint32_field(43) + claimed_denies: int = betterproto.uint32_field(44) + claimed_misses: int = betterproto.uint32_field(45) + misses: int = betterproto.uint32_field(46) + ability_upgrades: list[AbilityUpgrade] = betterproto.message_field(47) + additional_units_inventory: list[AdditionalUnitInventory] = betterproto.message_field(48) + permanent_buffs: list[PermanentBuff] = betterproto.message_field(57) + pro_name: str = betterproto.string_field(72) + real_name: str = betterproto.string_field(73) + custom_game_data: CustomGameData = betterproto.message_field(50) + active_plus_subscription: bool = betterproto.bool_field(51) + net_worth: int = betterproto.uint32_field(52) + bot_difficulty: int = betterproto.uint32_field(58) + hero_pick_order: int = betterproto.uint32_field(63) + hero_was_randomed: bool = betterproto.bool_field(64) + hero_was_dota_plus_suggestion: bool = betterproto.bool_field(69) + hero_damage_received: list[HeroDamageReceived] = betterproto.message_field(67) + hero_damage_dealt: list[HeroDamageReceived] = betterproto.message_field(79) + seconds_dead: int = betterproto.uint32_field(70) + gold_lost_to_death: int = betterproto.uint32_field(71) + lane_selection_flags: int = betterproto.uint32_field(75) + bounty_runes: int = betterproto.uint32_field(77) + outposts_captured: int = betterproto.uint32_field(78) + team_number: Team = betterproto.enum_field(80) + team_slot: int = betterproto.uint32_field(81) + selected_facet: int = betterproto.uint32_field(82) + + +@dataclass(eq=False, repr=False) +class AbilityUpgrade(betterproto.Message): + ability: int = betterproto.int32_field(1) + time: int = betterproto.uint32_field(2) + + +@dataclass(eq=False, repr=False) +class AdditionalUnitInventory(betterproto.Message): + unit_name: str = betterproto.string_field(1) + items: list[int] = betterproto.int32_field(2) + + +@dataclass(eq=False, repr=False) +class PermanentBuff(betterproto.Message): + permanent_buff: int = betterproto.uint32_field(1) + stack_count: int = betterproto.uint32_field(2) + grant_time: int = betterproto.uint32_field(3) + + +@dataclass(eq=False, repr=False) +class CustomGameData(betterproto.Message): + dota_team: int = betterproto.uint32_field(1) + winner: bool = betterproto.bool_field(2) + + +@dataclass(eq=False, repr=False) +class HeroDamageReceived(betterproto.Message): + pre_reduction: int = betterproto.uint32_field(1) + post_reduction: int = betterproto.uint32_field(2) + damage_type: HeroDamageType = betterproto.enum_field(3) + + +class HeroDamageType(betterproto.Enum): + Physical = 0 + Magical = 1 + Pure = 2 + + +@dataclass(eq=False, repr=False) +class HeroSelectEvent(betterproto.Message): + is_pick: bool = betterproto.bool_field(1) + team: int = betterproto.uint32_field(2) + hero_id: int = betterproto.uint32_field(3) + + +class ReplayState(betterproto.Enum): + Available = 0 + NotRecorded = 1 + Expired = 2 + + +@dataclass(eq=False, repr=False) +class BroadcasterChannel(betterproto.Message): + country_code: str = betterproto.string_field(1) + description: str = betterproto.string_field(2) + broadcaster_infos: list[BroadcasterInfo] = betterproto.message_field(3) + language_code: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class BroadcasterInfo(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + name: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class Coach(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + coach_name: str = betterproto.string_field(2) + coach_rating: int = betterproto.uint32_field(3) + coach_team: int = betterproto.uint32_field(4) + coach_party_id: int = betterproto.uint64_field(5) + is_private_coach: bool = betterproto.bool_field(6) + + +# MATCH MINIMAL + + +@dataclass(eq=False, repr=False) +class MatchMinimal(betterproto.Message): + match_id: int = betterproto.uint64_field(1) + start_time: float = betterproto.fixed32_field(2) + duration: int = betterproto.uint32_field(3) + game_mode: GameMode = betterproto.enum_field(4) + players: list[MatchMinimalPlayer] = betterproto.message_field(6) + tourney: MatchMinimalTourney = betterproto.message_field(7) + match_outcome: EMatchOutcome = betterproto.enum_field(8) + radiant_score: int = betterproto.uint32_field(9) + dire_score: int = betterproto.uint32_field(10) + lobby_type: int = betterproto.uint32_field(11) + + +@dataclass(eq=False, repr=False) +class MatchMinimalPlayer(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + hero_id: int = betterproto.uint32_field(2) + kills: int = betterproto.uint32_field(3) + deaths: int = betterproto.uint32_field(4) + assists: int = betterproto.uint32_field(5) + items: list[int] = betterproto.int32_field(6) + player_slot: int = betterproto.uint32_field(7) + pro_name: str = betterproto.string_field(8) + level: int = betterproto.uint32_field(9) + team_number: Team = betterproto.enum_field(10) + + +@dataclass(eq=False, repr=False) +class MatchMinimalTourney(betterproto.Message): + league_id: int = betterproto.uint32_field(1) + series_type: int = betterproto.uint32_field(8) + series_game: int = betterproto.uint32_field(9) + weekend_tourney_tournament_id: int = betterproto.uint32_field(10) + weekend_tourney_season_trophy_id: int = betterproto.uint32_field(11) + weekend_tourney_division: int = betterproto.uint32_field(12) + weekend_tourney_skill_level: int = betterproto.uint32_field(13) + radiant_team_id: int = betterproto.uint32_field(2) + radiant_team_name: str = betterproto.string_field(3) + radiant_team_logo: float = betterproto.fixed64_field(4) + radiant_team_logo_url: str = betterproto.string_field(14) + dire_team_id: int = betterproto.uint32_field(5) + dire_team_name: str = betterproto.string_field(6) + dire_team_logo: float = betterproto.fixed64_field(7) + dire_team_logo_url: str = betterproto.string_field(15) + + +# PROFILE REQUEST + + +@dataclass(eq=False, repr=False) +class SuccessfulHero(betterproto.Message): + hero_id: int = betterproto.uint32_field(1) + win_percent: float = betterproto.float_field(2) + longest_streak: int = betterproto.uint32_field(3) + + +@dataclass(eq=False, repr=False) +class RecentMatchInfo(betterproto.Message): + match_id: int = betterproto.uint64_field(1) + game_mode: GameMode = betterproto.enum_field(2) + kills: int = betterproto.uint32_field(3) + deaths: int = betterproto.uint32_field(4) + assists: int = betterproto.uint32_field(5) + duration: int = betterproto.uint32_field(6) + player_slot: int = betterproto.uint32_field(7) + match_outcome: EMatchOutcome = betterproto.enum_field(8) + timestamp: int = betterproto.uint32_field(9) + lobby_type: int = betterproto.uint32_field(10) + team_number: int = betterproto.uint32_field(11) + + +@dataclass(eq=False, repr=False) +class StickerbookPage(betterproto.Message): + page_num: int = betterproto.uint32_field(1) + event_id: EEvent = betterproto.enum_field(2) + team_id: int = betterproto.uint32_field(3) + stickers: list[StickerbookSticker] = betterproto.message_field(4) + page_type: EStickerbookPageType = betterproto.enum_field(5) + + +@dataclass(eq=False, repr=False) +class StickerbookSticker(betterproto.Message): + item_def_id: int = betterproto.uint32_field(1) + sticker_num: int = betterproto.uint32_field(2) + quality: int = betterproto.uint32_field(3) + position_x: float = betterproto.float_field(4) + position_y: float = betterproto.float_field(5) + position_z: float = betterproto.float_field(8) + rotation: float = betterproto.float_field(6) + scale: float = betterproto.float_field(7) + source_item_id: int = betterproto.uint64_field(9) + depth_bias: int = betterproto.uint32_field(10) + + +class EStickerbookPageType(betterproto.Enum): + Generic = 0 + Team = 1 + Talent = 2 diff --git a/steam/ext/dota2/protobufs/sdk.py b/steam/ext/dota2/protobufs/sdk.py new file mode 100644 index 00000000..a74d5493 --- /dev/null +++ b/steam/ext/dota2/protobufs/sdk.py @@ -0,0 +1,142 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: gcsdk_gcmessages.proto +# plugin: python-betterproto + +from __future__ import annotations + +from dataclasses import dataclass + +import betterproto + +from ....protobufs.msg import GCProtobufMessage +from ..enums import EMsg + + +class ESourceEngine(betterproto.Enum): + Source1 = 0 + Source2 = 1 + + +class PartnerAccountType(betterproto.Enum): + NONE = 0 + PerfectWorld = 1 + Invalid = 3 + + +class GCConnectionStatus(betterproto.Enum): + HaveSession = 0 + GcGoingDown = 1 + NoSession = 2 + NoSessionInLogonQueue = 3 + NoSteam = 4 + Suspended = 5 + SteamGoingDown = 6 + + +@dataclass(eq=False, repr=False) +class SOIDOwner(betterproto.Message): + type: int = betterproto.uint32_field(1) + id: int = betterproto.uint64_field(2) + + +@dataclass(eq=False, repr=False) +class SOCacheHaveVersion(betterproto.Message): + soid: SOIDOwner = betterproto.message_field(1) + version: float = betterproto.fixed64_field(2) + service_id: int = betterproto.uint32_field(3) + cached_file_version: int = betterproto.uint32_field(4) + + +class ClientHello(GCProtobufMessage, msg=EMsg.ClientHello): + version: int = betterproto.uint32_field(1) + socache_have_versions: list[SOCacheHaveVersion] = betterproto.message_field(2) + client_session_need: int = betterproto.uint32_field(3) + client_launcher: PartnerAccountType = betterproto.enum_field(4) + secret_key: str = betterproto.string_field(5) + client_language: int = betterproto.uint32_field(6) + engine: ESourceEngine = betterproto.enum_field(7) + steamdatagram_login: bytes = betterproto.bytes_field(8) + platform_id: int = betterproto.uint32_field(9) + game_msg: bytes = betterproto.bytes_field(10) + os_type: int = betterproto.int32_field(11) + render_system: int = betterproto.uint32_field(12) + render_system_req: int = betterproto.uint32_field(13) + screen_width: int = betterproto.uint32_field(14) + screen_height: int = betterproto.uint32_field(15) + screen_refresh: int = betterproto.uint32_field(16) + render_width: int = betterproto.uint32_field(17) + render_height: int = betterproto.uint32_field(18) + swap_width: int = betterproto.uint32_field(19) + swap_height: int = betterproto.uint32_field(20) + is_steam_china: bool = betterproto.bool_field(22) + is_steam_china_client: bool = betterproto.bool_field(24) + platform_name: str = betterproto.string_field(23) + + +@dataclass(eq=False, repr=False) +class CExtraMsgBlock(betterproto.Message): + msg_type: int = betterproto.uint32_field(1) + contents: bytes = betterproto.bytes_field(2) + msg_key: int = betterproto.uint64_field(3) + is_compressed: bool = betterproto.bool_field(4) + + +@dataclass(eq=False, repr=False) +class ClientWelcomeLocation(betterproto.Message): + latitude: float = betterproto.float_field(1) + longitude: float = betterproto.float_field(2) + country: str = betterproto.string_field(3) + + +class ConnectionStatus(GCProtobufMessage, msg=EMsg.ClientConnectionStatus): + status: GCConnectionStatus = betterproto.enum_field(1) + client_session_need: int = betterproto.uint32_field(2) + queue_position: int = betterproto.int32_field(3) + queue_size: int = betterproto.int32_field(4) + wait_seconds: int = betterproto.int32_field(5) + estimated_wait_seconds_remaining: int = betterproto.int32_field(6) + + +@dataclass(eq=False, repr=False) +class SOCacheSubscriptionCheck(betterproto.Message): + version: float = betterproto.fixed64_field(2) + owner_soid: SOIDOwner = betterproto.message_field(3) + service_id: int = betterproto.uint32_field(4) + service_list: list[int] = betterproto.uint32_field(5) + sync_version: float = betterproto.fixed64_field(6) + + +@dataclass(eq=False, repr=False) +class SOCacheSubscribedSubscribedType(betterproto.Message): + type_id: int = betterproto.int32_field(1) + object_data: list[bytes] = betterproto.bytes_field(2) + + +@dataclass(eq=False, repr=False) +class SOCacheSubscribed(betterproto.Message): + objects: list[SOCacheSubscribedSubscribedType] = betterproto.message_field(2) + version: float = betterproto.fixed64_field(3) + owner_soid: SOIDOwner = betterproto.message_field(4) + service_id: int = betterproto.uint32_field(5) + service_list: list[int] = betterproto.uint32_field(6) + sync_version: float = betterproto.fixed64_field(7) + + +class ClientWelcome(GCProtobufMessage, msg=EMsg.ClientWelcome): + version: int = betterproto.uint32_field(1) + game_data: bytes = betterproto.bytes_field(2) + outofdate_subscribed_caches: list[SOCacheSubscribed] = betterproto.message_field(3) + uptodate_subscribed_caches: list[SOCacheSubscriptionCheck] = betterproto.message_field(4) + location: ClientWelcomeLocation = betterproto.message_field(5) + save_game_key: bytes = betterproto.bytes_field(6) + gc_socache_file_version: int = betterproto.uint32_field(9) + txn_country_code: str = betterproto.string_field(10) + game_data2: bytes = betterproto.bytes_field(11) + rtime32_gc_welcome_timestamp: int = betterproto.uint32_field(12) + currency: int = betterproto.uint32_field(13) + balance: int = betterproto.uint32_field(14) + balance_url: str = betterproto.string_field(15) + has_accepted_china_ssa: bool = betterproto.bool_field(16) + is_banned_steam_china: bool = betterproto.bool_field(17) + additional_welcome_msgs: CExtraMsgBlock = betterproto.message_field(18) + # steam_learn_server_info: SteamLearnServerInfo = betterproto.message_field(20) # steam nonsense diff --git a/steam/ext/dota2/protobufs/shared_enums.py b/steam/ext/dota2/protobufs/shared_enums.py new file mode 100644 index 00000000..3929985a --- /dev/null +++ b/steam/ext/dota2/protobufs/shared_enums.py @@ -0,0 +1,137 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: dota_shared_enums.proto +# plugin: python-betterproto + + +import betterproto + + +class EMatchGroupServerStatus(betterproto.Enum): + OK = 0 + LimitedAvailability = 1 + Offline = 2 + + +class GameMode(betterproto.Enum): + NONE = 0 + AllPick = 1 + CaptainsMode = 2 + RandomDraft = 3 + SingleDraft = 4 + AllRandom = 5 + Intro = 6 + Diretide = 7 + ReverseCaptainsMode = 8 + Frostivus = 9 + Tutorial = 10 + MidOnly = 11 + LeastPlayed = 12 + NewPlayerMode = 13 + CompendiumMatch = 14 + Custom = 15 + CaptainsDraft = 16 + BalancedDraft = 17 + AbilityDraft = 18 + Event = 19 + AllRandomDeathMatch = 20 + Mid1v1 = 21 + AllDraft = 22 + Turbo = 23 + Mutation = 24 + CoachesChallenge = 25 + + +class Team(betterproto.Enum): + GoodGuys = 0 + BadGuys = 1 + Broadcaster = 2 + Spectator = 3 + PlayerPool = 4 + NoTeam = 5 + Custom1 = 6 + Custom2 = 7 + Custom3 = 8 + Custom4 = 9 + Custom5 = 10 + Custom6 = 11 + Custom7 = 12 + Custom8 = 13 + Neutrals = 14 + + +class EMatchOutcome(betterproto.Enum): + Unknown = 0 + RadVictory = 2 + DireVictory = 3 + NeutralVictory = 4 + NoTeamWinner = 5 + Custom1Victory = 6 + Custom2Victory = 7 + Custom3Victory = 8 + Custom4Victory = 9 + Custom5Victory = 10 + Custom6Victory = 11 + Custom7Victory = 12 + Custom8Victory = 13 + NotScoredPoorNetworkConditions = 64 + NotScoredLeaver = 65 + NotScoredServerCrash = 66 + NotScoredNeverStarted = 67 + NotScoredCanceled = 68 + NotScoredSuspicious = 69 + + +class MatchVote(betterproto.Enum): + Invalid = 0 + Positive = 1 + Negative = 2 + + +class EEvent(betterproto.Enum): + NONE = 0 + Diretide = 1 + SpringFestival = 2 + Frostivus2013 = 3 + Compendium2014 = 4 + NexonPcBang = 5 + PwrdDac2015 = 6 + NewBloom2015 = 7 + International2015 = 8 + FallMajor2015 = 9 + OraclePa = 10 + NewBloom2015Prebeast = 11 + Frostivus = 12 + WinterMajor2016 = 13 + International2016 = 14 + FallMajor2016 = 15 + WinterMajor2017 = 16 + NewBloom2017 = 17 + International2017 = 18 + PlusSubscription = 19 + SinglesDay2017 = 20 + Frostivus2017 = 21 + International2018 = 22 + Frostivus2018 = 23 + NewBloom2019 = 24 + International2019 = 25 + NewPlayerExperience = 26 + Frostivus2019 = 27 + NewBloom2020 = 28 + International2020 = 29 + TeamFandom = 30 + Diretide2020 = 31 + Spring2021 = 32 + Fall2021 = 33 + TeamFandomFall2021 = 34 + Team20212022Tour2 = 35 + International2022 = 36 + Team20212022Tour3 = 37 + TeamInternational2022 = 38 + PermanentGrants = 39 + MuertaReleaseSpring2023 = 40 + Team2023Tour1 = 41 + Team2023Tour2 = 42 + Team023Tour3 = 43 + International2023 = 45 + TenthAnniversary = 46 + Frostivus2023 = 48 diff --git a/steam/ext/dota2/protobufs/watch.py b/steam/ext/dota2/protobufs/watch.py new file mode 100644 index 00000000..bc5ece96 --- /dev/null +++ b/steam/ext/dota2/protobufs/watch.py @@ -0,0 +1,91 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: dota_gcmessages_client_watch.proto +# plugin: python-betterproto + +from __future__ import annotations + +from dataclasses import dataclass + +import betterproto + +from ....protobufs.msg import GCProtobufMessage +from ..enums import EMsg +from .common import MatchMinimal # noqa: TCH001 + +# FIND TOP SOURCE TV GAMES + + +@dataclass(eq=False, repr=False) +class SourceTVGameSmall(betterproto.Message): + activate_time: int = betterproto.uint32_field(1) + deactivate_time: int = betterproto.uint32_field(2) + server_steam_id: int = betterproto.uint64_field(3) + lobby_id: int = betterproto.uint64_field(4) + league_id: int = betterproto.uint32_field(5) + lobby_type: int = betterproto.uint32_field(6) + game_time: int = betterproto.int32_field(7) + delay: int = betterproto.uint32_field(8) + spectators: int = betterproto.uint32_field(9) + game_mode: int = betterproto.uint32_field(10) + average_mmr: int = betterproto.uint32_field(11) + match_id: int = betterproto.uint64_field(12) + series_id: int = betterproto.uint32_field(13) + team_name_radiant: str = betterproto.string_field(15) + team_name_dire: str = betterproto.string_field(16) + team_logo_radiant: float = betterproto.fixed64_field(24) + team_logo_dire: float = betterproto.fixed64_field(25) + team_id_radiant: int = betterproto.uint32_field(30) + team_id_dire: int = betterproto.uint32_field(31) + sort_score: int = betterproto.uint32_field(17) + last_update_time: float = betterproto.float_field(18) + radiant_lead: int = betterproto.int32_field(19) + radiant_score: int = betterproto.uint32_field(20) + dire_score: int = betterproto.uint32_field(21) + players: list[SourceTVGameSmallPlayer] = betterproto.message_field(22) + building_state: float = betterproto.fixed32_field(23) + weekend_tourney_tournament_id: int = betterproto.uint32_field(26) + weekend_tourney_division: int = betterproto.uint32_field(27) + weekend_tourney_skill_level: int = betterproto.uint32_field(28) + weekend_tourney_bracket_round: int = betterproto.uint32_field(29) + custom_game_difficulty: int = betterproto.uint32_field(32) + + +@dataclass(eq=False, repr=False) +class SourceTVGameSmallPlayer(betterproto.Message): + account_id: int = betterproto.uint32_field(1) + hero_id: int = betterproto.uint32_field(2) + team_slot: int = betterproto.uint32_field(3) + team: int = betterproto.uint32_field(4) + + +class ClientToGCFindTopSourceTVGames(GCProtobufMessage, msg=EMsg.ClientToGCFindTopSourceTVGames): + search_key: str = betterproto.string_field(1) + league_id: int = betterproto.uint32_field(2) + hero_id: int = betterproto.uint32_field(3) + start_game: int = betterproto.uint32_field(4) + game_list_index: int = betterproto.uint32_field(5) + lobby_ids: list[int] = betterproto.uint64_field(6) + + +class GCToClientFindTopSourceTVGamesResponse(GCProtobufMessage, msg=EMsg.GCToClientFindTopSourceTVGamesResponse): + search_key: str = betterproto.string_field(1) + league_id: int = betterproto.uint32_field(2) + hero_id: int = betterproto.uint32_field(3) + start_game: int = betterproto.uint32_field(4) + num_games: int = betterproto.uint32_field(5) + game_list_index: int = betterproto.uint32_field(6) + game_list: list[SourceTVGameSmall] = betterproto.message_field(7) + specific_games: bool = betterproto.bool_field(8) + bot_game: SourceTVGameSmall = betterproto.message_field(9) + + +# MATCHES MINIMAL + + +class ClientToGCMatchesMinimalRequest(GCProtobufMessage, msg=EMsg.ClientToGCMatchesMinimalRequest): + match_ids: list[int] = betterproto.uint64_field(1) + + +class ClientToGCMatchesMinimalResponse(GCProtobufMessage, msg=EMsg.ClientToGCMatchesMinimalResponse): + matches: list[MatchMinimal] = betterproto.message_field(1) + last_match: bool = betterproto.bool_field(2) diff --git a/steam/ext/dota2/state.py b/steam/ext/dota2/state.py new file mode 100644 index 00000000..c78c0dfe --- /dev/null +++ b/steam/ext/dota2/state.py @@ -0,0 +1,188 @@ +"""Licensed under The MIT License (MIT) - Copyright (c) 2020-present James H-B. See LICENSE""" + +from __future__ import annotations + +import asyncio +from functools import partial +from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, Unpack + +from ..._gc import GCState as GCState_ +from ...app import DOTA2 +from ...enums import IntEnum +from ...errors import WSException +from ...id import _ID64_TO_ID32 +from ...state import parser +from .models import PartialUser, User +from .protobufs import client_messages, common, sdk, watch + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from weakref import WeakValueDictionary + + from ...protobufs import friends + from ...types.id import ID32, ID64, Intable + from .client import Client + + # TODO: test this whole Kwargs thing, does it error if we don't provide some of them? + # what defaults does it assume? + + class MatchHistoryKwargs(TypedDict): + account_id: int + start_at_match_id: NotRequired[int] + matches_requested: int + hero_id: NotRequired[int] + include_practice_matches: bool + include_custom_games: bool + include_event_games: bool + request_id: NotRequired[int] + + class PostSocialMessageKwargs(TypedDict): + message: NotRequired[str] + match_id: NotRequired[int] + match_timestamp: NotRequired[int] + + class TopSourceTVGamesKwargs(TypedDict): + search_key: NotRequired[str] + league_id: NotRequired[int] + hero_id: NotRequired[int] + start_game: NotRequired[int] + game_list_index: NotRequired[int] + lobby_ids: NotRequired[list[int]] + + +class Result(IntEnum): + # TODO: is there an official list for this? + Invalid = 0 + OK = 1 + + +class GCState(GCState_[Any]): # TODO: implement basket-analogy for dota2 + client: Client # type: ignore # PEP 705 + _users: WeakValueDictionary[ID32, User] + _APP = DOTA2 # type: ignore + + def _store_user(self, proto: friends.CMsgClientPersonaStateFriend) -> User: + try: + user = self._users[_ID64_TO_ID32(proto.friendid)] + except KeyError: + user = User(state=self, proto=proto) + self._users[user.id] = user + else: + user._update(proto) + return user + + def get_partial_user(self, id: Intable) -> PartialUser: + return PartialUser(self, id) + + if TYPE_CHECKING: + + def get_user(self, id: ID32) -> User | None: ... + + async def fetch_user(self, user_id64: ID64) -> User: ... + + async def fetch_users(self, user_id64s: Iterable[ID64]) -> Sequence[User]: ... + + async def _maybe_user(self, id: Intable) -> User: ... + + async def _maybe_users(self, id64s: Iterable[ID64]) -> Sequence[User]: ... + + def _get_gc_message(self) -> sdk.ClientHello: + return sdk.ClientHello() + + @parser + def parse_client_goodbye(self, msg: sdk.ConnectionStatus | None = None) -> None: + if msg is None or msg.status == sdk.GCConnectionStatus.NoSession: + self.dispatch("gc_disconnect") + self._gc_connected.clear() + self._gc_ready.clear() + if msg is not None: + self.dispatch("gc_status_change", msg.status) + + @parser + async def parse_gc_client_connect(self, msg: sdk.ClientWelcome) -> None: + if not self._gc_ready.is_set(): + self._gc_ready.set() + self.dispatch("gc_ready") + + # dota fetch proto calls + # the difference between these calls and the ones in `client`/`models` is that + # they directly give proto-response while the latter modelize them into more convenient formats. + + async def fetch_top_source_tv_games( + self, *, timeout: float = 7.0, **kwargs: Unpack[TopSourceTVGamesKwargs] + ) -> list[watch.GCToClientFindTopSourceTVGamesResponse]: + """Fetch Top Source TV Games.""" + start_game = kwargs.get("start_game") or 0 + # # if start_game and (start_game < 0 or start_game > 90): # TODO: ??? + # # # in my experience it never answers in these cases + # # raise ValueError("start_game should be between 0 and 90 inclusively.") + + def check(start_game: int, msg: watch.GCToClientFindTopSourceTVGamesResponse) -> bool: + return msg.start_game == start_game + + futures = [ + self.ws.gc_wait_for( + watch.GCToClientFindTopSourceTVGamesResponse, + check=partial(check, start_game), + ) + for start_game in range(0, start_game + 1, 10) # TODO: is it correct as in same as it'd be missing + ] + await self.ws.send_gc_message(watch.ClientToGCFindTopSourceTVGames(**kwargs)) + async with asyncio.timeout(timeout): + responses = await asyncio.gather(*futures) + # each response.game_list is 10 games (except possibly last one if filtered by hero) + return responses + + async def fetch_dota2_profile(self, account_id: int) -> client_messages.ProfileResponse: + """Fetch user's dota 2 profile.""" + await self.ws.send_gc_message(client_messages.ProfileRequest(account_id=account_id)) + return await self.ws.gc_wait_for(client_messages.ProfileResponse) + + async def fetch_dota2_profile_card(self, account_id: int) -> common.ProfileCard: + """Fetch user's dota 2 profile card.""" + await self.ws.send_gc_message(client_messages.ClientToGCGetProfileCard(account_id=account_id)) + return await self.ws.gc_wait_for(common.ProfileCard, check=lambda msg: msg.account_id == account_id) + + async def fetch_match_history(self, **kwargs: Unpack[MatchHistoryKwargs]): + """Fetch match history.""" + await self.ws.send_gc_message(client_messages.GetPlayerMatchHistory(**kwargs)) + return await self.ws.gc_wait_for(client_messages.GetPlayerMatchHistoryResponse) + + async def fetch_matches_minimal( + self, match_ids: list[int], *, timeout: float = 7.0 + ) -> watch.ClientToGCMatchesMinimalResponse: + """Fetch matches minimal.""" + await self.ws.send_gc_message(watch.ClientToGCMatchesMinimalRequest(match_ids=match_ids)) + async with asyncio.timeout(timeout): + return await self.ws.gc_wait_for(watch.ClientToGCMatchesMinimalResponse) + + async def fetch_match_details(self, match_id: int, timeout: float = 7.0) -> client_messages.MatchDetailsResponse: + """Fetch match details.""" + await self.ws.send_gc_message(client_messages.MatchDetailsRequest(match_id=match_id)) + async with asyncio.timeout(timeout): + response = await self.ws.gc_wait_for( + client_messages.MatchDetailsResponse, check=lambda msg: msg.match.match_id == match_id + ) + if response.eresult != Result.OK: + raise WSException(response) + return response + + async def fetch_rank(self, rank_type: client_messages.ERankType) -> client_messages.GCToClientRankResponse: + """Fetch rank.""" + await self.ws.send_gc_message(client_messages.ClientToGCRankRequest(rank_type=rank_type)) + return await self.ws.gc_wait_for(client_messages.GCToClientRankResponse) + + async def post_social_message( + self, **kwargs: Unpack[PostSocialMessageKwargs] + ) -> client_messages.GCToClientSocialFeedPostMessageResponse: + """Post social message.""" + await self.ws.send_gc_message(client_messages.ClientToGCSocialFeedPostMessageRequest(**kwargs)) + response = await self.ws.gc_wait_for(client_messages.GCToClientSocialFeedPostMessageResponse) + if response.success != Result.OK: + raise WSException(response) + return response + + async def fetch_matchmaking_stats(self) -> client_messages.MatchmakingStatsResponse: + """Fetch matchmaking stats.""" + await self.ws.send_gc_message(client_messages.MatchmakingStatsRequest()) + return await self.ws.gc_wait_for(client_messages.MatchmakingStatsResponse) diff --git a/tests/unit/test_ext_dota2.py b/tests/unit/test_ext_dota2.py new file mode 100644 index 00000000..33f54f41 --- /dev/null +++ b/tests/unit/test_ext_dota2.py @@ -0,0 +1,4 @@ +from steam.ext import dota2 + +client = dota2.Client() +bot = dota2.Bot(command_prefix="!")