From a0676df7d742a6180eb59980b8c8839ad8353690 Mon Sep 17 00:00:00 2001 From: Battlefield Duck Date: Thu, 18 Jan 2024 09:54:05 +0800 Subject: [PATCH] Support Killing Floor Protocol --- opengsq/protocols/__init__.py | 1 + opengsq/protocols/killingfloor.py | 57 ++++++++++ opengsq/protocols/unreal2.py | 35 ++---- tests/protocols/test_killingfloor.py | 27 +++++ .../test_killingfloor/test_get_details.json | 16 +++ .../test_killingfloor/test_get_players.json | 100 ++++++++++++++++++ .../test_killingfloor/test_get_rules.json | 27 +++++ 7 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 opengsq/protocols/killingfloor.py create mode 100644 tests/protocols/test_killingfloor.py create mode 100644 tests/results/test_killingfloor/test_get_details.json create mode 100644 tests/results/test_killingfloor/test_get_players.json create mode 100644 tests/results/test_killingfloor/test_get_rules.json diff --git a/opengsq/protocols/__init__.py b/opengsq/protocols/__init__.py index 94834d0..1b31005 100644 --- a/opengsq/protocols/__init__.py +++ b/opengsq/protocols/__init__.py @@ -7,6 +7,7 @@ from opengsq.protocols.gamespy2 import GameSpy2 from opengsq.protocols.gamespy3 import GameSpy3 from opengsq.protocols.gamespy4 import GameSpy4 +from opengsq.protocols.killingfloor import KillingFloor from opengsq.protocols.minecraft import Minecraft from opengsq.protocols.quake1 import Quake1 from opengsq.protocols.quake2 import Quake2 diff --git a/opengsq/protocols/killingfloor.py b/opengsq/protocols/killingfloor.py new file mode 100644 index 0000000..9b79ea3 --- /dev/null +++ b/opengsq/protocols/killingfloor.py @@ -0,0 +1,57 @@ +from opengsq.binary_reader import BinaryReader +from opengsq.exceptions import InvalidPacketException +from opengsq.protocol_socket import UdpClient +from opengsq.protocols.unreal2 import Unreal2 + + +class KillingFloor(Unreal2): + """Killing Floor Protocol""" + full_name = 'Killing Floor Protocol' + + async def get_details(self): + response = await UdpClient.communicate(self, b'\x79\x00\x00\x00' + bytes([self._DETAILS])) + + # Remove the first 4 bytes \x80\x00\x00\x00 + br = BinaryReader(response[4:]) + header = br.read_byte() + + if header != self._DETAILS: + raise InvalidPacketException( + 'Packet header mismatch. Received: {}. Expected: {}.' + .format(chr(header), chr(self._DETAILS)) + ) + + details = {} + details['ServerId'] = br.read_long() # 0 + details['ServerIP'] = br.read_string() # empty + details['GamePort'] = br.read_long() + details['QueryPort'] = br.read_long() # 0 + details['ServerName'] = self._read_string(br) + details['MapName'] = self._read_string(br) + details['GameType'] = self._read_string(br) + details['NumPlayers'] = br.read_long() + details['MaxPlayers'] = br.read_long() + details['WaveCurrent'] = br.read_long() + details['WaveTotal'] = br.read_long() + details['Ping'] = br.read_long() + details['Flags'] = br.read_long() + details['Skill'] = self._read_string(br) + + return details + + +if __name__ == '__main__': + import asyncio + import json + + async def main_async(): + # killingfloor + killingFloor = KillingFloor(host='104.234.65.235', port=7708, timeout=10.0) + details = await killingFloor.get_details() + print(json.dumps(details, indent=None) + '\n') + rules = await killingFloor.get_rules() + print(json.dumps(rules, indent=None) + '\n') + players = await killingFloor.get_players() + print(json.dumps(players, indent=None) + '\n') + + asyncio.run(main_async()) diff --git a/opengsq/protocols/unreal2.py b/opengsq/protocols/unreal2.py index 9987102..0d7a0ec 100644 --- a/opengsq/protocols/unreal2.py +++ b/opengsq/protocols/unreal2.py @@ -32,26 +32,14 @@ async def get_details(self): details['ServerIP'] = br.read_string() # empty details['GamePort'] = br.read_long() details['QueryPort'] = br.read_long() # 0 - details['ServerName'] = self.__read_string(br) - details['MapName'] = self.__read_string(br) - details['GameType'] = self.__read_string(br) + details['ServerName'] = self._read_string(br) + details['MapName'] = self._read_string(br) + details['GameType'] = self._read_string(br) details['NumPlayers'] = br.read_long() details['MaxPlayers'] = br.read_long() - - if br.remaining_bytes() > 12: - try: - # Killing Floor - stream_position = br.stream_position - details['WaveCurrent'] = br.read_long() - details['WaveTotal'] = br.read_long() - details['Ping'] = br.read_long() - details['Flags'] = br.read_long() - details['Skill'] = self.__read_string(br) - except Exception: - br.stream_position = stream_position - details['Ping'] = br.read_long() - details['Flags'] = br.read_long() - details['Skill'] = self.__read_string(br) + details['Ping'] = br.read_long() + details['Flags'] = br.read_long() + details['Skill'] = self._read_string(br) return details @@ -72,8 +60,8 @@ async def get_rules(self): rules['Mutators'] = [] while not br.is_end(): - key = self.__read_string(br) - val = self.__read_string(br) + key = self._read_string(br) + val = self._read_string(br) if key.lower() == 'mutator': rules['Mutators'].append(val) @@ -100,7 +88,7 @@ async def get_players(self): while not br.is_end(): player = {} player['Id'] = br.read_long() - player['Name'] = self.__read_string(br) + player['Name'] = self._read_string(br) player['Ping'] = br.read_long() player['Score'] = br.read_long() player['StatsId'] = br.read_long() @@ -111,10 +99,9 @@ async def get_players(self): @staticmethod def strip_colors(text: bytes): """Strip color codes""" - string = re.compile(b'\x7f|[\x00-\x1a]|[\x1c-\x1f]').sub(b'', text) - return string.replace(b'\x1b@@', b'').replace(b'\x1b@', b'').replace(b'\x1b', b'') + return re.compile(b'\x1b...|[\x00-\x1a]').sub(b'', text) - def __read_string(self, br: BinaryReader): + def _read_string(self, br: BinaryReader): length = br.read_byte() string = br.read_string() diff --git a/tests/protocols/test_killingfloor.py b/tests/protocols/test_killingfloor.py new file mode 100644 index 0000000..970cbf6 --- /dev/null +++ b/tests/protocols/test_killingfloor.py @@ -0,0 +1,27 @@ +import os + +import pytest +from opengsq.protocols.killingfloor import KillingFloor + +from .result_handler import ResultHandler + +handler = ResultHandler(os.path.basename(__file__)[:-3]) +# handler.enable_save = True + +# Killing Floor +test = KillingFloor(host='104.234.65.235', port=7708) + +@pytest.mark.asyncio +async def test_get_details(): + result = await test.get_details() + await handler.save_result('test_get_details', result) + +@pytest.mark.asyncio +async def test_get_rules(): + result = await test.get_rules() + await handler.save_result('test_get_rules', result) + +@pytest.mark.asyncio +async def test_get_players(): + result = await test.get_players() + await handler.save_result('test_get_players', result) diff --git a/tests/results/test_killingfloor/test_get_details.json b/tests/results/test_killingfloor/test_get_details.json new file mode 100644 index 0000000..362e820 --- /dev/null +++ b/tests/results/test_killingfloor/test_get_details.json @@ -0,0 +1,16 @@ +{ + "ServerId": 0, + "ServerIP": "", + "GamePort": 7707, + "QueryPort": 0, + "ServerName": "|uDeep|/T\\||Noi| #2 < W", + "MapName": "KF-STALKER-DUTY-BASE(REMAKE)", + "GameType": "KFGameType", + "NumPlayers": 17, + "MaxPlayers": 32, + "WaveCurrent": 2, + "WaveTotal": 7, + "Ping": 0, + "Flags": 512, + "Skill": "0" +} diff --git a/tests/results/test_killingfloor/test_get_players.json b/tests/results/test_killingfloor/test_get_players.json new file mode 100644 index 0000000..c0e951a --- /dev/null +++ b/tests/results/test_killingfloor/test_get_players.json @@ -0,0 +1,100 @@ +[ + { + "Id": 769, + "Name": "[CHL] asein", + "Ping": 56, + "Score": 1500, + "StatsId": 536870912 + }, + { + "Id": 554, + "Name": "[MEX] Tologuin", + "Ping": 148, + "Score": 1515, + "StatsId": 536870912 + }, + { + "Id": 374, + "Name": "[CHL] fafe_asta", + "Ping": 60, + "Score": 984, + "StatsId": 536870912 + }, + { + "Id": 311, + "Name": "[ARG] Rekiam", + "Ping": 76, + "Score": 1129, + "StatsId": 536870912 + }, + { + "Id": 277, + "Name": "[ARG] KFL/UFOKILLER", + "Ping": 28, + "Score": 1716, + "StatsId": 536870912 + }, + { + "Id": 271, + "Name": "[KOR] Bohemian", + "Ping": 252, + "Score": 493, + "StatsId": 536870912 + }, + { + "Id": 199, + "Name": "[BRA] Finish", + "Ping": 40, + "Score": 0, + "StatsId": 0 + }, + { + "Id": 83, + "Name": "[CHL] LordChalox", + "Ping": 60, + "Score": 2282, + "StatsId": 536870912 + }, + { + "Id": 29, + "Name": "[CHL] Kriss_Nemesis", + "Ping": 52, + "Score": 226, + "StatsId": 536870912 + }, + { + "Id": 21, + "Name": "[ESP] HomerO", + "Ping": 176, + "Score": 175, + "StatsId": 536870912 + }, + { + "Id": 19, + "Name": "[CHL] Marcelo_Kris", + "Ping": 56, + "Score": 229, + "StatsId": 536870912 + }, + { + "Id": 18, + "Name": "[CHL] ZuraRxx", + "Ping": 92, + "Score": 2046, + "StatsId": 536870912 + }, + { + "Id": 17, + "Name": "[ARG] francomaldonado", + "Ping": 48, + "Score": 59, + "StatsId": 536870912 + }, + { + "Id": 15, + "Name": "[SLV] memtrix", + "Ping": 128, + "Score": 569, + "StatsId": 536870912 + } +] diff --git a/tests/results/test_killingfloor/test_get_rules.json b/tests/results/test_killingfloor/test_get_rules.json new file mode 100644 index 0000000..56a6b51 --- /dev/null +++ b/tests/results/test_killingfloor/test_get_rules.json @@ -0,0 +1,27 @@ +{ + "Mutators": [ + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase", + "ZNBase" + ], + "ServerMode": "dedicated", + "AdminName": "AdminEmail", + "ServerVersion": "1065", + "IsVacSecured": "true", + "MaxSpectators": "6", + "MapVoting": "true", + "KickVoting": "true", + "SP: Version": "750", + "SP: Min perk level": "6", + "SP: Max perk level": "99", + "SP: Num trader weapons": "140", + "SP: Perk 1": "Support Specialist", + "SP: Perk 2": "Berserker" +}