diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..21af950 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.13 diff --git a/QuakeLiveInterface/__init__.py b/QuakeLiveInterface/__init__.py deleted file mode 100644 index b974282..0000000 --- a/QuakeLiveInterface/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import * \ No newline at end of file diff --git a/tests/__init__.py b/QuakeLiveInterface/quakeliveinterface/__init__.py similarity index 100% rename from tests/__init__.py rename to QuakeLiveInterface/quakeliveinterface/__init__.py diff --git a/QuakeLiveInterface/client.py b/QuakeLiveInterface/quakeliveinterface/client.py similarity index 77% rename from QuakeLiveInterface/client.py rename to QuakeLiveInterface/quakeliveinterface/client.py index c41092d..bb035ff 100644 --- a/QuakeLiveInterface/client.py +++ b/QuakeLiveInterface/quakeliveinterface/client.py @@ -1,8 +1,10 @@ +import math from QuakeLiveInterface.connection import ServerConnection from QuakeLiveInterface.state import GameState +from QuakeLiveInterface.constants import WEAPONS class QuakeLiveClient: - def __init__(self, ip_address, port): + def __init__(self, ip_address: str, port: int): self.connection = ServerConnection(ip_address, port) self.game_state = GameState() @@ -13,12 +15,10 @@ def update_game_state(self): self.game_state.update(data_packet) except Exception as e: raise RuntimeError("Error while updating game state") from e + + def calculate_distance(self, pos1, pos2): + return math.sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(pos1, pos2))) - def get_player_position(self, player_id): - try: - return self.game_state.get_player_position(player_id) - except Exception as e: - raise RuntimeError("Error while retrieving player position") from e def get_item_location(self, item_id): try: @@ -26,18 +26,25 @@ def get_item_location(self, item_id): except Exception as e: raise RuntimeError("Error while retrieving item location") from e - def send_command(self, command): + def send_command(self, command: str): try: self.connection.send_command(command) except Exception as e: raise RuntimeError("Error while sending command") from e + def switch_weapon(self, weapon_id: int): + if weapon_id in WEAPONS: + self.send_command(WEAPONS[weapon_id]) + # Movement commands: + def aim(self, pitch, yaw): + self.send_command(f"+mlook;cl_pitchspeed {pitch};cl_yawspeed {yaw}") + def move_forward(self): self.send_command("+forward") def move_backward(self): - self.send_command("-forward") + self.send_command("+back") def move_left(self): self.send_command("+moveleft") @@ -71,15 +78,12 @@ def prev_weapon(self): self.send_command("weapprev") # Communication commands: - def say(self, message): + def say(self, message: str): self.send_command(f"say {message}") - def say_team(self, message): + def say_team(self, message: str): self.send_command(f"say_team {message}") - def voice_chat(self, voice_command): - self.send_command(f"voice_chat {voice_command}") - # Miscellaneous: def toggle_console(self): self.send_command("toggleconsole") diff --git a/QuakeLiveInterface/connection.py b/QuakeLiveInterface/quakeliveinterface/connection.py similarity index 85% rename from QuakeLiveInterface/connection.py rename to QuakeLiveInterface/quakeliveinterface/connection.py index 1401607..7facf94 100644 --- a/QuakeLiveInterface/connection.py +++ b/QuakeLiveInterface/quakeliveinterface/connection.py @@ -1,14 +1,13 @@ -import logging import socket import time -logging.basicConfig( - filename="quakelive_interface.log", - level=logging.DEBUG, - format="%(asctime)s [%(levelname)s] - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", +from loguru import logger + +logger.add( + "quakelive_interface.log", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} [{level}] - {message}", ) -logger = logging.getLogger(__name__) class ServerConnection: diff --git a/QuakeLiveInterface/quakeliveinterface/interface-plugin.py b/QuakeLiveInterface/quakeliveinterface/interface-plugin.py new file mode 100644 index 0000000..581cc94 --- /dev/null +++ b/QuakeLiveInterface/quakeliveinterface/interface-plugin.py @@ -0,0 +1,216 @@ +import minqlx +import json +import threading +import socket +from typing import List, Callable, Any, Tuple +from quakeliveinterface.utils.constants import CommandType, Direction, WeaponId, DEFAULT_PORT, STATE_REQUEST, COMMAND_PREFIX + +from loguru import logger + +logger.add( + "quakelive_interface.log", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} [{level}] - {message}", +) + +class Player: + def __init__(self, id: int, name: str, team: str, position: Tuple[float, float, float, float, float, float], + health: int, armor: int, weapons: List[int]): + self.id = id + self.name = name + self.team = team + self.position = position + self.health = health + self.armor = armor + self.weapons = weapons + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "team": self.team, + "position": self.position, + "health": self.health, + "armor": self.armor, + "weapons": self.weapons + } + +class Command: + def __init__(self, type: CommandType, handler: Callable[..., Any], arg_count: int): + self.type = type + self.handler = handler + self.arg_count = arg_count # expected number of args + +class StateExposer(minqlx.Plugin): + def __init__(self): + super().__init__() + self.set_cvar_once("qlx_stateExposerPort", str(DEFAULT_PORT)) + self.state_lock = threading.Lock() + self.current_state = {} + self.players = {} + self.add_hook("frame", self.handle_frame) + self.server_thread = threading.Thread(target=self.run_server) + self.server_thread.start() + self.add_hook("player_disconnect", self.handle_player_disconnect) + self.add_hook("map", self.handle_map) + + self.commands = { + CommandType.MOVE: Command(CommandType.MOVE, self.move_player, 3), + CommandType.AIM: Command(CommandType.AIM, self.aim_player, 3), + CommandType.WEAPON: Command(CommandType.WEAPON, self.switch_weapon, 2), + CommandType.FIRE: Command(CommandType.FIRE, self.fire_weapon, 1), + CommandType.JUMP: Command(CommandType.JUMP, self.player_jump, 1), + CommandType.SAY: Command(CommandType.SAY, self.player_say, 2), + } + + def handle_frame(self): + with self.state_lock: + self.players = { + p.id: Player( + p.id, p.name, p.team, p.position, + p.health, p.armor, p.weapons() + ) for p in self.players() + } + self.current_state = { + "players": [player.to_dict() for player in self.players.values()], + "items": [ + { + "type": item.type, + "position": item.position + } for item in self.items() + ] + } + + def run_server(self): + port = int(self.get_cvar("qlx_stateExposerPort")) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', port)) + s.listen() + while True: + conn, addr = s.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + if data == STATE_REQUEST: + with self.state_lock: + conn.sendall(json.dumps(self.current_state).encode()) + elif data.startswith(COMMAND_PREFIX): + command = data.decode().split(":", 1)[1] + result = self.execute_command(command) + conn.sendall(json.dumps({"result": result}).encode()) + + def execute_command(self, command_string: str) -> str: + try: + parts = command_string.split() + action = CommandType[parts[0].upper()] + args = parts[1:] + + if action in self.commands: + command = self.commands[action] + if len(args) != command.arg_count: + return f"Error: {action.name} command requires {command.arg_count} argument(s)" + return command.handler(*args) + else: + # For any other commands, pass them directly to the console + self.console_command(command_string) + return f"Executed console command: {command_string}" + + except KeyError: + return f"Error: Unknown command {parts[0]}" + except Exception as e: + self.msg(f"Error executing command: {command_string}. Error: {str(e)}") + return f"Error: {str(e)}" + + def move_player(self, player_id: str, direction: str, amount: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + amount = int(amount) + try: + direction = Direction(direction.lower()) + except ValueError: + return f"Invalid direction: {direction}" + + x, y, z, pitch, yaw, roll = player.position + if direction == Direction.FORWARD: + y += amount + elif direction == Direction.BACKWARD: + y -= amount + elif direction == Direction.LEFT: + x -= amount + elif direction == Direction.RIGHT: + x += amount + + player.position = (x, y, z, pitch, yaw, roll) + self.force_player_pos(player.id, player.position) + return f"Moved player {player.name} {direction.value} by {amount}" + + def aim_player(self, player_id: str, pitch: str, yaw: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + pitch, yaw = float(pitch), float(yaw) + x, y, z, _, _, roll = player.position + player.position = (x, y, z, pitch, yaw, roll) + self.force_player_pos(player.id, player.position) + return f"Aimed player {player.name} with pitch {pitch} and yaw {yaw}" + + def switch_weapon(self, player_id: str, weapon_id: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + try: + weapon = WeaponId(int(weapon_id)) + except ValueError: + return f"Invalid weapon ID: {weapon_id}" + + self.console_command(f"tell {player.id} weapon {weapon.value}") + return f"Switched player {player.name} to weapon {weapon.name}" + + def fire_weapon(self, player_id: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + self.console_command(f"tell {player.id} +attack;wait 10;-attack") + return f"Fired weapon for player {player.name}" + + def player_jump(self, player_id: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + self.console_command(f"tell {player.id} +moveup;wait 10;-moveup") + return f"Player {player.name} jumped" + + def player_say(self, player_id: str, message: str) -> str: + try: + player = self.players[int(player_id)] + except KeyError: + return f"Error: Player with id {player_id} not found" + + self.console_command(f"tell {player.id} say {message}") + return f"Player {player.name} said: {message}" + + def force_player_pos(self, player_id: int, position: Tuple[float, float, float, float, float, float]): + self.console_command(f"position {player_id} {' '.join(map(str, position))}") + + def handle_player_disconnect(self, player): + # Remove the player from our local players dict when they disconnect + if player.id in self.players: + del self.players[player.id] + + def handle_map(self, mapname, factory): + # Reset the players dict when a new map loads + self.players = {} + \ No newline at end of file diff --git a/QuakeLiveInterface/quakeliveinterface/state/__init__.py b/QuakeLiveInterface/quakeliveinterface/state/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/QuakeLiveInterface/state.py b/QuakeLiveInterface/quakeliveinterface/state/game_state.py similarity index 100% rename from QuakeLiveInterface/state.py rename to QuakeLiveInterface/quakeliveinterface/state/game_state.py diff --git a/QuakeLiveInterface/quakeliveinterface/state/item_state.py b/QuakeLiveInterface/quakeliveinterface/state/item_state.py new file mode 100644 index 0000000..e69de29 diff --git a/QuakeLiveInterface/quakeliveinterface/state/player_state.py b/QuakeLiveInterface/quakeliveinterface/state/player_state.py new file mode 100644 index 0000000..e69de29 diff --git a/QuakeLiveInterface/quakeliveinterface/tests/__init__.py b/QuakeLiveInterface/quakeliveinterface/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/QuakeLiveInterface/quakeliveinterface/tests/test_client.py similarity index 100% rename from tests/test_client.py rename to QuakeLiveInterface/quakeliveinterface/tests/test_client.py diff --git a/tests/test_connection.py b/QuakeLiveInterface/quakeliveinterface/tests/test_connection.py similarity index 100% rename from tests/test_connection.py rename to QuakeLiveInterface/quakeliveinterface/tests/test_connection.py diff --git a/tests/test_state.py b/QuakeLiveInterface/quakeliveinterface/tests/test_state.py similarity index 100% rename from tests/test_state.py rename to QuakeLiveInterface/quakeliveinterface/tests/test_state.py diff --git a/QuakeLiveInterface/quakeliveinterface/utils/__init__.py b/QuakeLiveInterface/quakeliveinterface/utils/__init__.py new file mode 100644 index 0000000..5e98ded --- /dev/null +++ b/QuakeLiveInterface/quakeliveinterface/utils/__init__.py @@ -0,0 +1,2 @@ +from .constants import * +from .helpers import * \ No newline at end of file diff --git a/QuakeLiveInterface/quakeliveinterface/utils/constants.py b/QuakeLiveInterface/quakeliveinterface/utils/constants.py new file mode 100644 index 0000000..ba8ea76 --- /dev/null +++ b/QuakeLiveInterface/quakeliveinterface/utils/constants.py @@ -0,0 +1,31 @@ +from enum import Enum, auto + +class CommandType(Enum): + MOVE = auto() + AIM = auto() + WEAPON = auto() + FIRE = auto() + JUMP = auto() + SAY = auto() + +class Direction(Enum): + FORWARD = "forward" + BACKWARD = "backward" + LEFT = "left" + RIGHT = "right" + +class WeaponId(Enum): + GAUNTLET = 1 + MACHINEGUN = 2 + SHOTGUN = 3 + GRENADE_LAUNCHER = 4 + ROCKET_LAUNCHER = 5 + LIGHTNING_GUN = 6 + RAILGUN = 7 + PLASMA_GUN = 8 + BFG = 9 + +# Add other constants as needed +DEFAULT_PORT = 27960 +STATE_REQUEST = b"get_state" +COMMAND_PREFIX = b"command:" \ No newline at end of file diff --git a/QuakeLiveInterface/quakeliveinterface/utils/helpers.py b/QuakeLiveInterface/quakeliveinterface/utils/helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 889e57a..8402dab 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,73 @@ # QuakeLiveInterface -Quake Live Interface is a Python library designed to provide a programmatic interface to a Quake Live game server. The library's main components are: - -- `ServerConnection`: A class that manages the TCP/IP connection to the Quake Live server. It sends commands to the server and receives data packets from the server. - -- `GameState`: A class that parses the data packets from the server into a more accessible format. The game state includes information about the player's position, the positions of other entities, and other game state information. - -- `QuakeLiveClient`: A class that encapsulates the connection to the server and the interpretation of game state data. It provides an intuitive interface for users to interact with the game. +This project provides an interface for Quake Live servers running minqlx. It allows external clients to control players and receive game state information. ## Installation The project uses Poetry for package management. +1. Install the packages using [Poetry](https://python-poetry.org/) ```bash $ poetry install ``` -### Usage +2. Ensure you have a Quake Live server running with minqlx installed. -To create a connection to a Quake Live server: +3. Copy the `quake_ai_interface` folder into your `minqlx-plugins` directory. -```python +4. Add `quake_ai_interface` to your `qlx_plugins` cvar in your server config. For example: -from QuakeLiveInterface.connection import ServerConnection + ``` + set qlx_plugins "plugin1, plugin2, quake_ai_interface" + ``` -connection = ServerConnection(server_ip, server_port) -connection.connect() -``` +5. (Optional) Set the port for the AI interface by adding the following to your server config: -To send a command to the server: + ``` + set qlx_aiInterfacePort "27960" + ``` -```python + If not set, the default port is 27960. -connection.send_command("some_command") -``` +6. Restart your Quake Live server. -To create a Quake Live client and interpret game state data: +## Architecture -```python +The Quake AI Interface consists of three main components: -from QuakeLiveInterface.client import QuakeLiveClient +1. MinQLX Plugin: This runs on the Quake Live server and handles the low-level interaction with the game. -client = QuakeLiveClient(server_ip, server_port) -client.connect() -game_state = client.get_game_state() -``` +2. Python API: This provides a high-level interface for controlling players and receiving game state information. + +3. AI Bot: This is where the decision-making logic resides. It uses the Python API to interact with the game. + +## Usage + +The client can interact with the game through the Python API. This API provides methods that correspond to the various commands available in the game. + +1. `move_player(player_id: int, direction: Direction, amount: float) -> str` + - Moves the player in the specified direction. + - Direction: An enum value from the Direction class (FORWARD, BACKWARD, LEFT, RIGHT) + - Amount: Float value representing the movement amount + +2. `aim_player(player_id: int, pitch: float, yaw: float) -> str` + - Aims the player's view. + - Pitch and Yaw: Float values representing the aim angles + +3. `switch_weapon(player_id: int, weapon: WeaponType) -> str` + - Switches the player's weapon. + - Weapon: An enum value from the WeaponType class (GAUNTLET, MACHINEGUN, SHOTGUN, etc.) + +4. `fire_weapon(player_id: int) -> str` + - Makes the player fire their current weapon. + +5. `jump_player(player_id: int) -> str` + - Makes the player jump. + +6. `get_game_state() -> GameState` + - Returns the current state of the game, including player positions, health, weapons, etc. + +The server will respond with a string indicating the result of the command. This could be a success message or an error message if the command was invalid or couldn't be executed. ### Testing @@ -53,3 +76,23 @@ To run tests: ```bash $ poetry run pytest ``` + +## Contributing + +Contributions to this project are welcome. Please submit pull requests with any improvements or bug fixes. When contributing, please: + +1. Add unit tests for any new features or bug fixes. +2. Update the documentation if you're changing the API or adding new features. +3. Follow the existing code style and conventions. + +## License +This project is licensed under the [GNU General Public License Version 3 (GPLv3)](./LICENSE) + +1. You are free to use, modify, and distribute this software. +2. If you distribute this software or any derivative works, you must: + - Make the source code available. + - License it under the same GPLv3 terms. + - Preserve the original copyright notices. +3. There is no warranty for this program, and the authors are not liable for any damages from its use. + +This license ensures that the software remains free and open source, promoting collaboration and shared improvements. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4e67694 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,89 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "maturin" +version = "1.6.0" +description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "maturin-1.6.0-py3-none-linux_armv6l.whl", hash = "sha256:d8620970bd0b6a0acb99dbd0b1c2ebb7a69909d25f6023bdff9635a39001aa51"}, + {file = "maturin-1.6.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd85edcb1b8e2bcddc1b7d16ce58ce00a66aa80c422745c8ad9e132ac40d4b48"}, + {file = "maturin-1.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:337899784955934dd67b30497d1dd5fab22da89f60bb079dbaf2eaa446b97a10"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:dbbbf25dc3c207b0a7bd4f3aea1df33d4f22b8508592796a6f36f4d8ed216db0"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d92b045e90ed919a8a2520dda64e3f384e5e746ea51e1498cc6ac3e9e5c76054"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d67ca8dc7f3b2314bd3bf83c4de52645e220ee312fd526e53acc6a735f233fad"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:aa4eb7dca7d246b466392f21016f67ff09a9aff2305fa714ca25a2344e4639e7"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:16ef860df20028618b5a064da06b02c1c47acba064a4d25aaf84662a459ec599"}, + {file = "maturin-1.6.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e931c92037128ade49cd26dd040d9c46ad8092d8170cc44f5c3a0b4a052d576"}, + {file = "maturin-1.6.0-py3-none-win32.whl", hash = "sha256:c87d1a7596c42b589099adb831343a56e02373588366e4cede96cbdf8bd68f9d"}, + {file = "maturin-1.6.0-py3-none-win_amd64.whl", hash = "sha256:a2a2436628c36d98dabd79b52256df7e12fc4fd1b122984d9373fdf918fd4609"}, + {file = "maturin-1.6.0-py3-none-win_arm64.whl", hash = "sha256:50133965e52d8b5b969381fee3fde111ae2383905cdaba7650f256e08ccddcd4"}, + {file = "maturin-1.6.0.tar.gz", hash = "sha256:b955025c24c8babc808db49e0ff90db8b4b1320dcc16b14eb26132841737230d"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +patchelf = ["patchelf"] +zig = ["ziglang (>=0.10.0,<0.13.0)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "6f7b26f1c8bface3fba3480535db11e6a5305cc97593a6dad66faea6d3be90b4" diff --git a/pyproject.toml b/pyproject.toml index 4c94c42..8e9e367 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,10 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" +loguru = "^0.7.2" +maturin = "^1.6.0" [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/research.md b/research.md index b791284..0b2d21e 100644 --- a/research.md +++ b/research.md @@ -50,3 +50,11 @@ Interpreting the network data sent between the Quake Live client and server will - Parse Game State: Once you understand the networking protocol, you can start to parse the game state updates from the captured packets. This will likely involve reading and interpreting binary data. - Maintain Local Game State: As you parse the game state updates, you will need to maintain a local copy of the game state for the AI to use. This game state should be updated every time a new packet is received. + + +### Resources +- https://euere.eu/ql/ +- https://github.com/MinoMino/minqlx +- https://github.com/mgaertne/shinqlx +- https://github.com/mightycow/uberdemotools +- https://github.com/quakelive-server-standards/quakelive-server-standards \ No newline at end of file