diff --git a/docs/tests/protocols/index.rst b/docs/tests/protocols/index.rst index 016b0f0..bb452ea 100644 --- a/docs/tests/protocols/index.rst +++ b/docs/tests/protocols/index.rst @@ -7,6 +7,7 @@ Protocols Tests test_gamespy4/index test_teamspeak3/index test_won/index + test_toxikk/index test_gamespy1/index test_minecraft/index test_raknet/index diff --git a/docs/tests/protocols/test_toxikk/index.rst b/docs/tests/protocols/test_toxikk/index.rst new file mode 100644 index 0000000..59b88d3 --- /dev/null +++ b/docs/tests/protocols/test_toxikk/index.rst @@ -0,0 +1,7 @@ +.. _test_toxikk: + +test_toxikk +=========== + +.. toctree:: + test_toxikk_status \ No newline at end of file diff --git a/docs/tests/protocols/test_toxikk/test_toxikk_status.rst b/docs/tests/protocols/test_toxikk/test_toxikk_status.rst new file mode 100644 index 0000000..e06ab34 --- /dev/null +++ b/docs/tests/protocols/test_toxikk/test_toxikk_status.rst @@ -0,0 +1,242 @@ +test_toxikk_status +================== + +Here are the results for the test method. + +.. code-block:: json + + { + "name": "TOXIKK Server", + "map": "BL-Foundation", + "game_type": "cruzade.CRZBloodLust", + "num_players": 1, + "max_players": 8, + "password_protected": false, + "stats_enabled": true, + "lan_mode": true, + "players": [], + "raw": { + "hostaddress": "10.13.37.149", + "hostport": 7777, + "num_players": 1, + "max_players": 8, + "lan_mode": true, + "uses_stats": true, + "owner_id": "0110000101734c32", + "owner_name": "TOXIKK Server", + "localized_settings": [ + { + "id": 32779, + "value_index": 8, + "advertisement_type": 1 + }, + { + "id": 0, + "value_index": 2, + "advertisement_type": 1 + }, + { + "id": 1, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 6, + "value_index": 1, + "advertisement_type": 1 + }, + { + "id": 7, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 8, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 9, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 10, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 11, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 12, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 13, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 14, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 15, + "value_index": 1, + "advertisement_type": 1 + }, + { + "id": 16, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 17, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 18, + "value_index": 0, + "advertisement_type": 1 + }, + { + "id": 19, + "value_index": 0, + "advertisement_type": 1 + } + ], + "settings_properties": [ + { + "id": 1073741825, + "data": "BL-Foundation", + "advertisement_type": 2 + }, + { + "id": 1073741826, + "data": "cruzade.CRZBloodLust", + "advertisement_type": 2 + }, + { + "id": 268435704, + "data": 20, + "advertisement_type": 1 + }, + { + "id": 268435705, + "data": 10, + "advertisement_type": 1 + }, + { + "id": 268435703, + "data": 7, + "advertisement_type": 1 + }, + { + "id": 1073741827, + "data": "MY SERVER", + "advertisement_type": 2 + }, + { + "id": 268435717, + "data": 0, + "advertisement_type": 1 + }, + { + "id": 1073741828, + "data": "INSTAGIB\u001cNO STEALTH\u001cSpawn Protection Time", + "advertisement_type": 2 + }, + { + "id": 268435706, + "data": 8, + "advertisement_type": 1 + }, + { + "id": 268435707, + "data": 0, + "advertisement_type": 0 + }, + { + "id": 268435708, + "data": 1, + "advertisement_type": 1 + }, + { + "id": 268435709, + "data": 12, + "advertisement_type": 1 + }, + { + "id": 1073741829, + "data": "76561197984599090", + "advertisement_type": 2 + }, + { + "id": 1073741830, + "data": "", + "advertisement_type": 2 + }, + { + "id": 1073741831, + "data": "", + "advertisement_type": 2 + }, + { + "id": 1073741832, + "data": "GAMIE ?", + "advertisement_type": 2 + }, + { + "id": 1073741833, + "data": "", + "advertisement_type": 2 + }, + { + "id": 1073741834, + "data": "", + "advertisement_type": 2 + }, + { + "id": 1073741837, + "data": "1.0000", + "advertisement_type": 2 + }, + { + "id": 1073741838, + "data": "1", + "advertisement_type": 2 + }, + { + "id": 1073741839, + "data": "1.2", + "advertisement_type": 2 + }, + { + "id": 1073741840, + "data": "DefaultGameList", + "advertisement_type": 2 + } + ], + "map": "BL-Foundation", + "gametype": "BloodLust", + "frag_limit": 20, + "time_limit": 10, + "numbots": 7, + "mutators": [ + "Instagib", + "No Stealth", + "Spawn Protection Time" + ], + "bot_skill": "Experienced", + "pure_server": 1, + "password": 0, + "vs_bots": "None", + "force_respawn": 0 + } + } \ No newline at end of file diff --git a/opengsq/protocols/__init__.py b/opengsq/protocols/__init__.py index 242ea7d..59259a1 100644 --- a/opengsq/protocols/__init__.py +++ b/opengsq/protocols/__init__.py @@ -21,6 +21,7 @@ from opengsq.protocols.scum import Scum from opengsq.protocols.source import Source from opengsq.protocols.teamspeak3 import TeamSpeak3 +from opengsq.protocols.toxikk import Toxikk from opengsq.protocols.udk import UDK from opengsq.protocols.unreal2 import Unreal2 from opengsq.protocols.ut3 import UT3 diff --git a/opengsq/protocols/toxikk.py b/opengsq/protocols/toxikk.py new file mode 100644 index 0000000..f84e3ff --- /dev/null +++ b/opengsq/protocols/toxikk.py @@ -0,0 +1,82 @@ +from opengsq.protocols.udk import UDK +from opengsq.responses.toxikk.status import Status + +class Toxikk(UDK): + GAMEMODE_NAMES = { + "cruzade.CRZBloodLust": "BloodLust", + "cruzade.CRZTeamGame": "Squad Assault", + "cruzade.CRZSquadSurvival": "Squad Survival", + "cruzade.CRZCellCapture": "Cell Capture", + "cruzade.CRZAreaDomination": "Area Domination", + "cruzade.CRZArchRivals": "Arch Rivals" + } + + BOT_SKILL_NAMES = { + 0: "Novice", + 1: "Average", + 2: "Experienced", + 3: "Skilled", + 4: "Adept", + 5: "Masterful", + 6: "Inhuman", + 7: "Godlike" + } + + VS_BOTS_NAMES = { + 0: "None", + 1: "1:1", + 2: "3:2", + 3: "2:1" + } + + full_name = "Toxikk Protocol" + + def __init__(self, host: str, port: int = 14001, timeout: float = 5.0): + super().__init__(host, port, timeout) + self.game_id = 0x4D5707DB + self.packet_version = 7 + + def _parse_response(self, buffer: bytes) -> dict: + base_response = super()._parse_response(buffer) + toxikk_properties = {} + + for prop in base_response['raw']['settings_properties']: + prop_id = prop['id'] + if prop_id == 1073741825: # Map + base_response['map'] = prop['data'] + toxikk_properties['map'] = prop['data'] + elif prop_id == 1073741826: # Game Type + base_response['game_type'] = prop['data'] + toxikk_properties['gametype'] = self.GAMEMODE_NAMES.get(prop['data'], prop['data']) + elif prop_id == 268435704: # Frag Limit + toxikk_properties['frag_limit'] = prop['data'] + elif prop_id == 268435705: # Time Limit + toxikk_properties['time_limit'] = prop['data'] + elif prop_id == 268435703: # Number of Bots + toxikk_properties['numbots'] = prop['data'] + elif prop_id == 1073741828: # Mutators + toxikk_properties['mutators'] = self._parse_mutators(prop['data']) + + for setting in base_response['raw']['localized_settings']: + setting_id = setting['id'] + value_index = setting['value_index'] + + if setting_id == 0: + toxikk_properties['bot_skill'] = self.BOT_SKILL_NAMES.get(value_index) + elif setting_id == 6: + toxikk_properties['pure_server'] = value_index + elif setting_id == 7: + base_response['password_protected'] = value_index == 1 + toxikk_properties['password'] = value_index + elif setting_id == 8: + toxikk_properties['vs_bots'] = self.VS_BOTS_NAMES.get(value_index) + elif setting_id == 10: + toxikk_properties['force_respawn'] = value_index + + base_response['raw'].update(toxikk_properties) + return base_response + + def _parse_mutators(self, mutator_value: any) -> list: + if not mutator_value or not isinstance(mutator_value, str): + return [] + return [m.title() for m in mutator_value.split('\x1c') if m] \ No newline at end of file diff --git a/opengsq/responses/toxikk/__init__.py b/opengsq/responses/toxikk/__init__.py new file mode 100644 index 0000000..2a4c93e --- /dev/null +++ b/opengsq/responses/toxikk/__init__.py @@ -0,0 +1 @@ +from .status import Status \ No newline at end of file diff --git a/opengsq/responses/toxikk/status.py b/opengsq/responses/toxikk/status.py new file mode 100644 index 0000000..a7eff99 --- /dev/null +++ b/opengsq/responses/toxikk/status.py @@ -0,0 +1,9 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List +from opengsq.responses.udk.status import Status as UDKStatus + +@dataclass +class Status(UDKStatus): + """Toxikk-specific status response""" + mutators: List[str] = field(default_factory=list) \ No newline at end of file diff --git a/tests/protocols/test_toxikk.py b/tests/protocols/test_toxikk.py new file mode 100644 index 0000000..c5e2b2f --- /dev/null +++ b/tests/protocols/test_toxikk.py @@ -0,0 +1,34 @@ +import pytest +from opengsq.protocols.toxikk import Toxikk +from ..result_handler import ResultHandler + +handler = ResultHandler(__file__) + +@pytest.mark.asyncio +async def test_toxikk_status(): + toxikk = Toxikk(host="10.13.37.149", port=14001) + result = await toxikk.get_status() + + print("\nToxikk Server Details:") + print(f"Server Name: {result.name}") + print(f"Map: {result.map}") + print(f"Game Type: {result.raw.get('gametype')}") + print(f"Players: {result.num_players}/{result.max_players}") + print(f"Time Limit: {result.raw.get('time_limit')} minutes") + print(f"Score Limit: {result.raw.get('frag_limit')} frags") + + print("\nBot Settings:") + print(f"Bot Count: {result.raw.get('numbots')}") + print(f"Bot Skill: {result.raw.get('bot_skill')}") + print(f"VS Bots Mode: {result.raw.get('vs_bots')}") + + print("\nServer Settings:") + print(f"Password Protected: {'Yes' if result.password_protected else 'No'}") + print(f"LAN Mode: {'Yes' if result.lan_mode else 'No'}") + print(f"Stats Enabled: {'Yes' if result.stats_enabled else 'No'}") + + print("\nMutators:") + mutators = result.raw.get('mutators', []) + print(", ".join(mutators) if mutators else "None") + + await handler.save_result("test_toxikk_status", result) \ No newline at end of file