Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ext.dota2 - Dota 2 Game Coordinator extension #460

Draft
wants to merge 60 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
de7a99f
🪧Compile protobufs into `.py` mirrors
Aluerie Jan 23, 2024
e21b9ef
🐸Initial implementation for Dota2 GC
Aluerie Jan 23, 2024
513ea0c
📃Initial docs for ext.dota2
Aluerie Jan 23, 2024
60e4c39
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 23, 2024
89bdc97
✒️Stealth edit to `fetch_top_source_tv_games` docstring
Aluerie Jan 23, 2024
dd0a3b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 23, 2024
6355e21
Apply suggestions from code review by Gobot
Aluerie Jan 23, 2024
89b5b01
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 23, 2024
ca12e0e
Apply suggestions from code review
Aluerie Jan 23, 2024
728fa0e
⛔Remove Steam NonSense
Aluerie Jan 24, 2024
f96967f
🔨Fix bad enum namings
Aluerie Jan 24, 2024
494bd5c
🪓 Total Overhaul of the PR
Aluerie Jan 24, 2024
f8bd9b6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 24, 2024
2e9383f
🖋️Small doc edit
Aluerie Jan 24, 2024
da096d4
🆘Apply suggestions from code review by Gobot
Aluerie Jan 25, 2024
ab90872
🩺 More feedback corrections
Aluerie Jan 25, 2024
c07dff9
🪿Remove `fmt: off` from betterproto Enums
Aluerie Jan 25, 2024
dd1b731
🤺Quotes ' -> "
Aluerie Jan 25, 2024
db78cd0
🪥 Few more brush ups
Aluerie Jan 25, 2024
8ec073f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 25, 2024
3c89f4a
🪥and more
Aluerie Jan 25, 2024
e353b49
📺LiveMatchPlayer should subclass Partial user
Aluerie Jan 25, 2024
49ac537
🦸‍♀️heroes property for LiveMatch
Aluerie Jan 25, 2024
9fd115f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 25, 2024
aa7433b
🤧fix import * abd `__all__`
Aluerie Jan 25, 2024
8561fcb
Merge branch 'introduce-ext.dota2' of https://github.com/Aluerie/stea…
Aluerie Jan 25, 2024
59bf6fd
🔣Doc String + Remove C prefix
Aluerie Jan 25, 2024
037355f
📈 Glicko Rating and Behavior Summary
Aluerie Jan 27, 2024
5a19a1d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 27, 2024
7e3ddb1
💥MatchDetails, MM stats, rating - very raw
Aluerie Feb 3, 2024
3b83928
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 3, 2024
b4bd089
Merge branch 'Gobot1234:main' into introduce-ext.dota2
Aluerie Sep 21, 2024
69d0c92
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 21, 2024
bd12ec8
Merge branch 'Gobot1234:main' into introduce-ext.dota2
Aluerie Sep 21, 2024
517e33f
🏅Implement RankTier Medals
Aluerie Sep 24, 2024
04fcdef
🔬Introduce Match Minimal
Aluerie Sep 29, 2024
5d50888
📂Divide `models.py` into a folder
Aluerie Sep 30, 2024
38a8f39
Create models.py
Aluerie Sep 30, 2024
702f883
🧓Match History
Aluerie Sep 30, 2024
f553a8d
📲Social Feed Post Message
Aluerie Sep 30, 2024
bfef6bd
🪥Some Brush-ups
Aluerie Sep 30, 2024
da9d459
〽️Instantiate Partial Match
Aluerie Sep 30, 2024
8d166c3
🖼️Profile Request
Aluerie Sep 30, 2024
37a8fa7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 30, 2024
55f0486
Doc Edits
Aluerie Sep 30, 2024
87e9220
Merge branch 'introduce-ext.dota2' of https://github.com/Aluerie/stea…
Aluerie Sep 30, 2024
39f907f
🔮Refactor things back to proper state -> models
Aluerie Oct 2, 2024
047da1f
🔝Separate state for TopSource too
Aluerie Oct 2, 2024
6abe799
🦜Separate state for MM stats
Aluerie Oct 2, 2024
12c6df1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2024
e47e624
🌐 `replay_url`, `metadata_url`
Aluerie Oct 2, 2024
4a54ad1
🙂Add Facets to protos
Aluerie Oct 3, 2024
f465544
🎶Fix Match History
Aluerie Oct 7, 2024
1a96c93
🆕Add Kez hero
Aluerie Nov 22, 2024
f8f9e4a
🪥`timeout` keyword arg for some state methods
Aluerie Dec 31, 2024
5319c4b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 31, 2024
031fef9
Merge branch 'Gobot1234:main' into introduce-ext.dota2
Aluerie Dec 31, 2024
186fe70
Bump typing-extensions from 4.10.0 to 4.12.2
Aluerie Jan 29, 2025
501c0f1
Revert "Bump typing-extensions from 4.10.0 to 4.12.2"
Aluerie Jan 31, 2025
e37e82c
Merge branch 'Gobot1234:main' into introduce-ext.dota2
Aluerie Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
🏅Implement RankTier Medals
and a few brush ups
  • Loading branch information
Aluerie committed Sep 24, 2024
commit 517e33fbab93a2aaa90b5e1ae4eca48d1ef7c55b
12 changes: 2 additions & 10 deletions steam/ext/dota2/client.py
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ async def top_live_matches(self, *, hero: Hero = MISSING, limit: int = 100) -> l
This is similar to game list in the Watch Tab of Dota 2 game app.
"Top matches" in this context means

* tournament matches
* featured tournament matches
* highest average MMR matches

Parameters
@@ -163,16 +163,8 @@ async def live_matches(
asyncio.TimeoutError
Request time-outed. The reason is usually Dota 2 Game Coordinator lagging or being down.
"""
future = self._state.ws.gc_wait_for(
watch.GCToClientFindTopSourceTVGamesResponse,
check=lambda msg: msg.specific_games == True,
)
await self._state.ws.send_gc_message(watch.ClientToGCFindTopSourceTVGames(lobby_ids=lobby_ids))

async with timeout(15.0):
response = await future
# todo: test with more than 10 lobby_ids, Game Coordinator will probably chunk it wrongly or fail at all

response = await self._state.fetch_live_matches(lobby_ids)
return [LiveMatch(self._state, match) for match in response.game_list]

async def match_details(self, match_id: int) -> MatchDetails:
64 changes: 64 additions & 0 deletions steam/ext/dota2/enums.py
Original file line number Diff line number Diff line change
@@ -15,8 +15,10 @@
"Hero",
"GameMode",
"LobbyType",
"RankTier",
)


# fmt: off
class Hero(IntEnum):
"""Enum representing Dota 2 hero.
@@ -144,6 +146,7 @@ class Hero(IntEnum):
VoidSpirit = 126
Snapfire = 128
Mars = 129
Ringmaster = 131
Dawnbreaker = 135
Marci = 136
PrimalBeast = 137
@@ -273,6 +276,7 @@ def DISPLAY_NAMES(cls: type[Self]) -> Mapping[Hero, str]: # type: ignore
cls.VoidSpirit : "Void Spirit",
cls.Snapfire : "Snapfire",
cls.Mars : "Mars",
cls.Ringmaster : "Ringmaster",
cls.Dawnbreaker : "Dawnbreaker",
cls.Marci : "Marci",
cls.PrimalBeast : "Primal Beast",
@@ -386,6 +390,66 @@ 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 display_name(self) -> str:
if self.value % 10 == 0:
return self.name
else:
return self.name[:-1] + ' ' + self.name[-1]



class EMsg(IntEnum):
# EGCBaseClientMsg - source: gcsystemmsgs.proto
PingRequest = 3001
19 changes: 13 additions & 6 deletions steam/ext/dota2/models.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
from ... import abc, user
from ..._gc.client import ClientUser as ClientUser_
from ...utils import DateTime
from .enums import GameMode, Hero, LobbyType
from .enums import GameMode, Hero, LobbyType, RankTier
from .protobufs import client_messages

if TYPE_CHECKING:
@@ -325,20 +325,27 @@ class BehaviorSummary:
class ProfileCard(Generic[UserT]):
def __init__(self, user: UserT, proto: common.ProfileCard):
self.user = user
self.slots = proto.slots
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 = proto.rank_tier
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
self.rank_tier_score = proto.rank_tier_score
self.leaderboard_rank_core = proto.leaderboard_rank_core
self.title = proto.title
"""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__} user={self.user!r}>"
52 changes: 37 additions & 15 deletions steam/ext/dota2/state.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
from ...id import _ID64_TO_ID32
from ...state import parser
from .models import PartialUser, User
from .protobufs import client_messages, common, sdk
from .protobufs import client_messages, common, sdk, watch

if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
@@ -54,32 +54,54 @@ 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 and the functions in `client`/`models` is that
# these give raw proto responses while the latter modelize/structure/refine them.

async def fetch_user_dota2_profile_card(self, user_id: int) -> common.ProfileCard:
"""Fetch User's Dota 2 Profile Card.

Contains basic info about the account. Kinda mirrors old profile page.
"""
await self.ws.send_gc_message(client_messages.ClientToGCGetProfileCard(account_id=user_id))
return await self.ws.gc_wait_for(
common.ProfileCard,
check=lambda msg: msg.account_id == user_id,
)

async def fetch_match_details(self, match_id: int) -> client_messages.MatchDetailsResponse:
"""Fetch Match Details.

This call is for already finished games. Contains most of the info that can be found in post-match stats.
"""
await self.ws.send_gc_message(client_messages.MatchDetailsRequest(match_id=match_id))
async with timeout(15.0):
return await self.ws.gc_wait_for(
client_messages.MatchDetailsResponse,
check=lambda msg: msg.match.match_id == match_id,
)

@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")
async def fetch_live_matches(self, lobby_ids: list[int]) -> watch.GCToClientFindTopSourceTVGamesResponse:
"""Fetch Live Match by lobby ids."""
await self.ws.send_gc_message(watch.ClientToGCFindTopSourceTVGames(lobby_ids=lobby_ids))
async with timeout(15.0):
# todo: test with more than 10 lobby_ids, Game Coordinator will probably chunk it wrongly or fail at all
return await self.ws.gc_wait_for(
watch.GCToClientFindTopSourceTVGamesResponse,
check=lambda msg: msg.specific_games == True,
)