├── .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 | [](https://www.codefactor.io/repository/github/py-mine/pymine-net)
3 | 
4 | 
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 |
--------------------------------------------------------------------------------