diff --git a/steam/client/builtins/misc.py b/steam/client/builtins/misc.py index c955a18..bf16148 100644 --- a/steam/client/builtins/misc.py +++ b/steam/client/builtins/misc.py @@ -2,10 +2,19 @@ Various features that don't have a category """ import logging +import sys from eventemitter import EventEmitter from steam.core.msg import MsgProto, get_um +from steam.enums import EResult, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType from steam.enums.emsg import EMsg from steam.util import WeakRefKeyDict +from steam.util.throttle import ConstantRateLimit + +if sys.version_info < (3,): + _range = xrange +else: + _range = range + class Misc(object): def __init__(self, *args, **kwargs): @@ -33,6 +42,31 @@ class Misc(object): self.send(message) + def get_leaderboard(self, app_id, name): + """Find a leaderboard + + :param app_id: application id + :type app_id: :class:`int` + :param name: leaderboard name + :type name: :class:`str` + :return: leaderboard instance + :rtype: :class:`SteamLeaderboard` + :raises: :class:`LookupError` on message timeout or error + """ + message = MsgProto(EMsg.ClientLBSFindOrCreateLB) + message.header.routing_appid = app_id + message.body.app_id = app_id + message.body.leaderboard_name = name + message.body.create_if_not_found = False + + resp = self.send_job_and_wait(message, timeout=15) + + if not resp: + raise LookupError("Didn't receive response within 15seconds :(") + if resp.eresult != EResult.OK: + raise LookupError(EResult(resp.eresult)) + + return SteamLeaderboard(self, app_id, name, resp) class SteamUnifiedMessages(EventEmitter): """Simple API for send/recv of unified messages @@ -126,3 +160,116 @@ class SteamUnifiedMessages(EventEmitter): return None else: return resp[0] + + +class SteamLeaderboard(object): + """Steam leaderboard object. + Generated via :meth:`Misc.get_leaderboard()` + Works more or less like a :class:`list` to access entries. + + .. note:: + Each slice will produce a message to steam. + Steam and protobufs might not like large slices. + Avoid accessing individual entries by index and instead use iteration or well sized slices. + + Example usage: + + .. code:: python + + lb = client.get_leaderboard(...) + + print len(lb) + + for entry in lb[:100]: # top 100 + pass + """ + app_id = 0 + name = '' #: leaderboard name + id = 0 #: leaderboard id + entry_count = 0 + sort_method = ELeaderboardSortMethod.NONE #: :class:`steam.enums.common.ELeaderboardSortMethod` + display_type = ELeaderboardDisplayType.NONE #: :class:`steam.enums.common.ELeaderboardDisplayType` + data_request = ELeaderboardDataRequest.Global #: :class:`steam.enums.common.ELeaderboardDataRequest` + + def __init__(self, steam, app_id, name, data): + self._steam = steam + self.app_id = app_id + + for field in data.DESCRIPTOR.fields: + if field.name.startswith('leaderboard_'): + self.__dict__[field.name.replace('leaderboard_', '')] = getattr(data, field.name) + + self.sort_method = ELeaderboardSortMethod(self.sort_method) + self.display_type = ELeaderboardDisplayType(self.display_type) + + def __repr__(self): + return "<%s(%d, %s, %d, %s, %s)>" % ( + self.__class__.__name__, + self.app_id, + self.name, + len(self), + self.sort_method, + self.display_type, + ) + + def __len__(self): + return self.entry_count + + def get_entries(self, start=0, end=0, data_request=ELeaderboardDataRequest.Global): + """Get leaderboard entries. + + :param start: start entry, not index (e.g. rank 1 is `start=1`) + :type start: :class:`int` + :param end: end entry, not index (e.g. only one entry then `start=1,end=1`) + :type end: :class:`int` + :param data_request: data being requested + :type data_request: :class:`steam.enums.common.ELeaderboardDataRequest` + :return: a list of entries, see `CMsgClientLBSGetLBEntriesResponse` + :rtype: :class:`list` + :raises: :class:`LookupError` on message timeout or error + """ + message = MsgProto(EMsg.ClientLBSGetLBEntries) + message.body.app_id = self.app_id + message.body.leaderboard_id = self.id + message.body.range_start = start + message.body.range_end = end + message.body.leaderboard_data_request = data_request + + resp = self._steam.send_job_and_wait(message, timeout=15) + + if not resp: + raise LookupError("Didn't receive response within 15seconds :(") + if resp.eresult != EResult.OK: + raise LookupError(EResult(resp.eresult)) + + return resp.entries + + def __getitem__(self, x): + if isinstance(x, slice): + stop_max = len(self) + start = 0 if x.start is None else x.start if x.start >= 0 else max(0, x.start + stop_max) + stop = stop_max if x.stop is None else x.stop if x.stop >= 0 else max(0, x.stop + stop_max) + step = x.step or 1 + if step < 0: + start, stop = stop, start + step = abs(step) + else: + start, stop, step = x, x + 1, 1 + + if start >= stop: return [] + + entries = self.get_entries(start+1, stop, self.data_request) + + return [entries[i] for i in _range(0, len(entries), step)] + + def __iter__(self): + def entry_generator(): + with ConstantRateLimit(1, 1, use_gevent=True) as r: + for i in _range(0, len(self), 500): + entries = self[i:i+500] + if not entries: + raise StopIteration + for entry in entries: + yield entry + r.wait() + return entry_generator() diff --git a/steam/enums/common.py b/steam/enums/common.py index 2168ce7..a5019e0 100644 --- a/steam/enums/common.py +++ b/steam/enums/common.py @@ -346,6 +346,27 @@ class EClientPersonaStateFlag(SteamIntEnum): ClanTag = 1024 Facebook = 2048 +class ELeaderboardDataRequest(SteamIntEnum): + Global = 0 + GlobalAroundUser = 1 + Friends = 2 + Users = 3 + +class ELeaderboardSortMethod(SteamIntEnum): + NONE = 0 + Ascending = 1 + Descending = 2 + +class ELeaderboardDisplayType(SteamIntEnum): + NONE = 0 + Numeric = 1 + TimeSeconds = 2 + TimeMilliSeconds = 3 + +class ELeaderboardUploadScoreMethod(SteamIntEnum): + NONE = 0 + KeepBest = 1 + ForceUpdate = 2 # Do not remove from sys import modules