diff --git a/docs/api/steam.game_servers.rst b/docs/api/steam.game_servers.rst new file mode 100644 index 0000000..fbf63c8 --- /dev/null +++ b/docs/api/steam.game_servers.rst @@ -0,0 +1,7 @@ +game_servers +============ + +.. automodule:: steam.game_servers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/steam.master_server.rst b/docs/api/steam.master_server.rst deleted file mode 100644 index 0ee1927..0000000 --- a/docs/api/steam.master_server.rst +++ /dev/null @@ -1,7 +0,0 @@ -master_server -============= - -.. automodule:: steam.master_server - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/steam.rst b/docs/api/steam.rst index 9e2c7df..24895ca 100644 --- a/docs/api/steam.rst +++ b/docs/api/steam.rst @@ -7,9 +7,9 @@ API Reference steam.client steam.core steam.enums + steam.game_servers steam.globalid steam.guard - steam.master_server steam.steamid steam.webapi steam.webauth diff --git a/steam/game_servers.py b/steam/game_servers.py index 228cfed..0303f63 100644 --- a/steam/game_servers.py +++ b/steam/game_servers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- r""" Master Server Query Protocol This module implements the legacy Steam master server protocol. @@ -38,8 +39,85 @@ Filter code What it does \\collapse_addr_hash\\1 Return only one server for each unique IP address matched \\gameaddr\\[ip] Return only servers on the specified IP address (port supported and optional) =========================== ========================================================================================================================= + +Examples +-------- + +Team Fortress 2 (Source) + +.. code:: python + + >>> from steam import game_servers as gs + >>> server_addr = next(gs.query_master(r'\appid\40\empty\1\secure\1')) # single TF2 Server + >>> gs.a2s_ping(server_addr) + 68.60899925231934 + >>> gs.a2s_info(server_addr) + {'_ping': 74.61714744567871, + '_type': 'source', + 'app_id': 40, + 'bots': 0, + 'environment': 'l', + 'folder': u'dmc', + 'game': u'DMC\t\t\t\t\t\t\t\t1', + 'map': u'crossfire', + 'max_players': 32, + 'name': u'\t\t\u2605\t\t All Guns party \u2605\t \tCrossfire 24/7\t\t', + 'players': 21, + 'protocol': 48, + 'server_type': 'd', + 'vac': 1, + 'visibility': 0} + >>> gs.a2s_players(server_addr) + [{'duration': 192.3097381591797, 'index': 0, 'name': '(2)Player', 'score': 4}, + {'duration': 131.6618194580078, 'index': 1, 'name': 'BOLT', 'score': 2}, + {'duration': 16.548809051513672, 'index': 2, 'name': 'Alpha', 'score': 0}, + {'duration': 1083.1539306640625, 'index': 3, 'name': 'Player', 'score': 29}, + {'duration': 446.7716064453125, 'index': 4, 'name': '(1)Player', 'score': 11}, + {'duration': 790.9588012695312, 'index': 5, 'name': 'ИВАНГАЙ', 'score': 11}] + >>> gs.a2s_rules(server_addr) + {'amx_client_languages': 1, + 'amx_nextmap': 'crossfire', + 'amx_timeleft': '00:00', + 'amxmodx_version': '1.8.2', + .... + +Ricohet (GoldSrc) + +.. code:: python + + >>> from steam import game_servers as gs + >>> server_addr = next(gs.query_master(r'\appid\60')) # get a single ip from hl2 master + >>> gs.a2s_info(server_addr, force_goldsrc=True) # only accept goldsrc response + {'_ping': 26.59320831298828, + '_type': 'goldsrc', + 'address': '127.0.0.1:27050', + 'bots': 0, + 'ddl': 0, + 'download_link': '', + 'environment': 'w', + 'folder': 'ricochet', + 'game': 'Ricochet', + 'link': '', + 'map': 'rc_deathmatch2', + 'max_players': 32, + 'mod': 1, + 'name': 'Anitalink.com Ricochet', + 'players': 1, + 'protocol': 47, + 'server_type': 'd', + 'size': 0, + 'type': 1, + 'vac': 1, + 'version': 1, + 'visibility': 0} + +API +--- """ import socket +from binascii import crc32 +from bz2 import decompress as _bz2_decompress +from re import match as _re_match from struct import pack as _pack, unpack_from as _unpack_from from time import time as _time from enum import IntEnum, Enum @@ -65,7 +143,7 @@ class MSServer: def query_master(filter_text=r'\napp\500', region=MSRegion.World, master=MSServer.Source): - r"""Generator that returns (IP,port) pairs of serveras + r"""Generator that returns (IP,port) pairs of servers .. warning:: Valve's master servers seem to be heavily rate limited. @@ -102,11 +180,11 @@ def query_master(filter_text=r'\napp\500', region=MSRegion.World, master=MSServe while True: ms.send(req_prefix + next_ip + req_suffix) - data = ms.recv(2048) - data = StructReader(data) + data = StructReader(ms.recv(2048)) # verify response header if data.read(6) != b'\xFF\xFF\xFF\xFF\x66\x0A': + ms.close() raise RuntimeError("Invalid response from master server") # read list of servers @@ -114,36 +192,106 @@ def query_master(filter_text=r'\napp\500', region=MSRegion.World, master=MSServe ip = '.'.join(map(str, data.unpack('>BBBB'))) port, = data.unpack('>H') - # check if we've reach the end of the list + # check if we've reached the end of the list if ip == '0.0.0.0' and port == 0: + ms.close() return yield ip, port next_ip = '{}:{}'.format(ip, port).encode('utf-8') + ms.close() + def _handle_a2s_response(sock): packet = sock.recv(2048) header, = _unpack_from('> 2, pkt_byte & 0xF, False # idx, total, compressed + elif payload_offset in (10, 12, 18): # Source + pkt_id, num_pkts, pkt_idx, = _unpack_from('