ValvePython / steam

☁️ Python package for interacting with Steam
http://steam.readthedocs.io
MIT License
1.06k stars 129 forks source link

Option to return a2s_rules as bytes instead of strings #358

Closed tjensen closed 2 years ago

tjensen commented 2 years ago

The steam.game_servers.a2s_rules function currently returns the server rules as a dictionary of strings to strings (Dict[str, str]), which is suitable for plain text rules. However, some game servers use the Steam A2S_RULES API to return binary data. For example, Bohemia Interactive's Arma and DayZ servers use the rules API to return required DLC and Mod information (see https://community.bistudio.com/wiki/Arma_3:_ServerBrowserProtocol3 for details).

Unfortunately, because steam.game_servers.a2s_rules decodes the A2S_RULES server response data as UTF-8 with "replace" as the errors option, important binary data is lost when querying an Arma or DayZ server's rules.

For example:

>>> pprint(a2s_rules(("192.169.82.235",2303)))
{'\x01\x01': '\x01\x01\x01\x02\x01\x02\x01\x02\x02���_\x04ITv�\x0eNamalsk '
             'Island#�:\x13Ԓ\x11\x0b'
             'Livonia DLC\x03\x0eCrimsonZamboni\x04dayz\x06sumrak',
 'allowedBuild': 0,
 'dedicated': 1,
 'island': 'namalsk',
 'language': 65545,
 'platform': 'win',
 'requiredBuild': 0,
 'requiredVersion': 113,
 'timeLeft': 15}

It would be very helpful if we could get a server's A2S_RULES information as raw bytes, instead of str objects. Perhaps an optional keyword argument could be added to the steam.game_servers.a2s_rules function? For example:

a2s_rules(("192.169.82.235",2303), binary=True)

The default should be binary=False to maintain backwards compatibility but when binary=True, the resulting rule names and values would be returned as bytes objects (i.e. Dict[bytes, bytes]), allowing the caller to further decode the binary data, as needed.

rossengeorgiev commented 2 years ago

Interesting. Not really a fan of the extra parameter. I would like this to he handled intelligently

I'm not quite sure I understand how ARMA rules protocol works, particularly the key escape files. We could simply not decode the value if the key is non ascii. That would work. Not sure if other games do similar hacks

tjensen commented 2 years ago

We could simply not decode the value if the key is non ascii.

I don't think that is 100% reliable. I can think of conditions where the values look like ASCII but are intended to be processed as binary data and the automatic conversion to strings/integers/floats results in a loss of information.

For example, if a server returns the rule b"foo": b"1.222222222222222222222222222222222222222222222222222222222222", the current code not only decodes it as UTF-8 but also converts the value to a float, so you end up with {"foo": 1.2222222222222223}. Attempting to reverse the conversation (using str(value).encode("utf8")) will result in b"1.2222222222222223" and a loss of data.

rossengeorgiev commented 2 years ago

Good point. In the binary=false case it should not mangle that binary data by trying to decode it as utf-8. I'm not sure how many such edge cases exist, but this one can definitely be handled

Maybe the decimal should be returned as a str by default

luckydonald commented 2 years ago

I think I would call the option raw: bool = True but otherwise looks like a useful addition to me. I would recommend to put it behind a single *, so it's forced to be a named keyword argument. def foo(first_param, *, raw=False)