You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
5.8 KiB

import io
from dataclasses import dataclass, field
from typing import Optional
from a2s.exceptions import BrokenMessageError, BufferExhaustedError
from a2s.defaults import DEFAULT_TIMEOUT, DEFAULT_ENCODING
from a2s.a2s_sync import request_sync
from a2s.a2s_async import request_async
from a2s.byteio import ByteReader
from a2s.datacls import DataclsMeta
A2S_INFO_RESPONSE = 0x49
A2S_INFO_RESPONSE_LEGACY = 0x6D
@dataclass
class SourceInfo(): # metaclass=DataclsMeta
"""Protocol version used by the server"""
protocol: Optional[int] = None
server_name: Optional[str] = None
map_name: Optional[str] = None
folder: Optional[str] = None
game: Optional[str] = None
app_id: Optional[int] = None
player_count: Optional[int] = None
max_players: Optional[int] = None
bot_count: Optional[int] = None
server_type: Optional[str] = None
platform: Optional[str] = None
password_protected: Optional[bool] = None
vac_enabled: Optional[bool] = None
version: Optional[str] = None
edf: Optional[int] = 0
port: Optional[int] = None
steam_id: Optional[int] = None
stv_port: Optional[int] = None
stv_name: Optional[str] = None
keywords: Optional[str] = None
game_id: Optional[int] = None
ping: Optional[float] = None
@property
def has_port(self):
return bool(self.edf & 0x80)
@property
def has_steam_id(self):
return bool(self.edf & 0x10)
@property
def has_stv(self):
return bool(self.edf & 0x40)
@property
def has_keywords(self):
return bool(self.edf & 0x20)
@property
def has_game_id(self):
return bool(self.edf & 0x01)
@dataclass
class GoldSrcInfo(): # metaclass=DataclsMeta
"""IP Address and port of the server"""
address: Optional[str] = None
server_name: Optional[str] = None
map_name: Optional[str] = None
folder: Optional[str] = None
game: Optional[str] = None
player_count: Optional[int] = None
max_players: Optional[int] = None
protocol: Optional[int] = None
server_type: Optional[str] = None
platform: Optional[str] = None
password_protected: Optional[bool] = None
is_mod: Optional[bool] = None
vac_enabled: Optional[bool] = None
bot_count: Optional[int] = None
mod_website: Optional[str] = None
mod_download: Optional[str] = None
mod_version: Optional[int] = None
mod_size: Optional[int] = None
multiplayer_only: Optional[bool] = False
uses_hl_dll: Optional[bool] = True
ping: Optional[float] = None
def info(address, timeout=DEFAULT_TIMEOUT, encoding=DEFAULT_ENCODING):
return request_sync(address, timeout, encoding, InfoProtocol)
async def ainfo(address, timeout=DEFAULT_TIMEOUT, encoding=DEFAULT_ENCODING):
return await request_async(address, timeout, encoding, InfoProtocol)
class InfoProtocol:
@staticmethod
def validate_response_type(response_type):
return response_type in (A2S_INFO_RESPONSE, A2S_INFO_RESPONSE_LEGACY)
@staticmethod
def serialize_request(challenge):
if challenge:
return b"\x54Source Engine Query\0" + challenge.to_bytes(4, "little")
else:
return b"\x54Source Engine Query\0"
@staticmethod
def deserialize_response(reader, response_type, ping):
if response_type == A2S_INFO_RESPONSE:
resp = parse_source(reader)
elif response_type == A2S_INFO_RESPONSE_LEGACY:
resp = parse_goldsrc(reader)
else:
raise Exception(str(response_type))
resp.ping = ping
return resp
def parse_source(reader):
resp = SourceInfo()
resp.protocol = reader.read_uint8()
resp.server_name = reader.read_cstring()
resp.map_name = reader.read_cstring()
resp.folder = reader.read_cstring()
resp.game = reader.read_cstring()
resp.app_id = reader.read_uint16()
resp.player_count = reader.read_uint8()
resp.max_players = reader.read_uint8()
resp.bot_count = reader.read_uint8()
resp.server_type = reader.read_char().lower()
resp.platform = reader.read_char().lower()
if resp.platform == "o": # Deprecated mac value
resp.platform = "m"
resp.password_protected = reader.read_bool()
resp.vac_enabled = reader.read_bool()
resp.version = reader.read_cstring()
try:
resp.edf = reader.read_uint8()
except BufferExhaustedError:
pass
if resp.has_port:
resp.port = reader.read_uint16()
if resp.has_steam_id:
resp.steam_id = reader.read_uint64()
if resp.has_stv:
resp.stv_port = reader.read_uint16()
resp.stv_name = reader.read_cstring()
if resp.has_keywords:
resp.keywords = reader.read_cstring()
if resp.has_game_id:
resp.game_id = reader.read_uint64()
return resp
def parse_goldsrc(reader):
resp = GoldSrcInfo()
resp.address = reader.read_cstring()
resp.server_name = reader.read_cstring()
resp.map_name = reader.read_cstring()
resp.folder = reader.read_cstring()
resp.game = reader.read_cstring()
resp.player_count = reader.read_uint8()
resp.max_players = reader.read_uint8()
resp.protocol = reader.read_uint8()
resp.server_type = reader.read_char()
resp.platform = reader.read_char()
resp.password_protected = reader.read_bool()
resp.is_mod = reader.read_bool()
# Some games don't send this section
if resp.is_mod and len(reader.peek()) > 2:
resp.mod_website = reader.read_cstring()
resp.mod_download = reader.read_cstring()
reader.read(1) # Skip a NULL byte
resp.mod_version = reader.read_uint32()
resp.mod_size = reader.read_uint32()
resp.multiplayer_only = reader.read_bool()
resp.uses_custom_dll = reader.read_bool()
resp.vac_enabled = reader.read_bool()
resp.bot_count = reader.read_uint8()
return resp