├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ └── codestyle.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── poetry.lock ├── pymine_net ├── __init__.py ├── enums.py ├── errors.py ├── net │ ├── asyncio │ │ ├── __init__.py │ │ ├── client.py │ │ ├── server.py │ │ └── stream.py │ ├── client.py │ ├── server.py │ ├── socket │ │ ├── __init__.py │ │ ├── client.py │ │ ├── server.py │ │ └── stream.py │ └── stream.py ├── packets │ ├── __init__.py │ ├── packet_map.py │ └── v_1_18_1 │ │ ├── handshaking │ │ └── handshake.py │ │ ├── login │ │ ├── compression.py │ │ └── login.py │ │ ├── play │ │ ├── advancement.py │ │ ├── animations.py │ │ ├── beacon.py │ │ ├── block.py │ │ ├── boss.py │ │ ├── chat.py │ │ ├── command.py │ │ ├── command_block.py │ │ ├── cooldown.py │ │ ├── crafting.py │ │ ├── difficulty.py │ │ ├── effect.py │ │ ├── entity.py │ │ ├── explosion.py │ │ ├── keep_alive.py │ │ ├── map.py │ │ ├── particle.py │ │ ├── player.py │ │ ├── player_list.py │ │ └── plugin_msg.py │ │ └── status │ │ └── status.py ├── strict_abc.py └── types │ ├── block_palette.py │ ├── buffer.py │ ├── chat.py │ ├── nbt.py │ ├── packet.py │ ├── packet_map.py │ ├── player.py │ ├── registry.py │ └── vector.py ├── pyproject.toml └── tests ├── sample_data ├── bigtest.nbt ├── level.dat ├── nantest.nbt ├── region │ ├── r.0.0.mca │ ├── r.0.1.mca │ ├── r.0.2.mca │ ├── r.0.3.mca │ └── r.0.4.mca └── test.json ├── test_buffer.py ├── test_nbt.py ├── test_net_asyncio.py ├── test_net_socket.py ├── test_packets.py └── test_strict_abc.py /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve! 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe Bug 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | ### Expected Behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | ### Screenshots / Proof 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | ### Additional Context 26 | Add any other context about the problem here, including OS, Python version, and other important contextual information. 27 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yml: -------------------------------------------------------------------------------- 1 | name: Auto Black & Isort 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | autocodestyle: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | 10 | - uses: actions/setup-python@v1 11 | 12 | - run: pip install black isort 13 | 14 | - name: autoblack_check 15 | id: black-check 16 | continue-on-error: true 17 | run: black --diff --check --config pyproject.toml . 1>.black_diff 18 | 19 | - name: autoblack_fix 20 | if: steps.black-check.outcome != 'success' && github.event_name != 'pull_request' 21 | run: black --config pyproject.toml . 22 | 23 | - name: autoblack_mark 24 | if: steps.black-check.outcome != 'success' && github.event_name == 'pull_request' 25 | run: | 26 | for s in $(cat .black_diff | sort | uniq | grep -e "--- " - | perl -n -e'/--- (.*)\t.*/gs && print($1,"\n")') 27 | do 28 | echo -e "::warning file=$s,line=0,title=Reformatting Needed::This file needs reformatting with black" 29 | done 30 | 31 | - name: isort_check 32 | id: isort-check 33 | continue-on-error: true 34 | run: isort --check . 35 | 36 | - name: isort_fix 37 | if: steps.isort-check.outcome != 'success' 38 | run: isort . 39 | 40 | - name: publish 41 | if: steps.black-check.outcome != 'success' && github.event_name != 'pull_request' 42 | run: | 43 | git config user.name "github-actions[bot]" 44 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 45 | git commit -am "apply black & isort" 46 | git push 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | .venv/ 4 | .pytest_cache/ 5 | dist/ 6 | 7 | # ci 8 | .black_diff 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pymine-Net-Net 2 | 3 | ## Contributing 4 | 5 | 6 | 1. [Create a bug report, feature request, or other issue](https://github.com/py-mine/Pymine-Net/issues), and assign yourself. 7 | 2. Fork the repository, or create a new branch. 8 | 3. Make your changes, with descriptive commit names. 9 | 4. [Create a pull request](https://github.com/py-mine/Pymine-Net/pulls) with a detailed description of the issue resolved, link the issue you created, and request a reviewer. 10 | 5. One of the main devs will review it and request changes if needed! 11 | 6. _You should probably also [join our Discord server](https://discord.gg/qcyufdMVQP), for news on the status and direction of the project_ 12 | 13 | 14 | ### General Guidelines 15 | 16 | - Formatting is enforced with the [black formatter](https://black.readthedocs.io/en/stable/index.html) as defined [here](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) 17 | - Use f-strings (`f"{thing}"`) instead of `.format()` where possible 18 | - Use concatenation instead of f-strings where possible, as concatenation is faster 19 | - Use `snake_case` for variables 20 | - Constants should either be loaded from a config file or be in `UPPER_SNAKE_CASE` 21 | - Lines shouldn't be longer than 100 characters. 22 | 23 | ### Imports 24 | 25 | - Imports should be sorted by size descending 26 | - Imports from `pymine_net/*` should be separated from the rest of the imports by a newline 27 | - The tool [isort](https://pycqa.github.io/isort/) should do this for you, if installed 28 | 29 | ### Tools 30 | 31 | #### VSCode 32 | 33 | [@The456gamer](https://github.com/the456gamer) has made a VSCode config to put in `.vscode/settings.json` [here](https://gist.github.com/the456gamer/cc3f6472391a8ae359be06547b07cdb2) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ### GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | #### 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the 19 | GNU General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | #### 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | #### 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | - a) under this License, provided that you make a good faith effort 58 | to ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | - b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | #### 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | - a) Give prominent notice with each copy of the object code that 74 | the Library is used in it and that the Library and its use are 75 | covered by this License. 76 | - b) Accompany the object code with a copy of the GNU GPL and this 77 | license document. 78 | 79 | #### 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, taken 82 | together, effectively do not restrict modification of the portions of 83 | the Library contained in the Combined Work and reverse engineering for 84 | debugging such modifications, if you also do each of the following: 85 | 86 | - a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this 90 | license document. 91 | - c) For a Combined Work that displays copyright notices during 92 | execution, include the copyright notice for the Library among 93 | these notices, as well as a reference directing the user to the 94 | copies of the GNU GPL and this license document. 95 | - d) Do one of the following: 96 | - 0) Convey the Minimal Corresponding Source under the terms of 97 | this License, and the Corresponding Application Code in a form 98 | suitable for, and under terms that permit, the user to 99 | recombine or relink the Application with a modified version of 100 | the Linked Version to produce a modified Combined Work, in the 101 | manner specified by section 6 of the GNU GPL for conveying 102 | Corresponding Source. 103 | - 1) Use a suitable shared library mechanism for linking with 104 | the Library. A suitable mechanism is one that (a) uses at run 105 | time a copy of the Library already present on the user's 106 | computer system, and (b) will operate properly with a modified 107 | version of the Library that is interface-compatible with the 108 | Linked Version. 109 | - e) Provide Installation Information, but only if you would 110 | otherwise be required to provide such information under section 6 111 | of the GNU GPL, and only to the extent that such information is 112 | necessary to install and execute a modified version of the 113 | Combined Work produced by recombining or relinking the Application 114 | with a modified version of the Linked Version. (If you use option 115 | 4d0, the Installation Information must accompany the Minimal 116 | Corresponding Source and Corresponding Application Code. If you 117 | use option 4d1, you must provide the Installation Information in 118 | the manner specified by section 6 of the GNU GPL for conveying 119 | Corresponding Source.) 120 | 121 | #### 5. Combined Libraries. 122 | 123 | You may place library facilities that are a work based on the Library 124 | side by side in a single library together with other library 125 | facilities that are not Applications and are not covered by this 126 | License, and convey such a combined library under terms of your 127 | choice, if you do both of the following: 128 | 129 | - a) Accompany the combined library with a copy of the same work 130 | based on the Library, uncombined with any other library 131 | facilities, conveyed under the terms of this License. 132 | - b) Give prominent notice with the combined library that part of it 133 | is a work based on the Library, and explaining where to find the 134 | accompanying uncombined form of the same work. 135 | 136 | #### 6. Revised Versions of the GNU Lesser General Public License. 137 | 138 | The Free Software Foundation may publish revised and/or new versions 139 | of the GNU Lesser General Public License from time to time. Such new 140 | versions will be similar in spirit to the present version, but may 141 | differ in detail to address new problems or concerns. 142 | 143 | Each version is given a distinguishing version number. If the Library 144 | as you received it specifies that a certain numbered version of the 145 | GNU Lesser General Public License "or any later version" applies to 146 | it, you have the option of following the terms and conditions either 147 | of that published version or of any later version published by the 148 | Free Software Foundation. If the Library as you received it does not 149 | specify a version number of the GNU Lesser General Public License, you 150 | may choose any version of the GNU Lesser General Public License ever 151 | published by the Free Software Foundation. 152 | 153 | If the Library as you received it specifies that a proxy can decide 154 | whether future versions of the GNU Lesser General Public License shall 155 | apply, that proxy's public statement of acceptance of any version is 156 | permanent authorization for you to choose that version for the 157 | Library. 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMine-Net 2 | [![CodeFactor](https://www.codefactor.io/repository/github/py-mine/pymine-net/badge)](https://www.codefactor.io/repository/github/py-mine/pymine-net) 3 | ![code size](https://img.shields.io/github/languages/code-size/py-mine/PyMine-Net?color=0FAE6E) 4 | ![code style](https://img.shields.io/badge/code%20style-black-000000.svg) 5 | 6 | *PyMine-Net - an extensible and modular Minecraft networking library in Python* 7 | 8 | ## Archivation 9 | 10 | PyMine-net is no longer maintained, however, in it's place, a new project called [mcproto](https://github.com/py-mine/mcproto) was created, so if you're 11 | here looking for a library that provides an easy way to work with the minecraft's protocol, feel free to check it out instead! 12 | 13 | ## Features 14 | - Buffer class with methods for dealing with various types and data formats used by Minecraft 15 | - High level abstractions for packets with a system to seperate different protocol's packets 16 | - Miscellaneous classes for dealing with different Minecraft data structures relevant to networking 17 | -------------------------------------------------------------------------------- /pymine_net/__init__.py: -------------------------------------------------------------------------------- 1 | import pymine_net.types.nbt as nbt # noqa: F401 2 | from pymine_net.net.asyncio.client import AsyncProtocolClient # noqa: F401 3 | from pymine_net.packets import load_packet_map # noqa: F401 4 | from pymine_net.types.buffer import Buffer # noqa: F401 5 | from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 6 | -------------------------------------------------------------------------------- /pymine_net/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | __all__ = ("GameState", "Direction", "Pose", "EntityModifier") 4 | 5 | 6 | class PacketDirection(Enum): 7 | CLIENTBOUND = "CLIENT-BOUND" 8 | SERVERBOUND = "SERVER-BOUND" 9 | 10 | 11 | class GameState(IntEnum): 12 | HANDSHAKING = 0 13 | STATUS = 1 14 | LOGIN = 2 15 | PLAY = 3 16 | 17 | 18 | class Direction(IntEnum): 19 | DOWN = 0 20 | UP = 1 21 | NORTH = 2 22 | SOUTH = 3 23 | WEST = 4 24 | EAST = 5 25 | 26 | 27 | class Pose(IntEnum): 28 | STANDING = 0 29 | FALL_FLYING = 1 30 | SLEEPING = 2 31 | SWIMMING = 3 32 | SPIN_ATTACK = 4 33 | SNEAKING = 5 34 | DYING = 6 35 | 36 | 37 | class EntityModifier(IntEnum): 38 | MODIFY = 0 # add / subtract amount 39 | MODIFY_PERCENT = 1 # add / subtract amount percent of the current value 40 | MODIFY_MULTIPLY_PERCENT = 2 # multiply by percent amount 41 | 42 | 43 | class MainHand(IntEnum): 44 | LEFT = 0 45 | RIGHT = 1 46 | 47 | 48 | class ChatMode(IntEnum): 49 | ENABLED = 0 50 | COMMANDS_ONLY = 1 51 | HIDDEN = 2 52 | 53 | 54 | class SkinPart(IntEnum): 55 | CAPE = 0x01 56 | JACKET = 0x02 57 | LEFT_SLEEVE = 0x04 58 | RIGHT_SLEEVE = 0x08 59 | LEFT_PANTS_LEG = 0x10 60 | RIGHT_PANTS_LEG = 0x20 61 | HAT = 0x40 62 | 63 | 64 | class GameMode(IntEnum): 65 | SURVIVAL = 0 66 | CREATIVE = 1 67 | ADVENTURE = 2 68 | SPECTATOR = 3 69 | 70 | 71 | class PlayerInfoAction(IntEnum): 72 | ADD_PLAYER = 0 73 | UPDATE_GAMEMODE = 1 74 | UPDATE_LATENCY = 2 75 | UPDATE_DISPLAY_NAME = 3 76 | REMOVE_PLAYER = 4 77 | -------------------------------------------------------------------------------- /pymine_net/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pymine_net.enums import GameState, PacketDirection 4 | 5 | 6 | class PyMineNetError(Exception): 7 | pass 8 | 9 | 10 | class UnknownPacketIdError(Exception): 11 | def __init__( 12 | self, 13 | protocol: Union[str, int], 14 | state: GameState, 15 | packet_id: int, 16 | direction: PacketDirection, 17 | ): 18 | super().__init__( 19 | f"Unknown packet ID 0x{packet_id:02X} (protocol={protocol}, state={state.name}, {direction.value})" 20 | ) 21 | 22 | self.protocol = protocol 23 | self.state = state 24 | self.packet_id = packet_id 25 | self.direction = direction 26 | 27 | 28 | class DuplicatePacketIdError(Exception): 29 | def __init__( 30 | self, 31 | protocol: Union[str, int], 32 | state: GameState, 33 | packet_id: int, 34 | direction: PacketDirection, 35 | ): 36 | super().__init__( 37 | f"Duplicate packet ID found (protocol={protocol}, state={state.name}, {direction}): 0x{packet_id:02X}" 38 | ) 39 | 40 | self.protocol = protocol 41 | self.state = state 42 | self.packet_id = packet_id 43 | self.direction = direction 44 | -------------------------------------------------------------------------------- /pymine_net/net/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import AsyncProtocolClient # noqa: F401 2 | from .server import AsyncProtocolServer, AsyncProtocolServerClient # noqa: F401 3 | from .stream import AsyncTCPStream, EncryptedAsyncTCPStream # noqa: F401 4 | -------------------------------------------------------------------------------- /pymine_net/net/asyncio/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Union 3 | 4 | from pymine_net.net.asyncio.stream import AsyncTCPStream 5 | from pymine_net.net.client import AbstractProtocolClient 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | from pymine_net.types.packet_map import PacketMap 8 | 9 | __all__ = ("AsyncProtocolClient",) 10 | 11 | 12 | class AsyncProtocolClient(AbstractProtocolClient): 13 | """An async connection over a TCP socket for reading + writing Minecraft packets.""" 14 | 15 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 16 | super().__init__(host, port, protocol, packet_map) 17 | 18 | self.stream: AsyncTCPStream = None 19 | 20 | async def connect(self) -> None: 21 | _, writer = await asyncio.open_connection(self.host, self.port) 22 | self.stream = AsyncTCPStream(writer) 23 | 24 | async def close(self) -> None: 25 | self.stream.close() 26 | await self.stream.wait_closed() 27 | 28 | async def read_packet(self) -> ClientBoundPacket: 29 | packet_length = await self.stream.read_varint() 30 | return self._decode_packet(await self.stream.readexactly(packet_length)) 31 | 32 | async def write_packet(self, packet: ServerBoundPacket) -> None: 33 | self.stream.write(self._encode_packet(packet)) 34 | await self.stream.drain() 35 | -------------------------------------------------------------------------------- /pymine_net/net/asyncio/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from abc import abstractmethod 3 | from typing import Dict, Tuple, Union 4 | 5 | from pymine_net.net.asyncio.stream import AsyncTCPStream 6 | from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient 7 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 8 | from pymine_net.types.packet_map import PacketMap 9 | 10 | __all__ = ("AsyncProtocolServerClient", "AsyncProtocolServer") 11 | 12 | 13 | class AsyncProtocolServerClient(AbstractProtocolServerClient): 14 | def __init__(self, stream: AsyncTCPStream, packet_map: PacketMap): 15 | super().__init__(stream, packet_map) 16 | self.stream = stream # redefine this cause typehints 17 | 18 | async def read_packet(self) -> ServerBoundPacket: 19 | length = await self.stream.read_varint() 20 | return self._decode_packet(await self.stream.readexactly(length)) 21 | 22 | async def write_packet(self, packet: ClientBoundPacket) -> None: 23 | self.stream.write(self._encode_packet(packet)) 24 | await self.stream.drain() 25 | 26 | 27 | class AsyncProtocolServer(AbstractProtocolServer): 28 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 29 | super().__init__(host, port, protocol, packet_map) 30 | 31 | self.connected_clients: Dict[Tuple[str, int], AsyncProtocolServerClient] = {} 32 | 33 | self.server: asyncio.AbstractServer = None 34 | 35 | async def run(self) -> None: 36 | self.server = await asyncio.start_server(self._client_connected_cb, self.host, self.port) 37 | 38 | async def close(self) -> None: 39 | self.server.close() 40 | await self.server.wait_closed() 41 | 42 | async def _client_connected_cb( 43 | self, _: asyncio.StreamReader, writer: asyncio.StreamWriter 44 | ) -> None: 45 | client = AsyncProtocolServerClient(AsyncTCPStream(writer), self.packet_map) 46 | 47 | self.connected_clients[client.stream.remote] = client 48 | 49 | await self.new_client_connected(client) 50 | 51 | @abstractmethod 52 | async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: 53 | pass 54 | -------------------------------------------------------------------------------- /pymine_net/net/asyncio/stream.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from asyncio import StreamWriter 3 | from typing import Tuple, Union 4 | 5 | from cryptography.hazmat.primitives.ciphers import Cipher 6 | 7 | from pymine_net.net.stream import AbstractTCPStream 8 | from pymine_net.types.buffer import Buffer 9 | 10 | __all__ = ("AsyncTCPStream", "EncryptedAsyncTCPStream") 11 | 12 | 13 | class AsyncTCPStream(AbstractTCPStream, StreamWriter): 14 | """Used for reading and writing from/to a connected client, merges functions of a StreamReader and StreamWriter. 15 | 16 | :param StreamReader reader: An asyncio.StreamReader instance. 17 | :param StreamWriter writer: An asyncio.StreamWriter instance. 18 | :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. 19 | """ 20 | 21 | def __init__(self, writer: StreamWriter): 22 | super().__init__(writer._transport, writer._protocol, writer._reader, writer._loop) 23 | 24 | self.remote: Tuple[str, int] = self.get_extra_info("peername") 25 | 26 | async def read(self, length: int = -1) -> Buffer: 27 | return Buffer(await self._reader.read(length)) 28 | 29 | async def readline(self) -> Buffer: 30 | return Buffer(await self._reader.readline()) 31 | 32 | async def readexactly(self, length: int) -> Buffer: 33 | return Buffer(await self._reader.readexactly(length)) 34 | 35 | async def readuntil(self, separator: bytes = b"\n") -> Buffer: 36 | return Buffer(await self._reader.readuntil(separator)) 37 | 38 | async def read_varint(self) -> int: 39 | value = 0 40 | 41 | for i in range(10): 42 | (byte,) = struct.unpack(">B", await self.readexactly(1)) 43 | value |= (byte & 0x7F) << 7 * i 44 | 45 | if not byte & 0x80: 46 | break 47 | 48 | if value & (1 << 31): 49 | value -= 1 << 32 50 | 51 | value_max = (1 << (32 - 1)) - 1 52 | value_min = -1 << (32 - 1) 53 | 54 | if not (value_min <= value <= value_max): 55 | raise ValueError( 56 | f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" 57 | ) 58 | 59 | return value 60 | 61 | 62 | class EncryptedAsyncTCPStream(AsyncTCPStream): 63 | """An encrypted version of an AsyncTCPStream, automatically encrypts and decrypts outgoing and incoming data. 64 | 65 | :param AsyncTCPStream stream: The original, stream-compatible object. 66 | :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. 67 | :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. 68 | :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. 69 | """ 70 | 71 | def __init__(self, stream: AsyncTCPStream, cipher: Cipher): 72 | super().__init__(stream) 73 | 74 | self.decryptor = cipher.decryptor() 75 | self.encryptor = cipher.encryptor() 76 | 77 | async def read(self, length: int = -1) -> Buffer: 78 | return Buffer(self.decryptor.update(await super().read(length))) 79 | 80 | async def readline(self) -> Buffer: 81 | return Buffer(self.decryptor.update(await super().readline())) 82 | 83 | async def readexactly(self, length: int) -> Buffer: 84 | return Buffer(self.decryptor.update(await super().readexactly(length))) 85 | 86 | async def readuntil(self, separator: bytes = b"\n") -> Buffer: 87 | return Buffer(self.decryptor.update(await super().readuntil(separator))) 88 | 89 | def write(self, data: Union[Buffer, bytes, bytearray]) -> None: 90 | super().write(self.encryptor.update(data)) 91 | 92 | def writelines(self, data: Union[Buffer, bytes, bytearray]) -> None: 93 | super().writelines(self.encryptor.update(data)) 94 | -------------------------------------------------------------------------------- /pymine_net/net/client.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | from abc import abstractmethod 3 | from typing import Type, Union 4 | 5 | from pymine_net.enums import GameState, PacketDirection 6 | from pymine_net.errors import UnknownPacketIdError 7 | from pymine_net.strict_abc import StrictABC 8 | from pymine_net.types.buffer import Buffer 9 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 10 | from pymine_net.types.packet_map import PacketMap 11 | 12 | 13 | class AbstractProtocolClient(StrictABC): 14 | """Abstract class for a connection over a TCP socket for reading + writing Minecraft packets.""" 15 | 16 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 17 | self.host = host 18 | self.port = port 19 | self.protocol = protocol 20 | self.packet_map = packet_map 21 | 22 | self.state = GameState.HANDSHAKING 23 | self.compression_threshold = -1 24 | 25 | @abstractmethod 26 | def connect(self) -> None: 27 | pass 28 | 29 | @abstractmethod 30 | def close(self) -> None: 31 | pass 32 | 33 | @staticmethod 34 | def _encode_packet(packet: ServerBoundPacket, compression_threshold: int = -1) -> Buffer: 35 | """Encodes and (if necessary) compresses a ServerBoundPacket.""" 36 | 37 | buf = Buffer().write_varint(packet.id).extend(packet.pack()) 38 | 39 | if compression_threshold >= 1: 40 | if len(buf) >= compression_threshold: 41 | buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) 42 | else: 43 | buf = Buffer().write_varint(0).extend(buf) 44 | 45 | return Buffer().write_varint(len(buf)).extend(buf) 46 | 47 | def _decode_packet(self, buf: Buffer) -> ClientBoundPacket: 48 | # decompress packet if necessary 49 | if self.compression_threshold >= 0: 50 | uncompressed_length = buf.read_varint() 51 | 52 | if uncompressed_length > 0: 53 | buf = Buffer(zlib.decompress(buf.read_bytes())) 54 | 55 | packet_id = buf.read_varint() 56 | 57 | # attempt to get packet class from given state and packet id 58 | try: 59 | packet_class: Type[ClientBoundPacket] = self.packet_map[ 60 | PacketDirection.CLIENTBOUND, self.state, packet_id 61 | ] 62 | except KeyError: 63 | raise UnknownPacketIdError(None, self.state, packet_id, PacketDirection.CLIENTBOUND) 64 | 65 | return packet_class.unpack(buf) 66 | 67 | @abstractmethod 68 | def read_packet(self) -> ClientBoundPacket: 69 | pass 70 | 71 | @abstractmethod 72 | def write_packet(self, packet: ServerBoundPacket) -> None: 73 | pass 74 | -------------------------------------------------------------------------------- /pymine_net/net/server.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | from abc import abstractmethod 3 | from typing import Dict, Tuple, Type, Union 4 | 5 | from pymine_net.enums import GameState, PacketDirection 6 | from pymine_net.errors import UnknownPacketIdError 7 | from pymine_net.net.stream import AbstractTCPStream 8 | from pymine_net.strict_abc import StrictABC 9 | from pymine_net.types.buffer import Buffer 10 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 11 | from pymine_net.types.packet_map import PacketMap 12 | 13 | 14 | class AbstractProtocolServerClient(StrictABC): 15 | __slots__ = ("stream", "packet_map", "state", "compression_threshold") 16 | 17 | def __init__(self, stream: AbstractTCPStream, packet_map: PacketMap): 18 | self.stream = stream 19 | self.packet_map = packet_map 20 | self.state = GameState.HANDSHAKING 21 | self.compression_threshold = -1 22 | 23 | def _encode_packet(self, packet: ClientBoundPacket) -> Buffer: 24 | """Encodes and (if necessary) compresses a ClientBoundPacket.""" 25 | 26 | buf = Buffer().write_varint(packet.id).extend(packet.pack()) 27 | 28 | if self.compression_threshold >= 1: 29 | if len(buf) >= self.compression_threshold: 30 | buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) 31 | else: 32 | buf = Buffer().write_varint(0).extend(buf) 33 | 34 | buf = Buffer().write_varint(len(buf)).extend(buf) 35 | return buf 36 | 37 | def _decode_packet(self, buf: Buffer) -> ServerBoundPacket: 38 | # decompress packet if necessary 39 | if self.compression_threshold >= 0: 40 | uncompressed_length = buf.read_varint() 41 | 42 | if uncompressed_length > 0: 43 | buf = Buffer(zlib.decompress(buf.read_bytes())) 44 | 45 | packet_id = buf.read_varint() 46 | 47 | # attempt to get packet class from given state and packet id 48 | try: 49 | packet_class: Type[ClientBoundPacket] = self.packet_map[ 50 | PacketDirection.SERVERBOUND, self.state, packet_id 51 | ] 52 | except KeyError: 53 | raise UnknownPacketIdError( 54 | self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND 55 | ) 56 | 57 | return packet_class.unpack(buf) 58 | 59 | @abstractmethod 60 | def read_packet(self) -> ServerBoundPacket: 61 | pass 62 | 63 | @abstractmethod 64 | def write_packet(self, packet: ClientBoundPacket) -> None: 65 | pass 66 | 67 | 68 | class AbstractProtocolServer(StrictABC): 69 | """Abstract class for a TCP server that handles Minecraft packets.""" 70 | 71 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 72 | self.host = host 73 | self.port = port 74 | self.protocol = protocol 75 | self.packet_map = packet_map 76 | 77 | self.connected_clients: Dict[Tuple[str, int], AbstractProtocolServerClient] = {} 78 | 79 | @abstractmethod 80 | def run(self) -> None: 81 | pass 82 | 83 | @abstractmethod 84 | def close(self) -> None: 85 | pass 86 | -------------------------------------------------------------------------------- /pymine_net/net/socket/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import SocketProtocolClient # noqa: F401 2 | from .server import SocketProtocolServer, SocketProtocolServerClient # noqa: F401 3 | from .stream import EncryptedSocketTCPStream, SocketTCPStream # noqa: F401 4 | -------------------------------------------------------------------------------- /pymine_net/net/socket/client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Union 3 | 4 | from pymine_net.net.client import AbstractProtocolClient 5 | from pymine_net.net.socket.stream import SocketTCPStream 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | from pymine_net.types.packet_map import PacketMap 8 | 9 | __all__ = ("SocketProtocolClient",) 10 | 11 | 12 | class SocketProtocolClient(AbstractProtocolClient): 13 | """A connection over a TCP socket for reading + writing Minecraft packets.""" 14 | 15 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 16 | super().__init__(host, port, protocol, packet_map) 17 | 18 | self.stream: SocketTCPStream = None 19 | 20 | def connect(self) -> None: 21 | sock = socket.create_connection((self.host, self.port)) 22 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 23 | 24 | self.stream = SocketTCPStream(sock) 25 | 26 | def close(self) -> None: 27 | self.stream.close() 28 | 29 | def read_packet(self) -> ClientBoundPacket: 30 | packet_length = self.stream.read_varint() 31 | return self._decode_packet(self.stream.read(packet_length)) 32 | 33 | def write_packet(self, packet: ServerBoundPacket) -> None: 34 | self.stream.write(self._encode_packet(packet)) 35 | -------------------------------------------------------------------------------- /pymine_net/net/socket/server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | from abc import abstractmethod 4 | from typing import Dict, List, Tuple, Union 5 | 6 | from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient 7 | from pymine_net.net.socket.stream import SocketTCPStream 8 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 9 | from pymine_net.types.packet_map import PacketMap 10 | 11 | 12 | class SocketProtocolServerClient(AbstractProtocolServerClient): 13 | def __init__(self, stream: SocketTCPStream, packet_map: PacketMap): 14 | super().__init__(stream, packet_map) 15 | self.stream = stream # redefine this cause typehints 16 | 17 | def read_packet(self) -> ServerBoundPacket: 18 | length = self.stream.read_varint() 19 | return self._decode_packet(self.stream.read(length)) 20 | 21 | def write_packet(self, packet: ClientBoundPacket) -> None: 22 | self.stream.write(self._encode_packet(packet)) 23 | 24 | 25 | class SocketProtocolServer(AbstractProtocolServer): 26 | def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): 27 | super().__init__(host, port, protocol, packet_map) 28 | 29 | self.connected_clients: Dict[Tuple[str, int], SocketProtocolServerClient] = {} 30 | 31 | self.sock: socket.socket = None 32 | self.threads: List[threading.Thread] = [] 33 | self.running = False 34 | 35 | def run(self) -> None: 36 | self.running = True 37 | 38 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as self.sock: 39 | self.sock.bind((self.host, self.port)) 40 | self.sock.listen(20) 41 | 42 | while self.running: 43 | connection, _ = self.sock.accept() 44 | self._client_connected_cb(connection) 45 | 46 | def close(self) -> None: 47 | self.sock.close() 48 | 49 | for thread in self.threads: 50 | thread.join(0.1) 51 | 52 | def _client_connected_cb(self, sock: socket.socket) -> None: 53 | client = SocketProtocolServerClient(SocketTCPStream(sock), self.packet_map) 54 | self.connected_clients[client.stream.remote] = client 55 | thread = threading.Thread(target=self._new_client_connected, args=(client,)) 56 | self.threads.append(thread) 57 | thread.start() 58 | 59 | def _new_client_connected(self, client: SocketProtocolServerClient) -> None: 60 | with client.stream.sock: 61 | self.new_client_connected(client) 62 | 63 | @abstractmethod 64 | def new_client_connected(self, client: SocketProtocolServerClient) -> None: 65 | pass 66 | -------------------------------------------------------------------------------- /pymine_net/net/socket/stream.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | from typing import Tuple, Union 4 | 5 | from cryptography.hazmat.primitives.ciphers import Cipher 6 | 7 | from pymine_net.net.stream import AbstractTCPStream 8 | from pymine_net.types.buffer import Buffer 9 | 10 | __all__ = ("SocketTCPStream", "EncryptedSocketTCPStream") 11 | 12 | 13 | class SocketTCPStream(AbstractTCPStream, socket.socket): 14 | """Used for reading and writing from/to a connected client, wraps a socket.socket. 15 | 16 | :param socket.socket sock: A socket.socket instance. 17 | :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. 18 | :ivar sock: 19 | """ 20 | 21 | __slots__ = ("sock",) 22 | 23 | def __init__(self, sock: socket.socket): 24 | self.sock = sock 25 | 26 | self.remote: Tuple[str, int] = sock.getsockname() 27 | 28 | def read(self, length: int) -> Buffer: 29 | result = Buffer() 30 | 31 | while len(result) < length: 32 | read_bytes = self.sock.recv(length - len(result)) 33 | 34 | if len(read_bytes) == 0: 35 | raise IOError("Server didn't respond with information!") 36 | 37 | result.extend(read_bytes) 38 | 39 | return result 40 | 41 | def write(self, data: Union[Buffer, bytes, bytearray]) -> None: 42 | self.sock.sendall(data) 43 | 44 | def close(self) -> None: 45 | self.sock.close() 46 | 47 | def read_varint(self) -> int: 48 | value = 0 49 | 50 | for i in range(10): 51 | (byte,) = struct.unpack(">B", self.read(1)) 52 | value |= (byte & 0x7F) << 7 * i 53 | 54 | if not byte & 0x80: 55 | break 56 | 57 | if value & (1 << 31): 58 | value -= 1 << 32 59 | 60 | value_max = (1 << (32 - 1)) - 1 61 | value_min = -1 << (32 - 1) 62 | 63 | if not (value_min <= value <= value_max): 64 | raise ValueError( 65 | f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" 66 | ) 67 | 68 | return value 69 | 70 | 71 | class EncryptedSocketTCPStream(SocketTCPStream): 72 | """Used for reading and writing from/to a connected client, wraps a socket.socket. 73 | 74 | :param socket.socket sock: A socket.socket instance. 75 | :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. 76 | :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. 77 | :ivar sock: 78 | :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. 79 | :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. 80 | """ 81 | 82 | def __init__(self, sock: socket.socket, cipher: Cipher): 83 | super().__init__(sock) 84 | 85 | self.decryptor = cipher.decryptor() 86 | self.encryptor = cipher.encryptor() 87 | 88 | def read(self, length: int) -> Buffer: 89 | return Buffer(self.decryptor.update(super().read(length))) 90 | 91 | def write(self, data: Union[Buffer, bytes, bytearray]) -> None: 92 | super().write(self.encryptor.update(data)) 93 | -------------------------------------------------------------------------------- /pymine_net/net/stream.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class AbstractTCPStream: 5 | """Abstract class for a TCP stream.""" 6 | 7 | @abstractmethod 8 | def read_varint(self) -> int: 9 | pass 10 | -------------------------------------------------------------------------------- /pymine_net/packets/__init__.py: -------------------------------------------------------------------------------- 1 | from .packet_map import load_packet_map # noqa: F401 2 | -------------------------------------------------------------------------------- /pymine_net/packets/packet_map.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import warnings 3 | from pathlib import Path 4 | from typing import Dict, Iterator, NoReturn, Type, Union 5 | 6 | from pymine_net.enums import GameState 7 | from pymine_net.types.packet import Packet 8 | from pymine_net.types.packet_map import DuplicatePacketIdError, PacketMap, StatePacketMap 9 | 10 | __all__ = ("load_packet_map",) 11 | 12 | 13 | # the directory this file is contained in 14 | FILE_DIR = Path(__file__).parent 15 | 16 | PROTOCOL_MAP = {757: "v_1_18_1"} 17 | 18 | 19 | def walk_packet_classes( 20 | protocol_name: str, state: GameState, debug: bool = False 21 | ) -> Iterator[Type[Packet]]: 22 | """ 23 | Try to import every pymine.packets.{protocol_name}.{state.name} package, 24 | and search it's __all__ for presence of subclasses of the Packet class. 25 | When a subclass is found, yield it. 26 | """ 27 | # Directories in the package under protocol_name should always contain 28 | # all of the GameState's enum names, as subpackages. So we can assume 29 | # that this path exists, and if not, it will result in an ImportError 30 | # later. 31 | path = str(Path(FILE_DIR, protocol_name, state.name).absolute()) 32 | prefix = f"pymine_net.packets.{protocol_name}.{state.name}" 33 | 34 | def on_error(name: str) -> NoReturn: 35 | raise ImportError(name=name) 36 | 37 | for module in pkgutil.walk_packages(path, prefix, onerror=on_error): 38 | if debug and not hasattr(module, "__all__"): 39 | warnings.warn(f"{module.__name__} is missing member __all__ and cannot be loaded.") 40 | continue 41 | 42 | for member_name in module.__all__: 43 | try: 44 | obj = getattr(module, member_name) 45 | except AttributeError: 46 | warnings.warn( 47 | f"{module.__name__} is missing member {member_name!r} present in __all__." 48 | ) 49 | continue 50 | 51 | if issubclass(obj, Packet): 52 | yield obj 53 | 54 | 55 | def load_packet_map(protocol: Union[int, str], *, debug: bool = False) -> PacketMap: 56 | """ 57 | Collects all packet classes for each GameState for given protocol to construct a PacketMap. 58 | 59 | - If passed protocol is an `int`, we treat it as a version number and use the corresponding 60 | protocol import name defined in PROTOCOL_MAP. 61 | - If it's a `str`, we treat it as the import name directly. 62 | 63 | This is useful for automatic decoding of incoming packet ids into packet classes for decoding 64 | the entire packet. 65 | """ 66 | 67 | if isinstance(protocol, int): 68 | # If given protocol was int (version) and it isn't present in 69 | # the protocol map, we end with ha KeyError since we don't know 70 | # what to import. 71 | protocol = PROTOCOL_MAP[protocol] 72 | 73 | packets: Dict[GameState, StatePacketMap] = {} 74 | 75 | for state in GameState: 76 | packet_classes = [ 77 | packet_class for packet_class in walk_packet_classes(protocol, state, debug) 78 | ] 79 | 80 | # attempt to create the StatePacketMap from the list of loaded packets. 81 | try: 82 | packets[state] = StatePacketMap.from_list(state, packet_classes, check_duplicates=debug) 83 | except DuplicatePacketIdError as exc: # re-raise with protocol included in exception 84 | raise DuplicatePacketIdError(protocol, exc.state, exc.packet_id, exc.direction) 85 | 86 | return PacketMap(protocol, packets) 87 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/handshaking/handshake.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pymine_net.types.buffer import Buffer 4 | from pymine_net.types.packet import ServerBoundPacket 5 | 6 | __all__ = ("HandshakeHandshake",) 7 | 8 | 9 | class HandshakeHandshake(ServerBoundPacket): 10 | """Initiates the connection between the server and client. (Client -> Server) 11 | 12 | :param int protocol: Protocol version to be used. 13 | :param str address: The host/address the client is connecting to. 14 | :param int port: The port the client is connection on. 15 | :param int next_state: The next state which the server should transfer to. 1 for status, 2 for login. 16 | :ivar int id: Unique packet ID. 17 | :ivar protocol: 18 | :ivar address: 19 | :ivar port: 20 | :ivar next_state: 21 | """ 22 | 23 | id = 0x00 24 | 25 | def __init__(self, protocol: int, address: str, port: int, next_state: int): 26 | super().__init__() 27 | 28 | self.protocol = protocol 29 | self.address = address 30 | self.port = port 31 | self.next_state = next_state 32 | 33 | def pack(self) -> Buffer: 34 | return ( 35 | Buffer() 36 | .write_varint(self.protocol) 37 | .write_string(self.address) 38 | .write("H", self.port) 39 | .write_varint(self.next_state) 40 | ) 41 | 42 | @classmethod 43 | def unpack(cls, buf: Buffer) -> HandshakeHandshake: 44 | return cls(buf.read_varint(), buf.read_string(), buf.read("H"), buf.read_varint()) 45 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/login/compression.py: -------------------------------------------------------------------------------- 1 | """Contains LoginSetCompression which is technically part of the login process.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket 7 | 8 | __all__ = ("LoginSetCompression",) 9 | 10 | 11 | class LoginSetCompression(ClientBoundPacket): 12 | """While not directly related to login, this packet is sent by the server during the login process. (Server -> Client) 13 | 14 | :param int compression_threshold: Compression level of future packets, -1 to disable compression. 15 | :ivar int id: Unique packet ID. 16 | :ivar compression_threshold: 17 | """ 18 | 19 | id = 0x03 20 | 21 | def __init__(self, compression_threshold: int = -1): 22 | super().__init__() 23 | 24 | self.compression_threshold = compression_threshold 25 | 26 | def pack(self) -> Buffer: 27 | return Buffer().write_varint(self.compression_threshold) 28 | 29 | @classmethod 30 | def unpack(cls, buf: Buffer) -> LoginSetCompression: 31 | return cls(buf.read_varint()) 32 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/login/login.py: -------------------------------------------------------------------------------- 1 | """Contains packets relating to client logins.""" 2 | 3 | from __future__ import annotations 4 | 5 | from uuid import UUID 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.chat import Chat 9 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 10 | 11 | __all__ = ( 12 | "LoginStart", 13 | "LoginEncryptionRequest", 14 | "LoginEncryptionResponse", 15 | "LoginSuccess", 16 | "LoginDisconnect", 17 | ) 18 | 19 | 20 | class LoginStart(ServerBoundPacket): 21 | """Packet from client asking to start login process. (Client -> Server) 22 | 23 | :param str username: Username of the client who sent the request. 24 | :ivar int id: Unique packet ID. 25 | :ivar username: 26 | """ 27 | 28 | id = 0x00 29 | 30 | def __init__(self, username: str): 31 | super().__init__() 32 | 33 | self.username = username 34 | 35 | def pack(self) -> Buffer: 36 | return Buffer().write_string(self.username) 37 | 38 | @classmethod 39 | def unpack(cls, buf: Buffer) -> LoginStart: 40 | return cls(buf.read_string()) 41 | 42 | 43 | class LoginEncryptionRequest(ClientBoundPacket): 44 | """Used by the server to ask the client to encrypt the login process. (Server -> Client) 45 | 46 | :param bytes public_key: Public key. 47 | :param bytes verify_token: Verify token. 48 | :ivar int id: Unique packet ID. 49 | :ivar public_key: 50 | """ 51 | 52 | id = 0x01 53 | 54 | def __init__(self, public_key: bytes, verify_token: bytes): 55 | super().__init__() 56 | 57 | self.public_key = public_key 58 | self.verify_token = verify_token 59 | 60 | def pack(self) -> Buffer: 61 | return ( 62 | Buffer() 63 | .write_string(" " * 20) 64 | .write_varint(len(self.public_key)) 65 | .write_bytes(self.public_key) 66 | .write_varint(len(self.verify_token)) 67 | .write_bytes(self.verify_token) 68 | ) 69 | 70 | @classmethod 71 | def unpack(cls, buf: Buffer) -> LoginEncryptionRequest: 72 | buf.read_string() 73 | 74 | return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) 75 | 76 | 77 | class LoginEncryptionResponse(ServerBoundPacket): 78 | """Response from the client to a LoginEncryptionRequest. (Client -> Server) 79 | 80 | :param bytes shared_key: The shared key used in the login process. 81 | :param bytes verify_token: The verify token used in the login process. 82 | :ivar int id: Unique packet ID. 83 | :ivar shared_key: 84 | :ivar verify_token: 85 | """ 86 | 87 | id = 0x01 88 | 89 | def __init__(self, shared_key: bytes, verify_token: bytes): 90 | super().__init__() 91 | 92 | self.shared_key = shared_key 93 | self.verify_token = verify_token 94 | 95 | def pack(self) -> Buffer: 96 | return ( 97 | Buffer() 98 | .write_varint(len(self.shared_key)) 99 | .write_bytes(self.shared_key) 100 | .write_varint(len(self.verify_token)) 101 | .write_bytes(self.verify_token) 102 | ) 103 | 104 | @classmethod 105 | def unpack(cls, buf: Buffer) -> LoginEncryptionResponse: 106 | return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) 107 | 108 | 109 | class LoginSuccess(ClientBoundPacket): 110 | """Sent by the server to denote a successful login. (Server -> Client) 111 | 112 | :param UUID uuid: The UUID of the connecting player/client. 113 | :param str username: The username of the connecting player/client. 114 | :ivar int id: Unique packet ID. 115 | :ivar uuid: 116 | :ivar username: 117 | """ 118 | 119 | id = 0x02 120 | 121 | def __init__(self, uuid: UUID, username: str): 122 | super().__init__() 123 | 124 | self.uuid = uuid 125 | self.username = username 126 | 127 | def pack(self) -> Buffer: 128 | return Buffer().write_uuid(self.uuid).write_string(self.username) 129 | 130 | @classmethod 131 | def unpack(cls, buf: Buffer) -> LoginSuccess: 132 | return cls(buf.read_uuid(), buf.read_string()) 133 | 134 | 135 | class LoginDisconnect(ClientBoundPacket): 136 | """Sent by the server to kick a player while in the login state. (Server -> Client) 137 | 138 | :param Chat reason: The reason for the disconnect. 139 | :ivar int id: Unique packet ID. 140 | """ 141 | 142 | id = 0x00 143 | 144 | def __init__(self, reason: Chat): 145 | super().__init__() 146 | 147 | self.reason = reason 148 | 149 | def pack(self) -> Buffer: 150 | return Buffer().write_chat(self.reason) 151 | 152 | @classmethod 153 | def unpack(cls, buf: Buffer) -> LoginDisconnect: 154 | return cls(buf.read_chat()) 155 | 156 | 157 | class LoginPluginRequest(ClientBoundPacket): 158 | """Sent by server to implement a custom handshaking flow. 159 | 160 | :param int message_id: Message id, generated by the server, should be unique to the connection. 161 | :param str channel: Channel identifier, name of the plugin channel used to send the data. 162 | :param bytes data: Data that is to be sent. 163 | :ivar int id: Unique packet ID. 164 | """ 165 | 166 | id = 0x04 167 | 168 | def __init__(self, message_id: int, channel: str, data: bytes): 169 | self.message_id = message_id 170 | self.channel = channel 171 | self.data = data 172 | 173 | def pack(self) -> Buffer: 174 | return ( 175 | Buffer().write_varint(self.message_id).write_string(self.channel).write_bytes(self.data) 176 | ) 177 | 178 | @classmethod 179 | def unpack(cls, buf: Buffer) -> LoginPluginRequest: 180 | return cls(buf.read_varint(), buf.read_string(), buf.read_bytes()) 181 | 182 | 183 | class LoginPluginResponse(ServerBoundPacket): 184 | """Response to LoginPluginRequest from client. 185 | 186 | :param int message_id: Message id, generated by the server, should be unique to the connection. 187 | :param Optional[bytes] data: Optional response data, present if client understood request. 188 | :ivar int id: Unique packet ID. 189 | """ 190 | 191 | id = 0x02 192 | 193 | def __init__(self, message_id: int, data: bytes = None): 194 | self.message_id = message_id 195 | self.data = data 196 | 197 | def pack(self) -> Buffer: 198 | buf = Buffer().write_varint(self.message_id) 199 | return buf.write_optional(buf.write_bytes, self.data) 200 | 201 | @classmethod 202 | def unpack(cls, buf: Buffer) -> LoginPluginResponse: 203 | return cls(buf.read_varint(), buf.read_optional(buf.read_bytes)) 204 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/advancement.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to advancements.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | 8 | __all__ = ("PlayAdvancementTab", "PlaySelectAdvancementTab") 9 | 10 | 11 | class PlayAdvancementTab(ServerBoundPacket): 12 | """Related to advancement tab menu, see here: https://wiki.vg/Protocol#Advancement_Tab (Client -> Server) 13 | 14 | :param int action: Either opened tab (0), or closed screen (1). 15 | :param int tab_id: The ID of the tab, only present if action is 0 (opened tab). 16 | :ivar int id: Unique packet ID. 17 | :ivar action: 18 | :ivar tab_id: 19 | """ 20 | 21 | id = 0x22 22 | 23 | def __init__(self, action: int, tab_id: int): 24 | super().__init__() 25 | 26 | self.action = action 27 | self.tab_id = tab_id 28 | 29 | @classmethod 30 | def unpack(cls, buf: Buffer) -> PlayAdvancementTab: 31 | return cls(buf.read_varint(), buf.read_optional(buf.read_varint)) 32 | 33 | 34 | class PlaySelectAdvancementTab(ClientBoundPacket): 35 | """Sent by the server to indicate that the client should switch advancement tab. Sent either when the client switches tab in the GUI or when an advancement in another tab is made. (Server -> Client) 36 | 37 | :param Optional[str] identifier: One of the following: minecraft:story/root, minecraft:nether/root, minecraft:end/root, minecraft:adventure/root, minecraft:husbandry/root. 38 | :ivar int id: Unique packet ID. 39 | :ivar Optional[str] identifier: 40 | """ 41 | 42 | id = 0x40 43 | 44 | def __init__(self, identifier: str = None): 45 | super().__init__() 46 | 47 | self.identifier = identifier 48 | 49 | def pack(self) -> Buffer: 50 | buf = Buffer() 51 | return buf.write_optional(buf.write_string, self.identifier) 52 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/animations.py: -------------------------------------------------------------------------------- 1 | """Contains animation packets.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | 8 | __all__ = ( 9 | "PlayEntityAnimation", 10 | "PlayBlockBreakAnimation", 11 | "PlayAnimationServerBound", 12 | "PlayOpenBook", 13 | ) 14 | 15 | 16 | class PlayEntityAnimation(ClientBoundPacket): 17 | """Sent whenever an entity should change animation. (Server -> Client) 18 | 19 | :param int entity_id: Entity ID of the digging entity. 20 | :param int animation: Value 0-5 which correspond to a specific animation (https://wiki.vg/Protocol#Entity_Animation_.28clientbound.29). 21 | :ivar int id: Unique packet ID. 22 | :ivar entity_id: 23 | :ivar animation: 24 | """ 25 | 26 | id = 0x06 27 | 28 | def __init__(self, entity_id: int, animation: int): 29 | super().__init__() 30 | 31 | self.entity_id = entity_id 32 | self.animation = animation 33 | 34 | def pack(self) -> Buffer: 35 | return Buffer().write_varint(self.entity_id).write("B", self.animation) 36 | 37 | 38 | class PlayBlockBreakAnimation(ClientBoundPacket): 39 | """Sent to play a block breaking animation. (Server -> Client) 40 | 41 | :param int entity_id: Entity ID of the entity which broke the block, or random. 42 | :param int x: The x coordinate of the location to play the animation. 43 | :param int y: The y coordinate of the location to play the animation. 44 | :param int z: The z coordinate of the location to play the animation. 45 | :param int stage: Stage from 0-9 in the breaking animation. 46 | :ivar int id: Unique packet ID. 47 | :ivar entity_id: 48 | :ivar x: 49 | :ivar y: 50 | :ivar z: 51 | :ivar stage: 52 | """ 53 | 54 | id = 0x09 55 | 56 | def __init__(self, entity_id: int, x: int, y: int, z: int, stage: int): 57 | super().__init__() 58 | 59 | self.entity_id = entity_id 60 | self.x, self.y, self.z = x, y, z 61 | self.stage = stage 62 | 63 | def pack(self) -> Buffer: 64 | return ( 65 | Buffer() 66 | .write_varint(self.entity_id) 67 | .write_position(self.x, self.y, self.z) 68 | .write("b", self.stage) 69 | ) 70 | 71 | 72 | class PlayAnimationServerBound(ServerBoundPacket): 73 | """Sent when a client's arm swings. (Client -> Server) 74 | 75 | :param int hand: Either main hand (0) or offhand (1). 76 | :ivar int id: Unique packet ID. 77 | :ivar hand: 78 | """ 79 | 80 | id = 0x2C 81 | 82 | def __init__(self, hand: int): 83 | super().__init__() 84 | 85 | self.hand = hand 86 | 87 | @classmethod 88 | def unpack(cls, buf: Buffer) -> PlayAnimationServerBound: 89 | return cls(buf.read_varint()) 90 | 91 | 92 | class PlayOpenBook(ClientBoundPacket): 93 | """Sent when a player right clicks a signed book. (Server -> Client) 94 | 95 | :param int hand: The hand used, either main (0) or offhand (1). 96 | :ivar int id: Unique packet ID. 97 | :ivar hand: 98 | """ 99 | 100 | id = 0x2D 101 | 102 | def __init__(self, hand: int): 103 | super().__init__() 104 | 105 | self.hand = hand 106 | 107 | def pack(self) -> Buffer: 108 | return Buffer().write_varint(self.hand) 109 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/beacon.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to beacons.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ServerBoundPacket 7 | 8 | __all__ = ("PlaySetBeaconEffect",) 9 | 10 | 11 | class PlaySetBeaconEffect(ServerBoundPacket): 12 | """Changes the effect of the current beacon. (Client -> Server) 13 | 14 | :param int primary_effect: A potion ID (https://minecraft.gamepedia.com/Data_values#Potions). 15 | :param int secondary_effect: A potion ID (https://minecraft.gamepedia.com/Data_values#Potions). 16 | :ivar int id: Unique packet ID. 17 | :ivar primary_effect: 18 | :ivar secondary_effect: 19 | """ 20 | 21 | id = 0x24 22 | 23 | def __init__(self, primary_effect: int, secondary_effect: int): 24 | super().__init__() 25 | 26 | self.primary_effect = primary_effect 27 | self.secondary_effect = secondary_effect 28 | 29 | @classmethod 30 | def unpack(cls, buf: Buffer) -> PlaySetBeaconEffect: 31 | return cls(buf.read_varint(), buf.read_varint()) 32 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/block.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to blocks.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Tuple 6 | 7 | import pymine_net.types.nbt as nbt 8 | from pymine_net.types.buffer import Buffer 9 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 10 | 11 | __all__ = ( 12 | "PlayBlockAction", 13 | "PlayBlockChange", 14 | "PlayQueryBlockNBT", 15 | "PlayPlayerBlockPlacement", 16 | "PlayNBTQueryResponse", 17 | "PlayMultiBlockChange", 18 | ) 19 | 20 | 21 | class PlayBlockAction(ClientBoundPacket): 22 | """This packet is used for a number of actions and animations performed by blocks. (Server -> Client) 23 | 24 | :param int x: The x coordinate of the location where this occurs. 25 | :param int y: The y coordinate of the location where this occurs. 26 | :param int z: The z coordinate of the location where this occurs. 27 | :param int action_id: Block action ID, see here: https://wiki.vg/Block_Actions. 28 | :param int action_param: Action param of the action, see here: https://wiki.vg/Block_Actions. 29 | :param int block_type: The type of block which the action is for. 30 | :ivar int id: Unique packet ID. 31 | :ivar x: 32 | :ivar y: 33 | :ivar z: 34 | :ivar action_id: 35 | :ivar action_param: 36 | :ivar block_type: 37 | """ 38 | 39 | id = 0x0B 40 | 41 | def __init__(self, x: int, y: int, z: int, action_id: int, action_param: int, block_type: int): 42 | super().__init__() 43 | 44 | self.x, self.y, self.z = x, y, z 45 | self.action_id = action_id 46 | self.action_param = action_param 47 | self.block_type = block_type 48 | 49 | def pack(self) -> Buffer: 50 | return ( 51 | Buffer() 52 | .write_position(self.x, self.y, self.z) 53 | .write("B", self.action_id) 54 | .write("B", self.action_param) 55 | .write_varint(self.block_type) 56 | ) 57 | 58 | 59 | class PlayBlockChange(ClientBoundPacket): 60 | """Fired when a block is changed within the render distance. (Server -> Client) 61 | 62 | :param int x: The x coordinate of the location where this occurs. 63 | :param int y: The y coordinate of the location where this occurs. 64 | :param int z: The z coordinate of the location where this occurs. 65 | :param int block_id: Block ID of what to change the block to. 66 | :ivar int id: Unique packet ID. 67 | :ivar x: 68 | :ivar y: 69 | :ivar z: 70 | :ivar block_id: 71 | """ 72 | 73 | id = 0x0C 74 | 75 | def __init__(self, x: int, y: int, z: int, block_id: int): 76 | super().__init__() 77 | 78 | self.x, self.y, self.z = x, y, z 79 | self.block_id = block_id 80 | 81 | def pack(self) -> Buffer: 82 | return Buffer().write_position(self.x, self.y, self.z).write_varint(self.block_id) 83 | 84 | 85 | class PlayQueryBlockNBT(ServerBoundPacket): 86 | """Used when SHIFT+F3+I is used on a block. (Client -> Server) 87 | 88 | :param int transaction_id: Number used to verify that a response matches. 89 | :param int x: The x coordinate of the block. 90 | :param int y: The y coordinate of the block. 91 | :param int z: The z coordinate of the block. 92 | :ivar int id: Unique packet ID. 93 | :ivar transaction_id: 94 | :ivar x: 95 | :ivar y: 96 | :ivar z: 97 | """ 98 | 99 | id = 0x01 100 | 101 | def __init__(self, transaction_id: int, x: int, y: int, z: int): 102 | super().__init__() 103 | 104 | self.transaction_id = transaction_id 105 | self.x, self.y, self.z = x, y, z 106 | 107 | @classmethod 108 | def unpack(cls, buf: Buffer) -> PlayQueryBlockNBT: 109 | return cls(buf.read_varint(), *buf.read_position()) 110 | 111 | 112 | class PlayPlayerBlockPlacement(ServerBoundPacket): 113 | """Sent by the client when it places a block. (Client -> Server) 114 | 115 | :param int hand: The hand used, either main hand (0), or offhand (1). 116 | :param int x: The x coordinate of the block. 117 | :param int y: The y coordinate of the block. 118 | :param int z: The z coordinate of the block. 119 | :param int face: The face of the block, see here: https://wiki.vg/Protocol#Player_Block_Placement. 120 | :param float cur_pos_x: The x position of the crosshair on the block. 121 | :param float cur_pos_y: The y position of the crosshair on the block. 122 | :param float cur_pos_z: The z position of the crosshair on the block. 123 | :param bool inside_block: True if the player's head is inside the block. 124 | :ivar int id: Unique packet ID. 125 | :ivar hand: 126 | :ivar x: 127 | :ivar y: 128 | :ivar z: 129 | :ivar face: 130 | :ivar cur_pos_x: 131 | :ivar cur_pos_y: 132 | :ivar cur_pos_z: 133 | :ivar inside_block: 134 | """ 135 | 136 | id = 0x2E 137 | 138 | def __init__( 139 | self, 140 | hand: int, 141 | x: int, 142 | y: int, 143 | z: int, 144 | face: int, 145 | cur_pos_x: float, 146 | cur_pos_y: float, 147 | cur_pos_z: float, 148 | inside_block: bool, 149 | ): 150 | super().__init__() 151 | 152 | self.hand = hand 153 | self.x, self.y, self.z = x, y, z 154 | self.face = face 155 | self.cur_pos_x = cur_pos_x 156 | self.cur_pos_y = cur_pos_y 157 | self.cur_pos_z = cur_pos_z 158 | self.inside_block = inside_block 159 | 160 | @classmethod 161 | def unpack(cls, buf: Buffer) -> PlayPlayerBlockPlacement: 162 | return cls( 163 | buf.read_varint(), 164 | *buf.read_position(), 165 | buf.read_varint(), 166 | buf.read("f"), 167 | buf.read("f"), 168 | buf.read("f"), 169 | buf.read("?"), 170 | ) 171 | 172 | 173 | class PlayNBTQueryResponse(ClientBoundPacket): 174 | """Sent by the client when it places a block. (Server -> Client) 175 | 176 | :param int transaction_id: 177 | :param nbt.TAG nbt_tag: 178 | :ivar int id: Unique packet ID. 179 | :ivar transaction_id: 180 | :ivar nbt_tag: 181 | """ 182 | 183 | id = 0x60 184 | 185 | def __init__(self, transaction_id: int, nbt_tag: nbt.TAG): 186 | super().__init__() 187 | 188 | self.transaction_id = transaction_id 189 | self.nbt_tag = nbt_tag 190 | 191 | def pack(self) -> Buffer: 192 | return Buffer().write_varint(self.transaction_id).write_nbt(self.nbt_tag) 193 | 194 | 195 | class PlayMultiBlockChange(ClientBoundPacket): 196 | """Sent whenever 2 or more blocks change in the same chunk on the same tick. (Server -> Client) 197 | 198 | :param int chunk_sect_x: The x coordinate of the chunk section. 199 | :param int chunk_sect_y: The y coordinate of the chunk section. 200 | :param int chunk_sect_z: The z coordinate of the chunk section. 201 | :param bool trust_edges: The inverse of preceding PlayUpdateLight packet's trust_edges bool 202 | :param list blocks: The changed blocks, formatted like [block_id, local_x, local_y, local_z]. 203 | :ivar int id: Unique packet ID. 204 | :ivar chunk_sect_x: 205 | :ivar chunk_sect_y: 206 | :ivar chunk_sect_z: 207 | :ivar trust_edges: 208 | :ivar blocks: 209 | """ 210 | 211 | id = 0x3F 212 | 213 | def __init__( 214 | self, 215 | chunk_sect_x: int, 216 | chunk_sect_y: int, 217 | chunk_sect_z: int, 218 | trust_edges: bool, 219 | blocks: List[Tuple[int, int, int, int]], 220 | ): 221 | super().__init__() 222 | 223 | self.chunk_sect_x = chunk_sect_x 224 | self.chunk_sect_y = chunk_sect_y 225 | self.chunk_sect_z = chunk_sect_z 226 | self.trust_edges = trust_edges 227 | self.blocks = blocks 228 | 229 | def pack(self) -> Buffer: 230 | buf = ( 231 | Buffer() 232 | .write_varint( 233 | ((self.chunk_sect_x & 0x3FFFFF) << 42) 234 | | (self.chunk_sect_y & 0xFFFFF) 235 | | ((self.chunk_sect_z & 0x3FFFFF) << 20) 236 | ) 237 | .write("?", self.trust_edges) 238 | .write_varint(len(self.blocks)) 239 | ) 240 | 241 | for block_id, local_x, local_y, local_z in self.blocks: 242 | buf.write_varint(block_id << 12 | (local_x << 8 | local_z << 4 | local_y)) 243 | 244 | return buf 245 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/boss.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to bosses.""" 2 | 3 | from __future__ import annotations 4 | 5 | from uuid import UUID 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.packet import ClientBoundPacket 9 | 10 | __all__ = ("PlayBossBar",) 11 | 12 | 13 | class PlayBossBar(ClientBoundPacket): 14 | """Used to send boss bar data. (Server -> Client) 15 | 16 | :param UUID uuid: UUID of the boss bar. 17 | :param int action: Action to take. 18 | :param dict **data: Data corresponding to the action. 19 | :ivar dict data: Data corresponding to the action. 20 | :ivar int id: Unique packet ID. 21 | :ivar uuid: 22 | :ivar action: 23 | """ 24 | 25 | id = 0x0D 26 | 27 | def __init__(self, uuid: UUID, action: int, **data: dict): 28 | super().__init__() 29 | 30 | self.uuid = uuid 31 | self.action = action 32 | self.data = data 33 | 34 | def pack(self) -> Buffer: 35 | buf = Buffer.write_uuid(self.uuid).write_varint(self.action) 36 | 37 | if self.action == 0: 38 | buf.write_chat(self.data["title"]).write("f", self.data["health"]).write_varint( 39 | self.data["color"] 40 | ).write_varint(self.data["division"]).write("B", self.data["flags"]) 41 | elif self.action == 2: 42 | buf.write("f", self.data["health"]) 43 | elif self.action == 3: 44 | buf.write_chat(self.data["title"]) 45 | elif self.action == 4: 46 | buf.write_varint(self.data["color"]).write_varint(self.data["division"]) 47 | elif self.action == 5: 48 | buf.write("B", self.data["flags"]) 49 | 50 | return buf 51 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/chat.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to the chat.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Optional, Tuple 6 | from uuid import UUID 7 | 8 | from pymine_net.types.buffer import Buffer 9 | from pymine_net.types.chat import Chat 10 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 11 | 12 | __all__ = ( 13 | "PlayChatMessageClientBound", 14 | "PlayChatMessageServerBound", 15 | "PlayTabCompleteClientBound", 16 | "PlayTabCompleteServerBound", 17 | ) 18 | 19 | 20 | class PlayChatMessageClientBound(ClientBoundPacket): 21 | """A chat message from the server to the client (Server -> Client) 22 | 23 | :param Chat data: The actual chat data. 24 | :param int position: Where on the GUI the message is to be displayed. 25 | :param UUID sender: Used by the Notchian client for the disableChat launch option. Setting both longs to 0 will always display the message regardless of the setting. 26 | :ivar int id: Unique packet ID. 27 | :ivar data: 28 | :ivar position: 29 | :ivar sender: 30 | """ 31 | 32 | id = 0x0F 33 | 34 | def __init__(self, data: Chat, position: int, sender: UUID): 35 | super().__init__() 36 | 37 | self.data = data 38 | self.position = position 39 | self.sender = sender 40 | 41 | def pack(self) -> Buffer: 42 | return Buffer().write_chat(self.data).write("b", self.position).write_uuid(self.sender) 43 | 44 | 45 | class PlayChatMessageServerBound(ServerBoundPacket): 46 | """A chat message from a client to the server. Can be a command. (Client -> Server) 47 | 48 | :param str message: The raw text sent by the client. 49 | :ivar int id: Unique packet ID. 50 | :ivar message: 51 | """ 52 | 53 | id = 0x03 54 | 55 | def __init__(self, message: str): 56 | super().__init__() 57 | 58 | self.message = message 59 | 60 | @classmethod 61 | def unpack(cls, buf: Buffer) -> PlayChatMessageServerBound: 62 | return cls(buf.read_string()) 63 | 64 | 65 | class PlayTabCompleteServerBound(ServerBoundPacket): 66 | """Used when a client wants to tab complete a chat message. (Client -> Server) 67 | 68 | :param int transaction_id: Number generated by the client. 69 | :param str text: All text behind / to the left of the cursor. 70 | :ivar int id: Unique packet ID. 71 | :ivar transaction_id: 72 | :ivar text: 73 | """ 74 | 75 | id = 0x06 76 | 77 | def __init__(self, transaction_id: int, text: str): 78 | super().__init__() 79 | 80 | self.transaction_id = transaction_id 81 | self.text = text 82 | 83 | @classmethod 84 | def unpack(cls, buf: Buffer) -> PlayTabCompleteServerBound: 85 | return cls(buf.read_varint(), buf.read_string()) 86 | 87 | 88 | class PlayTabCompleteClientBound(ClientBoundPacket): 89 | """Sends a list of auto-completions for a command. For regular chat, this would be a player username. Command names and parameters are also supported. (Server -> Client) 90 | 91 | :param int transaction_id: Number generated by the client. 92 | :param int start: Start of the text to replace. 93 | :param int length: Length of the text to replace. 94 | :param list matches: List of matches. 95 | :ivar int id: Unique packet ID. 96 | :ivar transaction_id: 97 | :ivar start: 98 | :ivar matches: 99 | """ 100 | 101 | id = 0x11 102 | 103 | def __init__(self, transaction_id: int, start: int, matches: List[Tuple[str, Optional[Chat]]]): 104 | super().__init__() 105 | 106 | self.transaction_id = transaction_id 107 | self.start = start 108 | self.matches = matches 109 | 110 | def pack(self) -> Buffer: 111 | buf = ( 112 | Buffer() 113 | .write_varint(self.id) 114 | .write_varint(self.start) 115 | .write_varint(self.length) 116 | .write_varint(len(self.matches)) 117 | ) 118 | 119 | for match, tooltip in self.matches: 120 | buf.write_string(match) 121 | 122 | if tooltip is not None: 123 | buf.write("?", True).write_chat(tooltip) 124 | else: 125 | buf.write("?", False) 126 | 127 | return buf 128 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/command.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to commands""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.packet import ClientBoundPacket 9 | 10 | __all__ = ("PlayDeclareCommands",) 11 | 12 | 13 | class PlayDeclareCommands(ClientBoundPacket): 14 | """Tells the clients what commands there are. (Server -> Client) 15 | 16 | :param List[dict] nodes: The command nodes, a list of dictionaries. The first item is assumed to be the root node. 17 | :ivar int id: Unique packet ID. 18 | :ivar nodes: 19 | """ 20 | 21 | id = 0x12 22 | 23 | def __init__(self, nodes: List[dict]) -> None: 24 | super().__init__() 25 | 26 | self.nodes = nodes 27 | 28 | def pack(self) -> Buffer: 29 | buf = Buffer().write_varint(len(self.nodes)) 30 | 31 | for node in self.nodes: 32 | buf.write_node(node) 33 | 34 | return buf.write_varint(0) 35 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/command_block.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to command blocks.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ServerBoundPacket 7 | 8 | __all__ = ( 9 | "PlayUpdateCommandBlock", 10 | "PlayUpdateCommandBlockMinecart", 11 | ) 12 | 13 | 14 | class PlayUpdateCommandBlock(ServerBoundPacket): 15 | """Used when a client updates a command block. (Client -> Server) 16 | 17 | :param int x: The x coordinate of the command block. 18 | :param int y: The y coordinate of the command block. 19 | :param int z: The z coordinate of the command block. 20 | :param str command: The command text in the command block. 21 | :param int mode: The mode which the command block is in. Either sequence (0), auto (1), or redstone (2). 22 | :param int flags: Other flags, see here: https://wiki.vg/Protocol#Update_Command_Block. 23 | :ivar int id: Unique packet ID. 24 | :ivar x: 25 | :ivar y: 26 | :ivar z: 27 | :ivar command: 28 | :ivar mode: 29 | :ivar flags: 30 | """ 31 | 32 | id = 0x26 33 | 34 | def __init__(self, x: int, y: int, z: int, command: str, mode: int, flags: int): 35 | super().__init__() 36 | 37 | self.x, self.y, self.z = x, y, z 38 | self.command = command 39 | self.mode = mode 40 | self.flags = flags 41 | 42 | @classmethod 43 | def unpack(cls, buf: Buffer) -> PlayUpdateCommandBlock: 44 | return cls(buf.read_position(), buf.read_string(), buf.read_varint(), buf.read("b")) 45 | 46 | 47 | class PlayUpdateCommandBlockMinecart(ServerBoundPacket): 48 | """Sent when the client updates a command block minecart. (Client -> Server) 49 | 50 | :param int entity_id: The ID of the entity (the minecart). 51 | :param str command: The command text in the command block. 52 | :param bool track_output: Whether output from the last command is saved. 53 | :ivar int id: Unique packet ID. 54 | :ivar entity_id: 55 | :ivar command: 56 | :ivar track_output: 57 | """ 58 | 59 | id = 0x27 60 | 61 | def __init__(self, entity_id: int, command: str, track_output: bool): 62 | super().__init__() 63 | 64 | self.entity_id = entity_id 65 | self.command = command 66 | self.track_output = track_output 67 | 68 | @classmethod 69 | def unpack(cls, buf: Buffer) -> PlayUpdateCommandBlockMinecart: 70 | return cls(buf.read_varint(), buf.read_string(), buf.read("?")) 71 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/cooldown.py: -------------------------------------------------------------------------------- 1 | """Contains the PlaySetCooldown packet.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket 7 | 8 | __all__ = ("PlaySetCooldown",) 9 | 10 | 11 | class PlaySetCooldown(ClientBoundPacket): 12 | """Applies a cooldown period to all items with the given type. (Server -> Client) 13 | 14 | Client bound(Server -> Client) 15 | :param int item_id: The unique id of the type of affected items. 16 | :param int cooldown_ticks: The length of the cooldown in in-game ticks. 17 | :ivar int id: The unique ID of the packet. 18 | :ivar item_id: 19 | :ivar cooldown_ticks: 20 | """ 21 | 22 | id = 0x17 23 | 24 | def __init__(self, item_id: int, cooldown_ticks: int) -> None: 25 | super().__init__() 26 | 27 | self.item_id = item_id 28 | self.cooldown_ticks = cooldown_ticks 29 | 30 | def pack(self) -> Buffer: 31 | return Buffer().write_varint(self.item_id).write_varint(self.cooldown_ticks) 32 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/crafting.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to crafting and recipes.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict, List 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 9 | 10 | __all__ = ( 11 | "PlayCraftRecipeRequest", 12 | "PlaySetDisplayedRecipe", 13 | "PlaySetRecipeBookState", 14 | "PlayCraftRecipeResponse", 15 | "PlayDeclareRecipes", 16 | "PlayUnlockRecipes", 17 | ) 18 | 19 | 20 | class PlayCraftRecipeRequest(ServerBoundPacket): 21 | """Sent when a client/player clicks a recipe in the crafting book that is craftable. (Client -> Server) 22 | 23 | :param int window_id: ID of the crafting table window. 24 | :param str recipe_identifier: The recipe identifier. 25 | :param bool make_all: Whether maximum amount that can be crafted is crafted. 26 | :ivar int id: Unique packet ID. 27 | :ivar window_id: 28 | :ivar recipe_identifier: 29 | :ivar make_all: 30 | """ 31 | 32 | id = 0x18 33 | 34 | def __init__(self, window_id: int, recipe_identifier: str, make_all: bool): 35 | super().__init__() 36 | 37 | self.window_id = window_id 38 | self.recipe_identifier = recipe_identifier 39 | self.make_all = make_all 40 | 41 | @classmethod 42 | def unpack(cls, buf: Buffer) -> PlayCraftRecipeRequest: 43 | return cls(buf.read("b"), buf.read_string(), buf.read("?")) 44 | 45 | 46 | class PlaySetDisplayedRecipe(ServerBoundPacket): 47 | """Replaces Recipe Book Data, type 0. See here: https://wiki.vg/Protocol#Set_Displayed_Recipe (Client -> Server) 48 | 49 | :param str recipe_id: The identifier for the recipe. 50 | :ivar int id: Unique packet ID. 51 | :ivar recipe_id: 52 | """ 53 | 54 | id = 0x1F 55 | 56 | def __init__(self, recipe_id: str): 57 | super().__init__() 58 | 59 | self.recipe_id = recipe_id 60 | 61 | @classmethod 62 | def unpack(cls, buf: Buffer) -> PlaySetDisplayedRecipe: 63 | return cls(buf.read_string()) 64 | 65 | 66 | class PlaySetRecipeBookState(ServerBoundPacket): 67 | """Replaces Recipe Book Data, type 1. See here: https://wiki.vg/Protocol#Set_Recipe_Book_State (Client -> Server) 68 | 69 | :param int book_id: One of the following: crafting (0), furnace (1), blast furnace (2), smoker (3). 70 | :param bool book_open: Whether the crafting book is open or not. 71 | :param bool filter_active: Unknown. 72 | :ivar int id: Unique packet ID. 73 | :ivar book_id: 74 | :ivar book_open: 75 | :ivar filter_active: 76 | """ 77 | 78 | id = 0x1E 79 | 80 | def __init__(self, book_id: int, book_open: bool, filter_active: bool): 81 | super().__init__() 82 | 83 | self.book_id = book_id 84 | self.book_open = book_open 85 | self.filter_active = filter_active 86 | 87 | @classmethod 88 | def unpack(cls, buf: Buffer) -> PlaySetRecipeBookState: 89 | return cls(buf.read_varint(), buf.read("?"), buf.read("?")) 90 | 91 | 92 | class PlayCraftRecipeResponse(ClientBoundPacket): 93 | """Response to a PlayCraftRecipeRequest, used to update the client's UI. (Server -> Client) 94 | 95 | :param int window_id: ID of the crafting table window. 96 | :param str recipe: The recipe identifier. 97 | :ivar int id: Unique packet ID. 98 | :ivar window_id: 99 | :ivar recipe: 100 | """ 101 | 102 | id = 0x31 103 | 104 | def __init__(self, window_id: int, recipe_identifier: str): 105 | super().__init__() 106 | 107 | self.window_id = window_id 108 | self.recipe_identifier = recipe_identifier 109 | 110 | def pack(self) -> Buffer: 111 | return Buffer().write("b", self.window_id).write_string(self.recipe_identifier) 112 | 113 | 114 | class PlayDeclareRecipes(ClientBoundPacket): 115 | """Sends all registered recipes to the client. (Server -> Client) 116 | 117 | :param Dict[str, dict] recipes: The recipes to be sent in the form {recipe_id: recipe_data}. 118 | :ivar int id: Unique packet ID. 119 | :ivar recipes: 120 | """ 121 | 122 | id = 0x66 123 | 124 | def __init__(self, recipes: Dict[str, dict]): 125 | super().__init__() 126 | 127 | self.recipes = recipes 128 | 129 | def pack(self) -> Buffer: 130 | buf = Buffer() 131 | 132 | for recipe_id, recipe in self.recipes.items(): 133 | buf.write_recipe(recipe_id, recipe) 134 | 135 | return buf 136 | 137 | 138 | class PlayUnlockRecipes(ClientBoundPacket): 139 | """Unlocks specified locked recipes for the client. (Server -> Client) 140 | 141 | :param int action: The action to be taken, see here: https://wiki.vg/Protocol#Unlock_Recipes. 142 | :param bool crafting_book_open: If true, then the crafting recipe book will be open when the player opens its inventory. 143 | :param bool crafting_book_filter_active: If true, then the filtering option is active when the players opens its inventory. 144 | :param bool smelting_book_open: If true, then the smelting recipe book will be open when the player opens its inventory. 145 | :param bool smelting_book_filter_active: If true, then the filtering option is active when the players opens its inventory. 146 | :param bool blast_furnace_book_open: If true, then the blast furnace recipe book will be open when the player opens its inventory. 147 | :param bool blast_furnace_book_filter_active: If true, then the filtering option is active when the players opens its inventory. 148 | :param bool smoker_book_open: If true, then the smoker recipe book will be open when the player opens its inventory. 149 | :param bool smoker_book_filter_active: If true, then the filtering option is active when the players opens its inventory. 150 | :param List[str] recipe_ids_1: First list of recipe identifiers. 151 | :param Optional[List[str]] recipe_ids_2: Second list of recipe identifiers. 152 | :ivar int id: Unique packet ID. 153 | :ivar action: 154 | :ivar crafting_book_open: 155 | :ivar crafting_book_filter_active: 156 | :ivar smelting_book_open: 157 | :ivar smelting_book_filter_active: 158 | :ivar blast_furnace_book_open: 159 | :ivar blast_furnace_book_filter_active: 160 | :ivar smoker_book_open: 161 | :ivar smoker_book_filter_active: 162 | :ivar recipe_ids_1: 163 | :ivar recipe_ids_2: 164 | """ 165 | 166 | id = 0x39 167 | 168 | def __init__( 169 | self, 170 | action: int, 171 | crafting_book_open: bool, 172 | crafting_book_filter_active: bool, 173 | smelting_book_open: bool, 174 | smelting_book_filter_active: bool, 175 | blast_furnace_book_open: bool, 176 | blast_furnace_book_filter_active: bool, 177 | smoker_book_open: bool, 178 | smoker_book_filter_active: bool, 179 | recipe_ids_1: List[str], 180 | recipe_ids_2: List[list] = None, 181 | ): 182 | super().__init__() 183 | 184 | self.action = action 185 | self.crafting_book_open = crafting_book_open 186 | self.crafting_book_filter_active = crafting_book_filter_active 187 | self.smelting_book_open = smelting_book_open 188 | self.smelting_book_filter_active = smelting_book_filter_active 189 | self.blast_furnace_book_open = blast_furnace_book_open 190 | self.blast_furnace_book_filter_active = blast_furnace_book_filter_active 191 | self.smoker_book_open = smoker_book_open 192 | self.smoker_book_filter_active = smoker_book_filter_active 193 | self.recipe_ids_1 = recipe_ids_1 194 | self.recipe_ids_2 = recipe_ids_2 195 | 196 | def pack(self) -> Buffer: 197 | buf = ( 198 | Buffer() 199 | .write_varint(self.action) 200 | .write("?", self.crafting_book_open) 201 | .write("?", self.crafting_book_filter_active) 202 | .write("?", self.smelting_book_open) 203 | .write("?", self.smelting_book_filter_active) 204 | .write("?", self.blast_furnace_book_open) 205 | .write("?", self.blast_furnace_book_filter_active) 206 | .write("?", self.smoker_book_open) 207 | .write("?", self.smoker_book_filter_active) 208 | .write_varint(len(self.recipe_ids_1)) 209 | ) 210 | 211 | for recipe_id in self.recipe_ids_1: 212 | buf.write_string(recipe_id) 213 | 214 | if self.recipe_ids_2: 215 | buf.write("?", True) 216 | 217 | for recipe_id in self.recipe_ids_2: 218 | buf.write_string(recipe_id) 219 | else: 220 | buf.write("?", False) 221 | 222 | return buf 223 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/difficulty.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to server difficulty.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | 8 | __all__ = ( 9 | "PlayServerDifficulty", 10 | "PlaySetDifficulty", 11 | "PlayLockDifficulty", 12 | ) 13 | 14 | 15 | class PlayServerDifficulty(ClientBoundPacket): 16 | """Used by the server to update the difficulty in the client's menu. (Server -> Client) 17 | 18 | :param int difficulty: The difficulty level, see here: https://wiki.vg/Protocol#Server_Difficulty. 19 | :param bool locked: Whether the difficulty is locked or not. 20 | :ivar int id: Unique packet ID. 21 | :ivar difficulty: 22 | :ivar locked: 23 | """ 24 | 25 | id = 0x0E 26 | 27 | def __init__(self, difficulty: int, locked: bool): 28 | super().__init__() 29 | 30 | self.difficulty = difficulty 31 | self.locked = locked 32 | 33 | def pack(self) -> Buffer: 34 | return Buffer().write("B", self.difficulty).write("?", self.locked) 35 | 36 | 37 | class PlaySetDifficulty(ServerBoundPacket): 38 | """Used by the client to set difficulty. Not used normally. (Client -> Server) 39 | 40 | :param int new_difficulty: The new difficulty. 41 | :ivar int id: Unique packet ID. 42 | :ivar new_difficulty: 43 | """ 44 | 45 | id = 0x02 46 | 47 | def __init__(self, new_difficulty: int) -> None: 48 | super().__init__() 49 | 50 | self.new_difficulty = new_difficulty 51 | 52 | @classmethod 53 | def unpack(cls, buf: Buffer) -> PlaySetDifficulty: 54 | return cls(buf.read_byte()) 55 | 56 | 57 | class PlayLockDifficulty(ServerBoundPacket): 58 | """Used to lock the difficulty. Only used on singleplayer. (Client -> Server) 59 | 60 | :param bool locked: Whether the difficulty is locked or not. 61 | :ivar int id: Unique packet ID. 62 | :ivar locked: 63 | """ 64 | 65 | id = 0x10 66 | 67 | def __init__(self, locked: bool): 68 | super().__init__() 69 | 70 | self.locked = locked 71 | 72 | @classmethod 73 | def unpack(cls, buf: Buffer) -> PlayLockDifficulty: 74 | return cls(buf.read("?")) 75 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/effect.py: -------------------------------------------------------------------------------- 1 | """For packets related to sound and particle effects.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket 7 | 8 | __all__ = ("PlayEffect", "PlayEntityEffect", "PlaySoundEffect") 9 | 10 | 11 | class PlayEffect(ClientBoundPacket): 12 | """Used to play a sound or particle effect. (Server -> Client) 13 | 14 | :param int effect_id: The ID of the effect to be played, see here: https://wiki.vg/Protocol#Effect. 15 | :param int x: The x coordinate where the sound/particle is played. 16 | :param int y: The y coordinate where the sound/particle is played. 17 | :param int z: The z coordinate where the sound/particle is played. 18 | :param int data: Extra data for certain effects. 19 | :param bool disable_relative_volume: If false, sound effects fade away with distance. 20 | :ivar int id: Unique packet ID. 21 | :ivar effect_id: 22 | :ivar x: 23 | :ivar y: 24 | :ivar z: 25 | :ivar data: 26 | :ivar disable_relative_volume: 27 | """ 28 | 29 | id = 0x23 30 | 31 | def __init__( 32 | self, effect_id: int, x: int, y: int, z: int, data: int, disable_relative_volume: bool 33 | ): 34 | super().__init__() 35 | 36 | self.effect_id = effect_id 37 | self.x = x 38 | self.y = y 39 | self.z = z 40 | self.data = data 41 | self.disable_relative_volume = disable_relative_volume 42 | 43 | def pack(self) -> Buffer: 44 | return ( 45 | Buffer() 46 | .write("i", self.effect_id) 47 | .write_position(self.x, self.y, self.z) 48 | .write("i", self.data) 49 | .write("?", self.disable_relative_volume) 50 | ) 51 | 52 | 53 | class PlayEntityEffect(ClientBoundPacket): 54 | """Gives a specific entity an effect. (Server -> Client) 55 | 56 | :param int entity_id: The ID of the entity to give the effect to. 57 | :param int effect_id: The ID of the effect to give. 58 | :param int amplifier: Amplification amount of effect. 59 | :param int duration: The duration of the effect in ticks. 60 | :param int flags: Bit field, see https://wiki.vg/Protocol#Entity_Effect 61 | :ivar int id: Unique packet ID. 62 | :ivar entity_id: 63 | :ivar effect_id: 64 | :ivar amplifier: 65 | :ivar duration: 66 | :ivar flags: 67 | """ 68 | 69 | id = 0x65 70 | 71 | def __init__(self, entity_id: int, effect_id: int, amplifier: int, duration: int, flags: int): 72 | self.entity_id = entity_id 73 | self.effect_id = effect_id 74 | self.amplifier = amplifier 75 | self.duration = duration 76 | self.flags = flags 77 | 78 | def pack(self) -> Buffer: 79 | return ( 80 | Buffer() 81 | .write_varint(self.entity_id) 82 | .write_byte(self.effect_id) 83 | .write_byte(self.amplifier) 84 | .write_varint(self.duration) 85 | .write_byte(self.flags) 86 | ) 87 | 88 | 89 | class PlaySoundEffect(ClientBoundPacket): 90 | """Used to play a hardcoded sound event. (Server -> Client) 91 | 92 | :param int sound_id: The ID of the sound to be played. 93 | :param int category: The sound category, see here: https://wiki.vg/Protocol#Sound_Effect. 94 | :param int x: The x coordinate of where the effect is to be played. 95 | :param int y: The y coordinate of where the effect is to be played. 96 | :param int z: The z coordinate of where the effect is to be played. 97 | :param float volume: Volume of the sound to be played, between 0.0 and 1.0. 98 | :param float pitch: The pitch that the sound should be played at, between 0.5 and 2.0. 99 | :ivar int id: Unique packet ID. 100 | :ivar sound_id: 101 | :ivar category: 102 | :ivar x: 103 | :ivar y: 104 | :ivar z: 105 | :ivar volume: 106 | :ivar pitch: 107 | """ 108 | 109 | id = 0x5D 110 | 111 | def __init__( 112 | self, sound_id: int, category: int, x: int, y: int, z: int, volume: float, pitch: float 113 | ): 114 | super().__init__() 115 | 116 | self.sound_id = sound_id 117 | self.category = category 118 | self.x = x 119 | self.y = y 120 | self.z = z 121 | self.volume = volume 122 | self.pitch = pitch 123 | 124 | def pack(self) -> Buffer: 125 | return ( 126 | Buffer() 127 | .write_varint(self.sound_id) 128 | .write_varint(self.category) 129 | .write("i", self.x * 8) 130 | .write("i", self.y * 8) 131 | .write("i", self.z * 8) 132 | .write("f", self.volume) 133 | .write("f", self.pitch) 134 | ) 135 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/entity.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to entities.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict, List, Optional, Tuple, Union 6 | 7 | import pymine_net.types.nbt as nbt 8 | from pymine_net.types.buffer import Buffer 9 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 10 | 11 | __all__ = ( 12 | "PlayBlockEntityData", 13 | "PlayQueryEntityNBT", 14 | "PlayInteractEntity", 15 | "PlayEntityStatus", 16 | "PlayEntityAction", 17 | "PlayEntityPosition", 18 | "PlayEntityPositionAndRotation", 19 | "PlayEntityRotation", 20 | "PlayRemoveEntityEffect", 21 | "PlayEntityHeadLook", 22 | "PlayAttachEntity", 23 | "PlayEntityVelocity", 24 | "PlayEntityTeleport", 25 | "PlayDestroyEntities", 26 | "PlayEntityMetadata", 27 | "PlayEntityEquipment", 28 | ) 29 | 30 | 31 | class PlayBlockEntityData(ClientBoundPacket): 32 | """Sets the block entity associated with the block at the given location. (Server -> Client). 33 | 34 | :param int x: The x coordinate of the position. 35 | :param int y: The y coordinate of the position. 36 | :param int z: The z coordinate of the position. 37 | :param int action: The action to be carried out (see https://wiki.vg/Protocol#Block_Entity_Data). 38 | :param nbt.TAG nbt_data: The nbt data associated with the action/block. 39 | :ivar int id: Unique packet ID. 40 | :ivar x: 41 | :ivar y: 42 | :ivar z: 43 | :ivar action: 44 | :ivar nbt_data: 45 | """ 46 | 47 | id = 0x0A 48 | 49 | def __init__(self, x: int, y: int, z: int, action: int, nbt_data: nbt.TAG): 50 | super().__init__() 51 | 52 | self.x = x 53 | self.y = y 54 | self.z = z 55 | self.action = action 56 | self.nbt_data = nbt_data 57 | 58 | def pack(self) -> Buffer: 59 | return ( 60 | Buffer() 61 | .write_position(self.x, self.y, self.z) 62 | .write("B", self.action) 63 | .write_nbt(self.nbt_data) 64 | ) 65 | 66 | 67 | class PlayQueryEntityNBT(ServerBoundPacket): 68 | """Sent by the client when Shift+F3+I is used. (Client -> Server) 69 | 70 | :param int transaction_id: Incremental ID used so the client can verify responses. 71 | :param int entity_id: The ID of the entity to query. 72 | :ivar int id: Unique packet ID. 73 | :ivar transaction_id: 74 | :ivar entity_id: 75 | """ 76 | 77 | id = 0x0C 78 | 79 | def __init__(self, transaction_id: int, entity_id: int): 80 | super().__init__() 81 | 82 | self.transaction_id = transaction_id 83 | self.entity_id = entity_id 84 | 85 | @classmethod 86 | def unpack(cls, buf: Buffer) -> PlayQueryEntityNBT: 87 | return cls(buf.read_varint(), buf.read_varint()) 88 | 89 | 90 | class PlayInteractEntity(ServerBoundPacket): 91 | """Sent when a client clicks another entity, see here: https://wiki.vg/Protocol#Interact_Entity. (Client -> Server) 92 | 93 | :param int entity_id: The ID of the entity interacted with. 94 | :param int type_: Either interact (0), attack (1), or interact at (2). 95 | :param Optional[int] target_x: The x coordinate of where the target is, can be None. 96 | :param Optional[int] target_y: The y coordinate of where the target is, can be None. 97 | :param Optional[int] target_z: The z coordinate of where the target is, can be None. 98 | :param Optional[int] hand: The hand used. 99 | :param bool sneaking: Whether the client was sneaking or not. 100 | :ivar int id: Unique packet ID. 101 | :ivar entity_id: 102 | :ivar type_: 103 | :ivar target_x: 104 | :ivar target_y: 105 | :ivar target_z: 106 | :ivar hand: 107 | :ivar sneaking: 108 | """ 109 | 110 | id = 0x0D 111 | 112 | def __init__( 113 | self, 114 | entity_id: int, 115 | type_: int, 116 | target_x: Optional[int], 117 | target_y: Optional[int], 118 | target_z: Optional[int], 119 | hand: Optional[int], 120 | sneaking: bool, 121 | ): 122 | super().__init__() 123 | 124 | self.entity_id = entity_id 125 | self.type_ = type_ 126 | self.target_x = target_x 127 | self.target_y = target_y 128 | self.target_z = target_z 129 | self.hand = hand 130 | self.sneaking = sneaking 131 | 132 | @classmethod 133 | def unpack(cls, buf: Buffer) -> PlayInteractEntity: 134 | return cls( 135 | buf.read_varint(), 136 | buf.read_varint(), 137 | buf.read_optional(buf.read_varint), 138 | buf.read_optional(buf.read_varint), 139 | buf.read_optional(buf.read_varint), 140 | buf.read_optional(buf.read_varint), 141 | buf.read("?"), 142 | ) 143 | 144 | 145 | class PlayEntityStatus(ClientBoundPacket): 146 | """Usually used to trigger an animation for an entity. (Server -> Client) 147 | 148 | :param int entity_id: The ID of the entity the status is for. 149 | :param int entity_status: Depends on the type of entity, see here: https://wiki.vg/Protocol#Entity_Status. 150 | :ivar int id: Unique packet ID. 151 | :ivar entity_id: 152 | :ivar entity_status: 153 | """ 154 | 155 | id = 0x1B 156 | 157 | def __init__(self, entity_id: int, entity_status: int): 158 | super().__init__() 159 | 160 | self.entity_id = entity_id 161 | self.entity_status = entity_status 162 | 163 | def pack(self) -> Buffer: 164 | return Buffer().write("i", self.entity_id).write("b", self.entity_status) 165 | 166 | 167 | class PlayEntityAction(ServerBoundPacket): 168 | """Sent by the client to indicate it has performed a certain action. (Client -> Server) 169 | 170 | :param int entity_id: The ID of the entity. 171 | :param int action_id: The action occurring, see here: https://wiki.vg/Protocol#Entity_Action. 172 | :param int jump_boost: Used with jumping while riding a horse. 173 | :ivar int id: Unique packet ID. 174 | :ivar entity_id: 175 | :ivar action_id: 176 | :ivar jump_boost: 177 | """ 178 | 179 | id = 0x1B 180 | 181 | def __init__(self, entity_id: int, action_id: int, jump_boost: int): 182 | super().__init__() 183 | 184 | self.entity_id = entity_id 185 | self.action_id = action_id 186 | self.jump_boost = jump_boost 187 | 188 | @classmethod 189 | def unpack(cls, buf: Buffer) -> PlayEntityAction: 190 | return cls(buf.read_varint(), buf.read_varint(), buf.read_varint()) 191 | 192 | 193 | class PlayEntityPosition(ClientBoundPacket): 194 | """Sent by the server when an entity moves less than 8 blocks. (Server -> Client) 195 | 196 | :param int entity_id: The Id of the entity moving. 197 | :param int dx: Delta (change in) x, -8 <-> 8. 198 | :param int dy: Delta (change in) y, -8 <-> 8. 199 | :param int dz: Delta (change in) z, -8 <-> 8. 200 | :param bool on_ground: Whether entity is on ground or not. 201 | :ivar int id: Unique packet ID. 202 | :ivar entity_id: 203 | :ivar dx: 204 | :ivar dy: 205 | :ivar dz: 206 | :ivar on_ground: 207 | """ 208 | 209 | id = 0x29 210 | 211 | def __init__(self, entity_id: int, dx: int, dy: int, dz: int, on_ground: bool): 212 | super().__init__() 213 | 214 | self.entity_id = entity_id 215 | self.dx, self.dy, self.dz = dx, dy, dz 216 | self.on_ground = on_ground 217 | 218 | def pack(self) -> Buffer: 219 | return ( 220 | Buffer() 221 | .write_varint(self.entity_id) 222 | .write("h", self.dx) 223 | .write("h", self.dy) 224 | .write("h", self.dz) 225 | .write("?", self.on_ground) 226 | ) 227 | 228 | 229 | class PlayEntityPositionAndRotation(ClientBoundPacket): 230 | """Sent by the server when an entity rotates and moves. (Server -> Client) 231 | 232 | :param int entity_id: The ID of the entity moving/rotationing. 233 | :param int dx: Delta (change in) x, -8 <-> 8. 234 | :param int dy: Delta (change in) y, -8 <-> 8. 235 | :param int dz: Delta (change in) z, -8 <-> 8. 236 | :param int yaw: The new yaw angle, the value being x/256 of a full rotation. 237 | :param int pitch: The new pitch angle, the value being x/256 of a full rotation. 238 | :param bool on_ground: Whether entity is on ground or not. 239 | :ivar int id: Unique packet ID. 240 | :ivar entity_id: 241 | :ivar dx: 242 | :ivar dy: 243 | :ivar dz: 244 | :ivar yaw: 245 | :ivar pitch: 246 | :ivar on_ground: 247 | """ 248 | 249 | id = 0x2A 250 | 251 | def __init__( 252 | self, entity_id: int, dx: int, dy: int, dz: int, yaw: int, pitch: int, on_ground: bool 253 | ): 254 | super().__init__() 255 | 256 | self.entity_id = entity_id 257 | self.dx, self.dy, self.dz = dx, dy, dz 258 | self.yaw = yaw 259 | self.pitch = pitch 260 | self.on_ground = on_ground 261 | 262 | def pack(self) -> Buffer: 263 | return ( 264 | Buffer() 265 | .write_varint(self.entity_id) 266 | .write("h", self.dx) 267 | .write("h", self.dy) 268 | .write("h", self.dz) 269 | .write_byte(self.yaw) 270 | .write_byte(self.pitch) 271 | .write("?", self.on_ground) 272 | ) 273 | 274 | 275 | class PlayEntityRotation(ClientBoundPacket): 276 | """Sent by the server when an entity rotates. (Server -> Client) 277 | 278 | :param int entity_id: The ID of the entity. 279 | :param int yaw: The new yaw angle, the value being x/256 of a full rotation. 280 | :param int pitch: The new pitch angle, the value being x/256 of a full rotation. 281 | :param bool on_ground: Whether entity is on ground or not. 282 | :ivar int id: Unique packet ID. 283 | :ivar entity_id: 284 | :ivar yaw: 285 | :ivar pitch: 286 | :ivar on_ground: 287 | """ 288 | 289 | id = 0x2B 290 | 291 | def __init__(self, entity_id: int, yaw: int, pitch: int, on_ground: bool): 292 | super().__init__() 293 | 294 | self.entity_id = entity_id 295 | self.yaw = yaw 296 | self.pitch = pitch 297 | self.on_ground = on_ground 298 | 299 | def pack(self) -> Buffer: 300 | return ( 301 | Buffer() 302 | .write_varint(self.entity_id) 303 | .write("b", self.yaw) 304 | .write("b", self.pitch) 305 | .write("?", self.on_ground) 306 | ) 307 | 308 | 309 | class PlayRemoveEntityEffect(ClientBoundPacket): 310 | """Sent by the server to remove an entity's effect. (Server -> Client) 311 | 312 | :param int entity_id: The new yaw angle, the value being x/256 of a full rotation. 313 | :ivar int id: Unique packet ID. 314 | :ivar entity_id: 315 | """ 316 | 317 | id = 0x3B 318 | 319 | def __init__(self, entity_id: int, effect_id: int): 320 | super().__init__() 321 | 322 | self.entity_id = entity_id 323 | self.effect_id = effect_id 324 | 325 | def pack(self) -> Buffer: 326 | return Buffer().write_varint(self.entity_id).write("b", self.effect_id) 327 | 328 | 329 | class PlayEntityHeadLook(ClientBoundPacket): 330 | """Changes the horizontal direction an entity's head is facing. (Server -> Client) 331 | 332 | :param int entity_id: The ID of the entity. 333 | :param int head_yaw: The new head yaw angle, the value being x/256 of a full rotation. 334 | :ivar int id: Unique packet ID. 335 | :ivar entity_id: 336 | :ivar head_yaw: 337 | """ 338 | 339 | id = 0x3E 340 | 341 | def __init__(self, entity_id: int, head_yaw: int): 342 | super().__init__() 343 | 344 | self.entity_id = entity_id 345 | self.head_yaw = head_yaw 346 | 347 | def pack(self) -> Buffer: 348 | return Buffer().write_varint(self.entity_id).write("B", self.head_yaw) 349 | 350 | 351 | class PlayAttachEntity(ClientBoundPacket): 352 | """Sent when one entity has been leashed to another entity. (Server -> Client) 353 | 354 | :param int attached_entity_id: The ID of the entity attached to the leash. 355 | :param int holding_entity_id: The ID of the entity holding the leash. 356 | :ivar int id: Unique packet ID. 357 | :ivar attached_entity_id: 358 | :ivar holding_entity_id: 359 | """ 360 | 361 | id = 0x4E 362 | 363 | def __init__(self, attached_entity_id: int, holding_entity_id: int): 364 | super().__init__() 365 | 366 | self.attached_entity_id = attached_entity_id 367 | self.holding_entity_id = holding_entity_id 368 | 369 | def pack(self) -> Buffer: 370 | return Buffer().write("i", self.attached_entity_id).write("i", self.holding_entity_id) 371 | 372 | 373 | class PlayEntityVelocity(ClientBoundPacket): 374 | """Sends the velocity of an entity in units of 1/8000 of a block per server tick. (Server -> Client) 375 | 376 | :param int entity_id: The ID of the entity. 377 | :param int velocity_x: The velocity in units of 1/8000 of a block per server tick in the x axis. 378 | :param int velocity_y: The velocity in units of 1/8000 of a block per server tick in the y axis. 379 | :param int velocity_z: The velocity in units of 1/8000 of a block per server tick in the z axis. 380 | :ivar int id: Unique packet ID. 381 | :ivar entity_id: 382 | :ivar velocity_x: 383 | :ivar velocity_y: 384 | :ivar velocity_z: 385 | """ 386 | 387 | id = 0x4F 388 | 389 | def __init__(self, entity_id: int, velocity_x: int, velocity_y: int, velocity_z: int): 390 | super().__init__() 391 | 392 | self.entity_id = entity_id 393 | self.velocity_x = velocity_x 394 | self.velocity_y = velocity_y 395 | self.velocity_z = velocity_z 396 | 397 | def pack(self) -> Buffer: 398 | return ( 399 | Buffer() 400 | .write_varint(self.entity_id) 401 | .write("h", self.velocity_x) 402 | .write("h", self.velocity_y) 403 | .write("h", self.velocity_z) 404 | ) 405 | 406 | 407 | class PlayEntityTeleport(ClientBoundPacket): 408 | """Sent when an entity moves more than 8 blocks. (Server -> Client) 409 | 410 | :param int entity_id: The ID of the entity. 411 | :param float x: The new x coordinate of the entity. 412 | :param float y: The new y coordinate of the entity. 413 | :param float z: The new z coordinate of the entity. 414 | :param int yaw: The new yaw angle, the value being x/256 of a full rotation. 415 | :param int pitch: The new pitch angle, the value being x/256 of a full rotation. 416 | :param bool on_ground: Whether or not the entity is on the ground. 417 | :ivar int id: Unique packet ID. 418 | :ivar entity_id: 419 | :ivar x: 420 | :ivar y: 421 | :ivar z: 422 | :ivar on_ground: 423 | """ 424 | 425 | id = 0x62 426 | 427 | def __init__( 428 | self, entity_id: int, x: float, y: float, z: float, yaw: int, pitch: int, on_ground: bool 429 | ): 430 | super().__init__() 431 | 432 | self.entity_id = entity_id 433 | self.x = x 434 | self.y = y 435 | self.z = z 436 | self.yaw = yaw 437 | self.pitch = pitch 438 | self.on_ground = on_ground 439 | 440 | def pack(self) -> Buffer: 441 | return ( 442 | Buffer() 443 | .write_varint(self.entity_id) 444 | .write("d", self.x) 445 | .write("d", self.y) 446 | .write("d", self.z) 447 | .write("i", self.yaw) 448 | .write("i", self.pitch) 449 | .write("?", self.on_ground) 450 | ) 451 | 452 | 453 | class PlayDestroyEntities(ClientBoundPacket): 454 | """Sent by the server when one or more entities are to be destroyed on the client. (Server -> Client) 455 | 456 | :param List[int] entity_ids: List of entity IDs for the client to destroy. 457 | :ivar int id: Unique packet ID. 458 | :ivar entity_ids: 459 | """ 460 | 461 | id = 0x3A 462 | 463 | def __init__(self, entity_ids: List[int]): 464 | super().__init__() 465 | 466 | self.entity_ids = entity_ids 467 | 468 | def pack(self) -> Buffer: 469 | buf = Buffer().write_varint(len(self.entity_ids)) 470 | 471 | for entity_id in self.entity_ids: 472 | buf.write_varint(entity_id) 473 | 474 | return buf 475 | 476 | 477 | class PlayEntityMetadata(ClientBoundPacket): 478 | """Updates one or more metadata properties for an existing entity. (Server -> Client) 479 | 480 | :param int entity_id: The ID of the entity the data is for. 481 | :param Dict[Tuple[int, int], object] metadata: The entity metadata, see here: https://wiki.vg/Protocol#Entity_Metadata. 482 | :ivar int id: Unique packet ID. 483 | :ivar entity_id: 484 | :ivar metadata: 485 | """ 486 | 487 | id = 0x4D 488 | 489 | def __init__(self, entity_id: int, metadata: Dict[Tuple[int, int], object]): 490 | super().__init__() 491 | 492 | self.entity_id = entity_id 493 | self.metadata = metadata 494 | 495 | def pack(self) -> Buffer: 496 | return Buffer().write_varint(self.entity_id).write_entity_metadata(self.metadata) 497 | 498 | 499 | class PlayEntityEquipment(ClientBoundPacket): 500 | """Sends data about the entity's equipped equipment. 501 | 502 | :param int entity_id: The ID of the entity the equipment data is for. 503 | :param List[Tuple[int, Dict[str, Union[int, nbt.TAG]]]] equipment: An array of equipment, see here: https://wiki.vg/Protocol#Entity_Equipment 504 | :ivar int id: Unique packet ID. 505 | :ivar entity_id: 506 | :ivar equipment: 507 | """ 508 | 509 | id = 0x50 510 | 511 | def __init__(self, entity_id: int, equipment: List[Tuple[int, Dict[str, Union[int, nbt.TAG]]]]): 512 | super().__init__() 513 | 514 | self.entity_id = entity_id 515 | self.equipment = equipment 516 | 517 | def pack(self) -> Buffer: 518 | buf = Buffer().write_varint(self.entity_id).write_varint(len(self.equipment)) 519 | 520 | for (slot_id, equipment) in self.equipment: 521 | buf.write("b", slot_id).write_slot(**equipment) 522 | 523 | return buf 524 | 525 | 526 | class PlayEntityProperties(ClientBoundPacket): 527 | """Sends information about certain attributes on an entity. (Server -> Client) 528 | 529 | :param int entity_id: The ID of the entity. 530 | :param List[dict] properties: Properties of the entity. 531 | :ivar int id: Unique packet ID. 532 | :ivar entity_id: 533 | :ivar properties: 534 | """ 535 | 536 | id = 0x64 537 | 538 | def __init__(self, entity_id: int, properties: List[dict]): 539 | super().__init__() 540 | 541 | self.entity_id = entity_id 542 | self.properties = properties 543 | 544 | def pack(self) -> Buffer: 545 | buf = Buffer().write_varint(self.entity_id) 546 | 547 | for prop in self.properties: 548 | buf.write_string(prop["key"]).write("d", prop["value"]).write_varint( 549 | len(prop["modifiers"]) 550 | ) 551 | 552 | for prop_modifier in prop["modifiers"]: 553 | buf.write_modifier(prop_modifier) 554 | 555 | return buf 556 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/explosion.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to entities.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Tuple 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.packet import ClientBoundPacket 9 | 10 | __all__ = ("PlayExplosion",) 11 | 12 | 13 | class PlayExplosion(ClientBoundPacket): 14 | """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). (Server -> Client) 15 | 16 | :param float x: The x coordinate of the explosion. 17 | :param float y: The y coordinate of the explosion. 18 | :param float z: The z coordinate of the explosion. 19 | :param float strength: Strength of the explosion, will summon a minecraft:explosion_emitter particle if >=2.0 else a minecraft:explosion particle. 20 | :param List[Tuple[int, int, int]] records: Array of bytes containing the coordinates of the blocks to destroy relative to the explosion's coordinates. 21 | :param float pmx: Velocity to add to the player's motion in the x axis due to the explosion. 22 | :param float pmy: Velocity to add to the player's motion in the y axis due to the explosion. 23 | :param float pmz: Velocity to add to the player's motion in the z axis due to the explosion. 24 | :ivar int id: 25 | :ivar x: 26 | :ivar y: 27 | :ivar z: 28 | :ivar strength: 29 | :ivar records: 30 | :ivar pmx: 31 | :ivar pmy: 32 | :ivar pmz: 33 | """ 34 | 35 | id = 0x1C 36 | 37 | def __init__( 38 | self, 39 | x: float, 40 | y: float, 41 | z: float, 42 | strength: float, 43 | records: List[Tuple[int, int, int]], 44 | pmx: float, 45 | pmy: float, 46 | pmz: float, 47 | ): 48 | super().__init__() 49 | 50 | self.x = x 51 | self.y = y 52 | self.z = z 53 | self.strength = strength 54 | self.records = records 55 | self.pmx = pmx 56 | self.pmy = pmy 57 | self.pmz = pmz 58 | 59 | def pack(self) -> Buffer: 60 | buf = ( 61 | Buffer() 62 | .write("f", self.x) 63 | .write("f", self.y) 64 | .write("f", self.z) 65 | .write("f", self.strength) 66 | .write("i", len(self.records)) 67 | ) 68 | 69 | for rx, ry, rz in self.records: 70 | buf.write_byte(rx).write_byte(ry).write_byte(rz) 71 | 72 | return buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) 73 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/keep_alive.py: -------------------------------------------------------------------------------- 1 | """Contains packets for maintaining the connection between client and server.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | 8 | __all__ = ("PlayKeepAliveClientBound", "PlayKeepAliveServerBound") 9 | 10 | 11 | class PlayKeepAliveClientBound(ClientBoundPacket): 12 | """Sent by the server in order to maintain connection with the client. (Server -> Client) 13 | 14 | :param int keep_alive_id: A randomly generated (by the server) integer/long. 15 | :ivar int id: Unique packet ID. 16 | :ivar keep_alive_id: 17 | """ 18 | 19 | id = 0x21 20 | 21 | def __init__(self, keep_alive_id: int): 22 | super().__init__() 23 | 24 | self.keep_alive_id = keep_alive_id 25 | 26 | def pack(self) -> Buffer: 27 | return Buffer().write("q", self.keep_alive_id) 28 | 29 | 30 | class PlayKeepAliveServerBound(ServerBoundPacket): 31 | """Sent by client in order to maintain connection with server. (Client -> Server) 32 | 33 | :param int keep_alive_id: A randomly generated (by the server) integer/long. 34 | :ivar int id: Unique packet ID. 35 | :ivar keep_alive_id: 36 | """ 37 | 38 | id = 0x0F 39 | 40 | def __init__(self, keep_alive_id: int): 41 | super().__init__() 42 | 43 | self.keep_alive_id = keep_alive_id 44 | 45 | @classmethod 46 | def unpack(cls, buf: Buffer) -> PlayKeepAliveServerBound: 47 | return cls(buf.read("q")) 48 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/map.py: -------------------------------------------------------------------------------- 1 | """Contains packets related to the in-game map item.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Optional, Tuple 6 | 7 | from pymine_net.types.buffer import Buffer 8 | from pymine_net.types.chat import Chat 9 | from pymine_net.types.packet import ClientBoundPacket 10 | 11 | __all__ = ("PlayMapData",) 12 | 13 | 14 | class PlayMapData(ClientBoundPacket): 15 | """Updates a rectangular area on a map item. (Server -> Client) 16 | 17 | :param int map_id: ID of the map to be modified. 18 | :param int scale: Zoom of the map (0 fully zoomed in - 4 fully zoomed out). 19 | :param bool locked: Whether the map has been locked in a cartography table or not. 20 | :param bool tracking_pos: Whether the player and other icons are shown on the map. 21 | :param List[Tuple[int, int, int, int, bool, Optional[Chat]]] icons: List of icons shown on the map. 22 | :param int columns: Number of columns being updated. 23 | :param Optional[int] rows: Number of rows being updated, only if columns > 0. 24 | :param Optional[int] x: X offset of the westernmost column, only if columns > 0. 25 | :param Optional[int] z: Z offset of the northernmost row, only if columns > 0. 26 | :param bytes data: The map data, see https://minecraft.fandom.com/wiki/Map_item_format. 27 | :ivar int id: 28 | :ivar map_id: 29 | :ivar scale: 30 | :ivar locked: 31 | :ivar tracking_pos: 32 | :ivar icons: 33 | :ivar columns: 34 | :ivar rows: 35 | :ivar x: 36 | :ivar z: 37 | :ivar data: 38 | """ 39 | 40 | id = 0x27 41 | 42 | def __init__( 43 | self, 44 | map_id: int, 45 | scale: int, 46 | locked: bool, 47 | tracking_pos: bool, 48 | icons: List[Tuple[int, int, int, int, bool, Optional[Chat]]], 49 | columns: int, 50 | rows: int = None, 51 | x: int = None, 52 | z: int = None, 53 | data: bytes = None, 54 | ): 55 | super().__init__() 56 | 57 | self.map_id = map_id 58 | self.scale = scale 59 | self.tracking_pos = tracking_pos 60 | self.locked = locked 61 | self.icons = icons 62 | self.columns = columns 63 | self.rows = rows 64 | self.x = x 65 | self.z = z 66 | self.data = data 67 | 68 | def pack(self) -> Buffer: 69 | buf = ( 70 | Buffer() 71 | .write_varint(self.map_id) 72 | .write_byte(self.scale) 73 | .write("?", self.locked) 74 | .write("?", self.tracking_pos) 75 | .write_varint(len(self.icons)) 76 | ) 77 | 78 | for (icon_type, x, z, direction, display_name) in self.icons: 79 | ( 80 | buf.write_varint(icon_type) 81 | .write_byte(x) 82 | .write_byte(z) 83 | .write_byte(direction) 84 | .write_optional(buf.write_chat, display_name) 85 | ) 86 | 87 | buf.write("B", self.columns) 88 | 89 | if len(self.columns) < 1: 90 | return buf 91 | 92 | return ( 93 | buf.write("B", self.rows) 94 | .write_byte(self.x) 95 | .write_byte(self.y) 96 | .write_varint(len(self.data)) 97 | .write_bytes(self.data) 98 | ) 99 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/particle.py: -------------------------------------------------------------------------------- 1 | """Contains packets that are related to particles.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket 7 | 8 | __all__ = ("PlayParticle",) 9 | 10 | 11 | class PlayParticle(ClientBoundPacket): 12 | """Sent by server to make the client display particles. (Server -> Client) 13 | 14 | :param int particle_id: ID of the particle. 15 | :param bool long_distance: If true, particle distance increases to 65536 from 256. 16 | :param int x: X coordinate of the particle. 17 | :param int y: Y coordinate of the particle. 18 | :param int z: Z coordinate of the particle. 19 | :param float particle_data: Particle data. 20 | :param int particle_count: How many particles to display. 21 | :param dict data: More particle data. 22 | :ivar int id: Unique packet ID. 23 | :ivar particle_id: 24 | :ivar long_distance: 25 | :ivar x: 26 | :ivar y: 27 | :ivar z: 28 | :ivar particle_data: 29 | :ivar particle_count: 30 | :ivar data: 31 | """ 32 | 33 | id = 0x24 34 | 35 | def __init__( 36 | self, 37 | particle_id: int, 38 | long_distance: bool, 39 | x: float, 40 | y: float, 41 | z: float, 42 | offset_x: float, 43 | offset_y: float, 44 | offset_z: float, 45 | particle_data: float, 46 | particle_count: int, 47 | data: dict, 48 | ): 49 | super().__init__() 50 | 51 | self.part_id = particle_id 52 | self.long_dist = long_distance 53 | self.x = x 54 | self.y = y 55 | self.z = z 56 | self.offset_x = offset_x 57 | self.offset_y = offset_y 58 | self.offset_z = offset_z 59 | self.particle_data = particle_data 60 | self.particle_count = particle_count 61 | self.data = data 62 | 63 | def pack(self) -> Buffer: 64 | return ( 65 | Buffer() 66 | .write("i", self.part_id) 67 | .write("?", self.long_dist) 68 | .write("d", self.x) 69 | .write("d", self.y) 70 | .write("d", self.z) 71 | .write("f", self.offset_x) 72 | .write("f", self.offset_y) 73 | .write("f", self.offset_z) 74 | .write("f", self.particle_data) 75 | .write("i", self.particle_count) 76 | .write_particle(self.data) 77 | ) 78 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/player_list.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pymine_net.types.buffer import Buffer 4 | from pymine_net.types.chat import Chat 5 | from pymine_net.types.packet import ClientBoundPacket 6 | 7 | __all__ = ("PlayPlayerListHeaderAndFooter",) 8 | 9 | 10 | class PlayPlayerListHeaderAndFooter(ClientBoundPacket): 11 | """Sent to display additional information above/below the client's player list. (Server -> Client) 12 | 13 | :param Chat header: Content to display above player list. 14 | :param Chat footer: Content to display below player list. 15 | :ivar int id: Unique packet ID. 16 | :ivar header: 17 | :ivar footer: 18 | """ 19 | 20 | id = 0x5F 21 | 22 | def __init__(self, header: Chat, footer: Chat): 23 | super().__init__() 24 | 25 | self.header = header 26 | self.footer = footer 27 | 28 | def pack(self) -> Buffer: 29 | return Buffer().write_chat(self.header).write_chat(self.footer) 30 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/play/plugin_msg.py: -------------------------------------------------------------------------------- 1 | """Contains packets relating to plugin channels and messages. See here: https://wiki.vg/Plugin_channels""" 2 | 3 | from __future__ import annotations 4 | 5 | from pymine_net.types.buffer import Buffer 6 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 7 | 8 | __all__ = ("PlayPluginMessageClientBound", "PlayPluginMessageServerBound") 9 | 10 | 11 | class PlayPluginMessageClientBound(ClientBoundPacket): 12 | """Used to send a "plugin message". See here https://wiki.vg/Protocol#Plugin_Message_.28serverbound.29 (Server -> Client) 13 | 14 | :param str channel: The plugin channel to be used. 15 | :param bytes data: Data to be sent to the client. 16 | :ivar int id: Unique packet ID. 17 | :ivar data: 18 | """ 19 | 20 | id = 0x18 21 | 22 | def __init__(self, channel: str, data: bytes): 23 | super().__init__() 24 | 25 | self.channel = channel 26 | self.data = data 27 | 28 | def pack(self) -> Buffer: 29 | return Buffer().write_string("self.channel").write_bytes(self.data) 30 | 31 | 32 | class PlayPluginMessageServerBound(ServerBoundPacket): 33 | """Used to send plugin data to the server (Client -> Server) 34 | 35 | :param str channel: The plugin channel being used. 36 | :param bytes data: Data to be sent to the client. 37 | :ivar int id: Unique packet ID. 38 | :ivar data: 39 | """ 40 | 41 | id = 0x0A 42 | 43 | def __init__(self, channel: str, data: bytes): 44 | super().__init__() 45 | 46 | self.channel = channel 47 | self.data = data 48 | 49 | @classmethod 50 | def unpack(cls, buf: Buffer) -> PlayPluginMessageServerBound: 51 | return cls(buf.read_string(), buf.read_bytes()) 52 | -------------------------------------------------------------------------------- /pymine_net/packets/v_1_18_1/status/status.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pymine_net.types.buffer import Buffer 4 | from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket 5 | 6 | __all__ = ("StatusStatusRequest", "StatusStatusResponse", "StatusStatusPingPong") 7 | 8 | 9 | class StatusStatusRequest(ServerBoundPacket): 10 | """Request from the client to get information on the server. (Client -> Server) 11 | 12 | :ivar int id: Unique packet ID. 13 | """ 14 | 15 | id = 0x00 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | def pack(self) -> Buffer: 21 | return Buffer() 22 | 23 | @classmethod 24 | def unpack(cls, buf: Buffer) -> StatusStatusRequest: 25 | return cls() 26 | 27 | 28 | class StatusStatusResponse(ClientBoundPacket): 29 | """Returns server status data back to the requesting client. (Server -> Client) 30 | 31 | :param dict data: JSON response data sent back to the client. 32 | :ivar int id: Unique packet ID. 33 | :ivar data: 34 | """ 35 | 36 | id = 0x00 37 | 38 | def __init__(self, data: dict): 39 | super().__init__() 40 | 41 | self.data = data 42 | 43 | def pack(self) -> Buffer: 44 | return Buffer().write_json(self.data) 45 | 46 | @classmethod 47 | def unpack(cls, buf: Buffer) -> StatusStatusResponse: 48 | return cls(buf.read_json()) 49 | 50 | 51 | class StatusStatusPingPong(ServerBoundPacket, ClientBoundPacket): 52 | """Ping pong? (Client <-> Server) 53 | 54 | :param int payload: A long number, randomly generated or what the client sent. 55 | :ivar int id: Unique packet ID. 56 | :ivar int payload: 57 | """ 58 | 59 | id = 0x01 60 | 61 | def __init__(self, payload: int): 62 | super().__init__() 63 | 64 | self.payload = payload 65 | 66 | @classmethod 67 | def unpack(cls, buf: Buffer) -> StatusStatusPingPong: 68 | return cls(buf.read("q")) 69 | 70 | def pack(self) -> Buffer: 71 | return Buffer().write("q", self.payload) 72 | -------------------------------------------------------------------------------- /pymine_net/strict_abc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains a "strict" implementation of abc.ABC, enforcing presnce of all abstract methods on class creation. 3 | 4 | - Subclasses of StrictABC must implement all abstract methods/classmethods/staticmethods. 5 | - A method's typehitns are also enforced, however this is only temporary and should be replaced 6 | with a type checker. This is currently here just to prevent some accidental misstyping since 7 | there's no type-checker yet, however this runtime enforcement isn't perfect and can't fully 8 | replace a type-chcker. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import inspect 14 | from abc import ABCMeta 15 | from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Type, Union, cast, overload 16 | 17 | if TYPE_CHECKING: 18 | from typing_extensions import Self 19 | 20 | 21 | __all__ = ("StrictABC", "optionalabstractmethod", "is_abstract") 22 | 23 | 24 | def optionalabstractmethod(funcobj: Callable) -> Callable: 25 | """Marks given function as an optional abstract method. 26 | 27 | This means it's presnce won't be required, but if it will be present, we can 28 | run the typing checks provided by the strict ABC. 29 | 30 | NOTE: This will get removed along with the entire type-enforcement system eventually. 31 | """ 32 | funcobj.__isoptionalabstractmethod__ = True 33 | return funcobj 34 | 35 | 36 | def is_abstract(funcobj: Callable) -> bool: 37 | """Checks whether a given function is an abstract function.""" 38 | return getattr(funcobj, "__isabstractmethod__", False) or getattr( 39 | funcobj, "__isoptionalabstractmethod__", False 40 | ) 41 | 42 | 43 | class StrictABCMeta(ABCMeta): 44 | """Strict extension of abc.ABCMeta, enforcing abstract methods on definitions and typing checks. 45 | 46 | Regular abc.ABCMeta only enforces abstract methods to be present on class initialization, 47 | however, this often isn't enough, because we usually want to get runtime errors immediately, 48 | whenever a class would be defined without implementing all of the abstract methods. With 49 | behavior like this, we can ensure that we encounter a runtime failure before this behavior ever 50 | gets a chance of affecting things, this is especially important for static and class methods which 51 | don't need initialization to be called. 52 | 53 | This class also introduces typing checks, which require the overridden methods to follow the 54 | type-hints specified in the initial abstract methods. This prevents bad unexpected behavior, 55 | and it enforces clean type checkable code.""" 56 | 57 | def __new__( 58 | mcls, 59 | name: str, 60 | bases: Tuple[Type[object], ...], 61 | dct: dict[str, object], 62 | definition_check: bool = True, 63 | typing_check: bool = True, 64 | ): 65 | cls = super().__new__(mcls, name, bases, dct) 66 | 67 | # Find all abstract methods and optional abstract methods which are still present and weren't overridden 68 | # excluding those defined in this class directly, since if a class defines new abstract method in it, 69 | # we obviously don't expect it to be implemented in that class. 70 | ab_methods = { 71 | method_name 72 | for method_name in cls.__abstractmethods__ 73 | if method_name not in cls.__dict__ 74 | } 75 | optional_ab_methods = { 76 | method_name 77 | for method_name in dir(cls) 78 | if getattr(getattr(cls, method_name), "__isoptionalabstractmethod__", False) 79 | and method_name not in cls.__dict__ 80 | } 81 | 82 | if definition_check and len(ab_methods) > 0: 83 | missing_methods_str = ", ".join(ab_methods) 84 | if len(optional_ab_methods) > 0: 85 | missing_methods_str += ", and optionally also: " + ", ".join(optional_ab_methods) 86 | raise TypeError( 87 | f"Can't define class '{name}' with unimplemented abstract methods: {missing_methods_str}." 88 | ) 89 | if typing_check: 90 | abc_classes = [] 91 | for base_cls in bases: 92 | if isinstance(base_cls, mcls) and base_cls not in {mcls, cls}: 93 | abc_classes.append(base_cls) 94 | 95 | mcls._check_annotations(cls, abc_classes) 96 | 97 | return cls 98 | 99 | @classmethod 100 | def _check_annotations(mcls, cls: Self, abc_classes: List[Self]) -> None: 101 | """Make sure all overridden methods in the class match the original definitions in ABCs. 102 | 103 | This works by finding every abstract method in the ABC classes (present in the classe's MRO), 104 | getting their type annotations and comparing them with the annotations in the overridden methods. 105 | 106 | If an abstract method definition is found, but overridden implementation in the class isn't present, 107 | this method will be skipped.""" 108 | # Get all of the defined abstract methods in the ABCs 109 | ab_methods = [] 110 | for abc_cls in abc_classes: 111 | for ab_method_name in abc_cls.__abstractmethods__: 112 | ab_method = getattr(abc_cls, ab_method_name) 113 | if not callable(ab_method): 114 | raise TypeError( 115 | f"Expected '{ab_method_name}' to be an abstractmethod, but it isn't callable." 116 | ) 117 | ab_methods.append((ab_method_name, ab_method)) 118 | 119 | # This code is incredibely inefficient and I hate it, it's only here 120 | # because I was forced to implement support for optional abstract methods 121 | # which should never have been a thing, oh well... 122 | for obj_name in dir(abc_cls): 123 | obj_value = getattr(abc_cls, obj_name) 124 | if getattr(obj_value, "__isoptionalabstractmethod__", False): 125 | if not callable(obj_value): 126 | raise TypeError( 127 | f"Expected '{obj_name}' to be an optional abstractmethod, but it isn't callable." 128 | ) 129 | ab_methods.append((obj_name, obj_value)) 130 | 131 | # Get matching overridden methods in the class, to the collected abstract methods 132 | _MISSING_SENTINEL = object() 133 | for ab_method_name, ab_method in ab_methods: 134 | defined_method = getattr(cls, ab_method_name, _MISSING_SENTINEL) 135 | 136 | if defined_method is _MISSING_SENTINEL: 137 | continue # Skip unoverridden abstract methods 138 | 139 | if not callable(defined_method): 140 | raise TypeError( 141 | f"Expected '{ab_method_name}' to be an abstractmethod, but it isn't callable." 142 | ) 143 | 144 | # Compare the annotations 145 | mcls._compare_annotations(ab_method, defined_method, ab_method_name) 146 | 147 | @classmethod 148 | def _compare_annotations( 149 | mcls, 150 | expected_f: Callable, 151 | compare_f: Callable, 152 | method_name: str, 153 | ) -> None: 154 | """Compare annotations between two given functions. 155 | 156 | - If the first (expected) function doesn't have any annotations, this check is skipped. 157 | - If the second (compare) function doesn't have any annotations, check fails (TypeError). 158 | - If the second (compare) function's signature isn't compatible the expected signature, check fails (TypeError). 159 | - Otherwise, if the signatures are compatible, the check is passed (returns None). 160 | 161 | NOTE: This check works, but it's not at all reliable, and it's behavior with forward references is very much 162 | incomplete and inaccurate, and it will never be accurate, because doing type-enforcing on runtime like this 163 | simply isn't something that the python's typing system was designed for, types should be ensured with a proper 164 | type-checker, not with runtime checks. This also completely lacks support for subtypes which don't pass issubclass 165 | check, such as protocols. And support for string forward annotations is hacky at best. 166 | 167 | This function is only here temporarily, until a type-checker is added which will replace this. 168 | """ 169 | try: 170 | expected_ann = inspect.get_annotations(expected_f, eval_str=True) 171 | except NameError: 172 | expected_ann = inspect.get_annotations(expected_f) 173 | try: 174 | compare_ann = inspect.get_annotations(compare_f, eval_str=True) 175 | except NameError: 176 | compare_ann = inspect.get_annotations(compare_f) 177 | 178 | err_msg = f"Mismatched annotations in '{method_name}'." 179 | 180 | if len(expected_ann) == 0: # Nothing to compare 181 | return 182 | 183 | if len(compare_ann) == 0: 184 | raise TypeError( 185 | err_msg 186 | + f" Compare method has no annotations, but some were expected ({expected_ann})." 187 | ) 188 | 189 | for key, exp_val in expected_ann.items(): 190 | if key not in compare_ann: 191 | raise TypeError( 192 | err_msg + f" Annotation for '{key}' not present, should be {exp_val}." 193 | ) 194 | 195 | cmp_val = compare_ann[key] 196 | 197 | # In case we weren't able to evaluate the string forward reference annotations 198 | # we can at least check if the string they hold is the same. This isn't ideal, 199 | # and can cause issues, and incorrect failures, but it's the best we can do. 200 | if isinstance(cmp_val, str) or isinstance(exp_val, str): 201 | status, msg = mcls._compare_forward_reference_annotations(exp_val, cmp_val) 202 | if status is True: 203 | return 204 | msg = cast(str, msg) 205 | raise TypeError(err_msg + f" Annotation for '{key}' isn't compatible, " + msg) 206 | 207 | try: 208 | if not issubclass(cmp_val, exp_val): 209 | raise TypeError( 210 | err_msg 211 | + f" Annotation for '{key}' isn't compatible, should be {exp_val}, got {cmp_val}." 212 | ) 213 | except TypeError: 214 | # This can happen when cmp_val isn't a class, for example with it set to `None` 215 | # Do a literal 'is' comparison here when that happens 216 | if cmp_val is not exp_val: 217 | raise TypeError( 218 | err_msg 219 | + f" Annotation for '{key}' isn't compatible, should be {exp_val}, got {cmp_val}." 220 | ) 221 | 222 | @overload 223 | @staticmethod 224 | def _compare_forward_reference_annotations( 225 | exp_val: str, cmp_val: object 226 | ) -> Tuple[bool, Optional[str]]: 227 | ... 228 | 229 | @overload 230 | @staticmethod 231 | def _compare_forward_reference_annotations( 232 | exp_val: object, cmp_val: str 233 | ) -> Tuple[bool, Optional[str]]: 234 | ... 235 | 236 | @overload 237 | @staticmethod 238 | def _compare_forward_reference_annotations( 239 | exp_val: str, cmp_val: str 240 | ) -> Tuple[bool, Optional[str]]: 241 | ... 242 | 243 | @staticmethod 244 | def _compare_forward_reference_annotations( 245 | exp_val: Union[str, object], 246 | cmp_val: Union[str, object], 247 | ) -> Tuple[bool, Optional[str]]: 248 | """This compares 2 annotations, out of which at least one is a string. 249 | 250 | This comparison isn't perfect and can result in succeeding for types which aren't 251 | actually matching type-wise, but they have the same string names. It can also fail 252 | to succeed for types which should in fact be matching. This can happen if the true 253 | types of those annotations weren't the same class, but rather a superclass and class, 254 | comparisons like these are impossible to resolve when we only know the string names. 255 | 256 | This method is temporary and will be removed, along with the entire type-checking 257 | functionality of the StrictABCMeta once a type-checker is in place. 258 | 259 | Returns a success state and message tuple, first argument of which is a bool 260 | telling us if the check succeeded, and second is optional message, which is present 261 | when the check failed (otherwise it's None), containing info on why did it fail. 262 | """ 263 | compare_checks: List[Tuple[str, str]] = [] 264 | if isinstance(cmp_val, str) and isinstance(exp_val, str): 265 | # When both objects are string forward reference annotations, 266 | # all we can do is try and compare these strings exactly 267 | compare_checks.append((cmp_val, exp_val)) 268 | else: 269 | real: object = cmp_val if isinstance(exp_val, str) else exp_val 270 | fwd: str = exp_val if isinstance(exp_val, str) else cast(str, cmp_val) 271 | 272 | # If we have a forward reference and an object to compare between each other, 273 | # try to use multiple ways of converting the real object into a string and 274 | # and store all of these for any comparison later 275 | if hasattr(real, "__name__"): 276 | compare_checks.append((fwd, getattr(real, "__name__"))) 277 | if hasattr(exp_val, "__qualname__"): 278 | compare_checks.append((fwd, getattr(real, "__qualname__"))) 279 | 280 | # There's no point in continuing if we haven't found any way to convert 281 | # the real object into a string. 282 | if len(compare_checks) == 0: 283 | return ( 284 | False, 285 | "unable to convert a real object to a string for comparison against a" 286 | f" string forward reference annotation. {real!r} ?= {fwd!r}", 287 | ) 288 | 289 | # Also try to get the "unqualified names" and if comparing those succeed, 290 | # we assume a success too. This works by only taking the last part of the 291 | # string split by dots. So for example from 'py_mine.strict_abc.ABCMeta', 292 | # we'd just get 'ABCMeta'. 293 | for opt1, opt2 in compare_checks.copy(): 294 | opt1_unqual = opt1.rsplit(".", maxsplit=1)[-1] 295 | opt2_unqual = opt2.rsplit(".", maxsplit=1)[-1] 296 | compare_checks.append((opt1_unqual, opt2_unqual)) 297 | 298 | # Check if any of the options in our compare checks succeeds, if it does, 299 | # we consider the annotations to be the same and mark the type check as passing. 300 | # Even though this usually covers most cases, it doesn't mean we're 100% certain 301 | # about this. It's possible that the same type was imported under a different name, 302 | # or that we received a forward reference for a protocol type, which is a valid 303 | # supertype of the compare value, but we weren't able to resolve that. 304 | for opt1, opt2 in compare_checks: 305 | if opt1 == opt2: 306 | return (True, None) 307 | 308 | return ( 309 | False, 310 | f" string forward references don't match ({exp_val!r} != {cmp_val!r})", 311 | ) 312 | 313 | 314 | class StrictABC(metaclass=StrictABCMeta): 315 | """Strict implementation of abc.ABC, including definition time checks and typing checks. 316 | 317 | Example: 318 | >>> class AbstractTest(StrinctABC): 319 | ... @abstractmethod 320 | ... def foo(self, x: int) -> int: 321 | ... pass 322 | >>> class Test(AbstractTest): 323 | ... def foo(self, x: int) -> int 324 | ... return 2 + x 325 | >>> class NotTest(AbstractTest): 326 | ... def bar(self, x: int) -> int: 327 | ... return 5 + x 328 | TypeError: Can't define class 'NotTest' with unimplemented abstract methods: foo. 329 | >>> class NotTest2(AbstractTest): 330 | ... def foo(self, x: str) -> str: 331 | ... return "hi " + x 332 | TypeError: Mismatched annotations in 'foo'. Annotation for 'x' isn't compatible, should be int, got str. 333 | >>> class ExtendedAbstractTest(StrictABC, definition_check=False): 334 | ... @classmethod 335 | ... @abstractmethod 336 | ... def bar(cls, x: str) -> str: 337 | ... pass 338 | >>> # No issues here 339 | 340 | For more info, check StrictABCMeta's doocstring.""" 341 | 342 | __slots__ = () 343 | -------------------------------------------------------------------------------- /pymine_net/types/block_palette.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from strict_abc import StrictABC 4 | 5 | __all__ = ("AbstractBlockPalette",) 6 | 7 | 8 | class AbstractBlockPalette(StrictABC): 9 | """ 10 | Used to encode/decode the types of Minecraft blocks to/from a compact format used when sending chunk data. 11 | - See: https://wiki.vg/Chunk_Format#Palettes 12 | - Currently unused because the Buffer implementation isn't complete 13 | """ 14 | 15 | @abstractmethod 16 | def get_bits_per_block(self) -> int: 17 | pass 18 | 19 | @abstractmethod 20 | def encode(self, block: str, props: dict = None) -> int: 21 | pass 22 | 23 | @abstractmethod 24 | def decode(self, state: int) -> dict: 25 | pass 26 | -------------------------------------------------------------------------------- /pymine_net/types/buffer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import struct 5 | import uuid 6 | from functools import partial 7 | from typing import Callable, Dict, Optional, Tuple, Union 8 | 9 | from pymine_net.enums import Direction, EntityModifier, Pose 10 | from pymine_net.types import nbt 11 | from pymine_net.types.chat import Chat 12 | from pymine_net.types.registry import Registry 13 | 14 | __all__ = ("Buffer",) 15 | 16 | 17 | class Buffer(bytearray): 18 | """A bytearray subclass that provides methods for reading/writing various Minecraft data-types sent in packets.""" 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.pos = 0 23 | 24 | def write_bytes(self, data: Union[bytes, bytearray]) -> Buffer: 25 | """Writes bytes to the buffer.""" 26 | 27 | return self.extend(data) 28 | 29 | def read_bytes(self, length: int = None) -> bytearray: 30 | """Reads bytes from the buffer, if length is None then all bytes are read.""" 31 | 32 | if length is None: 33 | length = len(self) 34 | 35 | try: 36 | return self[self.pos : self.pos + length] 37 | finally: 38 | self.pos += length 39 | 40 | def clear(self) -> None: 41 | """Resets the position and clears the bytearray.""" 42 | 43 | super().clear() 44 | self.pos = 0 45 | 46 | def reset(self) -> None: 47 | """Resets the position in the buffer.""" 48 | 49 | self.pos = 0 50 | 51 | def extend(self, data: Union[Buffer, bytes, bytearray]) -> Buffer: 52 | super().extend(data) 53 | return self 54 | 55 | def read_byte(self) -> int: 56 | """Reads a singular byte as an integer from the buffer.""" 57 | 58 | byte = self[self.pos] 59 | self.pos += 1 60 | return byte 61 | 62 | def write_byte(self, value: int) -> Buffer: 63 | """Writes a singular byte passed as an integer to the buffer.""" 64 | 65 | return self.extend(struct.pack(">b", value)) 66 | 67 | def read(self, fmt: str) -> Union[object, Tuple[object]]: 68 | """Using the given format, reads from the buffer and returns the unpacked value.""" 69 | 70 | unpacked = struct.unpack(">" + fmt, self.read_bytes(struct.calcsize(fmt))) 71 | 72 | if len(unpacked) == 1: 73 | return unpacked[0] 74 | 75 | return unpacked 76 | 77 | def write(self, fmt: str, *value: object) -> Buffer: 78 | """Using the given format and value, packs the value and writes it to the buffer.""" 79 | 80 | self.write_bytes(struct.pack(">" + fmt, *value)) 81 | return self 82 | 83 | def read_optional(self, reader: Callable) -> Optional[object]: 84 | """Reads an optional value from the buffer.""" 85 | 86 | if self.read("?"): 87 | return reader() 88 | 89 | def write_optional(self, writer: Callable, value: object = None) -> Buffer: 90 | """Writes an optional value to the buffer.""" 91 | 92 | if value is None: 93 | self.write("?", False) 94 | else: 95 | self.write("?", True) 96 | writer(value) 97 | 98 | return self 99 | 100 | def read_varint(self, max_bits: int = 32) -> int: 101 | """Reads a varint from the buffer.""" 102 | 103 | value = 0 104 | 105 | for i in range(10): 106 | byte = self.read("B") 107 | value |= (byte & 0x7F) << 7 * i 108 | 109 | if not byte & 0x80: 110 | break 111 | 112 | if value & (1 << 31): 113 | value -= 1 << 32 114 | 115 | value_max = (1 << (max_bits - 1)) - 1 116 | value_min = -1 << (max_bits - 1) 117 | 118 | if not (value_min <= value <= value_max): 119 | raise ValueError( 120 | f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" 121 | ) 122 | 123 | return value 124 | 125 | def write_varint(self, value: int, max_bits: int = 32) -> Buffer: 126 | """Writes a varint to the buffer.""" 127 | 128 | value_max = (1 << (max_bits - 1)) - 1 129 | value_min = -1 << (max_bits - 1) 130 | 131 | if not (value_min <= value <= value_max): 132 | raise ValueError( 133 | f"num doesn't fit in given range: {value_min} <= {value} < {value_max}" 134 | ) 135 | 136 | if value < 0: 137 | value += 1 << 32 138 | 139 | for _ in range(10): 140 | byte = value & 0x7F 141 | value >>= 7 142 | 143 | self.write("B", byte | (0x80 if value > 0 else 0)) 144 | 145 | if value == 0: 146 | break 147 | 148 | return self 149 | 150 | def read_optional_varint(self) -> Optional[int]: 151 | """Reads an optional (None if not present) varint from the buffer.""" 152 | 153 | value = self.read_varint() 154 | 155 | if value == 0: 156 | return None 157 | 158 | return value - 1 159 | 160 | def write_optional_varint(self, value: int = None) -> Buffer: 161 | """Writes an optional (None if not present) varint to the buffer.""" 162 | 163 | return self.write_varint(0 if value is None else value + 1) 164 | 165 | def read_string(self) -> str: 166 | """Reads a UTF8 string from the buffer.""" 167 | 168 | return self.read_bytes(self.read_varint(max_bits=16)).decode("utf-8") 169 | 170 | def write_string(self, value: str) -> Buffer: 171 | """Writes a string in UTF8 to the buffer.""" 172 | 173 | encoded = value.encode("utf-8") 174 | self.write_varint(len(encoded), max_bits=16).write_bytes(encoded) 175 | 176 | return self 177 | 178 | def read_json(self) -> object: 179 | """Reads json data from the buffer.""" 180 | 181 | return json.loads(self.read_string()) 182 | 183 | def write_json(self, value: object) -> Buffer: 184 | """Writes json data to the buffer.""" 185 | 186 | return self.write_string(json.dumps(value)) 187 | 188 | def read_nbt(self) -> nbt.TAG_Compound: 189 | """Reads an nbt tag from the buffer.""" 190 | 191 | return nbt.unpack(self[self.pos :]) 192 | 193 | def write_nbt(self, value: nbt.TAG = None) -> Buffer: 194 | """Writes an nbt tag to the buffer.""" 195 | 196 | if value is None: 197 | self.write_byte(0) 198 | else: 199 | self.write_bytes(value.pack()) 200 | 201 | return self 202 | 203 | def read_uuid(self) -> uuid.UUID: 204 | """Reads a UUID from the buffer.""" 205 | 206 | return uuid.UUID(bytes=bytes(self.read_bytes(16))) 207 | 208 | def write_uuid(self, value: uuid.UUID) -> Buffer: 209 | """Writes a UUID to the buffer.""" 210 | 211 | return self.write_bytes(value.bytes) 212 | 213 | def read_position(self) -> Tuple[int, int, int]: 214 | """Reads a Minecraft position (x, y, z) from the buffer.""" 215 | 216 | def from_twos_complement(num, bits): 217 | if num & (1 << (bits - 1)) != 0: 218 | num -= 1 << bits 219 | 220 | return num 221 | 222 | data = self.read("Q") 223 | 224 | return ( 225 | from_twos_complement(data >> 38, 26), 226 | from_twos_complement(data & 0xFFF, 12), 227 | from_twos_complement(data >> 12 & 0x3FFFFFF, 26), 228 | ) 229 | 230 | def read_chat(self) -> Chat: 231 | """Reads a chat message from the buffer.""" 232 | 233 | return Chat(self.read_json()) 234 | 235 | def write_chat(self, value: Chat) -> Buffer: 236 | """Writes a chat message to the buffer.""" 237 | 238 | return self.write_json(value.data) 239 | 240 | def write_position(self, x: int, y: int, z: int) -> Buffer: 241 | """Writes a Minecraft position (x, y, z) to the buffer.""" 242 | 243 | def to_twos_complement(num, bits): 244 | return num + (1 << bits) if num < 0 else num 245 | 246 | return self.write( 247 | "Q", 248 | to_twos_complement(x, 26) 249 | << 38 + to_twos_complement(z, 26) 250 | << 12 + to_twos_complement(y, 12), 251 | ) 252 | 253 | def read_slot(self, registry: Registry) -> dict: 254 | """Reads an inventory / container slot from the buffer.""" 255 | 256 | has_item_id = self.read_optional(self.read_varint) 257 | 258 | if has_item_id is None: 259 | return {"item": None} 260 | 261 | return { 262 | "item": registry.decode(self.read_varint()), 263 | "count": self.read("b"), 264 | "tag": self.read_nbt(), 265 | } 266 | 267 | def write_slot(self, item_id: int = None, count: int = 1, tag: nbt.TAG = None) -> Buffer: 268 | """Writes an inventory / container slot to the buffer.""" 269 | 270 | if item_id is None: 271 | self.write("?", False) 272 | else: 273 | self.write("?", True).write_varint(item_id).write("b", count).write_nbt(tag) 274 | 275 | def read_rotation(self) -> Tuple[float, float, float]: 276 | """Reads a rotation from the buffer.""" 277 | 278 | return self.read("fff") 279 | 280 | def write_rotation(self, x: float, y: float, z: float) -> Buffer: 281 | """Writes a rotation to the buffer.""" 282 | 283 | return self.write("fff", x, y, z) 284 | 285 | def read_direction(self) -> Direction: 286 | """Reads a direction from the buffer.""" 287 | 288 | return Direction(self.read_varint()) 289 | 290 | def write_direction(self, value: Direction) -> Buffer: 291 | """Writes a direction to the buffer.""" 292 | 293 | return self.write_varint(value.value) 294 | 295 | def read_pose(self) -> Pose: 296 | """Reads a pose from the buffer.""" 297 | 298 | return Pose(self.read_varint()) 299 | 300 | def write_pose(self, value: Pose) -> Buffer: 301 | """Writes a pose to the buffer.""" 302 | 303 | return self.write_varint(value.value) 304 | 305 | def write_recipe_item(self, value: Union[dict, str]) -> Buffer: 306 | """Writes a recipe item / slot to the buffer.""" 307 | 308 | if isinstance(value, dict): 309 | self.write_slot(**value) 310 | elif isinstance(value, str): 311 | self.write_slot(value) 312 | else: 313 | raise TypeError(f"Invalid type {type(value)}.") 314 | 315 | return self 316 | 317 | def write_ingredient(self, value: dict) -> Buffer: 318 | """Writes a part of a recipe to the buffer.""" 319 | 320 | self.write_varint(len(value)) 321 | 322 | for slot in value.values(): 323 | self.write_recipe_item(slot) 324 | 325 | def write_recipe(self, recipe_id: str, recipe: dict) -> Buffer: 326 | """Writes a recipe to the buffer.""" 327 | 328 | recipe_type = recipe["type"] 329 | 330 | self.write_string(recipe_type).write_string(recipe_id) 331 | 332 | if recipe.get("group") is None: 333 | recipe["group"] = "null" 334 | 335 | if recipe_type == "minecraft:crafting_shapeless": 336 | print(recipe.get("ingredients")) 337 | 338 | self.write_string(recipe["group"]).write_varint(len(recipe["ingredients"])) 339 | 340 | for ingredient in recipe.get("ingredients", []): 341 | self.write_ingredient(ingredient) 342 | 343 | self.write_recipe_item(recipe["result"]) 344 | elif recipe_type == "minecraft:crafting_shaped": 345 | self.write_varint(len(recipe["pattern"][0])).write_varint( 346 | len(recipe["pattern"]) 347 | ).write_string(recipe["group"]) 348 | 349 | for ingredient in recipe.get("ingredients", []): 350 | self.write_ingredient(ingredient) 351 | 352 | self.write_recipe_item(recipe["result"]) 353 | elif recipe_type[10:] in ("smelting", "blasting", "campfire_cooking"): 354 | print(recipe) 355 | 356 | ( 357 | self.write_string(recipe["group"]) 358 | .write_ingredient(recipe["ingredient"]) 359 | .write_recipe_item(recipe["result"]) 360 | .write("f", recipe["experience"]) 361 | .write(recipe["cookingtime"]) 362 | ) 363 | elif recipe_type == "minecraft:stonecutting": 364 | self.write_string(recipe["group"]).write_ingredient( 365 | recipe["ingredient"] 366 | ).write_recipe_item(recipe["result"]) 367 | elif recipe_type == "minecraft:smithing": 368 | self.write_ingredient(recipe["base"]).write_ingredient( 369 | recipe["addition"] 370 | ).write_ingredient(recipe["result"]) 371 | 372 | return self 373 | 374 | def read_villager(self) -> dict: 375 | """Reads villager data from the buffer.""" 376 | 377 | return { 378 | "kind": self.read_varint(), 379 | "profession": self.read_varint(), 380 | "level": self.read_varint(), 381 | } 382 | 383 | def write_villager(self, kind: int, profession: int, level: int) -> Buffer: 384 | return self.write_varint(kind).write_varint(profession).write_varint(level) 385 | 386 | def write_trade( 387 | self, 388 | in_item_1: dict, 389 | out_item: dict, 390 | disabled: bool, 391 | num_trade_usages: int, 392 | max_trade_usages: int, 393 | xp: int, 394 | special_price: int, 395 | price_multi: float, 396 | demand: int, 397 | in_item_2: dict = None, 398 | ) -> Buffer: 399 | self.write_slot(**in_item_1).write_slot(**out_item) 400 | 401 | if in_item_2 is not None: 402 | self.write("?", True).write_slot(**in_item_2) 403 | else: 404 | self.write("?", False) 405 | 406 | return ( 407 | self.write("?", disabled) 408 | .write("i", num_trade_usages) 409 | .write("i", max_trade_usages) 410 | .write("i", xp) 411 | .write("i", special_price) 412 | .write("f", price_multi) 413 | .write("i", demand) 414 | ) 415 | 416 | def read_particle(self) -> dict: 417 | particle = {} 418 | particle_id = particle["id"] = self.read_varint() 419 | 420 | if particle_id == 3 or particle_id == 23: 421 | particle["block_state"] = self.read_varint() 422 | elif particle_id == 14: 423 | particle["red"] = self.read("f") 424 | particle["green"] = self.read("f") 425 | particle["blue"] = self.read("f") 426 | particle["scale"] = self.read("f") 427 | elif particle_id == 32: 428 | particle["item"] = self.read_slot() 429 | 430 | return particle 431 | 432 | def write_particle(self, **value) -> Buffer: 433 | particle_id = value["particle_id"] 434 | 435 | if particle_id == 3 or particle_id == 23: 436 | self.write_varint(value["block_state"]) 437 | elif particle_id == 14: 438 | self.write("ffff", value["red"], value["green"], value["blue"], value["scale"]) 439 | elif particle_id == 32: 440 | self.write_slot(**value["item"]) 441 | 442 | return self 443 | 444 | def write_entity_metadata(self, value: Dict[Tuple[int, int], object]) -> Buffer: 445 | def _f_10(v): 446 | """This is basically write_optional_position. 447 | It's defined here because the function is too complex to be a lambda, 448 | so instead we just refer to this definition in the switch dict.""" 449 | self.write("?", v is not None) 450 | if v is not None: 451 | self.write_position(*v) 452 | 453 | # Define a switch dict, which holds functions taking 454 | # one argument (value) for each type number. 455 | sw = { 456 | 0: partial(self.write, "b"), 457 | 1: partial(self.write_varint), 458 | 2: partial(self.write, "f"), 459 | 3: self.write_string, 460 | 4: self.write_chat, 461 | 5: partial(self.write_optional, self.write_chat), 462 | 6: lambda v: self.write_slot(**v), 463 | 7: partial(self.write, "?"), 464 | 8: lambda v: self.write_rotation(*v), 465 | 9: lambda v: self.write_rotation(*v), 466 | 10: _f_10, 467 | 11: self.write_direction, 468 | 12: partial(self.write_optional, self.write_uuid), 469 | 13: self.write_block, 470 | 14: self.write_nbt, 471 | 15: lambda v: self.write_particle(**v), 472 | 16: lambda v: self.write_villager(*v), 473 | 17: self.write_optional_varint, 474 | 18: self.write_pose, 475 | } 476 | 477 | # index, type, value 478 | for (i, t), v in value.items(): 479 | self.write("B", i).write_varint(t) 480 | sw[t](v) 481 | 482 | self.write_bytes(b"\xFE") 483 | 484 | return self 485 | 486 | def read_modifier(self) -> Tuple[uuid.UUID, float, EntityModifier]: 487 | return (self.read_uuid(), self.read("f"), EntityModifier(self.read("b"))) 488 | 489 | def write_modifier(self, uuid_: uuid.UUID, amount: float, operation: EntityModifier): 490 | return self.write_uuid(uuid_).write("f", amount).write("b", operation) 491 | 492 | def write_node(self, node: dict) -> Buffer: 493 | node_flags = node["flags"] 494 | self.write_byte(node_flags).write_varint(len(node["children"])) 495 | 496 | for child in node["children"]: 497 | self.write_node(child) 498 | 499 | if node_flags & 0x08: 500 | self.write_varint(node["redirect_node"]) 501 | 502 | if 1 >= node_flags & 0x03 <= 2: # argument or literal node 503 | self.write_string(node["name"]) 504 | 505 | if node_flags & 0x3 == 2: # argument node 506 | self.write_string(node["parser"]) 507 | 508 | if node.get("properties"): 509 | for writer, data in node["properties"]: 510 | writer(data) 511 | 512 | if node_flags & 0x10: 513 | self.write_string(node["suggestions_type"]) 514 | 515 | return self 516 | -------------------------------------------------------------------------------- /pymine_net/types/chat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | 6 | class Chat: 7 | """ 8 | Stores Minecraft chat/message data. 9 | - Used for chat messages, disconnect messages, and anything else needing chat formatting. 10 | """ 11 | 12 | def __init__(self, data: Union[str, dict]): 13 | if isinstance(data, str): 14 | self.data = {"text": data} 15 | else: 16 | self.data = data 17 | 18 | def __eq__(self, other: Chat) -> bool: 19 | if not isinstance(other, Chat): 20 | return NotImplemented 21 | return self.data == other.data 22 | -------------------------------------------------------------------------------- /pymine_net/types/nbt.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gzip 4 | import struct 5 | from typing import List 6 | 7 | from mutf8 import decode_modified_utf8, encode_modified_utf8 8 | 9 | __all__ = ( 10 | "TAG", 11 | "TAG_End", 12 | "TAG_Byte", 13 | "TAG_Short", 14 | "TAG_Int", 15 | "TAG_Long", 16 | "TAG_Float", 17 | "TAG_Double", 18 | "TAG_Byte_Array", 19 | "TAG_String", 20 | "TAG_List", 21 | "TAG_Compound", 22 | "TAG_Int_Array", 23 | "TAG_Long_Array", 24 | "TYPES", 25 | "unpack", 26 | ) 27 | 28 | TYPES: List[TAG] = [] 29 | 30 | 31 | def unpack(buf, root_is_full: bool = True) -> TAG_Compound: 32 | """ 33 | Unpacks an NBT compound tag from a Buffer. 34 | - If root_is_full == True, it's expected that the root tag is prefixed with a tag ID and has a name. 35 | """ 36 | 37 | try: 38 | data = gzip.decompress(buf) 39 | except gzip.BadGzipFile: 40 | pass 41 | else: 42 | buf.clear() 43 | buf.write_bytes(data) 44 | 45 | if root_is_full: 46 | buf.read_byte() 47 | return TAG_Compound(TAG.unpack_name(buf), TAG_Compound.unpack_data(buf)) 48 | 49 | return TAG_Compound(None, TAG_Compound.unpack_data(buf)) 50 | 51 | 52 | class BufferUtil: 53 | """Reimplementation of some methods from buffer.py, needed to solve cyclic dependency issues.""" 54 | 55 | @staticmethod 56 | def unpack(buf, f: str) -> object: 57 | unpacked = struct.unpack(">" + f, buf.read_bytes(struct.calcsize(f))) 58 | 59 | if len(unpacked) == 1: 60 | return unpacked[0] 61 | 62 | return unpacked 63 | 64 | @staticmethod 65 | def pack(f: str, *data: object) -> bytes: 66 | return struct.pack(">" + f, *data) 67 | 68 | 69 | class TAG: 70 | """Base class for an NBT tag. 71 | 72 | :param str name: The name of the TAG. 73 | :ivar int id: The type ID. 74 | :ivar name 75 | """ 76 | 77 | id = None 78 | 79 | def __init__(self, name: str = None): 80 | self.id = self.__class__.id 81 | self.name = "" if name is None else name 82 | 83 | def pack_id(self) -> bytes: 84 | return BufferUtil.pack("b", self.id) 85 | 86 | @staticmethod 87 | def unpack_id(buf) -> int: 88 | return buf.read_byte() 89 | 90 | def pack_name(self) -> bytes: 91 | mutf8_name = encode_modified_utf8(self.name) 92 | return BufferUtil.pack("H", len(mutf8_name)) + mutf8_name 93 | 94 | @staticmethod 95 | def unpack_name(buf) -> str: 96 | return decode_modified_utf8(buf.read_bytes(buf.read("H"))) 97 | 98 | def pack_data(self) -> bytes: 99 | raise NotImplementedError(self.__class__.__name__) 100 | 101 | @classmethod 102 | def unpack_data(cls, buf) -> NotImplemented: 103 | raise NotImplementedError(cls.__name__) 104 | 105 | def pack(self) -> bytes: 106 | return self.pack_id() + self.pack_name() + self.pack_data() 107 | 108 | @classmethod 109 | def unpack(cls, buf) -> TAG: 110 | cls.unpack_id(buf) 111 | return cls(cls.unpack_name(buf), cls.unpack_data(buf)) 112 | 113 | def pretty(self, indent: int = 0) -> str: 114 | return (" " * indent) + f'{self.__class__.__name__}("{self.name}"): {self.data}' 115 | 116 | def __str__(self): 117 | return self.pretty() 118 | 119 | __repr__ = __str__ 120 | 121 | 122 | class TAG_End(TAG): 123 | id = 0 124 | 125 | def __init__(self, *args): 126 | super().__init__(None) 127 | 128 | def pack_name(self) -> bytes: 129 | return b"" 130 | 131 | @staticmethod 132 | def unpack_name(buf) -> None: 133 | return None 134 | 135 | def pack_data(self) -> bytes: 136 | return b"" 137 | 138 | @staticmethod 139 | def unpack_data(buf) -> None: 140 | pass 141 | 142 | def pretty(self, indent: int = 0) -> str: 143 | return (" " * indent) + "TAG_End(): 0" 144 | 145 | 146 | class TAG_Byte(TAG): 147 | """Used to represent a TAG_Byte, stores a single signed byte. 148 | 149 | :param str name: The name of the TAG. 150 | :param int data: A signed byte. 151 | :int id: The type ID of the TAG. 152 | """ 153 | 154 | id = 1 155 | 156 | def __init__(self, name: str, data: int): 157 | super().__init__(name) 158 | 159 | self.data = data 160 | 161 | def pack_data(self) -> bytes: 162 | return BufferUtil.pack("b", self.data) 163 | 164 | @staticmethod 165 | def unpack_data(buf) -> int: 166 | return buf.read_byte() 167 | 168 | 169 | class TAG_Short(TAG): 170 | """Used to represent a TAG_Short, stores a single short (2 byte int). 171 | 172 | :param str name: The name of the TAG. 173 | :param int data: A short (2 byte int). 174 | :int id: The type ID of the TAG. 175 | """ 176 | 177 | id = 2 178 | 179 | def __init__(self, name: str, data: int): 180 | super().__init__(name) 181 | 182 | self.data = data 183 | 184 | def pack_data(self) -> bytes: 185 | return BufferUtil.pack("h", self.data) 186 | 187 | @staticmethod 188 | def unpack_data(buf) -> int: 189 | return buf.read("h") 190 | 191 | 192 | class TAG_Int(TAG): 193 | """Used to represent a TAG_Int, stores an integer (4 bytes). 194 | 195 | :param str name: The name of the TAG. 196 | :param int data: A int (4 bytes). 197 | :int id: The type ID of the TAG. 198 | """ 199 | 200 | id = 3 201 | 202 | def __init__(self, name: str, data: int): 203 | super().__init__(name) 204 | 205 | self.data = data 206 | 207 | def pack_data(self) -> bytes: 208 | return BufferUtil.pack("i", self.data) 209 | 210 | @staticmethod 211 | def unpack_data(buf) -> int: 212 | return buf.read("i") 213 | 214 | 215 | class TAG_Long(TAG): 216 | """Used to represent a TAG_Long, stores a long long (8 byte int). 217 | 218 | :param str name: The name of the TAG. 219 | :param int data: A long long (8 byte int). 220 | :int id: The type ID of the TAG. 221 | """ 222 | 223 | id = 4 224 | 225 | def __init__(self, name: str, data: int): 226 | super().__init__(name) 227 | 228 | self.data = data 229 | 230 | def pack_data(self) -> bytes: 231 | return BufferUtil.pack("q", self.data) 232 | 233 | @staticmethod 234 | def unpack_data(buf) -> int: 235 | return buf.read("q") 236 | 237 | 238 | class TAG_Float(TAG): 239 | """Used to represent a TAG_Float, stores a float (4 bytes). 240 | 241 | :param str name: The name of the TAG. 242 | :param float data: A float (4 bytes). 243 | :int id: The type ID of the TAG. 244 | """ 245 | 246 | id = 5 247 | 248 | def __init__(self, name: str, data: float): 249 | super().__init__(name) 250 | 251 | self.data = data 252 | 253 | def pack_data(self) -> bytes: 254 | return BufferUtil.pack("f", self.data) 255 | 256 | @staticmethod 257 | def unpack_data(buf) -> float: 258 | return buf.read("f") 259 | 260 | 261 | class TAG_Double(TAG): 262 | """Used to represent a TAG_Double, stores a double (8 byte float). 263 | 264 | :param str name: The name of the TAG. 265 | :param float data: A double (8 byte float). 266 | :int id: The type ID of the TAG. 267 | """ 268 | 269 | id = 6 270 | 271 | def __init__(self, name: str, data: float): 272 | super().__init__(name) 273 | 274 | self.data = data 275 | 276 | def pack_data(self) -> bytes: 277 | return BufferUtil.pack("d", self.data) 278 | 279 | @staticmethod 280 | def unpack_data(buf) -> float: 281 | return buf.read("d") 282 | 283 | 284 | class TAG_Byte_Array(TAG, bytearray): 285 | """Used to represent a TAG_Byte_Array, stores an array of bytes. 286 | 287 | :param str name: The name of the TAG. 288 | :param bytearray data: Some bytes. 289 | :int id: The type ID of the TAG. 290 | """ 291 | 292 | id = 7 293 | 294 | def __init__(self, name: str, data: bytearray): 295 | TAG.__init__(self, name) 296 | 297 | if isinstance(data, str): 298 | print(f"WARNING: data passed was not bytes ({repr(data)})") 299 | data = data.encode("utf8") 300 | 301 | bytearray.__init__(self, data) 302 | 303 | def pack_data(self) -> bytes: 304 | return BufferUtil.pack("i", len(self)) + bytes(self) 305 | 306 | @staticmethod 307 | def unpack_data(buf) -> bytearray: 308 | return bytearray(buf.read_bytes(buf.read("i"))) 309 | 310 | def pretty(self, indent: int = 0) -> str: 311 | return f'{" " * 4 * indent}TAG_Byte_Array("{self.name}"): [{", ".join([str(v) for v in self])}]' 312 | 313 | 314 | class TAG_String(TAG): 315 | """Used to represent a TAG_String, stores a string. 316 | 317 | :param str name: The name of the TAG. 318 | :param str data: A string. 319 | :int id: The type ID of the TAG. 320 | """ 321 | 322 | id = 8 323 | 324 | def __init__(self, name: str, data: str): 325 | super().__init__(name) 326 | 327 | self.data = data 328 | 329 | def pack_data(self) -> bytes: 330 | mutf8_text = encode_modified_utf8(self.data) 331 | return BufferUtil.pack("H", len(mutf8_text)) + mutf8_text 332 | 333 | @staticmethod 334 | def unpack_data(buf) -> str: 335 | return decode_modified_utf8(buf.read_bytes(buf.read("H"))) 336 | 337 | def pretty(self, indent: int = 0) -> str: 338 | return f'{" " * 4 * indent}{self.__class__.__name__}("{self.name}"): {self.data}' 339 | 340 | 341 | class TAG_List(TAG, list): 342 | """Represents a TAG_List, a list of nameless and typeless tagss. 343 | 344 | :param str name: The name of the TAG. 345 | :param list data: A uniform list of TAGs. 346 | :int id: The type ID of the TAG. 347 | """ 348 | 349 | id = 9 350 | 351 | def __init__(self, name: str, data: List[TAG]): 352 | TAG.__init__(self, name) 353 | list.__init__(self, data) 354 | 355 | def pack_data(self) -> bytes: 356 | if len(self) > 0: 357 | return ( 358 | BufferUtil.pack("b", self[0].id) 359 | + BufferUtil.pack("i", len(self)) 360 | + b"".join([t.pack_data() for t in self]) 361 | ) 362 | 363 | return BufferUtil.pack("b", 0) + BufferUtil.pack("i", 0) 364 | 365 | @staticmethod 366 | def unpack_data(buf) -> List[TAG]: 367 | tag = TYPES[buf.read("b")] 368 | length = buf.read("i") 369 | 370 | return [tag(None, tag.unpack_data(buf)) for _ in range(length)] 371 | 372 | def pretty(self, indent: int = 0) -> str: 373 | tab = " " * 4 * indent 374 | nl = ",\n" 375 | 376 | return f'TAG_List("{self.name}"): [\n{nl.join([t.pretty(indent+1) for t in self])}\n{tab}]' 377 | 378 | 379 | class TAG_Compound(TAG, dict): 380 | """Represents a TAG_Compound, a list of named tags. 381 | 382 | :param str name: The name of the TAG. 383 | :param list data: A list of tags. 384 | :int id: The type ID of the TAG. 385 | """ 386 | 387 | id = 10 388 | 389 | def __init__(self, name: str, data: List[TAG]): 390 | TAG.__init__(self, name) 391 | dict.__init__(self, [(t.name, t) for t in data]) 392 | 393 | @property 394 | def data(self): 395 | return self.values() 396 | 397 | def __setitem__(self, key: str, value: TAG): 398 | value.name = key 399 | dict.__setitem__(self, key, value) 400 | 401 | def update(self, *args, **kwargs): 402 | dict.update(self, *args, **kwargs) 403 | 404 | for k, v in self.items(): 405 | v.name = k 406 | 407 | def pack_data(self) -> bytes: 408 | return b"".join([tag.pack() for tag in self.values()]) + b"\x00" 409 | 410 | @staticmethod 411 | def unpack_data(buf) -> List[TAG]: 412 | out = [] 413 | 414 | while True: 415 | tag = TYPES[buf.read("b")] 416 | 417 | if tag is TAG_End: 418 | break 419 | 420 | out.append(tag(tag.unpack_name(buf), tag.unpack_data(buf))) 421 | 422 | return out 423 | 424 | def pretty(self, indent: int = 0) -> str: 425 | tab = " " * 4 * indent 426 | nl = ",\n" 427 | 428 | return f'{tab}TAG_Compound("{self.name}"): [\n{nl.join([t.pretty(indent + 1) for t in self.values()])}\n{tab}]' 429 | 430 | 431 | class TAG_Int_Array(TAG, list): 432 | """Represents a TAG_Int_Array, a list of ints (4 bytes each). 433 | 434 | :param str name: The name of the TAG. 435 | :param list data: A list of ints (4 bytes each). 436 | :int id: The type ID of the TAG. 437 | """ 438 | 439 | id = 11 440 | 441 | def __init__(self, name: str, data: list): 442 | TAG.__init__(self, name) 443 | list.__init__(self, data) 444 | 445 | def pack_data(self) -> bytes: 446 | return BufferUtil.pack("i", len(self)) + b"".join( 447 | [BufferUtil.pack("i", num) for num in self] 448 | ) 449 | 450 | @staticmethod 451 | def unpack_data(buf) -> List[int]: 452 | return [buf.read("i") for _ in range(buf.read("i"))] 453 | 454 | def pretty(self, indent: int = 0) -> str: 455 | return ( 456 | f'{" " * 4 * indent}TAG_Int_Array("{self.name}"): [{", ".join([str(v) for v in self])}]' 457 | ) 458 | 459 | 460 | class TAG_Long_Array(TAG, list): 461 | """Represents a TAG_Long_Array, a list of long longs (8 byte ints). 462 | 463 | :param str name: The name of the TAG. 464 | :param list value: A list of long longs (8 byte ints). 465 | :int id: The type ID of the TAG. 466 | """ 467 | 468 | id = 12 469 | 470 | def __init__(self, name: str, data: List[int]): 471 | TAG.__init__(self, name) 472 | list.__init__(self, data) 473 | 474 | def pack_data(self) -> bytes: 475 | return BufferUtil.pack("i", len(self)) + b"".join( 476 | [BufferUtil.pack("q", num) for num in self] 477 | ) 478 | 479 | @staticmethod 480 | def unpack_data(buf) -> list: 481 | return [buf.read("q") for _ in range(buf.read("i"))] 482 | 483 | def pretty(self, indent: int = 0) -> str: 484 | return f'{" " * 4 * indent}TAG_Long_Array("{self.name}"): [{", ".join([str(v) for v in self])}]' 485 | 486 | 487 | TYPES.extend( 488 | [ 489 | TAG_End, 490 | TAG_Byte, 491 | TAG_Short, 492 | TAG_Int, 493 | TAG_Long, 494 | TAG_Float, 495 | TAG_Double, 496 | TAG_Byte_Array, 497 | TAG_String, 498 | TAG_List, 499 | TAG_Compound, 500 | TAG_Int_Array, 501 | TAG_Long_Array, 502 | ] 503 | ) 504 | -------------------------------------------------------------------------------- /pymine_net/types/packet.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import abstractmethod 4 | from typing import ClassVar, Optional 5 | 6 | from pymine_net.strict_abc import StrictABC, optionalabstractmethod 7 | from pymine_net.types.buffer import Buffer 8 | 9 | __all__ = ("Packet", "ServerBoundPacket", "ClientBoundPacket") 10 | 11 | 12 | class Packet(StrictABC): 13 | """Base Packet class. 14 | 15 | :cvar id: Packet identification number. Defaults to None. 16 | """ 17 | 18 | id: ClassVar[Optional[int]] = None 19 | 20 | 21 | class ServerBoundPacket(Packet): 22 | """Base Packet class for packets received from the client. (Client -> Server) 23 | 24 | :cvar id: Packet identification number. Defaults to None. 25 | """ 26 | 27 | @optionalabstractmethod 28 | def pack(self) -> Buffer: 29 | raise NotImplementedError 30 | 31 | @classmethod 32 | @abstractmethod 33 | def unpack(cls, buf: Buffer) -> ServerBoundPacket: 34 | pass 35 | 36 | 37 | class ClientBoundPacket(Packet): 38 | """Base Packet class for packets to be sent from the server. (Server -> Client) 39 | 40 | :cvar id: Packet identification number. Defaults to None. 41 | """ 42 | 43 | @abstractmethod 44 | def pack(self) -> Buffer: 45 | pass 46 | 47 | @classmethod 48 | @optionalabstractmethod 49 | def unpack(cls, buf: Buffer) -> ClientBoundPacket: 50 | raise NotImplementedError 51 | -------------------------------------------------------------------------------- /pymine_net/types/packet_map.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict, List, Tuple, Type, Union 4 | 5 | from pymine_net.enums import GameState, PacketDirection 6 | from pymine_net.errors import DuplicatePacketIdError 7 | from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket 8 | 9 | 10 | class StatePacketMap: 11 | """Stores a game state's packets separated into serverbound and clientbound.""" 12 | 13 | def __init__( 14 | self, 15 | state: GameState, 16 | server_bound: Dict[int, Type[ServerBoundPacket]], 17 | client_bound: Dict[int, Type[ClientBoundPacket]], 18 | ): 19 | self.state = state 20 | self.server_bound = server_bound 21 | self.client_bound = client_bound 22 | 23 | @classmethod 24 | def from_list( 25 | cls, 26 | state: GameState, 27 | packets: List[Type[Packet]], 28 | *, 29 | check_duplicates: bool = False, 30 | ) -> StatePacketMap: 31 | server_bound = {} 32 | client_bound = {} 33 | 34 | for packet in packets: 35 | if issubclass(packet, ServerBoundPacket): 36 | if check_duplicates and packet.id in server_bound: 37 | raise DuplicatePacketIdError( 38 | "unknown", state, packet.id, PacketDirection.SERVERBOUND 39 | ) 40 | server_bound[packet.id] = packet 41 | if issubclass(packet, ClientBoundPacket): 42 | if check_duplicates and packet.id in client_bound: 43 | raise DuplicatePacketIdError( 44 | "unknown", state, packet.id, PacketDirection.CLIENTBOUND 45 | ) 46 | client_bound[packet.id] = packet 47 | 48 | return cls(state, server_bound, client_bound) 49 | 50 | 51 | class PacketMap: 52 | """Stores a Minecraft protocol's packets""" 53 | 54 | def __init__(self, protocol: Union[str, int], packets: Dict[GameState, StatePacketMap]): 55 | self.protocol = protocol 56 | self.packets = packets 57 | 58 | def __getitem__(self, key: Tuple[PacketDirection, GameState, int]) -> Packet: 59 | direction, state, packet_id = key 60 | 61 | if direction is PacketDirection.CLIENTBOUND: 62 | return self.packets[state].client_bound[packet_id] 63 | 64 | return self.packets[state].server_bound[packet_id] 65 | -------------------------------------------------------------------------------- /pymine_net/types/player.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | import struct 5 | from typing import Dict, List, Optional, Set 6 | from uuid import UUID 7 | 8 | import pymine_net.types.nbt as nbt 9 | from pymine_net.enums import ChatMode, GameMode, MainHand, SkinPart 10 | from pymine_net.types.vector import Rotation, Vector3 11 | 12 | 13 | class PlayerProperty: 14 | __slots__ = ("name", "value", "signature") 15 | 16 | def __init__(self, name: str, value: str, signature: Optional[str] = None): 17 | self.name = name 18 | self.value = value 19 | self.signature = signature 20 | 21 | 22 | class Player: 23 | """Stores a player's settings, NBT data from disk, and other data.""" 24 | 25 | def __init__(self, entity_id: int, data: nbt.TAG_Compound): 26 | self.entity_id = entity_id 27 | 28 | self._data: Dict[ 29 | str, nbt.TAG 30 | ] = data # typehinted as Dict[str, nbt.TAG] for ease of development 31 | 32 | # attributes like player settings not stored in Player._data 33 | self.username: str = None 34 | self.properties: List[PlayerProperty] = [] 35 | self.latency = -1 36 | self.display_name: str = None 37 | 38 | # attributes from PlayClientSettings packet 39 | self.locale: str = None 40 | self.view_distance: int = None 41 | self.chat_mode: ChatMode = ChatMode.ENABLED 42 | self.chat_colors: bool = True 43 | self.displayed_skin_parts: Set[SkinPart] = set() 44 | self.main_hand: MainHand = MainHand.RIGHT 45 | self.enable_text_filtering: bool = False 46 | self.allow_server_listings: bool = True 47 | 48 | # attributes which should never ever change while client is connected 49 | self.uuid = UUID(bytes=struct.pack(">iiii", *self["UUID"])) 50 | 51 | def __getitem__(self, key: str) -> nbt.TAG: 52 | """Gets an NBT tag from the internal NBT compound tag.""" 53 | 54 | return self._data[key] 55 | 56 | def __setitem__(self, key: str, value: nbt.TAG) -> None: 57 | """Sets an NBT tag in the internal NBT compound tag.""" 58 | 59 | self._data[key] = value 60 | 61 | def get(self, key: str, default: object = None) -> Optional[nbt.TAG]: 62 | """Gets an NBT tag from the internal NBT compound tag.""" 63 | 64 | try: 65 | return self[key] 66 | except KeyError: 67 | return default 68 | 69 | @classmethod 70 | def new(cls, entity_id: int, uuid: UUID, spawn: Vector3, dimension: str) -> Player: 71 | """Creates a new player from the provided parameters.""" 72 | 73 | return cls(entity_id, cls.new_nbt(uuid, spawn, dimension)) 74 | 75 | @property 76 | def x(self) -> float: 77 | return self["Pos"][0].data 78 | 79 | @x.setter 80 | def x(self, x: float) -> None: 81 | self["Pos"][0] = nbt.TAG_Double(None, x) 82 | 83 | @property 84 | def y(self) -> float: 85 | return self["Pos"][1].data 86 | 87 | @y.setter 88 | def y(self, y: float) -> None: 89 | self["Pos"][1] = nbt.TAG_Double(None, y) 90 | 91 | @property 92 | def z(self) -> float: 93 | return self["Pos"][2].data 94 | 95 | @z.setter 96 | def z(self, z: float) -> None: 97 | self["Pos"][2] = nbt.TAG_Double(None, z) 98 | 99 | @property 100 | def position(self) -> Vector3: 101 | return Vector3(*[t.data for t in self["Pos"]]) 102 | 103 | @position.setter 104 | def position(self, position: Vector3) -> None: 105 | self["Pos"] = nbt.TAG_List( 106 | "Pos", 107 | [ 108 | nbt.TAG_Double(None, position.x), 109 | nbt.TAG_Double(None, position.y), 110 | nbt.TAG_Double(None, position.z), 111 | ], 112 | ) 113 | 114 | @property 115 | def motion(self) -> Vector3: 116 | return Vector3(*[t.data for t in self["Motion"]]) 117 | 118 | @motion.setter 119 | def motion(self, motion: Vector3) -> None: 120 | self["Motion"] = nbt.TAG_List( 121 | "Motion", 122 | [ 123 | nbt.TAG_Double(None, motion.x), 124 | nbt.TAG_Double(None, motion.y), 125 | nbt.TAG_Double(None, motion.z), 126 | ], 127 | ) 128 | 129 | @property 130 | def rotation(self) -> Rotation: 131 | return Rotation(self["Rotation"][0].data, self["Rotation"][1].data) 132 | 133 | @rotation.setter 134 | def rotation(self, rotation: Rotation) -> None: 135 | self["Rotation"] = nbt.TAG_List( 136 | "Rotation", [nbt.TAG_Float(None, rotation.yaw), nbt.TAG_Float(None, rotation.pitch)] 137 | ) 138 | 139 | @property 140 | def gamemode(self) -> GameMode: 141 | return GameMode(self["playerGameType"].data) 142 | 143 | @gamemode.setter 144 | def gamemode(self, gamemode: GameMode) -> None: 145 | self["playerGameType"] = nbt.TAG_Int("playerGameType", gamemode) 146 | 147 | def __repr__(self) -> str: 148 | return f"{self.__class__.__name__}({self.uuid})" 149 | 150 | @staticmethod 151 | def new_nbt(uuid: UUID, spawn: Vector3, dimension: str) -> nbt.TAG: 152 | """Generates new NBT data for a player.""" 153 | 154 | return nbt.TAG_Compound( 155 | "", 156 | [ 157 | nbt.TAG_List( 158 | "Pos", 159 | [ 160 | nbt.TAG_Double(None, spawn.x), 161 | nbt.TAG_Double(None, spawn.y), 162 | nbt.TAG_Double(None, spawn.z), 163 | ], 164 | ), 165 | nbt.TAG_List( 166 | "Motion", 167 | [nbt.TAG_Double(None, 0), nbt.TAG_Double(None, 0), nbt.TAG_Double(None, 0)], 168 | ), 169 | nbt.TAG_List("Rotation", [nbt.TAG_Float(None, 0), nbt.TAG_Float(None, 0)]), 170 | nbt.TAG_Float("FallDistance", 0), 171 | nbt.TAG_Short("Fire", -20), 172 | nbt.TAG_Short("Air", 300), 173 | nbt.TAG_Byte("OnGround", 1), 174 | nbt.TAG_Byte("NoGravity", 0), 175 | nbt.TAG_Byte("Invulnerable", 0), 176 | nbt.TAG_Int("PortalCooldown", 0), 177 | nbt.TAG_Int_Array("UUID", struct.unpack(">iiii", uuid.bytes)), 178 | nbt.TAG_String("CustomName", ""), 179 | nbt.TAG_Byte("CustomNameVisible", 0), 180 | nbt.TAG_Byte("Silent", 0), 181 | nbt.TAG_List("Passengers", []), 182 | nbt.TAG_Byte("Glowing", 0), 183 | nbt.TAG_List("Tags", []), 184 | nbt.TAG_Float("Health", 20), 185 | nbt.TAG_Float("AbsorptionAmount", 0), 186 | nbt.TAG_Short("HurtTime", 0), 187 | nbt.TAG_Int("HurtByTimestamp", 0), 188 | nbt.TAG_Short("DeathTime", 0), 189 | nbt.TAG_Byte("FallFlying", 0), 190 | # nbt.TAG_Int('SleepingX', 0), 191 | # nbt.TAG_Int('SleepingY', 0), 192 | # nbt.TAG_Int('SleepingZ', 0), 193 | nbt.TAG_Compound("Brain", [nbt.TAG_Compound("memories", [])]), 194 | nbt.TAG_List( 195 | "ivaributes", 196 | [ 197 | nbt.TAG_Compound( 198 | None, 199 | [ 200 | nbt.TAG_String("Name", "generic.max_health"), 201 | nbt.TAG_Double("Base", 20), 202 | nbt.TAG_List("Modifiers", []), 203 | ], 204 | ), 205 | nbt.TAG_Compound( 206 | None, 207 | [ 208 | nbt.TAG_String("Name", "generic.follow_range"), 209 | nbt.TAG_Double("Base", 32), 210 | nbt.TAG_List("Modifiers", []), 211 | ], 212 | ), 213 | nbt.TAG_Compound( 214 | None, 215 | [ 216 | nbt.TAG_String("Name", "generic.knockback_resistance"), 217 | nbt.TAG_Double("Base", 0), 218 | nbt.TAG_List("Modifiers", []), 219 | ], 220 | ), 221 | nbt.TAG_Compound( 222 | None, 223 | [ 224 | nbt.TAG_String("Name", "generic.movement_speed"), 225 | nbt.TAG_Double("Base", 1), 226 | nbt.TAG_List("Modifiers", []), 227 | ], 228 | ), 229 | nbt.TAG_Compound( 230 | None, 231 | [ 232 | nbt.TAG_String("Name", "generic.attack_damage"), 233 | nbt.TAG_Double("Base", 2), 234 | nbt.TAG_List("Modifiers", []), 235 | ], 236 | ), 237 | nbt.TAG_Compound( 238 | None, 239 | [ 240 | nbt.TAG_String("Name", "generic.armor"), 241 | nbt.TAG_Double("Base", 0), 242 | nbt.TAG_List("Modifiers", []), 243 | ], 244 | ), 245 | nbt.TAG_Compound( 246 | None, 247 | [ 248 | nbt.TAG_String("Name", "generic.armor_toughness"), 249 | nbt.TAG_Double("Base", 0), 250 | nbt.TAG_List("Modifiers", []), 251 | ], 252 | ), 253 | nbt.TAG_Compound( 254 | None, 255 | [ 256 | nbt.TAG_String("Name", "generic.attack_knockback"), 257 | nbt.TAG_Double("Base", 0), 258 | nbt.TAG_List("Modifiers", []), 259 | ], 260 | ), 261 | nbt.TAG_Compound( 262 | None, 263 | [ 264 | nbt.TAG_String("Name", "generic.attack_speed"), 265 | nbt.TAG_Double("Base", 4), 266 | nbt.TAG_List("Modifiers", []), 267 | ], 268 | ), 269 | nbt.TAG_Compound( 270 | None, 271 | [ 272 | nbt.TAG_String("Name", "generic.luck"), 273 | nbt.TAG_Double("Base", 0), 274 | nbt.TAG_List("Modifiers", []), 275 | ], 276 | ), 277 | ], 278 | ), 279 | nbt.TAG_List("ActiveEffects", []), 280 | nbt.TAG_Int("DataVersion", 2586), 281 | nbt.TAG_Int("playerGameType", GameMode.SURVIVAL), 282 | nbt.TAG_Int("previousPlayerGameType", -1), 283 | nbt.TAG_Int("Score", 0), 284 | nbt.TAG_String("Dimension", dimension), 285 | nbt.TAG_Int("SelectedItemSlot", 0), 286 | nbt.TAG_Compound( 287 | "SelectedItem", 288 | [ 289 | nbt.TAG_Byte("Count", 1), 290 | nbt.TAG_String("id", "minecraft:air"), 291 | nbt.TAG_Compound("tag", []), 292 | ], 293 | ), 294 | nbt.TAG_String("SpawnDimension", "overworld"), 295 | nbt.TAG_Int("SpawnX", spawn.x), 296 | nbt.TAG_Int("SpawnY", spawn.y), 297 | nbt.TAG_Int("SpawnZ", spawn.z), 298 | nbt.TAG_Byte("SpawnForced", 0), 299 | nbt.TAG_Int("foodLevel", 20), 300 | nbt.TAG_Float("foodExhaustionLevel", 0), 301 | nbt.TAG_Float("foodSaturationLevel", 5), 302 | nbt.TAG_Int("foodTickTimer", 0), 303 | nbt.TAG_Int("XpLevel", 0), 304 | nbt.TAG_Float("XpP", 0), 305 | nbt.TAG_Int("XpTotal", 0), 306 | nbt.TAG_Int("XpSeed", random.randint(-2147483648, 2147483647)), 307 | nbt.TAG_List("Inventory", []), 308 | nbt.TAG_List("EnderItems", []), 309 | nbt.TAG_Compound( 310 | "abilities", 311 | [ 312 | nbt.TAG_Float("walkSpeed", 0.1), 313 | nbt.TAG_Float("flySpeed", 0.05), 314 | nbt.TAG_Byte("mayfly", 0), 315 | nbt.TAG_Byte("flying", 0), 316 | nbt.TAG_Byte("invulnerable", 0), 317 | nbt.TAG_Byte("mayBuild", 1), 318 | nbt.TAG_Byte("instabuild", 0), 319 | ], 320 | ), 321 | # nbt.TAG_Compound('enteredNetherPosition', [nbt.TAG_Double('x', 0), nbt.TAG_Double('y', 0), nbt.TAG_Double('z', 0)]), 322 | # nbt.TAG_Compound('RootVehicle', [ 323 | # nbt.TAG_Int_Array('Attach', [0, 0, 0, 0]), 324 | # nbt.TAG_Compound('Entity', []) 325 | # ]), 326 | nbt.TAG_Byte("seenCredits", 0), 327 | nbt.TAG_Compound( 328 | "recipeBook", 329 | [ 330 | nbt.TAG_List("recipes", []), 331 | nbt.TAG_List("toBeDisplayed", []), 332 | nbt.TAG_Byte("isFilteringCraftable", 0), 333 | nbt.TAG_Byte("isGuiOpen", 0), 334 | nbt.TAG_Byte("isFurnaceFilteringCraftable", 0), 335 | nbt.TAG_Byte("isFurnaceGuiOpen", 0), 336 | nbt.TAG_Byte("isBlastingFurnaceFilteringCraftable", 0), 337 | nbt.TAG_Byte("isBlastingFurnaceGuiOpen", 0), 338 | nbt.TAG_Byte("isSmokerFilteringCraftable", 0), 339 | nbt.TAG_Byte("isSmokerGuiOpen", 0), 340 | ], 341 | ), 342 | ], 343 | ) 344 | -------------------------------------------------------------------------------- /pymine_net/types/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | __all__ = ("Registry",) 4 | 5 | 6 | class Registry: 7 | """Stores various Minecraft data like block types, block states, particles, fluids, entities, and more.""" 8 | 9 | def __init__( 10 | self, 11 | data: Union[dict, list, tuple], 12 | data_reversed: Union[dict, list, tuple] = None, 13 | ): 14 | self.data_reversed = data_reversed 15 | 16 | if isinstance(data, dict): 17 | self.data = data 18 | 19 | if data_reversed is None: 20 | self.data_reversed = {v: k for k, v in data.items()} 21 | # When we get an iterable, we want to treat the positions as the 22 | # IDs for the values. 23 | elif isinstance(data, (list, tuple)): 24 | self.data = {v: i for i, v in enumerate(data)} 25 | self.data_reversed = data 26 | else: 27 | raise TypeError( 28 | "Creating a registry from something other than a dict, tuple, or list isn't supported" 29 | ) 30 | 31 | def encode(self, key: object) -> object: 32 | """Key -> value, most likely an identifier to an integer.""" 33 | 34 | return self.data[key] 35 | 36 | def decode(self, value: object) -> object: 37 | """Value -> key, most likely a numeric id to a string identifier.""" 38 | 39 | return self.data_reversed[value] 40 | -------------------------------------------------------------------------------- /pymine_net/types/vector.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | T_Numeric = TypeVar("T_Numeric", bound=float) 4 | 5 | 6 | class Vector3(Generic[T_Numeric]): 7 | """ 8 | Stores three numeric values: x, y, z. 9 | - Used for position and movement data in the Player class. 10 | """ 11 | 12 | __slots__ = ("x", "y", "z") 13 | 14 | def __init__(self, x: T_Numeric, y: T_Numeric, z: T_Numeric): 15 | self.x = x 16 | self.y = y 17 | self.z = z 18 | 19 | 20 | class Rotation(Generic[T_Numeric]): 21 | """ 22 | Stores the pitch and yaw values of a rotation. 23 | - Used for storing rotation data in the Player class. 24 | """ 25 | 26 | __slots__ = ("yaw", "pitch") 27 | 28 | def __init__(self, yaw: T_Numeric, pitch: T_Numeric): 29 | self.yaw = yaw 30 | self.pitch = pitch 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py37', 'py38'] 4 | include = '\.pyi?$' 5 | 6 | [tool.pytest.ini_options] 7 | addopts = "--full-trace -rxP" 8 | testpaths = ["tests"] 9 | asyncio_mode = "auto" 10 | 11 | [tool.isort] 12 | profile="black" 13 | line_length=100 14 | 15 | [tool.poetry] 16 | name = "pymine-net" 17 | version = "0.2.2" 18 | description = "Networking library for Minecraft in Python" 19 | authors = ["PyMine-Net Developers & Contributors"] 20 | license = "LGPLv3" 21 | readme = "README.md" 22 | repository = "https://github.com/py-mine/pymine-net" 23 | keywords = ["Minecraft", "protocol", "networking"] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.7" 27 | mutf8 = "^1.0.6" 28 | cryptography = "^36.0.1" 29 | 30 | [tool.poetry.dev-dependencies] 31 | flake8 = "^4.0.1" 32 | black = "^22.1.0" 33 | pytest = "^7.0.1" 34 | colorama = "^0.4.4" 35 | isort = "^5.10.1" 36 | pytest-asyncio = "^0.18.1" 37 | 38 | [build-system] 39 | requires = ["poetry-core>=1.0.0"] 40 | build-backend = "poetry.core.masonry.api" 41 | -------------------------------------------------------------------------------- /tests/sample_data/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/bigtest.nbt -------------------------------------------------------------------------------- /tests/sample_data/level.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/level.dat -------------------------------------------------------------------------------- /tests/sample_data/nantest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/nantest.nbt -------------------------------------------------------------------------------- /tests/sample_data/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/region/r.0.0.mca -------------------------------------------------------------------------------- /tests/sample_data/region/r.0.1.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/region/r.0.1.mca -------------------------------------------------------------------------------- /tests/sample_data/region/r.0.2.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/region/r.0.2.mca -------------------------------------------------------------------------------- /tests/sample_data/region/r.0.3.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/region/r.0.3.mca -------------------------------------------------------------------------------- /tests/sample_data/region/r.0.4.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-mine/PyMine-Net/28c6086b80f3917f24a0b42ae68925dc516b4aef/tests/sample_data/region/r.0.4.mca -------------------------------------------------------------------------------- /tests/sample_data/test.json: -------------------------------------------------------------------------------- 1 | {"success":true,"online":true,"players_online":1,"players_max":20,"players_names":["Iapetus11","Sh_wayz","emeralddragonmc"],"latency":36.56,"version":{"brand":"Java Edition","software":"1.16.5","protocol":"ping 754","method":"ping"},"motd":{"text":"The test server for PyMine!"},"favicon":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAkUExURQAAAABTANv/60HzhBfdYq/9zQCqLAAtAACVKYL2rQB7GAAAAHbx+pYAAAAMdFJOU///////////////ABLfzs4AAAAJcEhZcwAADsMAAA7DAcdvqGQAAACTSURBVFhH7dTLCoMwGEThttZr3v99O0f+SEgsFLqb5NsowTkLQR/pTyPgHXhW4rjhGmDwkim85VvEMZDHs8R+WoTIKvHYxS1QjhH7M4C7iGOAIQ/irAjjTbj2GNiF4SEj0GeAMXKAsx4D/FAY8zFx5h4AEV4aA4bgnlg9hmMAdeTu5WWuATAoxXHDOfCrETAIpPQBYqCM0dVhenkAAAAASUVORK5CYII=","map":null,"gamemode":null,"cached":false,"cache_time":null} 2 | -------------------------------------------------------------------------------- /tests/test_buffer.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from pymine_net.types.buffer import Buffer 7 | 8 | VAR_INT_ERR_MSG = "Value doesn't fit in given range" 9 | VAR_INT_MAX = (1 << 31) - 1 10 | VAR_INT_MIN = -(1 << 31) 11 | 12 | 13 | def test_io(): 14 | buf = Buffer() 15 | 16 | assert buf == b"" 17 | assert buf.pos == 0 18 | 19 | buf = Buffer(b"\x02\x69\x00\x01\x02\x03") 20 | 21 | assert buf.read_byte() == b"\x02"[0] 22 | assert buf.read_bytes(1) == b"\x69" 23 | assert buf.read_bytes(2) == b"\x00\x01" 24 | assert buf.read_bytes() == b"\x02\x03" 25 | 26 | buf.reset() 27 | assert buf.pos == 0 28 | assert buf.read_bytes() == b"\x02\x69\x00\x01\x02\x03" 29 | buf.reset() 30 | 31 | 32 | def test_basic(): 33 | buf = Buffer() 34 | 35 | buf.write_string("abcdefhijklmnopqrstuvwxyz") 36 | buf.clear() 37 | 38 | assert buf.pos == 0 39 | assert buf == b"" 40 | 41 | assert buf.write("i", 123).write("b", 1).write("?", True).write("q", 1234567890456) == buf 42 | 43 | assert buf == b"\x00\x00\x00{\x01\x01\x00\x00\x01\x1fq\xfb\x06\x18" 44 | 45 | assert buf.read("i") == 123 46 | assert buf.read("b") == 1 47 | assert buf.read("?") is True 48 | assert buf.read("q") == 1234567890456 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "varint, error_msg", 53 | ( 54 | [0, None], 55 | [1, None], 56 | [2, None], 57 | [3749146, None], 58 | [-1, None], 59 | [-2, None], 60 | [-3, None], 61 | [-3749146, None], 62 | [VAR_INT_MAX, None], 63 | [VAR_INT_MAX + 1, VAR_INT_ERR_MSG], 64 | [VAR_INT_MIN, None], 65 | [VAR_INT_MIN - 1, VAR_INT_ERR_MSG], 66 | ), 67 | ) 68 | def test_varint(varint, error_msg): 69 | buf = Buffer() 70 | 71 | if error_msg: 72 | with pytest.raises(ValueError) as err: 73 | buf.write_varint(varint) 74 | assert error_msg in str(err) 75 | else: 76 | buf.write_varint(varint) 77 | assert buf.read_varint() == varint 78 | 79 | 80 | def test_optional_varint(): 81 | buf = Buffer() 82 | 83 | buf.write_optional_varint(1) 84 | buf.write_optional_varint(2) 85 | buf.write_optional_varint(None) 86 | buf.write_optional_varint(3) 87 | buf.write_optional_varint(None) 88 | 89 | assert buf.read_optional_varint() == 1 90 | assert buf.read_optional_varint() == 2 91 | assert buf.read_optional_varint() is None 92 | assert buf.read_optional_varint() == 3 93 | assert buf.read_optional_varint() is None 94 | 95 | 96 | def test_string(): 97 | buf = Buffer() 98 | 99 | buf.write_string("") 100 | buf.write_string("") 101 | buf.write_string("2") 102 | buf.write_string("") 103 | buf.write_string("") 104 | buf.write_string("2") 105 | buf.write_string("adkfj;adkfa;ldkfj\x01af\t\n\n00;\xc3\x85\xc3\x84\xc3\x96") 106 | buf.write_string("") 107 | buf.write_string("BrUh") 108 | buf.write_string("") 109 | 110 | assert buf.read_string() == "" 111 | assert buf.read_string() == "" 112 | assert buf.read_string() == "2" 113 | assert buf.read_string() == "" 114 | assert buf.read_string() == "" 115 | assert buf.read_string() == "2" 116 | assert buf.read_string() == "adkfj;adkfa;ldkfj\x01af\t\n\n00;\xc3\x85\xc3\x84\xc3\x96" 117 | assert buf.read_string() == "" 118 | assert buf.read_string() == "BrUh" 119 | assert buf.read_string() == "" 120 | 121 | 122 | def test_json(): 123 | buf = Buffer() 124 | 125 | data = json.loads(Path("tests", "sample_data", "test.json").read_text()) 126 | buf.write_json(data) 127 | 128 | for key, value in buf.read_json().items(): 129 | assert key in data 130 | assert data[key] == value 131 | -------------------------------------------------------------------------------- /tests/test_nbt.py: -------------------------------------------------------------------------------- 1 | import math 2 | from pathlib import Path 3 | 4 | from pymine_net import Buffer, nbt 5 | 6 | 7 | def test_bigtest(): # tests that loading bigtest.nbt works without errors 8 | buf = Buffer(Path("tests", "sample_data", "bigtest.nbt").read_bytes()) 9 | 10 | tag = nbt.unpack(buf) 11 | 12 | assert tag.pack() == buf 13 | 14 | 15 | def test_values_nantest(): # tests that the values for loading the nantest are accurate 16 | tag = nbt.unpack(Buffer(Path("tests", "sample_data", "nantest.nbt").read_bytes())) 17 | 18 | assert tag["Air"].data == 300 19 | assert tag["AttackTime"].data == 0 20 | assert tag["DeathTime"].data == 0 21 | assert tag["FallDistance"].data == 0.0 22 | assert tag["Fire"].data == -20 23 | assert tag["Health"].data == 20 24 | assert tag["HurtTime"].data == 0 25 | assert len(tag["Inventory"]) == 0 26 | assert len(tag["Motion"]) == 3 27 | assert tag["Motion"][0].data == 0 28 | assert tag["Motion"][1].data == 0 29 | assert tag["Motion"][2].data == 0 30 | assert tag["OnGround"].data == 1 31 | assert len(tag["Pos"]) == 3 32 | assert tag["Pos"][0].data == 0.0 33 | assert math.isnan(tag["Pos"][1].data) 34 | assert tag["Pos"][2].data == 0.0 35 | assert len(tag["Rotation"]) == 2 36 | assert tag["Rotation"][0].data == 164.3999481201172 37 | assert tag["Rotation"][1].data == -63.150203704833984 38 | -------------------------------------------------------------------------------- /tests/test_net_asyncio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from pymine_net.enums import GameState 6 | from pymine_net.net.asyncio import ( 7 | AsyncProtocolClient, 8 | AsyncProtocolServer, 9 | AsyncProtocolServerClient, 10 | ) 11 | from pymine_net.packets import load_packet_map 12 | from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake 13 | from pymine_net.packets.v_1_18_1.status.status import ( 14 | StatusStatusPingPong, 15 | StatusStatusRequest, 16 | StatusStatusResponse, 17 | ) 18 | 19 | TESTING_PROTOCOL = 757 20 | TESTING_HOST = "localhost" 21 | TESTING_PORT = 12345 22 | TESTING_RANDOM_LONG = 1234567890 23 | TESTING_STATUS_JSON = { 24 | "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, 25 | "players": { 26 | "max": 20, 27 | "online": 0, 28 | "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], 29 | }, 30 | "description": {"text": "Hello world"}, 31 | "favicon": None, 32 | } 33 | 34 | 35 | # proactor event loop vomits an error on exit on windows due to a Python bug 36 | @pytest.fixture 37 | def event_loop(): 38 | asyncio.set_event_loop(asyncio.SelectorEventLoop()) 39 | yield asyncio.get_event_loop() 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_asyncio_net_status(): 44 | class TestAsyncTCPServer(AsyncProtocolServer): 45 | async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: 46 | packet = await client.read_packet() 47 | assert isinstance(packet, HandshakeHandshake) 48 | assert packet.protocol == TESTING_PROTOCOL 49 | assert packet.address == "localhost" 50 | assert packet.port == TESTING_PORT 51 | assert packet.next_state == GameState.STATUS 52 | 53 | client.state = packet.next_state 54 | 55 | packet = await client.read_packet() 56 | assert isinstance(packet, StatusStatusRequest) 57 | 58 | await client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) 59 | 60 | packet = await client.read_packet() 61 | assert isinstance(packet, StatusStatusPingPong) 62 | assert packet.payload == TESTING_RANDOM_LONG 63 | await client.write_packet(packet) 64 | 65 | packet_map = load_packet_map(TESTING_PROTOCOL) 66 | 67 | server = TestAsyncTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) 68 | server_task = asyncio.create_task(server.run()) 69 | 70 | client = AsyncProtocolClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) 71 | await client.connect() 72 | 73 | await client.write_packet( 74 | HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) 75 | ) 76 | client.state = GameState.STATUS 77 | 78 | await client.write_packet(StatusStatusRequest()) 79 | 80 | packet = await client.read_packet() 81 | assert isinstance(packet, StatusStatusResponse) 82 | assert packet.data == TESTING_STATUS_JSON 83 | 84 | await client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) 85 | packet = await client.read_packet() 86 | assert isinstance(packet, StatusStatusPingPong) 87 | assert packet.payload == TESTING_RANDOM_LONG 88 | 89 | await client.close() 90 | 91 | server_task.cancel() 92 | await server.close() 93 | -------------------------------------------------------------------------------- /tests/test_net_socket.py: -------------------------------------------------------------------------------- 1 | # from concurrent.futures import ThreadPoolExecutor 2 | 3 | # from pymine_net.enums import GameState 4 | # from pymine_net.net.socket import ( 5 | # SocketProtocolClient, 6 | # SocketProtocolServer, 7 | # SocketProtocolServerClient, 8 | # ) 9 | # from pymine_net.packets import load_packet_map 10 | # from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake 11 | # from pymine_net.packets.v_1_18_1.status.status import ( 12 | # StatusStatusPingPong, 13 | # StatusStatusRequest, 14 | # StatusStatusResponse, 15 | # ) 16 | 17 | # TESTING_PROTOCOL = 757 18 | # TESTING_HOST = "localhost" 19 | # TESTING_PORT = 12345 20 | # TESTING_RANDOM_LONG = 1234567890 21 | # TESTING_STATUS_JSON = { 22 | # "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, 23 | # "players": { 24 | # "max": 20, 25 | # "online": 0, 26 | # "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], 27 | # }, 28 | # "description": {"text": "Hello world"}, 29 | # "favicon": None, 30 | # } 31 | 32 | 33 | # def test_socket_net_status(): 34 | # class TestSocketTCPServer(SocketProtocolServer): 35 | # def new_client_connected(self, client: SocketProtocolServerClient) -> None: 36 | # packet = client.read_packet() 37 | # assert isinstance(packet, HandshakeHandshake) 38 | # assert packet.protocol == TESTING_PROTOCOL 39 | # assert packet.address == "localhost" 40 | # assert packet.port == TESTING_PORT 41 | # assert packet.next_state == GameState.STATUS 42 | 43 | # client.state = packet.next_state 44 | 45 | # packet = client.read_packet() 46 | # assert isinstance(packet, StatusStatusRequest) 47 | 48 | # client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) 49 | 50 | # packet = client.read_packet() 51 | # assert isinstance(packet, StatusStatusPingPong) 52 | # assert packet.payload == TESTING_RANDOM_LONG 53 | # client.write_packet(packet) 54 | 55 | # packet_map = load_packet_map(TESTING_PROTOCOL) 56 | 57 | # server = TestSocketTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) 58 | # threadpool = ThreadPoolExecutor() 59 | # threadpool.submit(server.run) 60 | 61 | # client = SocketProtocolClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) 62 | 63 | # # retry connection a couple times before failing because the server takes some time to startup 64 | # for i in range(3): 65 | # try: 66 | # client.connect() 67 | # break 68 | # except ConnectionError: 69 | # if i >= 2: 70 | # raise 71 | 72 | # client.write_packet( 73 | # HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) 74 | # ) 75 | # client.state = GameState.STATUS 76 | 77 | # client.write_packet(StatusStatusRequest()) 78 | 79 | # packet = client.read_packet() 80 | # assert isinstance(packet, StatusStatusResponse) 81 | # assert packet.data == TESTING_STATUS_JSON 82 | 83 | # client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) 84 | # packet = client.read_packet() 85 | # assert isinstance(packet, StatusStatusPingPong) 86 | # assert packet.payload == TESTING_RANDOM_LONG 87 | 88 | # client.close() 89 | 90 | # threadpool.shutdown(wait=False, cancel_futures=True) 91 | # server.close() 92 | -------------------------------------------------------------------------------- /tests/test_packets.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Dict, Optional, Tuple, Union 3 | 4 | import colorama 5 | import pytest 6 | 7 | from pymine_net import load_packet_map 8 | from pymine_net.enums import GameState 9 | from pymine_net.strict_abc import is_abstract 10 | from pymine_net.types.buffer import Buffer 11 | from pymine_net.types.chat import Chat 12 | from pymine_net.types.packet import ServerBoundPacket 13 | 14 | colorama.init(autoreset=True) 15 | 16 | 17 | CHECKABLE_ANNOS = {bool, int, float, bytes, str, uuid.UUID, Chat} 18 | CHECKABLE_ANNOS.update({a.__name__ for a in CHECKABLE_ANNOS}) 19 | 20 | 21 | @pytest.mark.parametrize( # GameState: (clientbound, serverbound) 22 | "protocol, config", 23 | ( 24 | [ 25 | 757, 26 | { 27 | GameState.HANDSHAKING: (None, 0x0), 28 | GameState.STATUS: (0x1, 0x1), 29 | GameState.LOGIN: (0x4, 0x2), 30 | GameState.PLAY: (0x67, 0x2F), 31 | }, 32 | ], 33 | ), 34 | ) 35 | def test_ensure_all_packets( 36 | capsys, protocol: Union[int, str], config: Dict[GameState, Tuple[Optional[int], Optional[int]]] 37 | ): 38 | packet_map = load_packet_map(protocol, debug=True) 39 | initial_check = True 40 | 41 | print(f"PACKET CHECK (protocol={protocol}): ", end="") 42 | 43 | for state in GameState.__members__.values(): 44 | missing_clientbound = [] 45 | missing_serverbound = [] 46 | 47 | c = config[state] 48 | 49 | # check if there are supposed to be clientbound packets 50 | if c[0] is not None: 51 | for i in range(0, c[0]): # iterate through packet ids which should be there 52 | if i not in packet_map.packets[state].client_bound: 53 | missing_clientbound.append(f"0x{i:02X}") 54 | 55 | # check if there are supposed to be serverbound packets 56 | if c[1] is not None: 57 | for i in range(0, c[1]): # iterate through packet ids which should be there 58 | if i not in packet_map.packets[state].server_bound: 59 | missing_serverbound.append(f"0x{i:02X}") 60 | 61 | if (missing_serverbound or missing_clientbound) and initial_check: 62 | initial_check = False 63 | print() 64 | 65 | if missing_clientbound: 66 | print( 67 | f"{colorama.Style.BRIGHT}{colorama.Fore.RED}MISSING (state={state.name}, CLIENT-BOUND):{colorama.Style.RESET_ALL}", 68 | ", ".join(missing_clientbound), 69 | ) 70 | 71 | if missing_serverbound: 72 | print( 73 | f"{colorama.Style.BRIGHT}{colorama.Fore.RED}MISSING (state={state.name}, SERVER-BOUND):{colorama.Style.RESET_ALL}", 74 | ", ".join(missing_serverbound), 75 | ) 76 | 77 | if initial_check: 78 | print(f"{colorama.Style.BRIGHT}{colorama.Fore.GREEN}All packets present!") 79 | else: 80 | reason = "Missing packets\n" + capsys.readouterr().out 81 | pytest.xfail(reason=reason) 82 | 83 | 84 | @pytest.mark.parametrize("protocol", (757,)) 85 | def test_pack_clientbound_packets(protocol: Union[int, str]): 86 | packet_map = load_packet_map(protocol) 87 | 88 | # iterate through each packet class in the state list 89 | for state in GameState.__members__.values(): 90 | packet_classes = { 91 | **packet_map.packets[state].client_bound, 92 | **packet_map.packets[state].server_bound, 93 | }.values() 94 | 95 | for packet_class in packet_classes: 96 | # since ServerBoundPacket.pack(...) is an optional abstract method we have to 97 | # check if a ServerBoundPacket's pack() method is actually implemented or not 98 | if issubclass(packet_class, ServerBoundPacket): 99 | if is_abstract(packet_class.pack): 100 | continue 101 | 102 | annos = packet_class.__init__.__annotations__.copy() 103 | 104 | # get rid of return annotation if there is one 105 | annos.pop("return", None) 106 | 107 | kwargs = {} 108 | 109 | for anno_name, anno_type in annos.items(): 110 | if anno_type is bool or anno_type == "bool": 111 | kwargs[anno_name] = True 112 | elif anno_type is int or anno_type == "int": 113 | kwargs[anno_name] = 1 114 | elif anno_type is float or anno_type == "float": 115 | kwargs[anno_name] = 123.123 116 | elif anno_type is bytes or anno_type == "bytes": 117 | kwargs[anno_name] = b"abc\x01\x02\x03" 118 | elif anno_type is str or anno_type == "str": 119 | kwargs[anno_name] = "test string123 !@#$%^&*()-_=+" 120 | elif anno_type is uuid.UUID or anno_type == "UUID": 121 | kwargs[anno_name] = uuid.uuid4() 122 | elif anno_type is Chat or anno_type == "Chat": 123 | kwargs[anno_name] = Chat("test chat message 123") 124 | 125 | # not all args could have dummy values generated, so we can't test this packet 126 | if len(kwargs) != len(annos): 127 | continue 128 | 129 | packet = packet_class(**kwargs) # create instance of packet from dummy data 130 | buffer = packet.pack() # pack packet into a Buffer 131 | 132 | assert isinstance(buffer, Buffer) 133 | 134 | # some clientbound packets have a corresponding unpack method, so we can try to test that 135 | try: 136 | decoded_packet = packet_class.unpack(buffer) 137 | 138 | assert isinstance(decoded_packet, packet_class) 139 | assert decoded_packet.__dict__ == packet.__dict__ 140 | except NotImplementedError: 141 | pass 142 | 143 | # some packets don't take any arguments and the data sent is just the packet id 144 | if len(kwargs) > 0: 145 | assert len(buffer) >= 1 146 | -------------------------------------------------------------------------------- /tests/test_strict_abc.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from pymine_net.strict_abc import StrictABC, is_abstract, optionalabstractmethod 7 | 8 | 9 | class Helpers: 10 | @abstractmethod 11 | def helper_ab_func(self, x): 12 | ... 13 | 14 | @optionalabstractmethod 15 | def helper_optional_ab_func(self, x): 16 | ... 17 | 18 | def helper_func(self, x): 19 | return x 20 | 21 | @abstractmethod 22 | def helper_annotated_ab_func(self, x: int, y: int) -> int: 23 | ... 24 | 25 | @optionalabstractmethod 26 | def helper_annotated_optional_ab_func(self, x: int, y: int) -> int: 27 | ... 28 | 29 | def helper_annotated_func(self, x: int, y: int) -> int: 30 | return x + y 31 | 32 | def helper_unannoteated_func(self, x, y): 33 | return x + y 34 | 35 | 36 | def test_abc_creation(): 37 | test_cls = type( 38 | "TestAbstract", 39 | (StrictABC,), 40 | { 41 | "func": Helpers.helper_func, 42 | "ab_func": Helpers.helper_ab_func, 43 | "optional_ab_func": Helpers.helper_optional_ab_func, 44 | }, 45 | ) 46 | 47 | cases = [ 48 | ("func", {"optional_ab": False, "ab": False}), 49 | ("ab_func", {"optional_ab": False, "ab": True}), 50 | ("optional_ab_func", {"optional_ab": True, "ab": False}), 51 | ] 52 | 53 | for method_name, params in cases: 54 | # Make sure the class was properly made with the function 55 | assert hasattr(test_cls, method_name) 56 | fun = getattr(test_cls, method_name) 57 | # Make sure there's no decorator failure and the function is still callable 58 | assert callable(fun) 59 | # Make sure the function is following the expected params 60 | assert getattr(fun, "__isabstractmethod__", False) is params["ab"] 61 | assert getattr(fun, "__isoptionalabstractmethod__", False) is params["optional_ab"] 62 | 63 | # Make sure the class has __abstractmethods__ list, which only holds 64 | # the true abstractmethods (not optional abstractmethods) 65 | assert hasattr(test_cls, "__abstractmethods__") 66 | abstract_methods = getattr(test_cls, "__abstractmethods__") 67 | assert "ab_func" in abstract_methods 68 | assert "optional_ab_func" not in abstract_methods 69 | 70 | 71 | def test_abc_definition_enforcement(): 72 | test_cls = type("TestAbstract", (StrictABC,), {"foo": Helpers.helper_ab_func}) 73 | 74 | # Fail for class without overridden definition 75 | with pytest.raises(TypeError): 76 | type("Test", (test_cls,), {}) 77 | 78 | # Succeed for class with overridden definition 79 | type("Test", (test_cls,), {"foo": Helpers.helper_func}) 80 | 81 | # Succeed for class without overridden definition, with ignored definition check 82 | type("ExtendedTestAbstract", (test_cls,), {}, definition_check=False) 83 | 84 | 85 | def test_abc_no_optional_definition_enforcement(): 86 | test_cls = type("TestAbstract", (StrictABC,), {"foo": Helpers.helper_optional_ab_func}) 87 | 88 | # Should succeed, even without providing a definition of the optional abstract function 89 | type("Test", (test_cls,), {}) 90 | 91 | # Should succeed with overridden optional abstract function with valid function 92 | type("Test", (test_cls,), {"foo": Helpers.helper_func}) 93 | 94 | 95 | def test_abc_typing_enforcement(): 96 | test_cls = type("TestAbstract", (StrictABC,), {"foo": Helpers.helper_annotated_ab_func}) 97 | 98 | # Fail for class without matching type annotated overridden method 99 | with pytest.raises(TypeError): 100 | type("Test", (test_cls,), {"foo": Helpers.helper_unannoteated_func}) 101 | 102 | # Succeed for class with matching type annotated overridden method 103 | type("Test", (test_cls,), {"foo": Helpers.helper_annotated_func}) 104 | 105 | # Succeed for class wtthout matching type annotated overridden method, but without type-check enabled 106 | type("Test", (test_cls,), {"foo": Helpers.helper_unannoteated_func}, typing_check=False) 107 | 108 | # Succeed for unoverridden method (without definition check) 109 | type("Test", (test_cls,), {}, definition_check=False) 110 | 111 | 112 | def test_abc_optional_typing_enforcement(): 113 | test_cls = type( 114 | "TestAbstract", (StrictABC,), {"foo": Helpers.helper_annotated_optional_ab_func} 115 | ) 116 | 117 | # Succeed for unoverridden method (without definition check) 118 | type("Test", (test_cls,), {}, definition_check=False) 119 | 120 | # Succeed for class with matching type annotated overridden method 121 | type("Test", (test_cls,), {"foo": Helpers.helper_annotated_func}) 122 | 123 | # Fail for class without matching type annotated overridden maethod 124 | with pytest.raises(TypeError): 125 | type("Test", (test_cls,), {"foo": Helpers.helper_unannoteated_func}) 126 | 127 | 128 | def test_is_abstract(): 129 | # is_abstract check should obviously pass for an abstract method 130 | assert is_abstract(Helpers.helper_ab_func) is True 131 | # is_abstract check should also pass for optional abstract methods 132 | assert is_abstract(Helpers.helper_optional_ab_func) is True 133 | # is_abstract check should not pass for a regular method 134 | assert is_abstract(Helpers.helper_func) is False 135 | 136 | 137 | def test_forward_annotation_comparison(): 138 | compare = lambda x, y: StrictABC._compare_forward_reference_annotations(x, y)[0] 139 | 140 | # We should succeed for exactly same forward reference strings 141 | assert compare("Foo", "Foo") is True 142 | 143 | # We should also succeed for unqualified comparison for reference strings 144 | assert compare("py_mine.xyz.Foo", "Foo") is True 145 | assert compare("Foo", "py_mine.zyx.Foo") is True 146 | 147 | # We should fail if the strings are completely different 148 | assert compare("Foo", "Bar") is False 149 | 150 | Foo = type("Foo", (), {}) 151 | # We should succeed for comparing string "Foo" and real Foo named class 152 | assert compare(Foo, "Foo") is True 153 | assert compare("Foo", Foo) is True 154 | 155 | Bar = type("Bar", (), {}) 156 | # We should fail for comparing string "Foo" and real Bar named class 157 | assert compare(Bar, "Foo") is False 158 | assert compare("Foo", Bar) is False 159 | --------------------------------------------------------------------------------