From 5fd70217bd17bc6fafce2a9a950695211e8a7b36 Mon Sep 17 00:00:00 2001 From: Steven Wesner Date: Sat, 18 Jan 2025 18:10:58 -0500 Subject: [PATCH 1/3] Palworld REST API Support Palworld REST API Support --- README.md | 1 + docs/tests/protocols/test_palworld/index.rst | 7 ++ .../test_palworld/test_get_status.rst | 12 +++ opengsq/protocols/__init__.py | 1 + opengsq/protocols/palworld.py | 80 +++++++++++++++++++ opengsq/responses/palworld/__init__.py | 1 + opengsq/responses/palworld/status.py | 17 ++++ tests/protocols/test_palworld.py | 21 +++++ 8 files changed, 140 insertions(+) create mode 100644 docs/tests/protocols/test_palworld/index.rst create mode 100644 docs/tests/protocols/test_palworld/test_get_status.rst create mode 100644 opengsq/protocols/palworld.py create mode 100644 opengsq/responses/palworld/__init__.py create mode 100644 opengsq/responses/palworld/status.py create mode 100644 tests/protocols/test_palworld.py diff --git a/README.md b/README.md index c1f8159..ee1cd73 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ from opengsq.protocols import ( Kaillera, KillingFloor, Minecraft, + Palworld, Quake1, Quake2, Quake3, diff --git a/docs/tests/protocols/test_palworld/index.rst b/docs/tests/protocols/test_palworld/index.rst new file mode 100644 index 0000000..9c4da7d --- /dev/null +++ b/docs/tests/protocols/test_palworld/index.rst @@ -0,0 +1,7 @@ +.. _test_palworld: + +test_palworld +================= + +.. toctree:: + test_get_status diff --git a/docs/tests/protocols/test_palworld/test_get_status.rst b/docs/tests/protocols/test_palworld/test_get_status.rst new file mode 100644 index 0000000..f2bac6b --- /dev/null +++ b/docs/tests/protocols/test_palworld/test_get_status.rst @@ -0,0 +1,12 @@ +test_get_status +=============== + +Here are the results for the test method. + +.. code-block:: json + + { + "num_players": 3, + "max_players": 32, + "server_name": "A Palworld Server" + } diff --git a/opengsq/protocols/__init__.py b/opengsq/protocols/__init__.py index 90bd79b..c983d5e 100644 --- a/opengsq/protocols/__init__.py +++ b/opengsq/protocols/__init__.py @@ -10,6 +10,7 @@ from opengsq.protocols.kaillera import Kaillera from opengsq.protocols.killingfloor import KillingFloor from opengsq.protocols.minecraft import Minecraft +from opengsq.protocols.palworld import Palworld from opengsq.protocols.quake1 import Quake1 from opengsq.protocols.quake2 import Quake2 from opengsq.protocols.quake3 import Quake3 diff --git a/opengsq/protocols/palworld.py b/opengsq/protocols/palworld.py new file mode 100644 index 0000000..7289a53 --- /dev/null +++ b/opengsq/protocols/palworld.py @@ -0,0 +1,80 @@ +import struct +import time +import aiohttp + +from opengsq.responses.palworld import Status +from opengsq.binary_reader import BinaryReader +from opengsq.exceptions import InvalidPacketException +from opengsq.protocol_base import ProtocolBase +from opengsq.protocol_socket import UdpClient + + +class Palworld(ProtocolBase): + """ + This class represents the Palworld Protocol. It provides methods to interact with the Palworld Rest API. + """ + + full_name = "Palworld Protocol" + + def __init__(self, host: str, port: int, api_username: str, api_password: str, timeout: float = 5): + """ + Initializes the Palworld object with the given parameters. + + :param host: The host of the server. + :param port: The port of the server. + :param api_username: The API username. + :param api_password: The API password. + :param timeout: The timeout for the server connection. + """ + + super().__init__(host, port, timeout) + + if api_username is None: + raise ValueError("api_username must not be None") + if api_password is None: + raise ValueError("api_password must not be None") + + self.api_url = f"http://{self._host}:{self._port}/v1/api" + self.api_username = api_username + self.api_password = api_password + + async def api_request(self,url): + auth = aiohttp.BasicAuth(self.api_username,self.api_password) + async with aiohttp.ClientSession(auth=auth) as session: + async with session.get(url) as response: + data = await response.json() + return data + + async def get_status(self) -> Status: + """ + Asynchronously retrieves the status of the game server. The status includes the server state, name, player count and max player count. + """ + info_data = await self.api_request(f"{self.api_url}/info") + metrics_data = await self.api_request(f"{self.api_url}/metrics") + + server_name = info_data["servername"] + server_cur_players = metrics_data["currentplayernum"] + server_max_players = metrics_data["maxplayernum"] + + return Status( + server_name=server_name, + num_players=server_cur_players, + max_players=server_max_players, + ) + + +if __name__ == "__main__": + import asyncio + + async def main_async(): + palworld = Palworld( + host="79.136.0.124", + port=8212, + timeout=5.0, + api_username="admin", + api_password="", + ) + status = await palworld.get_status() + print(status) + + asyncio.run(main_async()) diff --git a/opengsq/responses/palworld/__init__.py b/opengsq/responses/palworld/__init__.py new file mode 100644 index 0000000..2f91dae --- /dev/null +++ b/opengsq/responses/palworld/__init__.py @@ -0,0 +1 @@ +from .status import Status diff --git a/opengsq/responses/palworld/status.py b/opengsq/responses/palworld/status.py new file mode 100644 index 0000000..2b3985a --- /dev/null +++ b/opengsq/responses/palworld/status.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass +class Status: + """ + Represents the status response from a server. + """ + + num_players: int + """The number of players currently connected to the server.""" + + max_players: int + """The maximum number of players that can connect to the server.""" + + server_name: str + """The name of the server.""" \ No newline at end of file diff --git a/tests/protocols/test_palworld.py b/tests/protocols/test_palworld.py new file mode 100644 index 0000000..41cbc5f --- /dev/null +++ b/tests/protocols/test_palworld.py @@ -0,0 +1,21 @@ +import pytest +from opengsq.protocols.palworld import Palworld + +from ..result_handler import ResultHandler + +handler = ResultHandler(__file__) +# handler.enable_save = True + +# Palworld +test = Palworld( + host="72.65.106.166", + port=8212, + api_username="admin", + api_password="admin", +) + + +@pytest.mark.asyncio +async def test_get_status(): + result = await test.get_status() + await handler.save_result("test_get_status", result) From 6bca6219ca47ad41bc923d1553811b7cf6a8dcb0 Mon Sep 17 00:00:00 2001 From: Steven Wesner Date: Sun, 19 Jan 2025 12:09:15 -0500 Subject: [PATCH 2/3] palworld.py removed unnecessary imports Removed unnecessary imports --- opengsq/protocols/palworld.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/opengsq/protocols/palworld.py b/opengsq/protocols/palworld.py index 7289a53..6de4c3c 100644 --- a/opengsq/protocols/palworld.py +++ b/opengsq/protocols/palworld.py @@ -3,10 +3,7 @@ import aiohttp from opengsq.responses.palworld import Status -from opengsq.binary_reader import BinaryReader -from opengsq.exceptions import InvalidPacketException from opengsq.protocol_base import ProtocolBase -from opengsq.protocol_socket import UdpClient class Palworld(ProtocolBase): From 84d1e9e62d3bbacad74682d4e12294b88834c965 Mon Sep 17 00:00:00 2001 From: Steven Wesner Date: Sun, 19 Jan 2025 12:21:53 -0500 Subject: [PATCH 3/3] palworld.py comment update Updated the comments to make it clear where the Asynch data retrieval is actually happening. --- opengsq/protocols/palworld.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/opengsq/protocols/palworld.py b/opengsq/protocols/palworld.py index 6de4c3c..e1e4700 100644 --- a/opengsq/protocols/palworld.py +++ b/opengsq/protocols/palworld.py @@ -8,7 +8,7 @@ class Palworld(ProtocolBase): """ - This class represents the Palworld Protocol. It provides methods to interact with the Palworld Rest API. + This class represents the Palworld Protocol. It provides methods to interact with the Palworld REST API. """ full_name = "Palworld Protocol" @@ -36,6 +36,9 @@ def __init__(self, host: str, port: int, api_username: str, api_password: str, t self.api_password = api_password async def api_request(self,url): + """ + Asynchronously retrieves data from the game server through the REST API. + """ auth = aiohttp.BasicAuth(self.api_username,self.api_password) async with aiohttp.ClientSession(auth=auth) as session: async with session.get(url) as response: @@ -44,7 +47,7 @@ async def api_request(self,url): async def get_status(self) -> Status: """ - Asynchronously retrieves the status of the game server. The status includes the server state, name, player count and max player count. + Retrieves the status of the game server. The status includes the server state, name, player count and max player count. """ info_data = await self.api_request(f"{self.api_url}/info") metrics_data = await self.api_request(f"{self.api_url}/metrics")