Compare commits
273 Commits
Author | SHA1 | Date |
---|---|---|
joodicator | bcd156e83b | |
joodicator | 33293a2395 | |
joodicator | bf49006553 | |
joodicator | 1ae9a2b48a | |
Andrew So | c04e831e19 | |
Mike Shlanta | 63b8e614cd | |
Mike Shlanta | c58e809dbf | |
Mike Shlanta | f01584ec01 | |
Mike Shlanta | fad399d5d7 | |
Andrew So | 99a97df753 | |
jyooru | 37bd22172c | |
jyooru | e40fb466ae | |
jyooru | 317f3e62b2 | |
jyooru | 550886b414 | |
Andrew So | 3db741ad52 | |
Andrew So | 0169e0aa75 | |
Andrew So | dd5c77545b | |
Andrew So | 6400602625 | |
joo | 2813d02ae7 | |
joo | 73728957e7 | |
joo | 93db454cb5 | |
joo | 7693961fb9 | |
joo | 252e60a6e2 | |
joo | 969419da3f | |
joo | 4052136d30 | |
Vesek | 4a4b699b85 | |
BD103 | 2e77cf5f77 | |
Tristan | 4e01fdd310 | |
joo | 903c20f9e2 | |
joo | f37feeca18 | |
joo | cf93923acc | |
joo | eae6e5c9cd | |
joo | e434497dc7 | |
joo | 3c84c2a429 | |
Radon Rosborough | 095191a77c | |
joo | 51fa0bd2d1 | |
Amund Eggen Svandal | d01dfd4812 | |
Amund Eggen Svandal | 1f42f61620 | |
Amund Eggen Svandal | 4a8fab1138 | |
laundmo | 2947aa6619 | |
joo | 2d9479cc12 | |
joo | fcacb8abf8 | |
joo | 3f4a5d46a6 | |
joo | 4c35517157 | |
joo | b79f8b30eb | |
joo | 3723655fa3 | |
joo | 84df884ca4 | |
joo | c6afe25429 | |
Sillyfrog | 8a098b399b | |
Sillyfrog | 3dcefae645 | |
Sillyfrog | 0d28271c96 | |
Sillyfrog | b582029099 | |
Tristan Gosselin-Hane | 7d9ffb8836 | |
Tristan Gosselin-Hane | e61cfffab1 | |
Tristan Gosselin-Hane | 1b714e6449 | |
Tristan Gosselin-Hane | 0343df918c | |
Tristan Gosselin-Hane | 9c08c6c9f5 | |
Tristan Gosselin-Hane | d26aacec28 | |
Tristan Gosselin-Hane | 8cb02e7f7f | |
Tristan Gosselin-Hane | 180c698ce1 | |
Tristan Gosselin-Hane | 7380bc2c61 | |
Tristan Gosselin-Hane | 428a599f40 | |
Tristan Gosselin-Hane | 76f7b4bdc9 | |
Tristan Gosselin-Hane | 5c6edf5e44 | |
joo | ff9a0813b6 | |
joo | b38adc1aa1 | |
Sillyfrog | 51c618aeb5 | |
Sillyfrog | e1afabcba5 | |
joo | 6f2f25656c | |
joodicator | bbbd3fb195 | |
Jim Chen | 6f54e852d4 | |
Jim Chen | c80cfd50fe | |
joodicator | 997f813a6c | |
joodicator | a03e1a7c47 | |
joo | e97458c970 | |
Sillyfrog | d90f08c503 | |
joodicator | e3d2b1a368 | |
joodicator | 234e57716c | |
joodicator | d1e1da85c8 | |
Sillyfrog | 6d9d15845a | |
joo | b83b33f8df | |
joodicator | 1012ee8640 | |
joodicator | a3357762d7 | |
Zachy | a6c11bbb34 | |
Zachy | b0a9a3693c | |
Zachy | e8a0e34aef | |
Zachy | 0b127da0ca | |
Zachy | a60b513e74 | |
Zachy | 93f6d269da | |
Zachy | 22008c5c5c | |
Zachy | 3c594a1386 | |
Zachy | 0fc8c3bbfe | |
Zachy | 7361f761f5 | |
Zachy | d7b560a9f4 | |
joodicator | 7b1567c352 | |
joodicator | 6ef868bc5b | |
joodicator | b3cf00a856 | |
joo | 6d62d3956a | |
joodicator | d24b6eaded | |
joodicator | faf02acf62 | |
joodicator | e4f8b5583a | |
joo | 4956d5e70d | |
joodicator | 41ea36c642 | |
joodicator | 24ca96accb | |
joo | 7ae6d599fb | |
joodicator | bf719611ec | |
joo | d627423949 | |
joodicator | f248006b86 | |
joodicator | 612fa8e324 | |
Amund Eggen Svandal | 1a1b9803f8 | |
Amund Eggen Svandal | 56d1300db1 | |
L1LxHa | 9b43d6f004 | |
Ammar Askar | b4c58477f4 | |
Ammar Askar | 6adefa8c75 | |
Amund Eggen Svandal | bea661860d | |
Amund Eggen Svandal | e21c0d877f | |
Amund Eggen Svandal | c67652d7e8 | |
Tristan Gosselin-Hane | 316ea4d63d | |
joo | 527f3d3146 | |
joo | 48e1003f42 | |
Billy SU | eb302094aa | |
joo | 0eec179f48 | |
joo | 720868fab7 | |
Zachy24 | 103b53a97a | |
Zachy | 409c619eb0 | |
Zachy | 6d6a592f07 | |
Zachy24 | da103c6d3c | |
Zachy24 | 4ba6a40df6 | |
Zachy | aeaf7b5bcb | |
Zachy | ed85cb793a | |
Zachy | 0198476fa9 | |
Zachy | e840fab267 | |
Zachy | 1a114c1b95 | |
Zachy | d20344cac1 | |
joo | f6f6511788 | |
joo | adc8d15ddc | |
joo | 745aa054b0 | |
joo | bea2222c58 | |
joo | 4b6feda1cb | |
joo | 61598eba75 | |
joo | 201e075591 | |
Zachy | d3a8cc8dfb | |
joo | 0a1776f97a | |
joo | d36a4170ed | |
joo | d36b652b69 | |
joo | c01f194d06 | |
joo | db714f9490 | |
joo | 8578326c2f | |
joo | 709b80b539 | |
joo | 92f2eff681 | |
joo | ab9ca6dfee | |
joo | ebee077303 | |
joo | f22447b97a | |
joo | bbf7200220 | |
joo | 52c0671f4f | |
joo | bca783115c | |
joo | 67344f2ceb | |
joo | 19a82f51ef | |
gurland | c584f29154 | |
joo | da46c4553d | |
joo | c90afe4424 | |
joo | 38fa39a236 | |
joo | ae0a3b3989 | |
TheSnoozer | 5c0c95068f | |
joo | ece90fcd9d | |
joo | 0ec2398fb4 | |
joo | bfaabcad58 | |
joo | f492adfeff | |
joo | 258c1f2566 | |
joo | da7c13076f | |
joo | 1766b30983 | |
joo | ec4d04c530 | |
joo | 8301f714d6 | |
joo | 979468b4f1 | |
joo | 53312f997b | |
joo | 3fb922b0d1 | |
TheSnoozer | 821dad72ca | |
TheSnoozer | 860628f64b | |
joo | af559e181a | |
joo | 88a5fdc637 | |
joo | ca100a5b1f | |
joo | c5bd055fa0 | |
TheSnoozer | 61d9695226 | |
joo | e9f095de42 | |
joo | 3269a022a8 | |
joo | b79e7b5f28 | |
joo | 9497aae8fa | |
joo | f1d04e6610 | |
joo | 593c98f168 | |
joo | 9765e936c9 | |
joo | 46e058dd08 | |
joo | f1ae765458 | |
Ammar Askar | 4a7d06c3cf | |
Ammar Askar | 997a59efb0 | |
Ammar Askar | ef790d2f08 | |
joo | 304f08bf8c | |
TheSnoozer | 61b07f52f2 | |
TheSnoozer | 64cf23436b | |
TheSnoozer | 2f0dbf5cbb | |
TheSnoozer | 7eef61bfde | |
TheSnoozer | 0c64623696 | |
TheSnoozer | ab71aeeb7d | |
TheSnoozer | 346b3081ec | |
TheSnoozer | 4a508f935b | |
TheSnoozer | 8552c6efe5 | |
TheSnoozer | f8781c19c8 | |
TheSnoozer | 6137436d03 | |
TheSnoozer | 5349ff2730 | |
TheSnoozer | d8fc742862 | |
TheSnoozer | 9ab2e1ae69 | |
TheSnoozer | fdb5a0bb72 | |
TheSnoozer | cc466bb0ea | |
TheSnoozer | 6fbf75203c | |
TheSnoozer | 93227e26fa | |
TheSnoozer | ca30ff2e74 | |
TheSnoozer | 3ad5d1abd5 | |
TheSnoozer | 81f2ae4070 | |
TheSnoozer | fecb1d10e9 | |
TheSnoozer | 0dc333237b | |
TheSnoozer | 6f52ceac0d | |
TheSnoozer | 9caff502ca | |
TheSnoozer | 42ede3f83d | |
joo | 89a1bfb796 | |
joo | 9e7e75f9a7 | |
joo | 33cd42848e | |
joo | 5aa2d3df59 | |
joo | cf464d2da2 | |
joo | cab8d56746 | |
Ammar Askar | f450ef5ff4 | |
Ammar Askar | d686b6487f | |
Ammar Askar | 5b261b840e | |
Ammar Askar | 4ce8c7f6ca | |
Ammar Askar | 8859e0f7bf | |
Ammar Askar | da967a4e56 | |
Ammar Askar | c87d7bc6f3 | |
Ammar Askar | d8226d266f | |
Ammar Askar | ca4fd6680e | |
joo | 2cf1d3cb03 | |
joo | 991f0b3da6 | |
joo | 5b5f36048c | |
joo | ece5fd903d | |
joo | 8d1dcec3e2 | |
joo | bcf22b8312 | |
joo | 0cc96f7dc5 | |
joo | b7290cf327 | |
joo | a1570bd3a9 | |
joo | 3f4571d9e9 | |
joo | 19cdf80952 | |
joo | 028ef3f802 | |
joo | 0d42c18211 | |
Nigel Todman | 0ffb08327a | |
joo | 623d2f00c9 | |
Nigel Todman | 5805d6e476 | |
joo | e2c4c97ea5 | |
joo | a77092572c | |
joo | 9e369cb938 | |
joo | b1edff913b | |
joo | df9171edd1 | |
joo | 3981c46569 | |
joo | f27689f729 | |
joo | 8eb1cdeee7 | |
joo | e99d2a4ef5 | |
joo | bc260b0a91 | |
joo | 66a0603acf | |
joo | 73672401ef | |
joo | b0f15ed5a2 | |
joo | 7fd37a79f2 | |
joo | 00ab1b4209 | |
Ammar Askar | 89ca73532a | |
Ammar Askar | 10fb291752 | |
Ammar Askar | 0c31e748e8 | |
Ammar Askar | 9aa369c7da | |
joo | f560f73df8 |
|
@ -81,5 +81,8 @@ target/
|
|||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
### Visual Studio
|
||||
.vscode
|
||||
|
||||
### pyCraft ###
|
||||
credentials
|
||||
|
|
41
.travis.yml
41
.travis.yml
|
@ -1,21 +1,36 @@
|
|||
language: python
|
||||
python: 2.7
|
||||
env:
|
||||
- TOX_ENV=py27
|
||||
- TOX_ENV=py33
|
||||
- TOX_ENV=py34
|
||||
- TOX_ENV=pypy
|
||||
- TOX_ENV=cover
|
||||
- TOX_ENV=flake8
|
||||
- TOX_ENV=pylint-errors
|
||||
- TOX_ENV=pylint-full
|
||||
- TOX_ENV=verify-manifest
|
||||
dist: focal
|
||||
python: 3.9
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: pypy3
|
||||
env: TOX_ENV=pypy
|
||||
- python: 3.5
|
||||
env: TOX_ENV=py35
|
||||
- python: 3.6
|
||||
env: TOX_ENV=py36
|
||||
- python: 3.7
|
||||
env: TOX_ENV=py37
|
||||
- python: 3.8
|
||||
env: TOX_ENV=py38
|
||||
- python: 3.9
|
||||
env: TOX_ENV=py39
|
||||
- python: 3.9
|
||||
env: TOX_ENV=flake8
|
||||
- python: 3.9
|
||||
env: TOX_ENV=pylint-errors
|
||||
- python: 3.9
|
||||
env: TOX_ENV=pylint-full
|
||||
- python: 3.9
|
||||
env: TOX_ENV=verify-manifest
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
- pip install python-coveralls
|
||||
script:
|
||||
- tox -e $TOX_ENV
|
||||
after_success:
|
||||
- if [ "$TOX_ENV" = "py27" ]; then tox -e coveralls; fi
|
||||
after_script:
|
||||
- if [ "$TOX_ENV" = "py39" ]; then tox -e coveralls; fi
|
||||
notifications:
|
||||
email: false
|
||||
|
|
53
README.rst
53
README.rst
|
@ -1,7 +1,7 @@
|
|||
pyCraft
|
||||
=======
|
||||
.. image:: https://travis-ci.org/ammaraskar/pyCraft.svg?branch=master
|
||||
:target: https://travis-ci.org/ammaraskar/pyCraft
|
||||
.. image:: https://app.travis-ci.com/ammaraskar/pyCraft.svg?branch=master
|
||||
:target: https://app.travis-ci.com/github/ammaraskar/pyCraft
|
||||
.. image:: https://readthedocs.org/projects/pycraft/badge/?version=latest
|
||||
:target: https://pycraft.readthedocs.org/en/latest
|
||||
.. image:: https://coveralls.io/repos/ammaraskar/pyCraft/badge.svg?branch=master
|
||||
|
@ -19,23 +19,54 @@ Detailed information for developers can be found here:
|
|||
``start.py`` is a basic example of a headless client using the library
|
||||
Use ``start.py --help`` for the options.
|
||||
|
||||
Python version
|
||||
--------------
|
||||
We aim to be compatible with the following python versions:
|
||||
Supported Minecraft versions
|
||||
----------------------------
|
||||
pyCraft is compatible with the following Minecraft releases:
|
||||
|
||||
* 1.8, 1.8.1, 1.8.2, 1.8.3, 1.8.4, 1.8.5, 1.8.6, 1.8.7, 1.8.8, 1.8.9
|
||||
* 1.9, 1.9.1, 1.9.2, 1.9.3, 1.9.4
|
||||
* 1.10, 1.10.1, 1.10.2
|
||||
* 1.11, 1.11.1, 1.11.2
|
||||
* 1.12, 1.12.1, 1.12.2
|
||||
* 1.13, 1.13.1, 1.13.2
|
||||
* 1.14, 1.14.1, 1.14.2, 1.14.3, 1.14.4
|
||||
* 1.15, 1.15.1, 1.15.2
|
||||
* 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5
|
||||
* 1.17, 1.17.1
|
||||
* 1.18, 1.18.1
|
||||
|
||||
In addition, some development snapshots and pre-release versions are supported:
|
||||
`<minecraft/__init__.py>`_ contains a full list of supported Minecraft versions
|
||||
and corresponding protocol version numbers.
|
||||
|
||||
Supported functionality
|
||||
-----------------------
|
||||
Although pyCraft is compatible any supported server, only a subset of all
|
||||
packets are currently decoded or encoded by the library: those necessary
|
||||
to remain connected to the server, those used for chat, and some others.
|
||||
|
||||
Developers wishing to use other functionality with pyCraft can contribute by
|
||||
implementing packet classes for the desired packets, adding them under
|
||||
`<minecraft/networking/packets>`_, and sending a pull request.
|
||||
|
||||
Supported Python versions
|
||||
-------------------------
|
||||
pyCraft is compatible with (at least) the following Python implementations:
|
||||
|
||||
* Python 2.7
|
||||
* Python 3.3
|
||||
* Python 3.4
|
||||
* Python 3.5
|
||||
* Python 3.6
|
||||
* Python 3.7
|
||||
* Python 3.8
|
||||
* Python 3.9
|
||||
* PyPy
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- `cryptography <https://github.com/pyca/cryptography#cryptography>`_
|
||||
- `cryptography <https://github.com/pyca/cryptography#cryptography>`_
|
||||
- `requests <http://docs.python-requests.org/en/latest/>`_
|
||||
- `future <http://python-future.org/>`_
|
||||
- `PyNBT <https://github.com/TkTech/PyNBT>`_
|
||||
|
||||
The requirements are also stored in ``requirements.txt``
|
||||
The requirements are also stored in ``setup.py``
|
||||
|
||||
See the installation instructions for the cryptography library here: `<https://cryptography.io/en/latest/installation/>`_
|
||||
but essentially ``pip install -r requirements.txt`` should cover everything.
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
# flake8: noqa
|
||||
|
||||
import sys
|
||||
import os
|
||||
os.chdir("..")
|
||||
import os.path
|
||||
from tox.config import parseconfig
|
||||
|
||||
from tox._config import parseconfig
|
||||
# This file is in pyCraft/bin/; it needs to execute in pyCraft/.
|
||||
os.chdir(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
print("language: python")
|
||||
print("python: 2.7")
|
||||
print("python: 3.5")
|
||||
print("env:")
|
||||
for env in parseconfig(None, 'tox').envlist:
|
||||
print(" - TOX_ENV=%s" % env)
|
||||
|
@ -15,7 +19,7 @@ print(" - pip install tox")
|
|||
print(" - pip install python-coveralls")
|
||||
print("script:")
|
||||
print(" - tox -e $TOX_ENV")
|
||||
print("after_success:")
|
||||
print(' - if [ "$TOX_ENV" = "py27 ]; then tox -e coveralls; fi')
|
||||
print("after_script:")
|
||||
print(' - if [ "$TOX_ENV" = "py35" ]; then tox -e coveralls; fi')
|
||||
print("notifications:")
|
||||
print(" email: false")
|
||||
|
|
|
@ -1,58 +1,52 @@
|
|||
Authentication
|
||||
==============
|
||||
|
||||
.. currentmodule:: authentication
|
||||
.. currentmodule:: minecraft.authentication
|
||||
.. _Yggdrasil: http://wiki.vg/Authentication
|
||||
.. _LoginResponse: http://wiki.vg/Authentication#Authenticate
|
||||
|
||||
The authentication module contains functions and classes to facilitate
|
||||
interfacing with Mojang's Yggdrasil_ service.
|
||||
interfacing with Mojang's Yggdrasil_ authentication service.
|
||||
|
||||
|
||||
Logging In
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The most common use for this module in the context of a client will be to
|
||||
log in to a Minecraft account. The convenience method
|
||||
log in to a Minecraft account. The first step to doing this is creating
|
||||
an instance of the AuthenticationToken class after which you may use the
|
||||
authenticate method with the user's username and password in order to make the AuthenticationToken valid.
|
||||
|
||||
.. autofunction:: login_to_minecraft
|
||||
.. autoclass:: AuthenticationToken
|
||||
:members: authenticate
|
||||
|
||||
should be used which will return a LoginResponse object. See LoginResponse_ for more details on the returned attributes
|
||||
|
||||
.. autoclass:: LoginResponse
|
||||
:members:
|
||||
|
||||
or raise a YggdrasilError on failure, for example if an incorrect username/password
|
||||
is provided or the web request failed
|
||||
Upon success, the function returns True, on failure a YggdrasilError
|
||||
is raised. This happens, for example if an incorrect username/password
|
||||
is provided or the web request failed.
|
||||
|
||||
.. autoexception:: YggdrasilError
|
||||
:members:
|
||||
|
||||
|
||||
Arbitary Requests
|
||||
Arbitrary Requests
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You may make any arbitary request to the Yggdrasil service with
|
||||
You may make any arbitrary request to the Yggdrasil service with the _make_request
|
||||
method passing in the AUTH_SERVER as the server parameter.
|
||||
|
||||
.. automodule:: authentication
|
||||
:members: BASE_URL
|
||||
.. automodule:: minecraft.authentication
|
||||
:members: AUTH_SERVER
|
||||
|
||||
.. autofunction:: make_request
|
||||
|
||||
.. autoclass:: Response
|
||||
:members:
|
||||
.. autofunction:: _make_request
|
||||
|
||||
|
||||
---------------
|
||||
Example Usage
|
||||
---------------
|
||||
An example of making an arbitary request can be seen here::
|
||||
An example of making an arbitrary request can be seen here::
|
||||
|
||||
url = authentication.BASE_URL + "session/minecraft/join"
|
||||
server_id = encryption.generate_verification_hash(packet.server_id, secret, packet.public_key)
|
||||
payload = {'accessToken': self.connection.login_response.access_token,
|
||||
'selectedProfile': self.connection.login_response.profile_id,
|
||||
'serverId': server_id}
|
||||
payload = {'username': username,
|
||||
'password': password}
|
||||
|
||||
authentication.make_request(url, payload)
|
||||
authentication._make_request(authentication.AUTH_SERVER, "signout", payload)
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ extensions = [
|
|||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.pngmath',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
|
|
|
@ -1,54 +1,83 @@
|
|||
Connecting to Servers
|
||||
======================
|
||||
|
||||
.. module:: network.connection
|
||||
.. module:: minecraft.networking.connection
|
||||
|
||||
Your primary dealings when connecting to a server will deal with the Connection class
|
||||
Your primary dealings when connecting to a server will be with the Connection class
|
||||
|
||||
.. autoclass:: network.connection.Connection
|
||||
.. autoclass:: Connection
|
||||
:members:
|
||||
|
||||
Writing Packets
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The packet class uses a lot of magic to work, here is how to use them.
|
||||
Look up the particular packet you need to deal with, for my example let's go with the ``KeepAlivePacket``
|
||||
Look up the particular packet you need to deal with, for this example
|
||||
let's go with the ``serverbound.play.KeepAlivePacket``
|
||||
|
||||
.. autoclass:: network.packets.KeepAlivePacket
|
||||
.. autoclass:: minecraft.networking.packets.serverbound.play.KeepAlivePacket
|
||||
:undoc-members:
|
||||
:inherited-members:
|
||||
:exclude-members: read, write
|
||||
:exclude-members: read, write, context, get_definition, get_id, id, packet_name, set_values
|
||||
|
||||
Pay close attention to the definition attribute, we're gonna be using that to assign values within the packet::
|
||||
Pay close attention to the definition attribute, and how our class variable corresponds to
|
||||
the name given from the definition::
|
||||
|
||||
packet = KeepAlivePacket()
|
||||
from minecraft.networking.packets import serverbound
|
||||
packet = serverbound.play.KeepAlivePacket()
|
||||
packet.keep_alive_id = random.randint(0, 5000)
|
||||
connection.write_packet(packet)
|
||||
|
||||
and just like that, the packet will be written out to the server
|
||||
and just like that, the packet will be written out to the server.
|
||||
|
||||
It is possible to implement your own custom packets by subclassing
|
||||
:class:`minecraft.networking.packets.Packet`. Read the docstrings and in
|
||||
packets.py and follow the examples in its subpackages for more details on
|
||||
how to do advanced tasks like having a packet that is compatible across
|
||||
multiple protocol versions.
|
||||
|
||||
Listening for Certain Packets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Let's look at how to listen for certain packets, the relevant method being
|
||||
Let's look at how to listen for certain packets, the relevant decorator being
|
||||
|
||||
.. automethod:: network.connection.Connection.register_packet_listener
|
||||
A decorator can be used to register a packet listener:
|
||||
|
||||
.. autodecorator:: Connection.listener
|
||||
|
||||
Example usage::
|
||||
|
||||
connection = Connection(options.address, options.port, auth_token=auth_token)
|
||||
connection.connect()
|
||||
|
||||
from minecraft.networking.packets.clientbound.play import ChatMessagePacket
|
||||
|
||||
@connection.listener(ChatMessagePacket)
|
||||
def print_chat(chat_packet):
|
||||
print "Position: " + str(chat_packet.position)
|
||||
print "Data: " + chat_packet.json_data
|
||||
|
||||
|
||||
Altenatively, packet listeners can also be registered seperate from the function definition.
|
||||
|
||||
.. automethod:: Connection.register_packet_listener
|
||||
|
||||
An example of this can be found in the ``start.py`` headless client, it is recreated here::
|
||||
|
||||
connection = Connection(address, port, login_response)
|
||||
connection = Connection(options.address, options.port, auth_token=auth_token)
|
||||
connection.connect()
|
||||
|
||||
def print_chat(chat_packet):
|
||||
print "Position: " + str(chat_packet.position)
|
||||
print "Data: " + chat_packet.json_data
|
||||
|
||||
from network.packets import ChatMessagePacket
|
||||
from minecraft.networking.packets.clientbound.play import ChatMessagePacket
|
||||
connection.register_packet_listener(print_chat, ChatMessagePacket)
|
||||
|
||||
The field names ``position`` and ``json_data`` are inferred by again looking at the definition attribute as before
|
||||
|
||||
.. autoclass:: network.packets.ChatMessagePacket
|
||||
|
||||
.. autoclass:: minecraft.networking.packets.clientbound.play.ChatMessagePacket
|
||||
:undoc-members:
|
||||
:inherited-members:
|
||||
:exclude-members: read, write
|
||||
:exclude-members: read, write, context, get_definition, get_id, id, packet_name, set_values
|
||||
|
|
|
@ -10,7 +10,8 @@ Welcome to pyCraft's documentation!
|
|||
between a Minecraft server as a client.
|
||||
|
||||
The authentication package contains utilities to manage communicating
|
||||
with Mojang's in order to log in with a minecraft account, edit profiles etc
|
||||
with Mojang's authentication servers in order to log in with a minecraft
|
||||
account, edit profiles etc
|
||||
|
||||
The Connection class under the networking package handles
|
||||
connecting to a server, sending packets, listening for packets etc
|
||||
|
|
|
@ -3,42 +3,546 @@ A modern, Python3-compatible, well-documented library for communicating
|
|||
with a MineCraft server.
|
||||
"""
|
||||
|
||||
__version__ = "0.3.0"
|
||||
from collections import OrderedDict, namedtuple
|
||||
import re
|
||||
|
||||
SUPPORTED_MINECRAFT_VERSIONS = {
|
||||
'1.8': 47,
|
||||
'1.8.1': 47,
|
||||
'1.8.2': 47,
|
||||
'1.8.3': 47,
|
||||
'1.8.4': 47,
|
||||
'1.8.5': 47,
|
||||
'1.8.6': 47,
|
||||
'1.8.7': 47,
|
||||
'1.8.8': 47,
|
||||
'1.9': 107,
|
||||
'1.9.1': 108,
|
||||
'1.9.2': 109,
|
||||
'1.9.3': 110,
|
||||
'1.9.4': 110,
|
||||
'1.10': 210,
|
||||
'1.10.1': 210,
|
||||
'1.10.2': 210,
|
||||
'16w32a': 301,
|
||||
'16w32b': 302,
|
||||
'16w33a': 303,
|
||||
'16w35a': 304,
|
||||
'16w36a': 305,
|
||||
'16w38a': 306,
|
||||
'16w39a': 307,
|
||||
'16w39b': 308,
|
||||
'16w39c': 309,
|
||||
'16w40a': 310,
|
||||
'16w41a': 311,
|
||||
'16w42a': 312,
|
||||
'16w43a': 313,
|
||||
'16w44a': 313,
|
||||
'1.11-pre1': 314,
|
||||
'1.11': 315,
|
||||
}
|
||||
# The version number of the most recent pyCraft release.
|
||||
__version__ = "0.7.0"
|
||||
|
||||
SUPPORTED_PROTOCOL_VERSIONS = sorted(SUPPORTED_MINECRAFT_VERSIONS.values())
|
||||
# This bit occurs in the protocol numbers of pre-release versions after 1.16.3.
|
||||
PRE = 1 << 30
|
||||
|
||||
# A record representing a Minecraft version in the following list.
|
||||
Version = namedtuple('Version', ('id', 'protocol', 'supported'))
|
||||
|
||||
# A list of Minecraft versions known to pyCraft, including all supported
|
||||
# versions as well as some unsupported versions (used by certain forward-
|
||||
# compatible code: e.g. when comparing the current protocol version with that
|
||||
# of an unsupported version), in chronological order of publication.
|
||||
#
|
||||
# The ID string of a version is the key used to identify it in
|
||||
# <https://launchermeta.mojang.com/mc/game/version_manifest.json>, or the 'id'
|
||||
# key in "version.json" in the corresponding ".jar" file distributed by Mojang.
|
||||
KNOWN_MINECRAFT_VERSION_RECORDS = [
|
||||
# id protocol supported
|
||||
Version('13w41a', 0, False),
|
||||
Version('13w41b', 0, False),
|
||||
Version('13w42a', 1, False),
|
||||
Version('13w42b', 1, False),
|
||||
Version('13w43a', 2, False),
|
||||
Version('1.7-pre', 3, False),
|
||||
Version('1.7.1-pre', 3, False),
|
||||
Version('1.7.2', 4, True),
|
||||
Version('13w47a', 4, False),
|
||||
Version('13w47b', 4, False),
|
||||
Version('13w47c', 4, False),
|
||||
Version('13w47d', 4, False),
|
||||
Version('13w47e', 4, False),
|
||||
Version('13w48a', 4, False),
|
||||
Version('13w48b', 4, False),
|
||||
Version('13w49a', 4, False),
|
||||
Version('1.7.3-pre', 4, False),
|
||||
Version('1.7.4', 4, True),
|
||||
Version('1.7.5', 4, True),
|
||||
Version('1.7.6-pre1', 5, False),
|
||||
Version('1.7.6-pre2', 5, False),
|
||||
Version('1.7.6', 5, True),
|
||||
Version('1.7.7', 5, True),
|
||||
Version('1.7.8', 5, True),
|
||||
Version('1.7.9', 5, True),
|
||||
Version('1.7.10-pre1', 5, False),
|
||||
Version('1.7.10-pre2', 5, False),
|
||||
Version('1.7.10-pre3', 5, False),
|
||||
Version('1.7.10-pre4', 5, False),
|
||||
Version('1.7.10', 5, True),
|
||||
Version('14w02a', 5, False),
|
||||
Version('14w02b', 5, False),
|
||||
Version('14w02c', 5, False),
|
||||
Version('14w03a', 6, False),
|
||||
Version('14w03b', 6, False),
|
||||
Version('14w04a', 7, False),
|
||||
Version('14w04b', 8, False),
|
||||
Version('14w05a', 9, False),
|
||||
Version('14w05b', 9, False),
|
||||
Version('14w06a', 10, False),
|
||||
Version('14w06b', 10, False),
|
||||
Version('14w07a', 11, False),
|
||||
Version('14w08a', 12, False),
|
||||
Version('14w10a', 13, False),
|
||||
Version('14w10b', 13, False),
|
||||
Version('14w10c', 13, False),
|
||||
Version('14w11a', 14, False),
|
||||
Version('14w11b', 14, False),
|
||||
Version('14w17a', 15, False),
|
||||
Version('14w18a', 16, False),
|
||||
Version('14w18b', 16, False),
|
||||
Version('14w19a', 17, False),
|
||||
Version('14w20a', 18, False),
|
||||
Version('14w20b', 18, False),
|
||||
Version('14w21a', 19, False),
|
||||
Version('14w21b', 20, False),
|
||||
Version('14w25a', 21, False),
|
||||
Version('14w25b', 22, False),
|
||||
Version('14w26a', 23, False),
|
||||
Version('14w26b', 24, False),
|
||||
Version('14w26c', 25, False),
|
||||
Version('14w27a', 26, False),
|
||||
Version('14w27b', 26, False),
|
||||
Version('14w28a', 27, False),
|
||||
Version('14w28b', 28, False),
|
||||
Version('14w29a', 29, False),
|
||||
Version('14w29a', 29, False),
|
||||
Version('14w30a', 30, False),
|
||||
Version('14w30b', 30, False),
|
||||
Version('14w30c', 31, False),
|
||||
Version('14w31a', 32, False),
|
||||
Version('14w32a', 33, False),
|
||||
Version('14w32b', 34, False),
|
||||
Version('14w32c', 35, False),
|
||||
Version('14w32d', 36, False),
|
||||
Version('14w33a', 37, False),
|
||||
Version('14w33b', 38, False),
|
||||
Version('14w33c', 39, False),
|
||||
Version('14w34a', 40, False),
|
||||
Version('14w34b', 41, False),
|
||||
Version('14w34c', 42, False),
|
||||
Version('14w34d', 43, False),
|
||||
Version('1.8-pre1', 44, False),
|
||||
Version('1.8-pre2', 45, False),
|
||||
Version('1.8-pre3', 46, False),
|
||||
Version('1.8', 47, True),
|
||||
Version('1.8.1-pre1', 47, False),
|
||||
Version('1.8.1-pre2', 47, False),
|
||||
Version('1.8.1-pre3', 47, False),
|
||||
Version('1.8.1-pre4', 47, False),
|
||||
Version('1.8.1-pre5', 47, False),
|
||||
Version('1.8.1', 47, True),
|
||||
Version('1.8.2-pre1', 47, False),
|
||||
Version('1.8.2-pre2', 47, False),
|
||||
Version('1.8.2-pre3', 47, False),
|
||||
Version('1.8.2-pre4', 47, False),
|
||||
Version('1.8.2-pre5', 47, False),
|
||||
Version('1.8.2-pre6', 47, False),
|
||||
Version('1.8.2-pre7', 47, False),
|
||||
Version('1.8.2', 47, True),
|
||||
Version('1.8.3', 47, True),
|
||||
Version('1.8.4', 47, True),
|
||||
Version('1.8.5', 47, True),
|
||||
Version('1.8.6', 47, True),
|
||||
Version('1.8.7', 47, True),
|
||||
Version('1.8.8', 47, True),
|
||||
Version('1.8.9', 47, True),
|
||||
Version('15w14a', 48, False),
|
||||
Version('15w31a', 49, False),
|
||||
Version('15w31b', 50, False),
|
||||
Version('15w31c', 51, False),
|
||||
Version('15w32a', 52, False),
|
||||
Version('15w32b', 53, False),
|
||||
Version('15w32c', 54, False),
|
||||
Version('15w33a', 55, False),
|
||||
Version('15w33b', 56, False),
|
||||
Version('15w33c', 57, False),
|
||||
Version('15w34a', 58, False),
|
||||
Version('15w34b', 59, False),
|
||||
Version('15w34c', 60, False),
|
||||
Version('15w34d', 61, False),
|
||||
Version('15w35a', 62, False),
|
||||
Version('15w35b', 63, False),
|
||||
Version('15w35c', 64, False),
|
||||
Version('15w35d', 65, False),
|
||||
Version('15w35e', 66, False),
|
||||
Version('15w36a', 67, False),
|
||||
Version('15w36b', 68, False),
|
||||
Version('15w36c', 69, False),
|
||||
Version('15w36d', 70, False),
|
||||
Version('15w37a', 71, False),
|
||||
Version('15w38a', 72, False),
|
||||
Version('15w38b', 73, False),
|
||||
Version('15w39a', 74, False),
|
||||
Version('15w39b', 74, False),
|
||||
Version('15w39c', 74, False),
|
||||
Version('15w40a', 75, False),
|
||||
Version('15w40b', 76, False),
|
||||
Version('15w41a', 77, False),
|
||||
Version('15w41b', 78, False),
|
||||
Version('15w42a', 79, False),
|
||||
Version('15w43a', 80, False),
|
||||
Version('15w43b', 81, False),
|
||||
Version('15w43c', 82, False),
|
||||
Version('15w44a', 83, False),
|
||||
Version('15w44b', 84, False),
|
||||
Version('15w45a', 85, False),
|
||||
Version('15w46a', 86, False),
|
||||
Version('15w47a', 87, False),
|
||||
Version('15w47b', 88, False),
|
||||
Version('15w47c', 89, False),
|
||||
Version('15w49a', 90, False),
|
||||
Version('15w49b', 91, False),
|
||||
Version('15w50a', 92, False),
|
||||
Version('15w51a', 93, False),
|
||||
Version('15w51b', 94, False),
|
||||
Version('16w02a', 95, False),
|
||||
Version('16w03a', 96, False),
|
||||
Version('16w04a', 97, False),
|
||||
Version('16w05a', 98, False),
|
||||
Version('16w05b', 99, False),
|
||||
Version('16w06a', 100, False),
|
||||
Version('16w07a', 101, False),
|
||||
Version('16w07b', 102, False),
|
||||
Version('1.9-pre1', 103, False),
|
||||
Version('1.9-pre2', 104, False),
|
||||
Version('1.9-pre3', 105, False),
|
||||
Version('1.9-pre4', 106, False),
|
||||
Version('1.9', 107, True),
|
||||
Version('1.9.1-pre1', 107, False),
|
||||
Version('1.9.1-pre2', 108, False),
|
||||
Version('1.9.1-pre3', 108, False),
|
||||
Version('1.9.1', 108, True),
|
||||
Version('1.RV-Pre1', 108, False),
|
||||
Version('1.9.2', 109, True),
|
||||
Version('16w14a', 109, False),
|
||||
Version('16w15a', 109, False),
|
||||
Version('16w15b', 109, False),
|
||||
Version('1.9.3-pre1', 109, False),
|
||||
Version('1.9.3-pre2', 110, False),
|
||||
Version('1.9.3-pre3', 110, False),
|
||||
Version('1.9.3', 110, True),
|
||||
Version('1.9.4', 110, True),
|
||||
Version('16w20a', 201, False),
|
||||
Version('16w21a', 202, False),
|
||||
Version('16w21b', 203, False),
|
||||
Version('1.10-pre1', 204, False),
|
||||
Version('1.10-pre2', 205, False),
|
||||
Version('1.10', 210, True),
|
||||
Version('1.10.1', 210, True),
|
||||
Version('1.10.2', 210, True),
|
||||
Version('16w32a', 301, True),
|
||||
Version('16w32b', 302, True),
|
||||
Version('16w33a', 303, True),
|
||||
Version('16w35a', 304, True),
|
||||
Version('16w36a', 305, True),
|
||||
Version('16w38a', 306, True),
|
||||
Version('16w39a', 307, True),
|
||||
Version('16w39b', 308, True),
|
||||
Version('16w39c', 309, True),
|
||||
Version('16w40a', 310, True),
|
||||
Version('16w41a', 311, True),
|
||||
Version('16w42a', 312, True),
|
||||
Version('16w43a', 313, True),
|
||||
Version('16w44a', 313, True),
|
||||
Version('1.11-pre1', 314, True),
|
||||
Version('1.11', 315, True),
|
||||
Version('16w50a', 316, True),
|
||||
Version('1.11.1', 316, True),
|
||||
Version('1.11.2', 316, True),
|
||||
Version('17w06a', 317, True),
|
||||
Version('17w13a', 318, True),
|
||||
Version('17w13b', 319, True),
|
||||
Version('17w14a', 320, True),
|
||||
Version('17w15a', 321, True),
|
||||
Version('17w16a', 322, True),
|
||||
Version('17w16b', 323, True),
|
||||
Version('17w17a', 324, True),
|
||||
Version('17w17b', 325, True),
|
||||
Version('17w18a', 326, True),
|
||||
Version('17w18b', 327, True),
|
||||
Version('1.12-pre1', 328, True),
|
||||
Version('1.12-pre2', 329, True),
|
||||
Version('1.12-pre3', 330, True),
|
||||
Version('1.12-pre4', 331, True),
|
||||
Version('1.12-pre5', 332, True),
|
||||
Version('1.12-pre6', 333, True),
|
||||
Version('1.12-pre7', 334, True),
|
||||
Version('1.12', 335, True),
|
||||
Version('17w31a', 336, True),
|
||||
Version('1.12.1-pre1', 337, True),
|
||||
Version('1.12.1', 338, True),
|
||||
Version('1.12.2-pre1', 339, True),
|
||||
Version('1.12.2-pre2', 339, True),
|
||||
Version('1.12.2', 340, True),
|
||||
Version('17w43a', 341, True),
|
||||
Version('17w43b', 342, True),
|
||||
Version('17w45a', 343, True),
|
||||
Version('17w45b', 344, True),
|
||||
Version('17w46a', 345, True),
|
||||
Version('17w47a', 346, True),
|
||||
Version('17w47b', 347, True),
|
||||
Version('17w48a', 348, True),
|
||||
Version('17w49a', 349, True),
|
||||
Version('17w49b', 350, True),
|
||||
Version('17w50a', 351, True),
|
||||
Version('18w01a', 352, True),
|
||||
Version('18w02a', 353, True),
|
||||
Version('18w03a', 354, True),
|
||||
Version('18w03b', 355, True),
|
||||
Version('18w05a', 356, True),
|
||||
Version('18w06a', 357, True),
|
||||
Version('18w07a', 358, True),
|
||||
Version('18w07b', 359, True),
|
||||
Version('18w07c', 360, True),
|
||||
Version('18w08a', 361, True),
|
||||
Version('18w08b', 362, True),
|
||||
Version('18w09a', 363, True),
|
||||
Version('18w10a', 364, True),
|
||||
Version('18w10b', 365, True),
|
||||
Version('18w10c', 366, True),
|
||||
Version('18w10d', 367, True),
|
||||
Version('18w11a', 368, True),
|
||||
Version('18w14a', 369, True),
|
||||
Version('18w14b', 370, True),
|
||||
Version('18w15a', 371, True),
|
||||
Version('18w16a', 372, True),
|
||||
Version('18w19a', 373, True),
|
||||
Version('18w19b', 374, True),
|
||||
Version('18w20a', 375, True),
|
||||
Version('18w20b', 376, True),
|
||||
Version('18w20c', 377, True),
|
||||
Version('18w21a', 378, True),
|
||||
Version('18w21b', 379, True),
|
||||
Version('18w22a', 380, True),
|
||||
Version('18w22b', 381, True),
|
||||
Version('18w22c', 382, True),
|
||||
Version('1.13-pre1', 383, True),
|
||||
Version('1.13-pre2', 384, True),
|
||||
Version('1.13-pre3', 385, True),
|
||||
Version('1.13-pre4', 386, True),
|
||||
Version('1.13-pre5', 387, True),
|
||||
Version('1.13-pre6', 388, True),
|
||||
Version('1.13-pre7', 389, True),
|
||||
Version('1.13-pre8', 390, True),
|
||||
Version('1.13-pre9', 391, True),
|
||||
Version('1.13-pre10', 392, True),
|
||||
Version('1.13', 393, True),
|
||||
Version('18w30a', 394, True),
|
||||
Version('18w30b', 395, True),
|
||||
Version('18w31a', 396, True),
|
||||
Version('18w32a', 397, True),
|
||||
Version('18w33a', 398, True),
|
||||
Version('1.13.1-pre1', 399, True),
|
||||
Version('1.13.1-pre2', 400, True),
|
||||
Version('1.13.1', 401, True),
|
||||
Version('1.13.2-pre1', 402, True),
|
||||
Version('1.13.2-pre2', 403, True),
|
||||
Version('1.13.2', 404, True),
|
||||
Version('18w43a', 441, True),
|
||||
Version('18w43b', 441, True),
|
||||
Version('18w43c', 442, True),
|
||||
Version('18w44a', 443, True),
|
||||
Version('18w45a', 444, True),
|
||||
Version('18w46a', 445, True),
|
||||
Version('18w47a', 446, True),
|
||||
Version('18w47b', 447, True),
|
||||
Version('18w48a', 448, True),
|
||||
Version('18w48b', 449, True),
|
||||
Version('18w49a', 450, True),
|
||||
Version('18w50a', 451, True),
|
||||
Version('19w02a', 452, True),
|
||||
Version('19w03a', 453, True),
|
||||
Version('19w03b', 454, True),
|
||||
Version('19w03c', 455, True),
|
||||
Version('19w04a', 456, True),
|
||||
Version('19w04b', 457, True),
|
||||
Version('19w05a', 458, True),
|
||||
Version('19w06a', 459, True),
|
||||
Version('19w07a', 460, True),
|
||||
Version('19w08a', 461, True),
|
||||
Version('19w08b', 462, True),
|
||||
Version('19w09a', 463, True),
|
||||
Version('19w11a', 464, True),
|
||||
Version('19w11b', 465, True),
|
||||
Version('19w12a', 466, True),
|
||||
Version('19w12b', 467, True),
|
||||
Version('19w13a', 468, True),
|
||||
Version('19w13b', 469, True),
|
||||
Version('19w14a', 470, True),
|
||||
Version('19w14b', 471, True),
|
||||
Version('1.14 Pre-Release 1', 472, True),
|
||||
Version('1.14 Pre-Release 2', 473, True),
|
||||
Version('1.14 Pre-Release 3', 474, True),
|
||||
Version('1.14 Pre-Release 4', 475, True),
|
||||
Version('1.14 Pre-Release 5', 476, True),
|
||||
Version('1.14', 477, True),
|
||||
Version('1.14.1 Pre-Release 1', 478, True),
|
||||
Version('1.14.1 Pre-Release 2', 479, True),
|
||||
Version('1.14.1', 480, True),
|
||||
Version('1.14.2 Pre-Release 1', 481, True),
|
||||
Version('1.14.2 Pre-Release 2', 482, True),
|
||||
Version('1.14.2 Pre-Release 3', 483, True),
|
||||
Version('1.14.2 Pre-Release 4', 484, True),
|
||||
Version('1.14.2', 485, True),
|
||||
Version('1.14.3-pre1', 486, True),
|
||||
Version('1.14.3-pre2', 487, True),
|
||||
Version('1.14.3-pre3', 488, True),
|
||||
Version('1.14.3-pre4', 489, True),
|
||||
Version('1.14.3', 490, True),
|
||||
Version('1.14.4-pre1', 491, True),
|
||||
Version('1.14.4-pre2', 492, True),
|
||||
Version('1.14.4-pre3', 493, True),
|
||||
Version('1.14.4-pre4', 494, True),
|
||||
Version('1.14.4-pre5', 495, True),
|
||||
Version('1.14.4-pre6', 496, True),
|
||||
Version('1.14.4-pre7', 497, True),
|
||||
Version('1.14.4', 498, True),
|
||||
Version('19w34a', 550, True),
|
||||
Version('19w35a', 551, True),
|
||||
Version('19w36a', 552, True),
|
||||
Version('19w37a', 553, True),
|
||||
Version('19w38a', 554, True),
|
||||
Version('19w38b', 555, True),
|
||||
Version('19w39a', 556, True),
|
||||
Version('19w40a', 557, True),
|
||||
Version('19w41a', 558, True),
|
||||
Version('19w42a', 559, True),
|
||||
Version('19w44a', 560, True),
|
||||
Version('19w45a', 561, True),
|
||||
Version('19w45b', 562, True),
|
||||
Version('19w46a', 563, True),
|
||||
Version('19w46b', 564, True),
|
||||
Version('1.15-pre1', 565, True),
|
||||
Version('1.15-pre2', 566, True),
|
||||
Version('1.15-pre3', 567, True),
|
||||
Version('1.15-pre4', 569, True),
|
||||
Version('1.15-pre5', 570, True),
|
||||
Version('1.15-pre6', 571, True),
|
||||
Version('1.15-pre7', 572, True),
|
||||
Version('1.15', 573, True),
|
||||
Version('1.15.1-pre1', 574, True),
|
||||
Version('1.15.1', 575, True),
|
||||
Version('1.15.2-pre1', 576, True),
|
||||
Version('1.15.2-pre2', 577, True),
|
||||
Version('1.15.2', 578, True),
|
||||
Version('20w06a', 701, True),
|
||||
Version('20w07a', 702, True),
|
||||
Version('20w08a', 703, True),
|
||||
Version('20w09a', 704, True),
|
||||
Version('20w10a', 705, True),
|
||||
Version('20w11a', 706, True),
|
||||
Version('20w12a', 707, True),
|
||||
Version('20w13a', 708, True),
|
||||
Version('20w13b', 709, True),
|
||||
Version('20w14a', 710, True),
|
||||
Version('20w15a', 711, True),
|
||||
Version('20w16a', 712, True),
|
||||
Version('20w17a', 713, True),
|
||||
Version('20w18a', 714, True),
|
||||
Version('20w19a', 715, True),
|
||||
Version('20w20a', 716, True),
|
||||
Version('20w20b', 717, True),
|
||||
Version('20w21a', 718, True),
|
||||
Version('20w22a', 719, True),
|
||||
Version('1.16-pre1', 721, True),
|
||||
Version('1.16-pre2', 722, True),
|
||||
Version('1.16-pre3', 725, True),
|
||||
Version('1.16-pre4', 727, True),
|
||||
Version('1.16-pre5', 729, True),
|
||||
Version('1.16-pre6', 730, True),
|
||||
Version('1.16-pre7', 732, True),
|
||||
Version('1.16-pre8', 733, True),
|
||||
Version('1.16-rc1', 734, True),
|
||||
Version('1.16', 735, True),
|
||||
Version('1.16.1', 736, True),
|
||||
Version('20w27a', 738, True),
|
||||
Version('20w28a', 740, True),
|
||||
Version('20w29a', 741, True),
|
||||
Version('20w30a', 743, True),
|
||||
Version('1.16.2-pre1', 744, True),
|
||||
Version('1.16.2-pre2', 746, True),
|
||||
Version('1.16.2-pre3', 748, True),
|
||||
Version('1.16.2-rc1', 749, True),
|
||||
Version('1.16.2-rc2', 750, True),
|
||||
Version('1.16.2', 751, True),
|
||||
Version('1.16.3-rc1', 752, True),
|
||||
Version('1.16.3', 753, True),
|
||||
Version('1.16.4-pre1', PRE | 1, True),
|
||||
Version('1.16.4-pre2', PRE | 2, True),
|
||||
Version('1.16.4-rc1', PRE | 3, True),
|
||||
Version('1.16.4', 754, True),
|
||||
Version('20w45a', PRE | 5, True),
|
||||
Version('20w46a', PRE | 6, True),
|
||||
Version('20w48a', PRE | 7, True),
|
||||
Version('20w49a', PRE | 8, False),
|
||||
Version('20w51a', PRE | 9, False),
|
||||
Version('1.16.5', 754, True),
|
||||
Version('21w03a', PRE | 11, False),
|
||||
Version('21w05a', PRE | 12, False),
|
||||
Version('21w05b', PRE | 13, False),
|
||||
Version('21w06a', PRE | 14, False),
|
||||
Version('21w07a', PRE | 15, False),
|
||||
Version('1.17-rc2', PRE | 35, False),
|
||||
Version('1.17', 755, True),
|
||||
Version('1.17.1', 756, True),
|
||||
Version('21w44a', PRE | 48, False),
|
||||
Version('1.18-rc4', PRE | 60, False),
|
||||
Version('1.18', 757, True),
|
||||
Version('1.18.1', 757, True),
|
||||
]
|
||||
|
||||
# An OrderedDict mapping the id string of each known Minecraft version to its
|
||||
# protocol version number, in chronological order of release.
|
||||
KNOWN_MINECRAFT_VERSIONS = OrderedDict()
|
||||
|
||||
# As KNOWN_MINECRAFT_VERSIONS, but only contains versions supported by pyCraft.
|
||||
SUPPORTED_MINECRAFT_VERSIONS = OrderedDict()
|
||||
|
||||
# As SUPPORTED_MINECRAFT_VERSIONS, but only contains release versions.
|
||||
RELEASE_MINECRAFT_VERSIONS = OrderedDict()
|
||||
|
||||
# A list of the protocol version numbers in KNOWN_MINECRAFT_VERSIONS
|
||||
# in the same order (chronological) but without duplicates.
|
||||
KNOWN_PROTOCOL_VERSIONS = []
|
||||
|
||||
# A list of the protocol version numbers in SUPPORTED_MINECRAFT_VERSIONS
|
||||
# in the same order (chronological) but without duplicates.
|
||||
SUPPORTED_PROTOCOL_VERSIONS = []
|
||||
|
||||
# A list of the protocol version numbers in RELEASE_MINECRAFT_VERSIONS
|
||||
# in the same order (chronological) but without duplicates.
|
||||
RELEASE_PROTOCOL_VERSIONS = []
|
||||
|
||||
# A dict mapping each protocol version number in KNOWN_PROTOCOL_VERSIONS to
|
||||
# its index within this list (used for efficient comparison of protocol
|
||||
# versions according to chronological release order).
|
||||
PROTOCOL_VERSION_INDICES = {}
|
||||
|
||||
|
||||
def initglobals(use_known_records=False):
|
||||
'''Initialise the above global variables, using
|
||||
'SUPPORTED_MINECRAFT_VERSIONS' as the source if 'use_known_records' is
|
||||
False (for backward compatibility, this is the default behaviour), or
|
||||
otherwise using 'KNOWN_MINECRAFT_VERSION_RECORDS' as the source.
|
||||
|
||||
This allows 'SUPPORTED_MINECRAFT_VERSIONS' or, respectively,
|
||||
'KNOWN_MINECRAFT_VERSION_RECORDS' to be updated by the library user
|
||||
during runtime and then the derived data to be updated as well, to allow
|
||||
for dynamic version support. All updates are done by reference to allow
|
||||
this to work elsewhere in the code.
|
||||
'''
|
||||
if use_known_records:
|
||||
# Update the variables that depend on KNOWN_MINECRAFT_VERSION_RECORDS.
|
||||
KNOWN_MINECRAFT_VERSIONS.clear()
|
||||
KNOWN_PROTOCOL_VERSIONS.clear()
|
||||
SUPPORTED_MINECRAFT_VERSIONS.clear()
|
||||
PROTOCOL_VERSION_INDICES.clear()
|
||||
for version in KNOWN_MINECRAFT_VERSION_RECORDS:
|
||||
KNOWN_MINECRAFT_VERSIONS[version.id] = version.protocol
|
||||
if version.protocol not in KNOWN_PROTOCOL_VERSIONS:
|
||||
PROTOCOL_VERSION_INDICES[version.protocol] \
|
||||
= len(KNOWN_PROTOCOL_VERSIONS)
|
||||
KNOWN_PROTOCOL_VERSIONS.append(version.protocol)
|
||||
if version.supported:
|
||||
SUPPORTED_MINECRAFT_VERSIONS[version.id] = version.protocol
|
||||
|
||||
# Update the variables that depend on SUPPORTED_MINECRAFT_VERSIONS.
|
||||
SUPPORTED_PROTOCOL_VERSIONS.clear()
|
||||
RELEASE_MINECRAFT_VERSIONS.clear()
|
||||
RELEASE_PROTOCOL_VERSIONS.clear()
|
||||
for (version_id, protocol) in SUPPORTED_MINECRAFT_VERSIONS.items():
|
||||
if re.match(r'\d+(\.\d+)+$', version_id):
|
||||
RELEASE_MINECRAFT_VERSIONS[version_id] = protocol
|
||||
if protocol not in RELEASE_PROTOCOL_VERSIONS:
|
||||
RELEASE_PROTOCOL_VERSIONS.append(protocol)
|
||||
if protocol not in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
SUPPORTED_PROTOCOL_VERSIONS.append(protocol)
|
||||
|
||||
|
||||
initglobals(use_known_records=True)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
"""
|
||||
Handles authentication with the Mojang authentication server.
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
from .exceptions import YggdrasilError
|
||||
|
||||
#: The base url for Ygdrassil requests
|
||||
AUTH_SERVER = "https://authserver.mojang.com"
|
||||
SESSION_SERVER = "https://sessionserver.mojang.com/session/minecraft"
|
||||
# Need this content type, or authserver will complain
|
||||
|
@ -86,7 +85,7 @@ class AuthenticationToken(object):
|
|||
|
||||
return True
|
||||
|
||||
def authenticate(self, username, password):
|
||||
def authenticate(self, username, password, invalidate_previous=False):
|
||||
"""
|
||||
Authenticates the user against https://authserver.mojang.com using
|
||||
`username` and `password` parameters.
|
||||
|
@ -95,6 +94,8 @@ class AuthenticationToken(object):
|
|||
username - An `str` object with the username (unmigrated accounts)
|
||||
or email address for a Mojang account.
|
||||
password - An `str` object with the password.
|
||||
invalidate_previous - A `bool`. When `True`, invalidate
|
||||
all previously acquired `access_token`s across all clients.
|
||||
|
||||
Returns:
|
||||
Returns `True` if successful.
|
||||
|
@ -112,11 +113,17 @@ class AuthenticationToken(object):
|
|||
"password": password
|
||||
}
|
||||
|
||||
req = _make_request(AUTH_SERVER, "authenticate", payload)
|
||||
if not invalidate_previous:
|
||||
# Include a `client_token` in the payload to prevent existing
|
||||
# `access_token`s from being invalidated. If `self.client_token`
|
||||
# is `None` generate a `client_token` using uuid4
|
||||
payload["clientToken"] = self.client_token or uuid.uuid4().hex
|
||||
|
||||
_raise_from_request(req)
|
||||
res = _make_request(AUTH_SERVER, "authenticate", payload)
|
||||
|
||||
json_resp = req.json()
|
||||
_raise_from_response(res)
|
||||
|
||||
json_resp = res.json()
|
||||
|
||||
self.username = username
|
||||
self.access_token = json_resp["accessToken"]
|
||||
|
@ -147,13 +154,13 @@ class AuthenticationToken(object):
|
|||
if self.client_token is None:
|
||||
raise ValueError("'client_token' is not set!")
|
||||
|
||||
req = _make_request(AUTH_SERVER,
|
||||
res = _make_request(AUTH_SERVER,
|
||||
"refresh", {"accessToken": self.access_token,
|
||||
"clientToken": self.client_token})
|
||||
|
||||
_raise_from_request(req)
|
||||
_raise_from_response(res)
|
||||
|
||||
json_resp = req.json()
|
||||
json_resp = res.json()
|
||||
|
||||
self.access_token = json_resp["accessToken"]
|
||||
self.client_token = json_resp["clientToken"]
|
||||
|
@ -179,14 +186,12 @@ class AuthenticationToken(object):
|
|||
if self.access_token is None:
|
||||
raise ValueError("'access_token' not set!")
|
||||
|
||||
req = _make_request(AUTH_SERVER, "validate",
|
||||
res = _make_request(AUTH_SERVER, "validate",
|
||||
{"accessToken": self.access_token})
|
||||
|
||||
# Validate returns 204 to indicate success
|
||||
# http://wiki.vg/Authentication#Response_3
|
||||
if req.status_code == 204:
|
||||
return True
|
||||
if _raise_from_request(req) is None:
|
||||
if res.status_code == 204:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
|
@ -206,10 +211,10 @@ class AuthenticationToken(object):
|
|||
Raises:
|
||||
minecraft.exceptions.YggdrasilError
|
||||
"""
|
||||
req = _make_request(AUTH_SERVER, "signout",
|
||||
res = _make_request(AUTH_SERVER, "signout",
|
||||
{"username": username, "password": password})
|
||||
|
||||
if _raise_from_request(req) is None:
|
||||
if _raise_from_response(res) is None:
|
||||
return True
|
||||
|
||||
def invalidate(self):
|
||||
|
@ -223,14 +228,13 @@ class AuthenticationToken(object):
|
|||
Raises:
|
||||
:class:`minecraft.exceptions.YggdrasilError`
|
||||
"""
|
||||
req = _make_request(AUTH_SERVER, "invalidate",
|
||||
res = _make_request(AUTH_SERVER, "invalidate",
|
||||
{"accessToken": self.access_token,
|
||||
"clientToken": self.client_token})
|
||||
|
||||
if not req.raise_for_status() and not req.text:
|
||||
return True
|
||||
else:
|
||||
raise YggdrasilError("Failed to invalidate tokens.")
|
||||
if res.status_code != 204:
|
||||
_raise_from_response(res)
|
||||
return True
|
||||
|
||||
def join(self, server_id):
|
||||
"""
|
||||
|
@ -251,15 +255,14 @@ class AuthenticationToken(object):
|
|||
err = "AuthenticationToken hasn't been authenticated yet!"
|
||||
raise YggdrasilError(err)
|
||||
|
||||
req = _make_request(SESSION_SERVER, "join",
|
||||
res = _make_request(SESSION_SERVER, "join",
|
||||
{"accessToken": self.access_token,
|
||||
"selectedProfile": self.profile.to_dict(),
|
||||
"serverId": server_id})
|
||||
|
||||
if not req.raise_for_status():
|
||||
return True
|
||||
else:
|
||||
_raise_from_request(req)
|
||||
if res.status_code != 204:
|
||||
_raise_from_response(res)
|
||||
return True
|
||||
|
||||
|
||||
def _make_request(server, endpoint, data):
|
||||
|
@ -274,31 +277,39 @@ def _make_request(server, endpoint, data):
|
|||
Returns:
|
||||
A `requests.Request` object.
|
||||
"""
|
||||
req = requests.post(server + "/" + endpoint, data=json.dumps(data),
|
||||
headers=HEADERS)
|
||||
return req
|
||||
res = requests.post(server + "/" + endpoint, data=json.dumps(data),
|
||||
headers=HEADERS, timeout=15)
|
||||
return res
|
||||
|
||||
|
||||
def _raise_from_request(req):
|
||||
def _raise_from_response(res):
|
||||
"""
|
||||
Raises an appropriate `YggdrasilError` based on the `status_code` and
|
||||
`json` of a `requests.Request` object.
|
||||
"""
|
||||
if req.status_code == requests.codes['ok']:
|
||||
if res.status_code == requests.codes['ok']:
|
||||
return None
|
||||
|
||||
exception = YggdrasilError()
|
||||
exception.status_code = res.status_code
|
||||
|
||||
try:
|
||||
json_resp = req.json()
|
||||
|
||||
if "error" not in json_resp and "errorMessage" not in json_resp:
|
||||
raise YggdrasilError("Malformed error message.")
|
||||
|
||||
json_resp = res.json()
|
||||
if not ("error" in json_resp and "errorMessage" in json_resp):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
message = "[{status_code}] Malformed error message: '{response_text}'"
|
||||
message = message.format(status_code=str(res.status_code),
|
||||
response_text=res.text)
|
||||
exception.args = (message,)
|
||||
else:
|
||||
message = "[{status_code}] {error}: '{error_message}'"
|
||||
message = message.format(status_code=str(req.status_code),
|
||||
message = message.format(status_code=str(res.status_code),
|
||||
error=json_resp["error"],
|
||||
error_message=json_resp["errorMessage"])
|
||||
except ValueError:
|
||||
message = "Unknown requests error. Status code: {}"
|
||||
message.format(str(req.status_code))
|
||||
exception.args = (message,)
|
||||
exception.yggdrasil_error = json_resp["error"]
|
||||
exception.yggdrasil_message = json_resp["errorMessage"]
|
||||
exception.yggdrasil_cause = json_resp.get("cause")
|
||||
|
||||
raise YggdrasilError(message)
|
||||
raise exception
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
"""
|
||||
This module stores code used for making pyCraft compatible with
|
||||
both Python2 and Python3 while using the same codebase.
|
||||
"""
|
||||
|
||||
# Raw input -> input shenangians
|
||||
# example
|
||||
# > from minecraft.compat import input
|
||||
# > input("asd")
|
||||
|
||||
# Hi, I'm pylint, and sometimes I act silly, at which point my programmer
|
||||
# overlords need to correct me.
|
||||
|
||||
# pylint: disable=undefined-variable,redefined-builtin,invalid-name
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
input = input
|
||||
|
||||
try:
|
||||
unicode = unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
# pylint: enable=undefined-variable,redefined-builtin,invalid-name
|
|
@ -6,8 +6,79 @@ Contains the `Exceptions` used by this library.
|
|||
class YggdrasilError(Exception):
|
||||
"""
|
||||
Base `Exception` for the Yggdrasil authentication service.
|
||||
|
||||
:param str message: A human-readable string representation of the error.
|
||||
:param int status_code: Initial value of :attr:`status_code`.
|
||||
:param str yggdrasil_error: Initial value of :attr:`yggdrasil_error`.
|
||||
:param str yggdrasil_message: Initial value of :attr:`yggdrasil_message`.
|
||||
:param str yggdrasil_cause: Initial value of :attr:`yggdrasil_cause`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message=None,
|
||||
status_code=None,
|
||||
yggdrasil_error=None,
|
||||
yggdrasil_message=None,
|
||||
yggdrasil_cause=None,
|
||||
):
|
||||
super(YggdrasilError, self).__init__(message)
|
||||
self.status_code = status_code
|
||||
self.yggdrasil_error = yggdrasil_error
|
||||
self.yggdrasil_message = yggdrasil_message
|
||||
self.yggdrasil_cause = yggdrasil_cause
|
||||
|
||||
status_code = None
|
||||
"""`int` or `None`. The associated HTTP status code. May be set."""
|
||||
|
||||
yggdrasil_error = None
|
||||
"""`str` or `None`. The `"error"` field of the Yggdrasil response: a short
|
||||
description such as `"Method Not Allowed"` or
|
||||
`"ForbiddenOperationException"`. May be set.
|
||||
"""
|
||||
|
||||
yggdrasil_message = None
|
||||
"""`str` or `None`. The `"errorMessage"` field of the Yggdrasil response:
|
||||
a longer description such as `"Invalid credentials. Invalid username or
|
||||
password."`. May be set.
|
||||
"""
|
||||
|
||||
yggdrasil_cause = None
|
||||
"""`str` or `None`. The `"cause"` field of the Yggdrasil response: a string
|
||||
containing additional information about the error. May be set.
|
||||
"""
|
||||
|
||||
|
||||
class VersionMismatch(Exception):
|
||||
pass
|
||||
class ConnectionFailure(Exception):
|
||||
"""Raised by 'minecraft.networking.Connection' when a connection attempt
|
||||
fails.
|
||||
"""
|
||||
|
||||
|
||||
class VersionMismatch(ConnectionFailure):
|
||||
"""Raised by 'minecraft.networking.Connection' when connection is not
|
||||
possible due to a difference between the server's and client's
|
||||
supported protocol versions.
|
||||
"""
|
||||
|
||||
|
||||
class LoginDisconnect(ConnectionFailure):
|
||||
"""Raised by 'minecraft.networking.Connection' when a connection attempt
|
||||
is terminated by the server sending a Disconnect packet, during login,
|
||||
with an unknown message format.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidState(ConnectionFailure):
|
||||
"""Raised by 'minecraft.networking.Connection' when a connection attempt
|
||||
fails due to to the internal state of the Connection being unsuitable,
|
||||
for example if there is an existing ongoing connection.
|
||||
"""
|
||||
|
||||
|
||||
class IgnorePacket(Exception):
|
||||
"""This exception may be raised from within a packet handler, such as
|
||||
`PacketReactor.react' or a packet listener added with
|
||||
`Connection.register_packet_listener', to stop any subsequent handlers
|
||||
from being called on that particular packet.
|
||||
"""
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
from __future__ import print_function
|
||||
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
from zlib import decompress
|
||||
from threading import RLock
|
||||
import zlib
|
||||
import threading
|
||||
import socket
|
||||
import time
|
||||
import timeit
|
||||
import select
|
||||
import sys
|
||||
import json
|
||||
|
||||
from future.utils import raise_
|
||||
import re
|
||||
|
||||
from .types import VarInt
|
||||
from . import packets
|
||||
from . import encryption
|
||||
from .. import SUPPORTED_PROTOCOL_VERSIONS
|
||||
from .. import SUPPORTED_MINECRAFT_VERSIONS
|
||||
from ..exceptions import VersionMismatch
|
||||
from .packets import clientbound, serverbound
|
||||
from . import packets, encryption
|
||||
from .. import (
|
||||
utility, KNOWN_MINECRAFT_VERSIONS, SUPPORTED_MINECRAFT_VERSIONS,
|
||||
SUPPORTED_PROTOCOL_VERSIONS, PROTOCOL_VERSION_INDICES
|
||||
)
|
||||
from ..exceptions import (
|
||||
VersionMismatch, LoginDisconnect, IgnorePacket, InvalidState
|
||||
)
|
||||
|
||||
|
||||
STATE_STATUS = 1
|
||||
|
@ -33,6 +33,33 @@ class ConnectionContext(object):
|
|||
def __init__(self, **kwds):
|
||||
self.protocol_version = kwds.get('protocol_version')
|
||||
|
||||
def protocol_earlier(self, other_pv):
|
||||
"""Returns True if the protocol version of this context was published
|
||||
earlier than 'other_pv', or else False."""
|
||||
return utility.protocol_earlier(self.protocol_version, other_pv)
|
||||
|
||||
def protocol_earlier_eq(self, other_pv):
|
||||
"""Returns True if the protocol version of this context was published
|
||||
earlier than, or is equal to, 'other_pv', or else False."""
|
||||
return utility.protocol_earlier_eq(self.protocol_version, other_pv)
|
||||
|
||||
def protocol_later(self, other_pv):
|
||||
"""Returns True if the protocol version of this context was published
|
||||
later than 'other_pv', or else False."""
|
||||
return utility.protocol_earlier(other_pv, self.protocol_version)
|
||||
|
||||
def protocol_later_eq(self, other_pv):
|
||||
"""Returns True if the protocol version of this context was published
|
||||
later than, or is equal to, 'other_pv', or else False."""
|
||||
return utility.protocol_earlier_eq(other_pv, self.protocol_version)
|
||||
|
||||
def protocol_in_range(self, start_pv, end_pv):
|
||||
"""Returns True if the protocol version of this context was published
|
||||
later than, or is equal to, 'start_pv' and was published earlier
|
||||
than 'end_pv' (analogously to Python's 'range' function)."""
|
||||
return (utility.protocol_earlier(self.protocol_version, end_pv) and
|
||||
utility.protocol_earlier_eq(start_pv, self.protocol_version))
|
||||
|
||||
|
||||
class _ConnectionOptions(object):
|
||||
def __init__(self, address=None, port=None, compression_threshold=-1,
|
||||
|
@ -57,6 +84,7 @@ class Connection(object):
|
|||
initial_version=None,
|
||||
allowed_versions=None,
|
||||
handle_exception=None,
|
||||
handle_exit=None,
|
||||
):
|
||||
"""Sets up an instance of this object to be able to connect to a
|
||||
minecraft server.
|
||||
|
@ -66,35 +94,52 @@ class Connection(object):
|
|||
|
||||
:param address: address of the server to connect to
|
||||
:param port(int): port of the server to connect to
|
||||
:param auth_token: :class:`authentication.AuthenticationToken` object.
|
||||
If None, no authentication is attempted and the
|
||||
server is assumed to be running in offline mode.
|
||||
:param auth_token: :class:`minecraft.authentication.AuthenticationToken`
|
||||
object. If None, no authentication is attempted and
|
||||
the server is assumed to be running in offline mode.
|
||||
:param username: Username string; only applicable in offline mode.
|
||||
:param initial_version: A Minecraft version string or protocol version
|
||||
number to use if the server's protocol version
|
||||
cannot be determined. (Although it is now
|
||||
somewhat inaccurate, this name is retained for
|
||||
backward compatibility.)
|
||||
:param initial_version: A Minecraft version ID string or protocol
|
||||
version number to use if the server's protocol
|
||||
version cannot be determined. (Although it is
|
||||
now somewhat inaccurate, this name is retained
|
||||
for backward compatibility.)
|
||||
:param allowed_versions: A set of versions, each being a Minecraft
|
||||
version string or protocol version number,
|
||||
version ID string or protocol version number,
|
||||
restricting the versions that the client may
|
||||
use in connecting to the server.
|
||||
:param handle_exception: A function to be called when an exception
|
||||
occurs in the client's networking thread,
|
||||
taking 2 arguments: the exception object `e'
|
||||
as in `except Exception as e', and a 3-tuple
|
||||
given by sys.exc_info(); or None for the
|
||||
default behaviour of raising the exception
|
||||
from its original context; or False for no
|
||||
action. In any case, the networking thread
|
||||
will terminate, the exception will be
|
||||
available via the `exception' and `exc_info'
|
||||
attributes of the `Connection' instance.
|
||||
"""
|
||||
:param handle_exception: The final exception handler. This is triggered
|
||||
when an exception occurs in the networking
|
||||
thread that is not caught normally. After
|
||||
any other user-registered exception handlers
|
||||
are run, the final exception (which may be the
|
||||
original exception or one raised by another
|
||||
handler) is passed, regardless of whether or
|
||||
not it was caught by another handler, to the
|
||||
final handler, which may be a function obeying
|
||||
the protocol of 'register_exception_handler';
|
||||
the value 'None', meaning that if the
|
||||
exception was otherwise uncaught, it is
|
||||
re-raised from the networking thread after
|
||||
closing the connection; or the value 'False',
|
||||
meaning that the exception is never re-raised.
|
||||
:param handle_exit: A function to be called when a connection to a
|
||||
server terminates, not caused by an exception,
|
||||
and not with the intention to automatically
|
||||
reconnect. Exceptions raised from this function
|
||||
will be handled by any matching exception handlers.
|
||||
""" # NOQA
|
||||
|
||||
# This lock is re-entrant because it may be acquired in a re-entrant
|
||||
# manner from within an outgoing packet
|
||||
self._write_lock = RLock()
|
||||
|
||||
self._write_lock = Lock()
|
||||
self.networking_thread = None
|
||||
self.new_networking_thread = None
|
||||
self.packet_listeners = []
|
||||
self.early_packet_listeners = []
|
||||
self.outgoing_packet_listeners = []
|
||||
self.early_outgoing_packet_listeners = []
|
||||
self._exception_handlers = []
|
||||
|
||||
def proto_version(version):
|
||||
if isinstance(version, str):
|
||||
|
@ -113,36 +158,47 @@ class Connection(object):
|
|||
allowed_versions = set(map(proto_version, allowed_versions))
|
||||
self.allowed_proto_versions = allowed_versions
|
||||
|
||||
latest_allowed_proto = max(self.allowed_proto_versions,
|
||||
key=PROTOCOL_VERSION_INDICES.get)
|
||||
|
||||
if initial_version is None:
|
||||
self.default_proto_version = max(self.allowed_proto_versions)
|
||||
self.default_proto_version = latest_allowed_proto
|
||||
else:
|
||||
self.default_proto_version = proto_version(initial_version)
|
||||
|
||||
self.context = ConnectionContext(
|
||||
protocol_version=max(self.allowed_proto_versions))
|
||||
self.context = ConnectionContext(protocol_version=latest_allowed_proto)
|
||||
|
||||
self.options = _ConnectionOptions()
|
||||
self.options.address = address
|
||||
self.options.port = port
|
||||
self.auth_token = auth_token
|
||||
self.username = username
|
||||
self.connected = False
|
||||
|
||||
self.handle_exception = handle_exception
|
||||
self.exception, self.exc_info = None, None
|
||||
self.handle_exit = handle_exit
|
||||
|
||||
# The reactor handles all the default responses to packets,
|
||||
# it should be changed per networking state
|
||||
self.reactor = PacketReactor(self)
|
||||
|
||||
def _start_network_thread(self):
|
||||
"""May safely be called multiple times."""
|
||||
if self.networking_thread is None:
|
||||
self.networking_thread = NetworkingThread(self)
|
||||
self.networking_thread.start()
|
||||
elif self.networking_thread.interrupt:
|
||||
# This thread will wait until the previous thread exits, and then
|
||||
# set `networking_thread' to itself.
|
||||
NetworkingThread(self, previous=self.networking_thread).start()
|
||||
with self._write_lock:
|
||||
if self.networking_thread is not None and \
|
||||
not self.networking_thread.interrupt or \
|
||||
self.new_networking_thread is not None:
|
||||
raise InvalidState('A networking thread is already running.')
|
||||
elif self.networking_thread is None:
|
||||
self.networking_thread = NetworkingThread(self)
|
||||
self.networking_thread.start()
|
||||
else:
|
||||
# This thread will wait until the existing thread exits, and
|
||||
# then set 'networking_thread' to itself and
|
||||
# 'new_networking_thread' to None.
|
||||
self.new_networking_thread \
|
||||
= NetworkingThread(self, previous=self.networking_thread)
|
||||
self.new_networking_thread.start()
|
||||
|
||||
def write_packet(self, packet, force=False):
|
||||
"""Writes a packet to the server.
|
||||
|
@ -158,24 +214,104 @@ class Connection(object):
|
|||
"""
|
||||
packet.context = self.context
|
||||
if force:
|
||||
self._write_lock.acquire()
|
||||
if self.options.compression_enabled:
|
||||
packet.write(self.socket, self.options.compression_threshold)
|
||||
else:
|
||||
packet.write(self.socket)
|
||||
self._write_lock.release()
|
||||
with self._write_lock:
|
||||
self._write_packet(packet)
|
||||
else:
|
||||
self._outgoing_packet_queue.append(packet)
|
||||
|
||||
def register_packet_listener(self, method, *args):
|
||||
def listener(self, *packet_types, **kwds):
|
||||
"""
|
||||
Shorthand decorator to register a function as a packet listener.
|
||||
|
||||
Wraps :meth:`minecraft.networking.connection.register_packet_listener`
|
||||
:param packet_types: Packet types to listen for.
|
||||
:param kwds: Keyword arguments for `register_packet_listener`
|
||||
"""
|
||||
def listener_decorator(handler_func):
|
||||
self.register_packet_listener(handler_func, *packet_types, **kwds)
|
||||
return handler_func
|
||||
|
||||
return listener_decorator
|
||||
|
||||
def exception_handler(self, *exc_types, **kwds):
|
||||
"""
|
||||
Shorthand decorator to register a function as an exception handler.
|
||||
"""
|
||||
def exception_handler_decorator(handler_func):
|
||||
self.register_exception_handler(handler_func, *exc_types, **kwds)
|
||||
return handler_func
|
||||
|
||||
return exception_handler_decorator
|
||||
|
||||
def register_packet_listener(self, method, *packet_types, **kwds):
|
||||
"""
|
||||
Registers a listener method which will be notified when a packet of
|
||||
a selected type is received
|
||||
a selected type is received.
|
||||
|
||||
If :class:`minecraft.networking.connection.IgnorePacket` is raised from
|
||||
within this method, no subsequent handlers will be called. If
|
||||
'early=True', this has the additional effect of preventing the default
|
||||
in-built action; this could break the internal state of the
|
||||
'Connection', so should be done with care. If, in addition,
|
||||
'outgoing=True', this will prevent the packet from being written to the
|
||||
network.
|
||||
|
||||
:param method: The method which will be called back with the packet
|
||||
:param args: The packets to listen for
|
||||
:param packet_types: The packets to listen for
|
||||
:param outgoing: If 'True', this listener will be called on outgoing
|
||||
packets just after they are sent to the server, rather
|
||||
than on incoming packets.
|
||||
:param early: If 'True', this listener will be called before any
|
||||
built-in default action is carried out, and before any
|
||||
listeners with 'early=False' are called. If
|
||||
'outgoing=True', the listener will be called before the
|
||||
packet is written to the network, rather than afterwards.
|
||||
"""
|
||||
self.packet_listeners.append(packets.PacketListener(method, *args))
|
||||
outgoing = kwds.pop('outgoing', False)
|
||||
early = kwds.pop('early', False)
|
||||
target = self.packet_listeners if not early and not outgoing \
|
||||
else self.early_packet_listeners if early and not outgoing \
|
||||
else self.outgoing_packet_listeners if not early \
|
||||
else self.early_outgoing_packet_listeners
|
||||
target.append(packets.PacketListener(method, *packet_types, **kwds))
|
||||
|
||||
def register_exception_handler(self, handler_func, *exc_types, **kwds):
|
||||
"""
|
||||
Register a function to be called when an unhandled exception occurs
|
||||
in the networking thread.
|
||||
|
||||
When multiple exception handlers are registered, they act like 'except'
|
||||
clauses in a Python 'try' clause, with the earliest matching handler
|
||||
catching the exception, and any later handlers catching any uncaught
|
||||
exception raised from within an earlier handler.
|
||||
|
||||
Regardless of the presence or absence of matching handlers, any such
|
||||
exception will cause the connection and the networking thread to
|
||||
terminate, the final exception handler will be called (see the
|
||||
'handle_exception' argument of the 'Connection' contructor), and the
|
||||
original exception - or the last exception raised by a handler - will
|
||||
be set as the 'exception' and 'exc_info' attributes of the
|
||||
'Connection'.
|
||||
|
||||
:param handler_func: A function taking two arguments: the exception
|
||||
object 'e' as in 'except Exception as e:', and the corresponding
|
||||
3-tuple given by 'sys.exc_info()'. The return value of the function is
|
||||
ignored, but any exception raised in it replaces the original
|
||||
exception, and may be passed to later exception handlers.
|
||||
|
||||
:param exc_types: The types of exceptions that this handler shall
|
||||
catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is
|
||||
empty, the handler will catch all exceptions.
|
||||
|
||||
:param early: If 'True', the exception handler is registered before
|
||||
any existing exception handlers in the handling order.
|
||||
"""
|
||||
early = kwds.pop('early', False)
|
||||
assert not kwds, 'Unexpected keyword arguments: %r' % (kwds,)
|
||||
if early:
|
||||
self._exception_handlers.insert(0, (handler_func, exc_types))
|
||||
else:
|
||||
self._exception_handlers.append((handler_func, exc_types))
|
||||
|
||||
def _pop_packet(self):
|
||||
# Pops the topmost packet off the outgoing queue and writes it out
|
||||
|
@ -189,12 +325,25 @@ class Connection(object):
|
|||
if len(self._outgoing_packet_queue) == 0:
|
||||
return False
|
||||
else:
|
||||
packet = self._outgoing_packet_queue.popleft()
|
||||
self._write_packet(self._outgoing_packet_queue.popleft())
|
||||
return True
|
||||
|
||||
def _write_packet(self, packet):
|
||||
# Immediately writes the given packet to the network. The caller must
|
||||
# have the write lock acquired before calling this method.
|
||||
try:
|
||||
for listener in self.early_outgoing_packet_listeners:
|
||||
listener.call_packet(packet)
|
||||
|
||||
if self.options.compression_enabled:
|
||||
packet.write(self.socket, self.options.compression_threshold)
|
||||
else:
|
||||
packet.write(self.socket)
|
||||
return True
|
||||
|
||||
for listener in self.outgoing_packet_listeners:
|
||||
listener.call_packet(packet)
|
||||
except IgnorePacket:
|
||||
pass
|
||||
|
||||
def status(self, handle_status=None, handle_ping=False):
|
||||
"""Issue a status request to the server and then disconnect.
|
||||
|
@ -208,24 +357,28 @@ class Connection(object):
|
|||
which prints the latency to standard outout, or
|
||||
False, to prevent measurement of the latency.
|
||||
"""
|
||||
self._connect()
|
||||
self._handshake(next_state=STATE_STATUS)
|
||||
self._start_network_thread()
|
||||
with self._write_lock: # pylint: disable=not-context-manager
|
||||
self._check_connection()
|
||||
|
||||
self.reactor = StatusReactor(self, do_ping=handle_ping is not False)
|
||||
self._connect()
|
||||
self._handshake(next_state=STATE_STATUS)
|
||||
self._start_network_thread()
|
||||
|
||||
if handle_status is False:
|
||||
self.reactor.handle_status = lambda *args, **kwds: None
|
||||
elif handle_status is not None:
|
||||
self.reactor.handle_status = handle_status
|
||||
do_ping = handle_ping is not False
|
||||
self.reactor = StatusReactor(self, do_ping=do_ping)
|
||||
|
||||
if handle_ping is False:
|
||||
self.reactor.handle_ping = lambda *args, **kwds: None
|
||||
elif handle_ping is not None:
|
||||
self.reactor.handle_ping = handle_ping
|
||||
if handle_status is False:
|
||||
self.reactor.handle_status = lambda *args, **kwds: None
|
||||
elif handle_status is not None:
|
||||
self.reactor.handle_status = handle_status
|
||||
|
||||
request_packet = packets.RequestPacket()
|
||||
self.write_packet(request_packet)
|
||||
if handle_ping is False:
|
||||
self.reactor.handle_ping = lambda *args, **kwds: None
|
||||
elif handle_ping is not None:
|
||||
self.reactor.handle_ping = handle_ping
|
||||
|
||||
request_packet = serverbound.status.RequestPacket()
|
||||
self.write_packet(request_packet)
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
|
@ -234,12 +387,15 @@ class Connection(object):
|
|||
"""
|
||||
# Hold the lock throughout, in case connect() is called from the
|
||||
# networking thread while another connection is in progress.
|
||||
with self._write_lock:
|
||||
with self._write_lock: # pylint: disable=not-context-manager
|
||||
self._check_connection()
|
||||
|
||||
# It is important that this is set correctly even when connecting
|
||||
# in status mode, as some servers, e.g. SpigotMC with the
|
||||
# ProtocolSupport plugin, use it to determine the correct response.
|
||||
self.context.protocol_version = max(self.allowed_proto_versions)
|
||||
self.context.protocol_version \
|
||||
= max(self.allowed_proto_versions,
|
||||
key=PROTOCOL_VERSION_INDICES.get)
|
||||
|
||||
self.spawned = False
|
||||
self._connect()
|
||||
|
@ -248,7 +404,7 @@ class Connection(object):
|
|||
# process of determining the server's version, and immediately
|
||||
# connect.
|
||||
self._handshake(next_state=STATE_PLAYING)
|
||||
login_start_packet = packets.LoginStartPacket()
|
||||
login_start_packet = serverbound.login.LoginStartPacket()
|
||||
if self.auth_token:
|
||||
login_start_packet.name = self.auth_token.profile.name
|
||||
else:
|
||||
|
@ -259,10 +415,16 @@ class Connection(object):
|
|||
# Determine the server's protocol version by first performing a
|
||||
# status query.
|
||||
self._handshake(next_state=STATE_STATUS)
|
||||
self.write_packet(packets.RequestPacket())
|
||||
self.write_packet(serverbound.status.RequestPacket())
|
||||
self.reactor = PlayingStatusReactor(self)
|
||||
self._start_network_thread()
|
||||
|
||||
def _check_connection(self):
|
||||
if self.networking_thread is not None and \
|
||||
not self.networking_thread.interrupt or \
|
||||
self.new_networking_thread is not None:
|
||||
raise InvalidState('There is an existing connection.')
|
||||
|
||||
def _connect(self):
|
||||
# Connect a socket to the server and create a file object from the
|
||||
# socket.
|
||||
|
@ -271,33 +433,54 @@ class Connection(object):
|
|||
# the socket itself will mostly be used to write data upstream to
|
||||
# the server.
|
||||
self._outgoing_packet_queue = deque()
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect((self.options.address, self.options.port))
|
||||
self.file_object = self.socket.makefile("rb", 0)
|
||||
|
||||
def disconnect(self):
|
||||
""" Terminate the existing server connection, if there is one. """
|
||||
if self.networking_thread is not None:
|
||||
with self._write_lock:
|
||||
info = socket.getaddrinfo(self.options.address, self.options.port,
|
||||
0, socket.SOCK_STREAM)
|
||||
|
||||
# Prefer to use IPv4 (for backward compatibility with previous
|
||||
# versions that always resolved hostnames to IPv4 addresses),
|
||||
# then IPv6, then other address families.
|
||||
def key(ai):
|
||||
return 0 if ai[0] == socket.AF_INET else \
|
||||
1 if ai[0] == socket.AF_INET6 else 2
|
||||
ai_faml, ai_type, ai_prot, _ai_cnam, ai_addr = min(info, key=key)
|
||||
|
||||
self.socket = socket.socket(ai_faml, ai_type, ai_prot)
|
||||
self.socket.connect(ai_addr)
|
||||
self.file_object = self.socket.makefile("rb", 0)
|
||||
self.options.compression_enabled = False
|
||||
self.options.compression_threshold = -1
|
||||
self.connected = True
|
||||
|
||||
def disconnect(self, immediate=False):
|
||||
"""Terminate the existing server connection, if there is one.
|
||||
If 'immediate' is True, do not attempt to write any packets.
|
||||
"""
|
||||
with self._write_lock: # pylint: disable=not-context-manager
|
||||
self.connected = False
|
||||
|
||||
if not immediate and self.socket is not None:
|
||||
# Flush any packets remaining in the queue.
|
||||
while self._pop_packet():
|
||||
pass
|
||||
|
||||
if self.new_networking_thread is not None:
|
||||
self.new_networking_thread.interrupt = True
|
||||
elif self.networking_thread is not None:
|
||||
self.networking_thread.interrupt = True
|
||||
|
||||
if self.socket is not None:
|
||||
if hasattr(self.socket, 'actual_socket'):
|
||||
# pylint: disable=no-member
|
||||
actual_socket = self.socket.actual_socket
|
||||
else:
|
||||
actual_socket = self.socket
|
||||
|
||||
try:
|
||||
actual_socket.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
finally:
|
||||
actual_socket.close()
|
||||
self.socket = None
|
||||
if self.socket is not None:
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
finally:
|
||||
self.file_object.close()
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _handshake(self, next_state=STATE_PLAYING):
|
||||
handshake = packets.HandShakePacket()
|
||||
handshake = serverbound.handshake.HandShakePacket()
|
||||
handshake.protocol_version = self.context.protocol_version
|
||||
handshake.server_address = self.options.address
|
||||
handshake.server_port = self.options.port
|
||||
|
@ -306,22 +489,86 @@ class Connection(object):
|
|||
self.write_packet(handshake)
|
||||
|
||||
def _handle_exception(self, exc, exc_info):
|
||||
final_handler = self.handle_exception
|
||||
|
||||
# Call the current PacketReactor's exception handler.
|
||||
try:
|
||||
exc.exc_info = exc_info # For backward compatibility.
|
||||
if self.reactor.handle_exception(exc, exc_info):
|
||||
return
|
||||
except Exception as new_exc:
|
||||
exc, exc_info = new_exc, sys.exc_info()
|
||||
|
||||
# Call the user-registered exception handlers in order.
|
||||
for handler, exc_types in self._exception_handlers:
|
||||
if not exc_types or isinstance(exc, exc_types):
|
||||
try:
|
||||
handler(exc, exc_info)
|
||||
caught = True
|
||||
break
|
||||
except Exception as new_exc:
|
||||
exc, exc_info = new_exc, sys.exc_info()
|
||||
else:
|
||||
caught = False
|
||||
|
||||
# Call the user-specified final exception handler.
|
||||
if final_handler not in (None, False):
|
||||
try:
|
||||
final_handler(exc, exc_info)
|
||||
except Exception as new_exc:
|
||||
exc, exc_info = new_exc, sys.exc_info()
|
||||
|
||||
# For backward compatibility, try to set the 'exc_info' attribute.
|
||||
try:
|
||||
exc.exc_info = exc_info
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
if self.reactor.handle_exception(exc, exc_info):
|
||||
return
|
||||
|
||||
# Record the exception.
|
||||
self.exception, self.exc_info = exc, exc_info
|
||||
if self.handle_exception is None:
|
||||
raise_(*exc_info)
|
||||
elif self.handle_exception is not False:
|
||||
self.handle_exception(exc, exc_info)
|
||||
|
||||
# The following condition being false indicates that an exception
|
||||
# handler has initiated a new connection, meaning that we should not
|
||||
# interfere with the connection state. Otherwise, make sure that any
|
||||
# current connection is completely terminated.
|
||||
if (self.new_networking_thread or self.networking_thread).interrupt:
|
||||
self.disconnect(immediate=True)
|
||||
|
||||
# If allowed by the final exception handler, re-raise the exception.
|
||||
if final_handler is None and not caught:
|
||||
exc_value, exc_tb = exc_info[1:]
|
||||
raise exc_value.with_traceback(exc_tb)
|
||||
|
||||
def _version_mismatch(self, server_protocol=None, server_version=None):
|
||||
if server_protocol is None:
|
||||
server_protocol = KNOWN_MINECRAFT_VERSIONS.get(server_version)
|
||||
|
||||
if server_protocol is None:
|
||||
vs = 'version' if server_version is None else \
|
||||
('version of %s' % server_version)
|
||||
else:
|
||||
vs = ('protocol version of %d' % server_protocol) + \
|
||||
('' if server_version is None else ' (%s)' % server_version)
|
||||
ss = 'supported, but not allowed for this connection' \
|
||||
if server_protocol in SUPPORTED_PROTOCOL_VERSIONS \
|
||||
else 'not supported'
|
||||
err = VersionMismatch("Server's %s is %s." % (vs, ss))
|
||||
err.server_protocol = server_protocol
|
||||
err.server_version = server_version
|
||||
raise err
|
||||
|
||||
def _handle_exit(self):
|
||||
if not self.connected and self.handle_exit is not None:
|
||||
self.handle_exit()
|
||||
|
||||
def _react(self, packet):
|
||||
self.reactor.react(packet)
|
||||
try:
|
||||
for listener in self.early_packet_listeners:
|
||||
listener.call_packet(packet)
|
||||
self.reactor.react(packet)
|
||||
for listener in self.packet_listeners:
|
||||
listener.call_packet(packet)
|
||||
except IgnorePacket:
|
||||
pass
|
||||
|
||||
|
||||
class NetworkingThread(threading.Thread):
|
||||
|
@ -336,71 +583,62 @@ class NetworkingThread(threading.Thread):
|
|||
|
||||
def run(self):
|
||||
try:
|
||||
if self.previous_thread is not None:
|
||||
if self.previous_thread.is_alive():
|
||||
self.previous_thread.join()
|
||||
with self.connection._write_lock:
|
||||
self.connection.networking_thread = self
|
||||
self.connection.new_networking_thread = None
|
||||
self._run()
|
||||
except BaseException as e:
|
||||
self.connection._handle_exit()
|
||||
except Exception as e:
|
||||
self.interrupt = True
|
||||
self.connection._handle_exception(e, sys.exc_info())
|
||||
finally:
|
||||
self.connection.networking_thread = None
|
||||
with self.connection._write_lock:
|
||||
self.connection.networking_thread = None
|
||||
|
||||
def _run(self):
|
||||
if self.previous_thread is not None:
|
||||
if self.previous_thread.is_alive():
|
||||
self.previous_thread.join()
|
||||
self.previous_thread = None
|
||||
self.connection.networking_thread = self
|
||||
|
||||
while not self.interrupt:
|
||||
# Attempt to write out as many as 300 packets as possible every
|
||||
# 0.05 seconds (20 ticks per second)
|
||||
# Attempt to write out as many as 300 packets.
|
||||
num_packets = 0
|
||||
self.connection._write_lock.acquire()
|
||||
try:
|
||||
while self.connection._pop_packet():
|
||||
num_packets += 1
|
||||
if num_packets >= 300:
|
||||
break
|
||||
exc_info = None
|
||||
except:
|
||||
exc_info = sys.exc_info()
|
||||
self.connection._write_lock.release()
|
||||
with self.connection._write_lock:
|
||||
try:
|
||||
while not self.interrupt and self.connection._pop_packet():
|
||||
num_packets += 1
|
||||
if num_packets >= 300:
|
||||
break
|
||||
exc_info = None
|
||||
except IOError:
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
# Read and react to as many as 50 packets
|
||||
num_packets = 0
|
||||
# If any packets remain to be written, resume writing as soon
|
||||
# as possible after reading any available packets; otherwise,
|
||||
# wait for up to 50ms (1 tick) for new packets to arrive.
|
||||
if self.connection._outgoing_packet_queue:
|
||||
read_timeout = 0
|
||||
else:
|
||||
read_timeout = 0.05
|
||||
|
||||
# Read and react to as many as 50 packets.
|
||||
while num_packets < 50 and not self.interrupt:
|
||||
packet = self.connection.reactor.read_packet(
|
||||
self.connection.file_object)
|
||||
self.connection.file_object, timeout=read_timeout)
|
||||
if not packet:
|
||||
break
|
||||
num_packets += 1
|
||||
self.connection._react(packet)
|
||||
read_timeout = 0
|
||||
|
||||
# Do not raise an IOError if it occurred while a disconnect
|
||||
# packet was received, as this may be part of an orderly
|
||||
# disconnection.
|
||||
if packet.packet_name == 'disconnect' and \
|
||||
exc_info is not None and isinstance(exc_info[1], IOError):
|
||||
# Ignore the earlier exception if a disconnect packet is
|
||||
# received, as it may have been caused by trying to write to
|
||||
# the closed socket, which does not represent a program error.
|
||||
if exc_info is not None and packet.packet_name == "disconnect":
|
||||
exc_info = None
|
||||
|
||||
try:
|
||||
self.connection._react(packet)
|
||||
for listener in self.connection.packet_listeners:
|
||||
listener.call_packet(packet)
|
||||
except IgnorePacket:
|
||||
pass
|
||||
|
||||
if exc_info is not None:
|
||||
raise_(*exc_info)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
class IgnorePacket(Exception):
|
||||
"""
|
||||
This exception may be raised from within a packet handler, such as
|
||||
`PacketReactor.react' or a packet listener added with
|
||||
`Connection.register_packet_listener', to stop any subsequent handlers from
|
||||
being called on that particular packet.
|
||||
"""
|
||||
pass
|
||||
exc_value, exc_tb = exc_info[1:]
|
||||
raise exc_value.with_traceback(exc_tb)
|
||||
|
||||
|
||||
class PacketReactor(object):
|
||||
|
@ -408,9 +646,9 @@ class PacketReactor(object):
|
|||
Reads and reacts to packets
|
||||
"""
|
||||
state_name = None
|
||||
TIME_OUT = 0
|
||||
|
||||
get_clientbound_packets = staticmethod(lambda context: set())
|
||||
# Handshaking is considered the "default" state
|
||||
get_clientbound_packets = staticmethod(clientbound.handshake.get_packets)
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
@ -419,8 +657,10 @@ class PacketReactor(object):
|
|||
packet.get_id(context): packet
|
||||
for packet in self.__class__.get_clientbound_packets(context)}
|
||||
|
||||
def read_packet(self, stream):
|
||||
ready_to_read = select.select([stream], [], [], self.TIME_OUT)[0]
|
||||
def read_packet(self, stream, timeout=0):
|
||||
# Block for up to `timeout' seconds waiting for `stream' to become
|
||||
# readable, returning `None' if the timeout elapses.
|
||||
ready_to_read = select.select([stream], [], [], timeout)[0]
|
||||
|
||||
if ready_to_read:
|
||||
length = VarInt.read(stream)
|
||||
|
@ -436,7 +676,9 @@ class PacketReactor(object):
|
|||
if self.connection.options.compression_enabled:
|
||||
decompressed_size = VarInt.read(packet_data)
|
||||
if decompressed_size > 0:
|
||||
decompressed_packet = decompress(packet_data.read())
|
||||
decompressor = zlib.decompressobj()
|
||||
decompressed_packet = decompressor.decompress(
|
||||
packet_data.read())
|
||||
assert len(decompressed_packet) == decompressed_size, \
|
||||
'decompressed length %d, but expected %d' % \
|
||||
(len(decompressed_packet), decompressed_size)
|
||||
|
@ -447,30 +689,37 @@ class PacketReactor(object):
|
|||
packet_id = VarInt.read(packet_data)
|
||||
|
||||
# If we know the structure of the packet, attempt to parse it
|
||||
# otherwise just skip it
|
||||
# otherwise, just return an instance of the base Packet class.
|
||||
if packet_id in self.clientbound_packets:
|
||||
packet = self.clientbound_packets[packet_id]()
|
||||
packet.context = self.connection.context
|
||||
packet.read(packet_data)
|
||||
return packet
|
||||
else:
|
||||
return packets.Packet(context=self.connection.context)
|
||||
packet = packets.Packet()
|
||||
packet.context = self.connection.context
|
||||
packet.id = packet_id
|
||||
return packet
|
||||
else:
|
||||
return None
|
||||
|
||||
def react(self, packet):
|
||||
"""Called with each incoming packet after early packet listeners are
|
||||
run (if none of them raise 'IgnorePacket'), but before regular
|
||||
packet listeners are run. If this method raises 'IgnorePacket', no
|
||||
subsequent packet listeners will be called for this packet.
|
||||
"""
|
||||
raise NotImplementedError("Call to base reactor")
|
||||
|
||||
""" Called when an exception is raised in the networking thread. If this
|
||||
method returns True, the default action will be prevented and the
|
||||
exception ignored (but the networking thread will still terminate).
|
||||
"""
|
||||
def handle_exception(self, exc, exc_info):
|
||||
"""Called when an exception is raised in the networking thread. If this
|
||||
method returns True, the default action will be prevented and the
|
||||
exception ignored (but the networking thread will still terminate).
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
class LoginReactor(PacketReactor):
|
||||
get_clientbound_packets = staticmethod(packets.state_login_clientbound)
|
||||
get_clientbound_packets = staticmethod(clientbound.login.get_packets)
|
||||
|
||||
def react(self, packet):
|
||||
if packet.packet_name == "encryption request":
|
||||
|
@ -486,7 +735,7 @@ class LoginReactor(PacketReactor):
|
|||
if self.connection.auth_token is not None:
|
||||
self.connection.auth_token.join(server_id)
|
||||
|
||||
encryption_response = packets.EncryptionResponsePacket()
|
||||
encryption_response = serverbound.login.EncryptionResponsePacket()
|
||||
encryption_response.shared_secret = encrypted_secret
|
||||
encryption_response.verify_token = token
|
||||
|
||||
|
@ -504,37 +753,54 @@ class LoginReactor(PacketReactor):
|
|||
encryption.EncryptedFileObjectWrapper(
|
||||
self.connection.file_object, decryptor)
|
||||
|
||||
if packet.packet_name == "disconnect":
|
||||
self.connection.disconnect()
|
||||
elif packet.packet_name == "disconnect":
|
||||
# Receiving a disconnect packet in the login state indicates an
|
||||
# abnormal condition. Raise an exception explaining the situation.
|
||||
try:
|
||||
msg = json.loads(packet.json_data)['text']
|
||||
except (ValueError, TypeError, KeyError):
|
||||
msg = packet.json_data
|
||||
match = re.match(r"Outdated (client! Please use|server!"
|
||||
r" I'm still on) (?P<ver>\S+)$", msg)
|
||||
if match:
|
||||
ver = match.group('ver')
|
||||
self.connection._version_mismatch(server_version=ver)
|
||||
raise LoginDisconnect('The server rejected our login attempt '
|
||||
'with: "%s".' % msg)
|
||||
|
||||
if packet.packet_name == "login success":
|
||||
elif packet.packet_name == "login success":
|
||||
self.connection.reactor = PlayingReactor(self.connection)
|
||||
|
||||
if packet.packet_name == "set compression":
|
||||
elif packet.packet_name == "set compression":
|
||||
self.connection.options.compression_threshold = packet.threshold
|
||||
self.connection.options.compression_enabled = True
|
||||
|
||||
elif packet.packet_name == "login plugin request":
|
||||
self.connection.write_packet(
|
||||
serverbound.login.PluginResponsePacket(
|
||||
message_id=packet.message_id, successful=False))
|
||||
|
||||
|
||||
class PlayingReactor(PacketReactor):
|
||||
get_clientbound_packets = staticmethod(packets.state_playing_clientbound)
|
||||
get_clientbound_packets = staticmethod(clientbound.play.get_packets)
|
||||
|
||||
def react(self, packet):
|
||||
if packet.packet_name == "set compression":
|
||||
self.connection.options.compression_threshold = packet.threshold
|
||||
self.connection.options.compression_enabled = True
|
||||
|
||||
if packet.packet_name == "keep alive":
|
||||
keep_alive_packet = packets.KeepAlivePacketServerbound()
|
||||
elif packet.packet_name == "keep alive":
|
||||
keep_alive_packet = serverbound.play.KeepAlivePacket()
|
||||
keep_alive_packet.keep_alive_id = packet.keep_alive_id
|
||||
self.connection.write_packet(keep_alive_packet)
|
||||
|
||||
if packet.packet_name == "player position and look":
|
||||
if self.connection.context.protocol_version >= 107:
|
||||
teleport_confirm = packets.TeleportConfirmPacket()
|
||||
elif packet.packet_name == "player position and look":
|
||||
if self.connection.context.protocol_later_eq(107):
|
||||
teleport_confirm = serverbound.play.TeleportConfirmPacket()
|
||||
teleport_confirm.teleport_id = packet.teleport_id
|
||||
self.connection.write_packet(teleport_confirm)
|
||||
else:
|
||||
position_response = packets.PositionAndLookPacket()
|
||||
position_response = serverbound.play.PositionAndLookPacket()
|
||||
position_response.x = packet.x
|
||||
position_response.feet_y = packet.y
|
||||
position_response.z = packet.z
|
||||
|
@ -544,12 +810,12 @@ class PlayingReactor(PacketReactor):
|
|||
self.connection.write_packet(position_response)
|
||||
self.connection.spawned = True
|
||||
|
||||
if packet.packet_name == "disconnect":
|
||||
elif packet.packet_name == "disconnect":
|
||||
self.connection.disconnect()
|
||||
|
||||
|
||||
class StatusReactor(PacketReactor):
|
||||
get_clientbound_packets = staticmethod(packets.state_status_clientbound)
|
||||
get_clientbound_packets = staticmethod(clientbound.status.get_packets)
|
||||
|
||||
def __init__(self, connection, do_ping=False):
|
||||
super(StatusReactor, self).__init__(connection)
|
||||
|
@ -559,7 +825,7 @@ class StatusReactor(PacketReactor):
|
|||
if packet.packet_name == "response":
|
||||
status_dict = json.loads(packet.json_response)
|
||||
if self.do_ping:
|
||||
ping_packet = packets.PingPacket()
|
||||
ping_packet = serverbound.status.PingPacket()
|
||||
# NOTE: it may be better to depend on the `monotonic' package
|
||||
# or something similar for more accurate time measurement.
|
||||
ping_packet.time = int(1000 * timeit.default_timer())
|
||||
|
@ -568,10 +834,11 @@ class StatusReactor(PacketReactor):
|
|||
self.connection.disconnect()
|
||||
self.handle_status(status_dict)
|
||||
|
||||
elif packet.packet_name == "ping" and self.do_ping:
|
||||
now = int(1000 * timeit.default_timer())
|
||||
self.connection.disconnect()
|
||||
self.handle_ping(now - packet.time)
|
||||
elif packet.packet_name == "ping":
|
||||
if self.do_ping:
|
||||
now = int(1000 * timeit.default_timer())
|
||||
self.connection.disconnect()
|
||||
self.handle_ping(now - packet.time)
|
||||
|
||||
def handle_status(self, status_dict):
|
||||
print(status_dict)
|
||||
|
@ -595,12 +862,9 @@ class PlayingStatusReactor(StatusReactor):
|
|||
|
||||
proto = status['version']['protocol']
|
||||
if proto not in self.connection.allowed_proto_versions:
|
||||
vstr = ('%d (%s)' % (proto, status['version']['name'])) \
|
||||
if 'name' in status['version'] else str(proto)
|
||||
sstr = 'supported, but not allowed for this connection' \
|
||||
if proto in SUPPORTED_PROTOCOL_VERSIONS else 'not supported'
|
||||
raise VersionMismatch("Server's protocol version of %s is %s."
|
||||
% (vstr, sstr))
|
||||
self.connection._version_mismatch(
|
||||
server_protocol=proto,
|
||||
server_version=status['version'].get('name'))
|
||||
|
||||
self.handle_proto_version(proto)
|
||||
|
||||
|
@ -615,5 +879,6 @@ class PlayingStatusReactor(StatusReactor):
|
|||
if isinstance(exc, EOFError):
|
||||
# An exception of this type may indicate that the server does not
|
||||
# properly support status queries, so we treat it as non-fatal.
|
||||
self.connection.disconnect(immediate=True)
|
||||
self.handle_failure()
|
||||
return True
|
||||
|
|
|
@ -73,6 +73,9 @@ class EncryptedFileObjectWrapper(object):
|
|||
def fileno(self):
|
||||
return self.actual_file_object.fileno()
|
||||
|
||||
def close(self):
|
||||
self.actual_file_object.close()
|
||||
|
||||
|
||||
class EncryptedSocketWrapper(object):
|
||||
def __init__(self, socket, encryptor, decryptor):
|
||||
|
@ -88,3 +91,9 @@ class EncryptedSocketWrapper(object):
|
|||
|
||||
def fileno(self):
|
||||
return self.actual_socket.fileno()
|
||||
|
||||
def close(self):
|
||||
return self.actual_socket.close()
|
||||
|
||||
def shutdown(self, *args, **kwds):
|
||||
return self.actual_socket.shutdown(*args, **kwds)
|
||||
|
|
|
@ -1,770 +0,0 @@
|
|||
from io import BytesIO
|
||||
from zlib import compress
|
||||
|
||||
from .types import (
|
||||
VarInt, Integer, Float, Double, UnsignedShort, Long, Byte, UnsignedByte,
|
||||
String, VarIntPrefixedByteArray, Boolean, UUID
|
||||
)
|
||||
|
||||
|
||||
class PacketBuffer(object):
|
||||
def __init__(self):
|
||||
self.bytes = BytesIO()
|
||||
|
||||
def send(self, value):
|
||||
"""
|
||||
Writes the given bytes to the buffer, designed to emulate socket.send
|
||||
:param value: The bytes to write
|
||||
"""
|
||||
self.bytes.write(value)
|
||||
|
||||
def read(self, length=None):
|
||||
return self.bytes.read(length)
|
||||
|
||||
def recv(self, length=None):
|
||||
return self.read(length)
|
||||
|
||||
def reset(self):
|
||||
self.bytes = BytesIO()
|
||||
|
||||
def reset_cursor(self):
|
||||
self.bytes.seek(0)
|
||||
|
||||
def get_writable(self):
|
||||
return self.bytes.getvalue()
|
||||
|
||||
|
||||
class PacketListener(object):
|
||||
def __init__(self, callback, *args):
|
||||
self.callback = callback
|
||||
self.packets_to_listen = []
|
||||
for arg in args:
|
||||
if issubclass(arg, Packet):
|
||||
self.packets_to_listen.append(arg)
|
||||
|
||||
def call_packet(self, packet):
|
||||
for packet_type in self.packets_to_listen:
|
||||
if isinstance(packet, packet_type):
|
||||
self.callback(packet)
|
||||
|
||||
|
||||
class Packet(object):
|
||||
packet_name = "base"
|
||||
id = None
|
||||
definition = None
|
||||
|
||||
# To define the packet ID, either:
|
||||
# 1. Define the attribute `id', of type int, in a subclass; or
|
||||
# 2. Override `get_id' in a subclass and return the correct packet ID
|
||||
# for the given ConnectionContext. This is necessary if the packet ID
|
||||
# has changed across protocol versions, for example.
|
||||
@classmethod
|
||||
def get_id(cls, context):
|
||||
return cls.id
|
||||
|
||||
# To define the network data layout of a packet, either:
|
||||
# 1. Define the attribute `definition', a list of fields, each of which
|
||||
# is a dict mapping attribute names to data types; or
|
||||
# 2. Override `get_definition' in a subclass and return the correct
|
||||
# definition for the given ConnectionContext. This may be necessary
|
||||
# if the layout has changed across protocol versions, for example; or
|
||||
# 3. Override the methods `read' and/or `write' in a subclass. This may be
|
||||
# necessary if the packet layout cannot be described as a list of
|
||||
# fields.
|
||||
@classmethod
|
||||
def get_definition(cls, context):
|
||||
return cls.definition
|
||||
|
||||
def __init__(self, context=None, **kwargs):
|
||||
self.context = context
|
||||
self.set_values(**kwargs)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._context
|
||||
|
||||
@context.setter
|
||||
def context(self, _context):
|
||||
self._context = _context
|
||||
self._context_changed()
|
||||
|
||||
def _context_changed(self):
|
||||
if self._context is not None:
|
||||
self.id = self.get_id(self._context)
|
||||
self.definition = self.get_definition(self._context)
|
||||
else:
|
||||
self.id = None
|
||||
self.definition = None
|
||||
|
||||
def set_values(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
return self
|
||||
|
||||
def read(self, file_object):
|
||||
for field in self.definition:
|
||||
for var_name, data_type in field.items():
|
||||
value = data_type.read(file_object)
|
||||
setattr(self, var_name, value)
|
||||
|
||||
def write(self, socket, compression_threshold=None):
|
||||
# buffer the data since we need to know the length of each packet's
|
||||
# payload
|
||||
packet_buffer = PacketBuffer()
|
||||
# write packet's id right off the bat in the header
|
||||
VarInt.send(self.id, packet_buffer)
|
||||
|
||||
for field in self.definition:
|
||||
for var_name, data_type in field.items():
|
||||
data = getattr(self, var_name)
|
||||
data_type.send(data, packet_buffer)
|
||||
|
||||
# compression_threshold of None means compression is disabled
|
||||
if compression_threshold is not None:
|
||||
if len(packet_buffer.get_writable()) > compression_threshold != -1:
|
||||
# compress the current payload
|
||||
packet_data = packet_buffer.get_writable()
|
||||
compressed_data = compress(packet_data)
|
||||
packet_buffer.reset()
|
||||
# write out the length of the uncompressed payload
|
||||
VarInt.send(len(packet_data), packet_buffer)
|
||||
# write the compressed payload itself
|
||||
packet_buffer.send(compressed_data)
|
||||
else:
|
||||
# write out a 0 to indicate uncompressed data
|
||||
packet_data = packet_buffer.get_writable()
|
||||
packet_buffer.reset()
|
||||
VarInt.send(0, packet_buffer)
|
||||
packet_buffer.send(packet_data)
|
||||
|
||||
VarInt.send(len(packet_buffer.get_writable()), socket) # Packet Size
|
||||
socket.send(packet_buffer.get_writable()) # Packet Payload
|
||||
|
||||
def __str__(self):
|
||||
str = type(self).__name__
|
||||
if self.id is not None:
|
||||
str = '0x%02X %s' % (self.id, str)
|
||||
if self.definition is not None:
|
||||
fields = {a: getattr(self, a) for d in self.definition for a in d}
|
||||
str = '%s %s' % (str, fields)
|
||||
return str
|
||||
|
||||
|
||||
# Handshake State
|
||||
# ==============
|
||||
class HandShakePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "handshake"
|
||||
definition = [
|
||||
{'protocol_version': VarInt},
|
||||
{'server_address': String},
|
||||
{'server_port': UnsignedShort},
|
||||
{'next_state': VarInt}]
|
||||
|
||||
|
||||
def state_handshake_clientbound(context):
|
||||
return {
|
||||
|
||||
}
|
||||
|
||||
|
||||
def state_handshake_serverbound(context):
|
||||
return {
|
||||
HandShakePacket
|
||||
}
|
||||
|
||||
|
||||
# Status State
|
||||
# ==============
|
||||
class ResponsePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "response"
|
||||
definition = [
|
||||
{'json_response': String}]
|
||||
|
||||
|
||||
class PingPacketResponse(Packet):
|
||||
id = 0x01
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
||||
|
||||
|
||||
def state_status_clientbound(context):
|
||||
return {
|
||||
ResponsePacket,
|
||||
PingPacketResponse,
|
||||
}
|
||||
|
||||
|
||||
class RequestPacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "request"
|
||||
definition = []
|
||||
|
||||
|
||||
class PingPacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
||||
|
||||
|
||||
def state_status_serverbound(context):
|
||||
return {
|
||||
RequestPacket,
|
||||
PingPacket
|
||||
}
|
||||
|
||||
|
||||
# Login State
|
||||
# ==============
|
||||
class DisconnectPacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "disconnect"
|
||||
definition = [
|
||||
{'json_data': String}]
|
||||
|
||||
|
||||
class EncryptionRequestPacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "encryption request"
|
||||
definition = [
|
||||
{'server_id': String},
|
||||
{'public_key': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
|
||||
class LoginSuccessPacket(Packet):
|
||||
id = 0x02
|
||||
packet_name = "login success"
|
||||
definition = [
|
||||
{'UUID': String},
|
||||
{'Username': String}]
|
||||
|
||||
|
||||
class SetCompressionPacket(Packet):
|
||||
id = 0x03
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
|
||||
def state_login_clientbound(context):
|
||||
return {
|
||||
DisconnectPacket,
|
||||
EncryptionRequestPacket,
|
||||
LoginSuccessPacket,
|
||||
SetCompressionPacket
|
||||
}
|
||||
|
||||
|
||||
class LoginStartPacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "login start"
|
||||
definition = [
|
||||
{'name': String}]
|
||||
|
||||
|
||||
class EncryptionResponsePacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "encryption response"
|
||||
definition = [
|
||||
{'shared_secret': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
|
||||
def state_login_serverbound(context):
|
||||
return {
|
||||
LoginStartPacket,
|
||||
EncryptionResponsePacket
|
||||
}
|
||||
|
||||
|
||||
# Playing State
|
||||
# ==============
|
||||
|
||||
class KeepAlivePacket(Packet):
|
||||
packet_name = "keep alive"
|
||||
definition = [
|
||||
{'keep_alive_id': VarInt}]
|
||||
|
||||
|
||||
class KeepAlivePacketClientbound(KeepAlivePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x1F if context.protocol_version >= 107 else \
|
||||
0x00
|
||||
|
||||
|
||||
class KeepAlivePacketServerbound(KeepAlivePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0B if context.protocol_version >= 107 else \
|
||||
0x00
|
||||
|
||||
|
||||
class JoinGamePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x23 if context.protocol_version >= 107 else \
|
||||
0x01
|
||||
|
||||
packet_name = "join game"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'entity_id': Integer},
|
||||
{'game_mode': UnsignedByte},
|
||||
{'dimension': Integer if context.protocol_version >= 108 else Byte},
|
||||
{'difficulty': UnsignedByte},
|
||||
{'max_players': UnsignedByte},
|
||||
{'level_type': String},
|
||||
{'reduced_debug_info': Boolean}])
|
||||
|
||||
|
||||
class ChatMessagePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0F if context.protocol_version >= 107 else \
|
||||
0x02
|
||||
|
||||
packet_name = "chat message"
|
||||
definition = [
|
||||
{'json_data': String},
|
||||
{'position': Byte}]
|
||||
|
||||
|
||||
class PlayerPositionAndLookPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2E if context.protocol_version >= 107 else \
|
||||
0x08
|
||||
|
||||
packet_name = "player position and look"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'x': Double},
|
||||
{'y': Double},
|
||||
{'z': Double},
|
||||
{'yaw': Float},
|
||||
{'pitch': Float},
|
||||
{'flags': Byte},
|
||||
{'teleport_id': VarInt} if context.protocol_version >= 107 else {},
|
||||
])
|
||||
|
||||
FLAG_REL_X = 0x01
|
||||
FLAG_REL_Y = 0x02
|
||||
FLAG_REL_Z = 0x04
|
||||
FLAG_REL_YAW = 0x08
|
||||
FLAG_REL_PITCH = 0x10
|
||||
|
||||
class PositionAndLook(object):
|
||||
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
|
||||
|
||||
def __init__(self, **kwds):
|
||||
for attr in self.__slots__:
|
||||
setattr(self, attr, kwds.get(attr))
|
||||
|
||||
# Update a PositionAndLook instance using this packet.
|
||||
def apply(self, target):
|
||||
# pylint: disable=no-member
|
||||
if self.flags & self.FLAG_REL_X:
|
||||
target.x += self.x
|
||||
else:
|
||||
target.x = self.x
|
||||
|
||||
if self.flags & self.FLAG_REL_Y:
|
||||
target.y += self.y
|
||||
else:
|
||||
target.y = self.y
|
||||
|
||||
if self.flags & self.FLAG_REL_Z:
|
||||
target.z += self.z
|
||||
else:
|
||||
target.z = self.z
|
||||
|
||||
if self.flags & self.FLAG_REL_YAW:
|
||||
target.yaw += self.yaw
|
||||
else:
|
||||
target.yaw = self.yaw
|
||||
|
||||
if self.flags & self.FLAG_REL_PITCH:
|
||||
target.pitch += self.pitch
|
||||
else:
|
||||
target.pitch = self.pitch
|
||||
|
||||
self.yaw %= 360
|
||||
self.pitch %= 360
|
||||
|
||||
|
||||
class DisconnectPacketPlayState(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x1A if context.protocol_version >= 107 else \
|
||||
0x40
|
||||
|
||||
packet_name = "disconnect"
|
||||
|
||||
definition = [
|
||||
{'json_data': String}]
|
||||
|
||||
|
||||
class SetCompressionPacketPlayState(Packet):
|
||||
# Note: removed between protocol versions 47 and 107.
|
||||
id = 0x46
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
|
||||
class PlayerListItemPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2D if context.protocol_version >= 107 else \
|
||||
0x38
|
||||
|
||||
packet_name = "player list item"
|
||||
|
||||
class PlayerList(object):
|
||||
__slots__ = 'players_by_uuid'
|
||||
|
||||
def __init__(self):
|
||||
self.players_by_uuid = dict()
|
||||
|
||||
class PlayerListItem(object):
|
||||
__slots__ = (
|
||||
'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name')
|
||||
|
||||
def __init__(self, **kwds):
|
||||
for key, val in kwds.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
class PlayerProperty(object):
|
||||
__slots__ = 'name', 'value', 'signature'
|
||||
|
||||
def read(self, file_object):
|
||||
self.name = String.read(file_object)
|
||||
self.value = String.read(file_object)
|
||||
is_signed = Boolean.read(file_object)
|
||||
if is_signed:
|
||||
self.signature = String.read(file_object)
|
||||
else:
|
||||
self.signature = None
|
||||
|
||||
class Action(object):
|
||||
__slots__ = 'uuid'
|
||||
|
||||
def read(self, file_object):
|
||||
self.uuid = UUID.read(file_object)
|
||||
self._read(file_object)
|
||||
|
||||
def _read(self, file_object):
|
||||
raise NotImplementedError(
|
||||
'This abstract method must be overridden in a subclass.')
|
||||
|
||||
@classmethod
|
||||
def type_from_id(cls, action_id):
|
||||
subcls = {
|
||||
0: PlayerListItemPacket.AddPlayerAction,
|
||||
1: PlayerListItemPacket.UpdateGameModeAction,
|
||||
2: PlayerListItemPacket.UpdateLatencyAction,
|
||||
3: PlayerListItemPacket.UpdateDisplayNameAction,
|
||||
4: PlayerListItemPacket.RemovePlayerAction
|
||||
}.get(action_id)
|
||||
if subcls is None:
|
||||
raise ValueError("Unknown player list action ID: %s."
|
||||
% action_id)
|
||||
return subcls
|
||||
|
||||
class AddPlayerAction(Action):
|
||||
__slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name'
|
||||
|
||||
def _read(self, file_object):
|
||||
self.name = String.read(file_object)
|
||||
prop_count = VarInt.read(file_object)
|
||||
self.properties = []
|
||||
for i in range(prop_count):
|
||||
property = PlayerListItemPacket.PlayerProperty()
|
||||
property.read(file_object)
|
||||
self.properties.append(property)
|
||||
self.gamemode = VarInt.read(file_object)
|
||||
self.ping = VarInt.read(file_object)
|
||||
has_display_name = Boolean.read(file_object)
|
||||
if has_display_name:
|
||||
self.display_name = String.read(file_object)
|
||||
else:
|
||||
self.display_name = None
|
||||
|
||||
def apply(self, player_list):
|
||||
player = PlayerListItemPacket.PlayerListItem(
|
||||
uuid=self.uuid,
|
||||
name=self.name,
|
||||
properties=self.properties,
|
||||
gamemode=self.gamemode,
|
||||
ping=self.ping,
|
||||
display_name=self.display_name)
|
||||
player_list.players_by_uuid[self.uuid] = player
|
||||
|
||||
class UpdateGameModeAction(Action):
|
||||
__slots__ = 'gamemode'
|
||||
|
||||
def _read(self, file_object):
|
||||
self.gamemode = VarInt.read(file_object)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.gamemode = self.gamemode
|
||||
|
||||
class UpdateLatencyAction(Action):
|
||||
__slots__ = 'ping'
|
||||
|
||||
def _read(self, file_object):
|
||||
self.ping = VarInt.read(file_object)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.ping = self.ping
|
||||
|
||||
class UpdateDisplayNameAction(Action):
|
||||
__slots__ = 'display_name'
|
||||
|
||||
def _read(self, file_object):
|
||||
has_display_name = Boolean.read(file_object)
|
||||
if has_display_name:
|
||||
self.display_name = String.read(file_object)
|
||||
else:
|
||||
self.display_name = None
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.display_name = self.display_name
|
||||
|
||||
class RemovePlayerAction(Action):
|
||||
def _read(self, file_object):
|
||||
pass
|
||||
|
||||
def apply(self, player_list):
|
||||
if self.uuid in player_list.players_by_uuid:
|
||||
del player_list.players_by_uuid[self.uuid]
|
||||
|
||||
def read(self, file_object):
|
||||
action_id = VarInt.read(file_object)
|
||||
self.action_type = PlayerListItemPacket.Action.type_from_id(action_id)
|
||||
action_count = VarInt.read(file_object)
|
||||
self.actions = []
|
||||
for i in range(action_count):
|
||||
action = self.action_type()
|
||||
action.read(file_object)
|
||||
self.actions.append(action)
|
||||
|
||||
def apply(self, player_list):
|
||||
for action in self.actions:
|
||||
action.apply(player_list)
|
||||
|
||||
def write(self, socket, compression_threshold=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MapPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x24 if context.protocol_version >= 107 else \
|
||||
0x34
|
||||
|
||||
packet_name = 'map'
|
||||
|
||||
class MapIcon(object):
|
||||
__slots__ = 'type', 'direction', 'location'
|
||||
|
||||
def __init__(self, type, direction, location):
|
||||
self.type = type
|
||||
self.direction = direction
|
||||
self.location = location
|
||||
|
||||
def __repr__(self):
|
||||
return ('MapIcon(type=%s, direction=%s, location=%s)'
|
||||
% (self.type, self.direction, self.location))
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
class Map(object):
|
||||
__slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height',
|
||||
'is_tracking_position')
|
||||
|
||||
def __init__(self, id=None, scale=None, width=128, height=128):
|
||||
self.id = id
|
||||
self.scale = scale
|
||||
self.icons = []
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.pixels = bytearray(0 for i in range(width*height))
|
||||
self.is_tracking_position = True
|
||||
|
||||
def __repr__(self):
|
||||
return ('Map(id=%s, scale=%s, icons=%s, width=%s, height=%s)' % (
|
||||
self.id, self.scale, self.icons, self.width, self.height))
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
class MapSet(object):
|
||||
__slots__ = 'maps_by_id'
|
||||
|
||||
def __init__(self):
|
||||
self.maps_by_id = dict()
|
||||
|
||||
def __repr__(self):
|
||||
return 'MapSet(%s)' % ', '.join(self.maps_by_id.values())
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def read(self, file_object):
|
||||
self.map_id = VarInt.read(file_object)
|
||||
self.scale = Byte.read(file_object)
|
||||
|
||||
if self.context.protocol_version >= 107:
|
||||
self.is_tracking_position = Boolean.read(file_object)
|
||||
else:
|
||||
self.is_tracking_position = True
|
||||
|
||||
icon_count = VarInt.read(file_object)
|
||||
self.icons = []
|
||||
for i in range(icon_count):
|
||||
type, direction = divmod(UnsignedByte.read(file_object), 16)
|
||||
x = Byte.read(file_object)
|
||||
z = Byte.read(file_object)
|
||||
icon = MapPacket.MapIcon(type, direction, (x, z))
|
||||
self.icons.append(icon)
|
||||
self.width = UnsignedByte.read(file_object)
|
||||
if self.width:
|
||||
self.height = UnsignedByte.read(file_object)
|
||||
x = Byte.read(file_object)
|
||||
z = Byte.read(file_object)
|
||||
self.offset = (x, z)
|
||||
self.pixels = VarIntPrefixedByteArray.read(file_object)
|
||||
else:
|
||||
self.height = 0
|
||||
self.offset = None
|
||||
self.pixels = None
|
||||
|
||||
def apply_to_map(self, map):
|
||||
map.id = self.map_id
|
||||
map.scale = self.scale
|
||||
map.icons[:] = self.icons
|
||||
if self.pixels is not None:
|
||||
for i in range(len(self.pixels)):
|
||||
x = self.offset[0] + i % self.width
|
||||
z = self.offset[1] + i // self.width
|
||||
map.pixels[x + map.width * z] = self.pixels[i]
|
||||
map.is_tracking_position = self.is_tracking_position
|
||||
|
||||
def apply_to_map_set(self, map_set):
|
||||
map = map_set.maps_by_id.get(self.map_id)
|
||||
if map is None:
|
||||
map = MapPacket.Map(self.map_id)
|
||||
map_set.maps_by_id[self.map_id] = map
|
||||
self.apply_to_map(map)
|
||||
|
||||
def write(self, socket, compression_threshold=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
return 'MapPacket(%s)' % ', '.join(
|
||||
'%s=%r' % (k, v)
|
||||
for (k, v) in self.__dict__.items()
|
||||
if k != 'pixels')
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
def state_playing_clientbound(context):
|
||||
packets = {
|
||||
KeepAlivePacketClientbound,
|
||||
JoinGamePacket,
|
||||
ChatMessagePacket,
|
||||
PlayerPositionAndLookPacket,
|
||||
MapPacket,
|
||||
PlayerListItemPacket,
|
||||
DisconnectPacketPlayState,
|
||||
}
|
||||
if context.protocol_version <= 47:
|
||||
packets |= {
|
||||
SetCompressionPacketPlayState,
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class ChatPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x02 if context.protocol_version >= 107 else \
|
||||
0x01
|
||||
|
||||
@staticmethod
|
||||
def get_max_length(context):
|
||||
return 256 if context.protocol_version >= 306 else \
|
||||
100
|
||||
|
||||
@property
|
||||
def max_length(self):
|
||||
if self.context is not None:
|
||||
return self.get_max_length(self.context)
|
||||
|
||||
packet_name = "chat"
|
||||
definition = [
|
||||
{'message': String}]
|
||||
|
||||
|
||||
class PositionAndLookPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0D if context.protocol_version >= 107 else \
|
||||
0x06
|
||||
|
||||
packet_name = "position and look"
|
||||
definition = [
|
||||
{'x': Double},
|
||||
{'feet_y': Double},
|
||||
{'z': Double},
|
||||
{'yaw': Float},
|
||||
{'pitch': Float},
|
||||
{'on_ground': Boolean}]
|
||||
|
||||
|
||||
class TeleportConfirmPacket(Packet):
|
||||
# Note: added between protocol versions 47 and 107.
|
||||
id = 0x00
|
||||
packet_name = "teleport confirm"
|
||||
definition = [
|
||||
{'teleport_id': VarInt}]
|
||||
|
||||
|
||||
class AnimationPacketServerbound(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x1A if context.protocol_version >= 107 else \
|
||||
0x0A
|
||||
|
||||
packet_name = "animation"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'hand': VarInt} if context.protocol_version >= 107 else {}])
|
||||
HAND_MAIN = 0
|
||||
HAND_OFF = 1
|
||||
|
||||
|
||||
def state_playing_serverbound(context):
|
||||
packets = {
|
||||
KeepAlivePacketServerbound,
|
||||
ChatPacket,
|
||||
PositionAndLookPacket,
|
||||
AnimationPacketServerbound,
|
||||
}
|
||||
if context.protocol_version >= 107:
|
||||
packets |= {
|
||||
TeleportConfirmPacket,
|
||||
}
|
||||
return packets
|
|
@ -0,0 +1,62 @@
|
|||
'''
|
||||
NOTE: The packet classes exported by this module are included only for backward
|
||||
compatibility, and should not be used in new code, as (1) they do not include
|
||||
all packets present in pyCraft, and (2) some are named oddly, for historical
|
||||
reasons.
|
||||
|
||||
Use the packet classes under packets.clientbound.* and
|
||||
packets.serverbound.* instead.
|
||||
'''
|
||||
|
||||
# Packet-Related Utilities
|
||||
from .packet_buffer import PacketBuffer
|
||||
from .packet_listener import PacketListener
|
||||
|
||||
# Abstract Packet Classes
|
||||
from .packet import Packet
|
||||
from .keep_alive_packet import AbstractKeepAlivePacket
|
||||
from .plugin_message_packet import AbstractPluginMessagePacket
|
||||
|
||||
|
||||
# Legacy Packets (Handshake State)
|
||||
from .clientbound.handshake import get_packets as state_handshake_clientbound
|
||||
from .serverbound.handshake import HandShakePacket
|
||||
from .serverbound.handshake import get_packets as state_handshake_serverbound
|
||||
|
||||
# Legacy Packets (Status State)
|
||||
from .clientbound.status import ResponsePacket
|
||||
from .clientbound.status import PingResponsePacket as PingPacketResponse
|
||||
from .clientbound.status import get_packets as state_status_clientbound
|
||||
from .serverbound.status import RequestPacket
|
||||
from .serverbound.status import PingPacket
|
||||
from .serverbound.status import get_packets as state_status_serverbound
|
||||
|
||||
# Legacy Packets (Login State)
|
||||
from .clientbound.login import DisconnectPacket
|
||||
from .clientbound.login import EncryptionRequestPacket
|
||||
from .clientbound.login import LoginSuccessPacket
|
||||
from .clientbound.login import SetCompressionPacket
|
||||
from .clientbound.login import get_packets as state_login_clientbound
|
||||
from .serverbound.login import LoginStartPacket
|
||||
from .serverbound.login import EncryptionResponsePacket
|
||||
from .serverbound.login import get_packets as state_login_serverbound
|
||||
|
||||
# Legacy Packets (Playing State)
|
||||
from .keep_alive_packet import KeepAlivePacket
|
||||
from .clientbound.play import KeepAlivePacket as KeepAlivePacketClientbound
|
||||
from .serverbound.play import KeepAlivePacket as KeepAlivePacketServerbound
|
||||
from .clientbound.play import JoinGamePacket
|
||||
from .clientbound.play import ChatMessagePacket
|
||||
from .clientbound.play import PlayerPositionAndLookPacket
|
||||
from .clientbound.play import DisconnectPacket as DisconnectPacketPlayState
|
||||
from .clientbound.play import (
|
||||
SetCompressionPacket as SetCompressionPacketPlayState
|
||||
)
|
||||
from .clientbound.play import PlayerListItemPacket
|
||||
from .clientbound.play import MapPacket
|
||||
from .clientbound.play import get_packets as state_playing_clientbound
|
||||
from .serverbound.play import ChatPacket
|
||||
from .serverbound.play import PositionAndLookPacket
|
||||
from .serverbound.play import TeleportConfirmPacket
|
||||
from .serverbound.play import AnimationPacket as AnimationPacketServerbound
|
||||
from .serverbound.play import get_packets as state_playing_serverbound
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Contains the clientbound packets for `pyminecraft`.
|
||||
"""
|
|
@ -0,0 +1,3 @@
|
|||
# Formerly known as state_handshake_clientbound.
|
||||
def get_packets(context):
|
||||
return set()
|
|
@ -0,0 +1,99 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, String, VarIntPrefixedByteArray, TrailingByteArray, UUID,
|
||||
)
|
||||
|
||||
|
||||
# Formerly known as state_login_clientbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
DisconnectPacket,
|
||||
EncryptionRequestPacket,
|
||||
LoginSuccessPacket,
|
||||
SetCompressionPacket,
|
||||
}
|
||||
if context.protocol_later_eq(385):
|
||||
packets |= {
|
||||
PluginRequestPacket,
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class DisconnectPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x00 if context.protocol_later_eq(391) else \
|
||||
0x01 if context.protocol_later_eq(385) else \
|
||||
0x00
|
||||
|
||||
packet_name = "disconnect"
|
||||
definition = [
|
||||
{'json_data': String}]
|
||||
|
||||
|
||||
class EncryptionRequestPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x01 if context.protocol_later_eq(391) else \
|
||||
0x02 if context.protocol_later_eq(385) else \
|
||||
0x01
|
||||
|
||||
packet_name = "encryption request"
|
||||
definition = [
|
||||
{'server_id': String},
|
||||
{'public_key': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
|
||||
class LoginSuccessPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x02 if context.protocol_later_eq(391) else \
|
||||
0x03 if context.protocol_later_eq(385) else \
|
||||
0x02
|
||||
|
||||
packet_name = "login success"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'UUID': UUID if context.protocol_later_eq(707) else String},
|
||||
{'Username': String}
|
||||
])
|
||||
|
||||
|
||||
class SetCompressionPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x03 if context.protocol_later_eq(391) else \
|
||||
0x04 if context.protocol_later_eq(385) else \
|
||||
0x03
|
||||
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
|
||||
class PluginRequestPacket(Packet):
|
||||
""" NOTE: pyCraft's default behaviour on receiving a 'PluginRequestPacket'
|
||||
is to send a corresponding 'PluginResponsePacket' with
|
||||
'successful=False'. To override this, set a packet listener that:
|
||||
|
||||
(1) has the keyword argument 'early=True' set when calling
|
||||
'register_packet_listener'; and
|
||||
|
||||
(2) raises 'minecraft.networking.connection.IgnorePacket' after
|
||||
sending a corresponding 'PluginResponsePacket'.
|
||||
|
||||
Otherwise, one 'PluginRequestPacket' may result in multiple responses,
|
||||
which contravenes Minecraft's protocol.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x04 if context.protocol_later_eq(391) else \
|
||||
0x00
|
||||
|
||||
packet_name = 'login plugin request'
|
||||
definition = [
|
||||
{'message_id': VarInt},
|
||||
{'channel': String},
|
||||
{'data': TrailingByteArray}]
|
|
@ -0,0 +1,428 @@
|
|||
from minecraft import PRE
|
||||
from minecraft.networking.packets import (
|
||||
Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket
|
||||
)
|
||||
|
||||
from minecraft.networking.types import (
|
||||
FixedPoint, Integer, Angle, UnsignedByte, Byte, Boolean, UUID, Short,
|
||||
VarInt, Double, Float, String, Enum, Difficulty, Long, Vector, Direction,
|
||||
PositionAndLook, multi_attribute_alias, attribute_transform,
|
||||
)
|
||||
|
||||
from .combat_event_packet import (
|
||||
CombatEventPacket, EnterCombatEventPacket, EndCombatEventPacket,
|
||||
DeathCombatEventPacket,
|
||||
)
|
||||
from .map_packet import MapPacket
|
||||
from .player_list_item_packet import PlayerListItemPacket
|
||||
from .player_position_and_look_packet import PlayerPositionAndLookPacket
|
||||
from .spawn_object_packet import SpawnObjectPacket
|
||||
from .block_change_packet import BlockChangePacket, MultiBlockChangePacket
|
||||
from .explosion_packet import ExplosionPacket
|
||||
from .sound_effect_packet import SoundEffectPacket
|
||||
from .face_player_packet import FacePlayerPacket
|
||||
from .join_game_and_respawn_packets import JoinGamePacket, RespawnPacket
|
||||
|
||||
|
||||
# Formerly known as state_playing_clientbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
KeepAlivePacket,
|
||||
JoinGamePacket,
|
||||
ServerDifficultyPacket,
|
||||
ChatMessagePacket,
|
||||
PlayerPositionAndLookPacket,
|
||||
MapPacket,
|
||||
PlayerListItemPacket,
|
||||
DisconnectPacket,
|
||||
SpawnPlayerPacket,
|
||||
EntityVelocityPacket,
|
||||
EntityPositionDeltaPacket,
|
||||
TimeUpdatePacket,
|
||||
UpdateHealthPacket,
|
||||
ExplosionPacket,
|
||||
SpawnObjectPacket,
|
||||
BlockChangePacket,
|
||||
MultiBlockChangePacket,
|
||||
RespawnPacket,
|
||||
PluginMessagePacket,
|
||||
PlayerListHeaderAndFooterPacket,
|
||||
EntityLookPacket,
|
||||
ResourcePackSendPacket
|
||||
}
|
||||
|
||||
if context.protocol_earlier_eq(47):
|
||||
packets |= {
|
||||
SetCompressionPacket,
|
||||
}
|
||||
|
||||
if context.protocol_earlier(PRE | 15):
|
||||
packets |= {
|
||||
CombatEventPacket,
|
||||
}
|
||||
else:
|
||||
packets |= {
|
||||
EnterCombatEventPacket,
|
||||
EndCombatEventPacket,
|
||||
DeathCombatEventPacket,
|
||||
}
|
||||
|
||||
if context.protocol_later_eq(94):
|
||||
packets |= {
|
||||
SoundEffectPacket,
|
||||
}
|
||||
|
||||
if context.protocol_later_eq(352):
|
||||
packets |= {
|
||||
FacePlayerPacket
|
||||
}
|
||||
|
||||
return packets
|
||||
|
||||
|
||||
class KeepAlivePacket(AbstractKeepAlivePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x21 if context.protocol_later_eq(755) else \
|
||||
0x1F if context.protocol_later_eq(741) else \
|
||||
0x20 if context.protocol_later_eq(721) else \
|
||||
0x21 if context.protocol_later_eq(550) else \
|
||||
0x20 if context.protocol_later_eq(471) else \
|
||||
0x21 if context.protocol_later_eq(389) else \
|
||||
0x20 if context.protocol_later_eq(345) else \
|
||||
0x1F if context.protocol_later_eq(332) else \
|
||||
0x20 if context.protocol_later_eq(318) else \
|
||||
0x1F if context.protocol_later_eq(107) else \
|
||||
0x00
|
||||
|
||||
|
||||
class ServerDifficultyPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0E if context.protocol_later_eq(755) else \
|
||||
0x0D if context.protocol_later_eq(721) else \
|
||||
0x0E if context.protocol_later_eq(550) else \
|
||||
0x0D if context.protocol_later_eq(332) else \
|
||||
0x0E if context.protocol_later_eq(318) else \
|
||||
0x0D if context.protocol_later_eq(70) else \
|
||||
0x41
|
||||
|
||||
packet_name = 'server difficulty'
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'difficulty': UnsignedByte},
|
||||
{'is_locked': Boolean} if context.protocol_later_eq(464) else {},
|
||||
])
|
||||
|
||||
# These aliases declare the Enum type corresponding to each field:
|
||||
Difficulty = Difficulty
|
||||
|
||||
|
||||
class ChatMessagePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0F if context.protocol_later_eq(755) else \
|
||||
0x0E if context.protocol_later_eq(721) else \
|
||||
0x0F if context.protocol_later_eq(550) else \
|
||||
0x0E if context.protocol_later_eq(343) else \
|
||||
0x0F if context.protocol_later_eq(332) else \
|
||||
0x10 if context.protocol_later_eq(317) else \
|
||||
0x0F if context.protocol_later_eq(107) else \
|
||||
0x02
|
||||
|
||||
packet_name = "chat message"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'json_data': String},
|
||||
{'position': Byte},
|
||||
{'sender': UUID} if context.protocol_later_eq(718) else {},
|
||||
])
|
||||
|
||||
class Position(Enum):
|
||||
CHAT = 0 # A player-initiated chat message.
|
||||
SYSTEM = 1 # The result of running a command.
|
||||
GAME_INFO = 2 # Displayed above the hotbar in vanilla clients.
|
||||
|
||||
|
||||
class DisconnectPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x1A if context.protocol_later_eq(755) else \
|
||||
0x19 if context.protocol_later_eq(741) else \
|
||||
0x1A if context.protocol_later_eq(721) else \
|
||||
0x1B if context.protocol_later_eq(550) else \
|
||||
0x1A if context.protocol_later_eq(471) else \
|
||||
0x1B if context.protocol_later_eq(345) else \
|
||||
0x1A if context.protocol_later_eq(332) else \
|
||||
0x1B if context.protocol_later_eq(318) else \
|
||||
0x1A if context.protocol_later_eq(107) else \
|
||||
0x40
|
||||
|
||||
packet_name = "disconnect"
|
||||
|
||||
definition = [
|
||||
{'json_data': String}]
|
||||
|
||||
|
||||
class SetCompressionPacket(Packet):
|
||||
# Note: removed between protocol versions 47 and 107.
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x03 if context.protocol_later_eq(755) else \
|
||||
0x46
|
||||
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
|
||||
class SpawnPlayerPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x04 if context.protocol_later_eq(721) else \
|
||||
0x05 if context.protocol_later_eq(67) else \
|
||||
0x0C
|
||||
|
||||
packet_name = 'spawn player'
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'entity_id': VarInt},
|
||||
{'player_UUID': UUID},
|
||||
{'x': Double} if context.protocol_later_eq(100)
|
||||
else {'x': FixedPoint(Integer)},
|
||||
{'y': Double} if context.protocol_later_eq(100)
|
||||
else {'y': FixedPoint(Integer)},
|
||||
{'z': Double} if context.protocol_later_eq(100)
|
||||
else {'z': FixedPoint(Integer)},
|
||||
{'yaw': Angle},
|
||||
{'pitch': Angle},
|
||||
{'current_item': Short} if context.protocol_earlier_eq(49) else {},
|
||||
# TODO: read entity metadata (protocol < 550)
|
||||
])
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector tuple.
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
# Access the 'yaw', 'pitch' fields as a Direction tuple.
|
||||
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
||||
|
||||
# Access the 'x', 'y', 'z', 'yaw', 'pitch' fields as a PositionAndLook.
|
||||
# NOTE: modifying the object retrieved from this property will not change
|
||||
# the packet; it can only be changed by attribute or property assignment.
|
||||
position_and_look = multi_attribute_alias(
|
||||
PositionAndLook, 'x', 'y', 'z', 'yaw', 'pitch')
|
||||
|
||||
|
||||
class EntityVelocityPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x4F if context.protocol_later_eq(755) else \
|
||||
0x46 if context.protocol_later_eq(721) else \
|
||||
0x47 if context.protocol_later_eq(707) else \
|
||||
0x46 if context.protocol_later_eq(550) else \
|
||||
0x45 if context.protocol_later_eq(471) else \
|
||||
0x41 if context.protocol_later_eq(461) else \
|
||||
0x42 if context.protocol_later_eq(451) else \
|
||||
0x41 if context.protocol_later_eq(389) else \
|
||||
0x40 if context.protocol_later_eq(352) else \
|
||||
0x3F if context.protocol_later_eq(345) else \
|
||||
0x3E if context.protocol_later_eq(336) else \
|
||||
0x3D if context.protocol_later_eq(332) else \
|
||||
0x3B if context.protocol_later_eq(86) else \
|
||||
0x3C if context.protocol_later_eq(77) else \
|
||||
0x3B if context.protocol_later_eq(67) else \
|
||||
0x12
|
||||
|
||||
packet_name = 'entity velocity'
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'entity_id': VarInt},
|
||||
{'velocity_x': Short},
|
||||
{'velocity_y': Short},
|
||||
{'velocity_z': Short}
|
||||
])
|
||||
|
||||
|
||||
class EntityPositionDeltaPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x29 if context.protocol_later_eq(755) else \
|
||||
0x27 if context.protocol_later_eq(741) else \
|
||||
0x28 if context.protocol_later_eq(721) else \
|
||||
0x29 if context.protocol_later_eq(550) else \
|
||||
0x28 if context.protocol_later_eq(389) else \
|
||||
0x27 if context.protocol_later_eq(345) else \
|
||||
0x26 if context.protocol_later_eq(318) else \
|
||||
0x25 if context.protocol_later_eq(94) else \
|
||||
0x26 if context.protocol_later_eq(70) else \
|
||||
0x15
|
||||
|
||||
packet_name = "entity position delta"
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
delta_type = FixedPoint(Short, 12) \
|
||||
if context.protocol_later_eq(106) else \
|
||||
FixedPoint(Byte)
|
||||
return [
|
||||
{'entity_id': VarInt},
|
||||
{'delta_x_float': delta_type},
|
||||
{'delta_y_float': delta_type},
|
||||
{'delta_z_float': delta_type},
|
||||
{'on_ground': Boolean},
|
||||
]
|
||||
|
||||
# The following transforms are retained for backward compatibility;
|
||||
# they represent the delta values as fixed-point integers with 12 bits
|
||||
# of fractional part, regardless of the protocol version.
|
||||
delta_x = attribute_transform(
|
||||
'delta_x_float', lambda x: int(x * 4096), lambda x: x / 4096)
|
||||
delta_y = attribute_transform(
|
||||
'delta_y_float', lambda y: int(y * 4096), lambda y: y / 4096)
|
||||
delta_z = attribute_transform(
|
||||
'delta_z_float', lambda z: int(z * 4096), lambda z: z / 4096)
|
||||
|
||||
|
||||
class TimeUpdatePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x59 if context.protocol_later_eq(PRE | 48) else \
|
||||
0x58 if context.protocol_later_eq(755) else \
|
||||
0x4E if context.protocol_later_eq(721) else \
|
||||
0x4F if context.protocol_later_eq(550) else \
|
||||
0x4E if context.protocol_later_eq(471) else \
|
||||
0x4A if context.protocol_later_eq(461) else \
|
||||
0x4B if context.protocol_later_eq(451) else \
|
||||
0x4A if context.protocol_later_eq(389) else \
|
||||
0x49 if context.protocol_later_eq(352) else \
|
||||
0x48 if context.protocol_later_eq(345) else \
|
||||
0x47 if context.protocol_later_eq(336) else \
|
||||
0x46 if context.protocol_later_eq(318) else \
|
||||
0x44 if context.protocol_later_eq(94) else \
|
||||
0x43 if context.protocol_later_eq(70) else \
|
||||
0x03
|
||||
|
||||
packet_name = "time update"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'world_age': Long},
|
||||
{'time_of_day': Long},
|
||||
])
|
||||
|
||||
|
||||
class UpdateHealthPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x52 if context.protocol_later_eq(755) else \
|
||||
0x49 if context.protocol_later_eq(721) else \
|
||||
0x4A if context.protocol_later_eq(707) else \
|
||||
0x49 if context.protocol_later_eq(550) else \
|
||||
0x48 if context.protocol_later_eq(471) else \
|
||||
0x44 if context.protocol_later_eq(461) else \
|
||||
0x45 if context.protocol_later_eq(451) else \
|
||||
0x44 if context.protocol_later_eq(389) else \
|
||||
0x43 if context.protocol_later_eq(352) else \
|
||||
0x42 if context.protocol_later_eq(345) else \
|
||||
0x41 if context.protocol_later_eq(336) else \
|
||||
0x40 if context.protocol_later_eq(318) else \
|
||||
0x3E if context.protocol_later_eq(86) else \
|
||||
0x3F if context.protocol_later_eq(77) else \
|
||||
0x3E if context.protocol_later_eq(67) else \
|
||||
0x06
|
||||
|
||||
packet_name = 'update health'
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'health': Float},
|
||||
{'food': VarInt},
|
||||
{'food_saturation': Float}
|
||||
])
|
||||
|
||||
|
||||
class PluginMessagePacket(AbstractPluginMessagePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x18 if context.protocol_later_eq(755) else \
|
||||
0x17 if context.protocol_later_eq(741) else \
|
||||
0x18 if context.protocol_later_eq(721) else \
|
||||
0x19 if context.protocol_later_eq(550) else \
|
||||
0x18 if context.protocol_later_eq(471) else \
|
||||
0x19 if context.protocol_later_eq(345) else \
|
||||
0x18 if context.protocol_later_eq(332) else \
|
||||
0x19 if context.protocol_later_eq(318) else \
|
||||
0x18 if context.protocol_later_eq(70) else \
|
||||
0x3F
|
||||
|
||||
|
||||
class PlayerListHeaderAndFooterPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x5F if context.protocol_later_eq(PRE | 48) else \
|
||||
0x5E if context.protocol_later_eq(755) else \
|
||||
0x53 if context.protocol_later_eq(721) else \
|
||||
0x54 if context.protocol_later_eq(550) else \
|
||||
0x53 if context.protocol_later_eq(471) else \
|
||||
0x5F if context.protocol_later_eq(461) else \
|
||||
0x50 if context.protocol_later_eq(451) else \
|
||||
0x4F if context.protocol_later_eq(441) else \
|
||||
0x4E if context.protocol_later_eq(393) else \
|
||||
0x4A if context.protocol_later_eq(338) else \
|
||||
0x49 if context.protocol_later_eq(335) else \
|
||||
0x47 if context.protocol_later_eq(110) else \
|
||||
0x48 if context.protocol_later_eq(107) else \
|
||||
0x47
|
||||
|
||||
packet_name = 'player list header and footer'
|
||||
definition = [
|
||||
{'header': String},
|
||||
{'footer': String}]
|
||||
|
||||
|
||||
class EntityLookPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2B if context.protocol_later_eq(755) else \
|
||||
0x29 if context.protocol_later_eq(741) else \
|
||||
0x2A if context.protocol_later_eq(721) else \
|
||||
0x2B if context.protocol_later_eq(550) else \
|
||||
0x2A if context.protocol_later_eq(389) else \
|
||||
0x29 if context.protocol_later_eq(345) else \
|
||||
0x28 if context.protocol_later_eq(318) else \
|
||||
0x27 if context.protocol_later_eq(94) else \
|
||||
0x28 if context.protocol_later_eq(70) else \
|
||||
0x16
|
||||
|
||||
packet_name = 'entity look'
|
||||
definition = [
|
||||
{'entity_id': VarInt},
|
||||
{'yaw': Angle},
|
||||
{'pitch': Angle},
|
||||
{'on_ground': Boolean}
|
||||
]
|
||||
|
||||
|
||||
class ResourcePackSendPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x3C if context.protocol_later_eq(PRE | 15) else \
|
||||
0x39 if context.protocol_later_eq(PRE | 8) else \
|
||||
0x38 if context.protocol_later_eq(741) else \
|
||||
0x39 if context.protocol_later_eq(721) else \
|
||||
0x3A if context.protocol_later_eq(550) else \
|
||||
0x39 if context.protocol_later_eq(471) else \
|
||||
0x37 if context.protocol_later_eq(461) else \
|
||||
0x38 if context.protocol_later_eq(451) else \
|
||||
0x37 if context.protocol_later_eq(389) else \
|
||||
0x36 if context.protocol_later_eq(352) else \
|
||||
0x35 if context.protocol_later_eq(345) else \
|
||||
0x34 if context.protocol_later_eq(336) else \
|
||||
0x33 if context.protocol_later_eq(332) else \
|
||||
0x34 if context.protocol_later_eq(318) else \
|
||||
0x32 if context.protocol_later_eq(70) else \
|
||||
0x48
|
||||
|
||||
packet_name = "resource pack send"
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
return [
|
||||
{"url": String},
|
||||
{"hash": String},
|
||||
{"forced": Boolean} if context.protocol_later_eq(PRE | 5) else {},
|
||||
{"forced_message": String}
|
||||
if context.protocol_later_eq(PRE | 15) else {},
|
||||
]
|
|
@ -0,0 +1,156 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types import (
|
||||
Type, VarInt, VarLong, UnsignedLong, Integer, UnsignedByte, Position,
|
||||
Vector, MutableRecord, PrefixedArray, Boolean, attribute_alias,
|
||||
multi_attribute_alias,
|
||||
)
|
||||
|
||||
|
||||
class BlockChangePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0C if context.protocol_later_eq(755) else \
|
||||
0x0B if context.protocol_later_eq(721) else \
|
||||
0x0C if context.protocol_later_eq(550) else \
|
||||
0x0B if context.protocol_later_eq(332) else \
|
||||
0x0C if context.protocol_later_eq(318) else \
|
||||
0x0B if context.protocol_later_eq(67) else \
|
||||
0x24 if context.protocol_later_eq(62) else \
|
||||
0x23
|
||||
|
||||
packet_name = 'block change'
|
||||
definition = [
|
||||
{'location': Position},
|
||||
{'block_state_id': VarInt}]
|
||||
block_state_id = 0
|
||||
|
||||
# For protocols before 347: an accessor for (block_state_id >> 4).
|
||||
@property
|
||||
def blockId(self):
|
||||
return self.block_state_id >> 4
|
||||
|
||||
@blockId.setter
|
||||
def blockId(self, block_id):
|
||||
self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4)
|
||||
|
||||
# For protocols before 347: an accessor for (block_state_id & 0xF).
|
||||
@property
|
||||
def blockMeta(self):
|
||||
return self.block_state_id & 0xF
|
||||
|
||||
@blockMeta.setter
|
||||
def blockMeta(self, meta):
|
||||
self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF)
|
||||
|
||||
# This alias is retained for backward compatibility.
|
||||
blockStateId = attribute_alias('block_state_id')
|
||||
|
||||
|
||||
class MultiBlockChangePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x3F if context.protocol_later_eq(755) else \
|
||||
0x3B if context.protocol_later_eq(741) else \
|
||||
0x0F if context.protocol_later_eq(721) else \
|
||||
0x10 if context.protocol_later_eq(550) else \
|
||||
0x0F if context.protocol_later_eq(343) else \
|
||||
0x10 if context.protocol_later_eq(332) else \
|
||||
0x11 if context.protocol_later_eq(318) else \
|
||||
0x10 if context.protocol_later_eq(67) else \
|
||||
0x22
|
||||
|
||||
packet_name = 'multi block change'
|
||||
|
||||
# Only used in protocol 741 and later.
|
||||
class ChunkSectionPos(Vector, Type):
|
||||
@classmethod
|
||||
def read(cls, file_object):
|
||||
value = UnsignedLong.read(file_object)
|
||||
y = value | ~0xFFFFF if value & 0x80000 else value & 0xFFFFF
|
||||
value >>= 20
|
||||
z = value | ~0x3FFFFF if value & 0x200000 else value & 0x3FFFFF
|
||||
value >>= 22
|
||||
x = value | ~0x3FFFFF if value & 0x200000 else value
|
||||
return cls(x, y, z)
|
||||
|
||||
@classmethod
|
||||
def send(cls, pos, socket):
|
||||
x, y, z = pos
|
||||
value = (x & 0x3FFFFF) << 42 | (z & 0x3FFFFF) << 20 | y & 0xFFFFF
|
||||
UnsignedLong.send(value, socket)
|
||||
|
||||
class Record(MutableRecord, Type):
|
||||
__slots__ = 'x', 'y', 'z', 'block_state_id'
|
||||
|
||||
def __init__(self, **kwds):
|
||||
self.block_state_id = 0
|
||||
super(MultiBlockChangePacket.Record, self).__init__(**kwds)
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector of ints.
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
# For protocols before 347: an accessor for (block_state_id >> 4).
|
||||
@property
|
||||
def blockId(self):
|
||||
return self.block_state_id >> 4
|
||||
|
||||
@blockId.setter
|
||||
def blockId(self, block_id):
|
||||
self.block_state_id = self.block_state_id & 0xF | block_id << 4
|
||||
|
||||
# For protocols before 347: an accessor for (block_state_id & 0xF).
|
||||
@property
|
||||
def blockMeta(self):
|
||||
return self.block_state_id & 0xF
|
||||
|
||||
@blockMeta.setter
|
||||
def blockMeta(self, meta):
|
||||
self.block_state_id = self.block_state_id & ~0xF | meta & 0xF
|
||||
|
||||
# This alias is retained for backward compatibility.
|
||||
blockStateId = attribute_alias('block_state_id')
|
||||
|
||||
@classmethod
|
||||
def read_with_context(cls, file_object, context):
|
||||
record = cls()
|
||||
if context.protocol_later_eq(741):
|
||||
value = VarLong.read(file_object)
|
||||
record.block_state_id = value >> 12
|
||||
record.x = (value >> 8) & 0xF
|
||||
record.z = (value >> 4) & 0xF
|
||||
record.y = value & 0xF
|
||||
else:
|
||||
h_position = UnsignedByte.read(file_object)
|
||||
record.x = h_position >> 4
|
||||
record.z = h_position & 0xF
|
||||
record.y = UnsignedByte.read(file_object)
|
||||
record.block_state_id = VarInt.read(file_object)
|
||||
return record
|
||||
|
||||
@classmethod
|
||||
def send_with_context(self, record, socket, context):
|
||||
if context.protocol_later_eq(741):
|
||||
value = record.block_state_id << 12 | \
|
||||
(record.x & 0xF) << 8 | \
|
||||
(record.z & 0xF) << 4 | \
|
||||
record.y & 0xF
|
||||
VarLong.send(value, socket)
|
||||
else:
|
||||
UnsignedByte.send(record.x << 4 | record.z & 0xF, socket)
|
||||
UnsignedByte.send(record.y, socket)
|
||||
VarInt.send(record.block_state_id, socket)
|
||||
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'chunk_section_pos': MultiBlockChangePacket.ChunkSectionPos},
|
||||
{'invert_trust_edges': Boolean}
|
||||
if context.protocol_later_eq(748) else {}, # Provisional field name.
|
||||
{'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)},
|
||||
] if context.protocol_later_eq(741) else [
|
||||
{'chunk_x': Integer},
|
||||
{'chunk_z': Integer},
|
||||
{'records': PrefixedArray(VarInt, MultiBlockChangePacket.Record)},
|
||||
])
|
||||
|
||||
# Access the 'chunk_x' and 'chunk_z' fields as a tuple.
|
||||
# Only used prior to protocol 741.
|
||||
chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z')
|
|
@ -0,0 +1,169 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from minecraft import PRE
|
||||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Integer, String, MutableRecord
|
||||
)
|
||||
|
||||
|
||||
# Note: this packet was removed in Minecraft 21w07a (protocol PRE|15)
|
||||
# and replaced with the separate EnterCombatEvent, EndCombatEvent, and
|
||||
# DeathCombatEvent packets. These are subclasses of CombatEventPacket, so
|
||||
# that code written to listen for CombatEventPacket instances should in most
|
||||
# cases continue to work without modification.
|
||||
class CombatEventPacket(Packet):
|
||||
@classmethod
|
||||
def get_id(cls, context):
|
||||
return cls.deprecated() if context.protocol_later_eq(PRE | 15) else \
|
||||
0x31 if context.protocol_later_eq(741) else \
|
||||
0x32 if context.protocol_later_eq(721) else \
|
||||
0x33 if context.protocol_later_eq(550) else \
|
||||
0x32 if context.protocol_later_eq(471) else \
|
||||
0x30 if context.protocol_later_eq(451) else \
|
||||
0x2F if context.protocol_later_eq(389) else \
|
||||
0x2E if context.protocol_later_eq(345) else \
|
||||
0x2D if context.protocol_later_eq(336) else \
|
||||
0x2C if context.protocol_later_eq(332) else \
|
||||
0x2D if context.protocol_later_eq(318) else \
|
||||
0x2C if context.protocol_later_eq(86) else \
|
||||
0x2D if context.protocol_later_eq(80) else \
|
||||
0x2C if context.protocol_later_eq(67) else \
|
||||
0x42
|
||||
|
||||
packet_name = 'combat event'
|
||||
|
||||
fields = 'event',
|
||||
|
||||
# The abstract type of the 'event' field of this packet.
|
||||
class EventType(MutableRecord, metaclass=ABCMeta):
|
||||
__slots__ = ()
|
||||
type_from_id_dict = {}
|
||||
|
||||
# Read the fields of the event (not including the ID) from the file.
|
||||
@abstractmethod
|
||||
def read(self, file_object):
|
||||
pass
|
||||
|
||||
# Write the fields of the event (not including the ID) to the buffer.
|
||||
@abstractmethod
|
||||
def write(self, packet_buffer):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def type_from_id(cls, event_id):
|
||||
subcls = cls.type_from_id_dict.get(event_id)
|
||||
if subcls is None:
|
||||
raise ValueError('Unknown combat event ID: %s.' % event_id)
|
||||
return subcls
|
||||
|
||||
class EnterCombatEvent(EventType):
|
||||
__slots__ = ()
|
||||
id = 0
|
||||
|
||||
def read(self, file_object):
|
||||
pass
|
||||
|
||||
def write(self, packet_buffer):
|
||||
pass
|
||||
EventType.type_from_id_dict[EnterCombatEvent.id] = EnterCombatEvent
|
||||
|
||||
class EndCombatEvent(EventType):
|
||||
__slots__ = 'duration', 'entity_id'
|
||||
id = 1
|
||||
|
||||
def read(self, file_object):
|
||||
self.duration = VarInt.read(file_object)
|
||||
self.entity_id = Integer.read(file_object)
|
||||
|
||||
def write(self, packet_buffer):
|
||||
VarInt.send(self.duration, packet_buffer)
|
||||
Integer.send(self.entity_id, packet_buffer)
|
||||
EventType.type_from_id_dict[EndCombatEvent.id] = EndCombatEvent
|
||||
|
||||
class EntityDeadEvent(EventType):
|
||||
__slots__ = 'player_id', 'entity_id', 'message'
|
||||
id = 2
|
||||
|
||||
def read(self, file_object):
|
||||
self.player_id = VarInt.read(file_object)
|
||||
self.entity_id = Integer.read(file_object)
|
||||
self.message = String.read(file_object)
|
||||
|
||||
def write(self, packet_buffer):
|
||||
VarInt.send(self.player_id, packet_buffer)
|
||||
Integer.send(self.entity_id, packet_buffer)
|
||||
String.send(self.message, packet_buffer)
|
||||
EventType.type_from_id_dict[EntityDeadEvent.id] = EntityDeadEvent
|
||||
|
||||
def read(self, file_object):
|
||||
if self.context and self.context.protocol_later_eq(PRE | 15):
|
||||
self.deprecated()
|
||||
event_id = VarInt.read(file_object)
|
||||
self.event = CombatEventPacket.EventType.type_from_id(event_id)()
|
||||
self.event.read(file_object)
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
if self.context and self.context.protocol_later_eq(PRE | 15):
|
||||
self.deprecated()
|
||||
VarInt.send(self.event.id, packet_buffer)
|
||||
self.event.write(packet_buffer)
|
||||
|
||||
@staticmethod
|
||||
def deprecated():
|
||||
raise NotImplementedError(
|
||||
'`CombatEventPacket` was removed in Minecraft snapshot 21w07a '
|
||||
'(protocol version 2**30 + 15). In this and later versions, one '
|
||||
'of the subclasses '
|
||||
+ repr(SpecialisedCombatEventPacket.__subclasses__()) + ' must be '
|
||||
'used directly for usage like that which generates this message.')
|
||||
|
||||
|
||||
# Contains the behaviour common to all concrete CombatEventPacket subclasses
|
||||
class SpecialisedCombatEventPacket(CombatEventPacket):
|
||||
def __init__(self, *args, **kwds):
|
||||
super(SpecialisedCombatEventPacket, self).__init__(*args, **kwds)
|
||||
|
||||
# Prior to Minecraft 21w07a, instances of CombatEventPacket had a
|
||||
# single 'event' field giving a 'MutableRecord' of one of three types
|
||||
# corresponding to the type of combat event represented. For backward
|
||||
# compatibility, we here present a similar interface, giving the packet
|
||||
# object itself as the 'event', which should work identically in most
|
||||
# use cases, since it is a virtual subclass of, and has attributes of
|
||||
# the same names and contents as those of, the previous event records.
|
||||
self.event = self
|
||||
|
||||
# The 'get_id', 'fields', 'read', and 'write_fields' attributes of the
|
||||
# 'Packet' base class are all overridden in 'CombatEventPacket'. We desire
|
||||
# the default behaviour of these attributes, so we restore them here:
|
||||
get_id = Packet.__dict__['get_id']
|
||||
fields = Packet.__dict__['fields']
|
||||
read = Packet.__dict__['read']
|
||||
write_fields = Packet.__dict__['write_fields']
|
||||
|
||||
|
||||
@CombatEventPacket.EnterCombatEvent.register # virtual subclass
|
||||
class EnterCombatEventPacket(SpecialisedCombatEventPacket):
|
||||
packet_name = 'enter combat event'
|
||||
id = 0x34
|
||||
definition = []
|
||||
|
||||
|
||||
@CombatEventPacket.EndCombatEvent.register # virtual subclass
|
||||
class EndCombatEventPacket(SpecialisedCombatEventPacket):
|
||||
packet_name = 'end combat event'
|
||||
id = 0x33
|
||||
definition = [
|
||||
{'duration': VarInt},
|
||||
{'entity_id': Integer}]
|
||||
|
||||
|
||||
@CombatEventPacket.EntityDeadEvent.register # virtual subclass
|
||||
class DeathCombatEventPacket(SpecialisedCombatEventPacket):
|
||||
packet_name = 'death combat event'
|
||||
id = 0x35
|
||||
definition = [
|
||||
{'player_id': VarInt},
|
||||
{'entity_id': Integer},
|
||||
{'message': String}]
|
|
@ -0,0 +1,60 @@
|
|||
from minecraft.networking.types import (
|
||||
Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type,
|
||||
VarInt,
|
||||
)
|
||||
from minecraft.networking.packets import Packet
|
||||
|
||||
|
||||
class ExplosionPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x1C if context.protocol_later_eq(755) else \
|
||||
0x1B if context.protocol_later_eq(741) else \
|
||||
0x1C if context.protocol_later_eq(721) else \
|
||||
0x1D if context.protocol_later_eq(550) else \
|
||||
0x1C if context.protocol_later_eq(471) else \
|
||||
0x1E if context.protocol_later_eq(389) else \
|
||||
0x1D if context.protocol_later_eq(345) else \
|
||||
0x1C if context.protocol_later_eq(332) else \
|
||||
0x1D if context.protocol_later_eq(318) else \
|
||||
0x1C if context.protocol_later_eq(80) else \
|
||||
0x1B if context.protocol_later_eq(67) else \
|
||||
0x27
|
||||
|
||||
packet_name = 'explosion'
|
||||
|
||||
class Record(Vector, Type):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def read(cls, file_object):
|
||||
return cls(*(Byte.read(file_object) for i in range(3)))
|
||||
|
||||
@classmethod
|
||||
def send(cls, record, socket):
|
||||
for coord in record:
|
||||
Byte.send(coord, socket)
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
return [
|
||||
{'x': Float},
|
||||
{'y': Float},
|
||||
{'z': Float},
|
||||
{'radius': Float},
|
||||
|
||||
{'records': PrefixedArray(VarInt, ExplosionPacket.Record)}
|
||||
if context.protocol_later_eq(755) else
|
||||
{'records': PrefixedArray(Integer, ExplosionPacket.Record)},
|
||||
|
||||
{'player_motion_x': Float},
|
||||
{'player_motion_y': Float},
|
||||
{'player_motion_z': Float},
|
||||
]
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector tuple.
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
# Access the 'player_motion_{x,y,z}' fields as a Vector tuple.
|
||||
player_motion = multi_attribute_alias(
|
||||
Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z')
|
|
@ -0,0 +1,79 @@
|
|||
from minecraft.networking.types import (
|
||||
VarInt, Double, Boolean, OriginPoint, Vector, multi_attribute_alias
|
||||
)
|
||||
|
||||
from minecraft.networking.packets import Packet
|
||||
|
||||
|
||||
class FacePlayerPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x37 if context.protocol_later_eq(755) else \
|
||||
0x33 if context.protocol_later_eq(741) else \
|
||||
0x34 if context.protocol_later_eq(721) else \
|
||||
0x35 if context.protocol_later_eq(550) else \
|
||||
0x34 if context.protocol_later_eq(471) else \
|
||||
0x32 if context.protocol_later_eq(451) else \
|
||||
0x31 if context.protocol_later_eq(389) else \
|
||||
0x30
|
||||
|
||||
packet_name = 'face player'
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return ('origin', 'x', 'y', 'z', 'entity_id', 'entity_origin') \
|
||||
if self.context.protocol_later_eq(353) else \
|
||||
('entity_id', 'x', 'y', 'z')
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector tuple.
|
||||
target = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
def read(self, file_object):
|
||||
if self.context.protocol_later_eq(353):
|
||||
self.origin = VarInt.read(file_object)
|
||||
self.x = Double.read(file_object)
|
||||
self.y = Double.read(file_object)
|
||||
self.z = Double.read(file_object)
|
||||
is_entity = Boolean.read(file_object)
|
||||
if is_entity:
|
||||
# If the entity given by entity ID cannot be found,
|
||||
# this packet should be treated as if is_entity was false.
|
||||
self.entity_id = VarInt.read(file_object)
|
||||
self.entity_origin = VarInt.read(file_object)
|
||||
else:
|
||||
self.entity_id = None
|
||||
|
||||
else: # Protocol version 352
|
||||
is_entity = Boolean.read(file_object)
|
||||
self.entity_id = VarInt.read(file_object) if is_entity else None
|
||||
if not is_entity:
|
||||
self.x = Double.read(file_object)
|
||||
self.y = Double.read(file_object)
|
||||
self.z = Double.read(file_object)
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
if self.context.protocol_later_eq(353):
|
||||
VarInt.send(self.origin, packet_buffer)
|
||||
Double.send(self.x, packet_buffer)
|
||||
Double.send(self.y, packet_buffer)
|
||||
Double.send(self.z, packet_buffer)
|
||||
if self.entity_id is not None:
|
||||
Boolean.send(True, packet_buffer)
|
||||
VarInt.send(self.entity_id, packet_buffer)
|
||||
VarInt.send(self.entity_origin, packet_buffer)
|
||||
else:
|
||||
Boolean.send(False, packet_buffer)
|
||||
|
||||
else: # Protocol version 352
|
||||
if self.entity_id is not None:
|
||||
Boolean.send(True, packet_buffer)
|
||||
VarInt.send(self.entity_id, packet_buffer)
|
||||
else:
|
||||
Boolean.send(False, packet_buffer)
|
||||
Double.send(self.x, packet_buffer)
|
||||
Double.send(self.y, packet_buffer)
|
||||
Double.send(self.z, packet_buffer)
|
||||
|
||||
# These aliases declare the Enum type corresponding to each field:
|
||||
Origin = OriginPoint
|
||||
EntityOrigin = OriginPoint
|
|
@ -0,0 +1,212 @@
|
|||
import pynbt
|
||||
|
||||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types import (
|
||||
NBT, Integer, Boolean, UnsignedByte, String, Byte, Long, VarInt,
|
||||
PrefixedArray, Difficulty, GameMode, Dimension,
|
||||
)
|
||||
|
||||
|
||||
def nbt_to_snbt(tag):
|
||||
'''Convert a pyNBT tag to SNBT ("stringified NBT") format.'''
|
||||
scalars = {
|
||||
pynbt.TAG_Byte: 'b',
|
||||
pynbt.TAG_Short: 's',
|
||||
pynbt.TAG_Int: '',
|
||||
pynbt.TAG_Long: 'l',
|
||||
pynbt.TAG_Float: 'f',
|
||||
pynbt.TAG_Double: 'd',
|
||||
}
|
||||
if type(tag) in scalars:
|
||||
return repr(tag.value) + scalars[type(tag)]
|
||||
|
||||
arrays = {
|
||||
pynbt.TAG_Byte_Array: 'B',
|
||||
pynbt.TAG_Int_Array: 'I',
|
||||
pynbt.TAG_Long_Array: 'L',
|
||||
}
|
||||
if type(tag) in arrays:
|
||||
return '[' + arrays[type(tag)] + ';' + \
|
||||
','.join(map(repr, tag.value)) + ']'
|
||||
|
||||
if isinstance(tag, pynbt.TAG_String):
|
||||
return repr(tag.value)
|
||||
|
||||
if isinstance(tag, pynbt.TAG_List):
|
||||
return '[' + ','.join(map(nbt_to_snbt, tag.value)) + ']'
|
||||
|
||||
if isinstance(tag, pynbt.TAG_Compound):
|
||||
return '{' + ','.join(n + ':' + nbt_to_snbt(v)
|
||||
for (n, v) in tag.items()) + '}'
|
||||
|
||||
raise TypeError('Unknown NBT tag type: %r' % type(tag))
|
||||
|
||||
|
||||
class AbstractDimensionPacket(Packet):
|
||||
''' The abstract superclass of JoinGamePacket and RespawnPacket, containing
|
||||
common definitions relating to their 'dimension' field.
|
||||
'''
|
||||
def field_string(self, field):
|
||||
# pylint: disable=no-member
|
||||
if self.context.protocol_later_eq(748) and field == 'dimension':
|
||||
return nbt_to_snbt(self.dimension)
|
||||
elif self.context.protocol_earlier(718) and field == 'dimension':
|
||||
return Dimension.name_from_value(self.dimension)
|
||||
return super(AbstractDimensionPacket, self).field_string(field)
|
||||
|
||||
|
||||
class JoinGamePacket(AbstractDimensionPacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x26 if context.protocol_later_eq(755) else \
|
||||
0x24 if context.protocol_later_eq(741) else \
|
||||
0x25 if context.protocol_later_eq(721) else \
|
||||
0x26 if context.protocol_later_eq(550) else \
|
||||
0x25 if context.protocol_later_eq(389) else \
|
||||
0x24 if context.protocol_later_eq(345) else \
|
||||
0x23 if context.protocol_later_eq(332) else \
|
||||
0x24 if context.protocol_later_eq(318) else \
|
||||
0x23 if context.protocol_later_eq(107) else \
|
||||
0x01
|
||||
|
||||
packet_name = "join game"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'entity_id': Integer},
|
||||
{'is_hardcore': Boolean} if context.protocol_later_eq(738) else {},
|
||||
{'game_mode': UnsignedByte},
|
||||
{'previous_game_mode': UnsignedByte}
|
||||
if context.protocol_later_eq(730) else {},
|
||||
{'world_names': PrefixedArray(VarInt, String)}
|
||||
if context.protocol_later_eq(722) else {},
|
||||
{'dimension_codec': NBT}
|
||||
if context.protocol_later_eq(718) else {},
|
||||
{'dimension':
|
||||
NBT if context.protocol_later_eq(748) else
|
||||
String if context.protocol_later_eq(718) else
|
||||
Integer if context.protocol_later_eq(108) else
|
||||
Byte},
|
||||
{'world_name': String} if context.protocol_later_eq(722) else {},
|
||||
{'hashed_seed': Long} if context.protocol_later_eq(552) else {},
|
||||
{'difficulty': UnsignedByte} if context.protocol_earlier(464) else {},
|
||||
{'max_players':
|
||||
VarInt if context.protocol_later_eq(749) else UnsignedByte},
|
||||
{'level_type': String} if context.protocol_earlier(716) else {},
|
||||
{'render_distance': VarInt} if context.protocol_later_eq(468) else {},
|
||||
{'simulation_distance': VarInt}
|
||||
if context.protocol_later_eq(757) else {},
|
||||
{'reduced_debug_info': Boolean},
|
||||
{'respawn_screen': Boolean} if context.protocol_later_eq(571) else {},
|
||||
{'is_debug': Boolean} if context.protocol_later_eq(716) else {},
|
||||
{'is_flat': Boolean} if context.protocol_later_eq(716) else {},
|
||||
])
|
||||
|
||||
# These aliases declare the Enum type corresponding to each field:
|
||||
Difficulty = Difficulty
|
||||
GameMode = GameMode
|
||||
|
||||
# Accesses the 'game_mode' field appropriately depending on the protocol.
|
||||
# Can be set or deleted when 'context' is undefined.
|
||||
@property
|
||||
def game_mode(self):
|
||||
if self.context.protocol_later_eq(738):
|
||||
return self._game_mode_738
|
||||
else:
|
||||
return self._game_mode_0
|
||||
|
||||
@game_mode.setter
|
||||
def game_mode(self, value):
|
||||
self._game_mode_738 = value
|
||||
self._game_mode_0 = value
|
||||
|
||||
@game_mode.deleter
|
||||
def game_mode(self):
|
||||
del self._game_mode_738
|
||||
del self._game_mode_0
|
||||
|
||||
# Accesses the 'is_hardcore' field, or its equivalent in older protocols.
|
||||
# Can be set or deleted when 'context' is undefined.
|
||||
@property
|
||||
def is_hardcore(self):
|
||||
if self.context.protocol_later_eq(738):
|
||||
return self._is_hardcore
|
||||
else:
|
||||
return bool(self._game_mode_0 & GameMode.HARDCORE)
|
||||
|
||||
@is_hardcore.setter
|
||||
def is_hardcore(self, value):
|
||||
self._is_hardcore = value
|
||||
self._game_mode_0 = \
|
||||
getattr(self, '_game_mode_0', 0) | GameMode.HARDCORE \
|
||||
if value else \
|
||||
getattr(self, '_game_mode_0', 0) & ~GameMode.HARDCORE
|
||||
|
||||
@is_hardcore.deleter
|
||||
def is_hardcore(self):
|
||||
if hasattr(self, '_is_hardcore'):
|
||||
del self._is_hardcore
|
||||
if hasattr(self, '_game_mode_0'):
|
||||
self._game_mode_0 &= ~GameMode.HARDCORE
|
||||
|
||||
# Accesses the component of the 'game_mode' field without any hardcore bit,
|
||||
# version-independently. Can be set or deleted when 'context' is undefined.
|
||||
@property
|
||||
def pure_game_mode(self):
|
||||
if self.context.protocol_later_eq(738):
|
||||
return self._game_mode_738
|
||||
else:
|
||||
return self._game_mode_0 & ~GameMode.HARDCORE
|
||||
|
||||
@pure_game_mode.setter
|
||||
def pure_game_mode(self, value):
|
||||
self._game_mode_738 = value
|
||||
self._game_mode_0 = \
|
||||
value & ~GameMode.HARDCORE | \
|
||||
getattr(self, '_game_mode_0', 0) & GameMode.HARDCORE
|
||||
|
||||
def field_string(self, field):
|
||||
if field == 'dimension_codec':
|
||||
# pylint: disable=no-member
|
||||
return nbt_to_snbt(self.dimension_codec)
|
||||
return super(JoinGamePacket, self).field_string(field)
|
||||
|
||||
|
||||
class RespawnPacket(AbstractDimensionPacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x3D if context.protocol_later_eq(755) else \
|
||||
0x39 if context.protocol_later_eq(741) else \
|
||||
0x3A if context.protocol_later_eq(721) else \
|
||||
0x3B if context.protocol_later_eq(550) else \
|
||||
0x3A if context.protocol_later_eq(471) else \
|
||||
0x38 if context.protocol_later_eq(461) else \
|
||||
0x39 if context.protocol_later_eq(451) else \
|
||||
0x38 if context.protocol_later_eq(389) else \
|
||||
0x37 if context.protocol_later_eq(352) else \
|
||||
0x36 if context.protocol_later_eq(345) else \
|
||||
0x35 if context.protocol_later_eq(336) else \
|
||||
0x34 if context.protocol_later_eq(332) else \
|
||||
0x35 if context.protocol_later_eq(318) else \
|
||||
0x33 if context.protocol_later_eq(70) else \
|
||||
0x07
|
||||
|
||||
packet_name = 'respawn'
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'dimension':
|
||||
NBT if context.protocol_later_eq(748) else
|
||||
String if context.protocol_later_eq(718) else
|
||||
Integer},
|
||||
{'world_name': String} if context.protocol_later_eq(719) else {},
|
||||
{'difficulty': UnsignedByte} if context.protocol_earlier(464) else {},
|
||||
{'hashed_seed': Long} if context.protocol_later_eq(552) else {},
|
||||
{'game_mode': UnsignedByte},
|
||||
{'previous_game_mode': UnsignedByte}
|
||||
if context.protocol_later_eq(730) else {},
|
||||
{'level_type': String} if context.protocol_earlier(716) else {},
|
||||
{'is_debug': Boolean} if context.protocol_later_eq(716) else {},
|
||||
{'is_flat': Boolean} if context.protocol_later_eq(716) else {},
|
||||
{'copy_metadata': Boolean} if context.protocol_later_eq(714) else {},
|
||||
])
|
||||
|
||||
# These aliases declare the Enum type corresponding to each field:
|
||||
Difficulty = Difficulty
|
||||
GameMode = GameMode
|
|
@ -0,0 +1,167 @@
|
|||
from minecraft import PRE
|
||||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Byte, Boolean, UnsignedByte, VarIntPrefixedByteArray, String,
|
||||
MutableRecord
|
||||
)
|
||||
|
||||
|
||||
class MapPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x27 if context.protocol_later_eq(755) else \
|
||||
0x25 if context.protocol_later_eq(741) else \
|
||||
0x26 if context.protocol_later_eq(721) else \
|
||||
0x27 if context.protocol_later_eq(550) else \
|
||||
0x26 if context.protocol_later_eq(389) else \
|
||||
0x25 if context.protocol_later_eq(345) else \
|
||||
0x24 if context.protocol_later_eq(334) else \
|
||||
0x25 if context.protocol_later_eq(318) else \
|
||||
0x24 if context.protocol_later_eq(107) else \
|
||||
0x34
|
||||
|
||||
packet_name = 'map'
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = 'id', 'scale', 'icons', 'width', 'height', 'pixels'
|
||||
if self.context.protocol_later_eq(107):
|
||||
fields += 'is_tracking_position',
|
||||
if self.context.protocol_later_eq(452):
|
||||
fields += 'is_locked',
|
||||
return fields
|
||||
|
||||
def field_string(self, field):
|
||||
if field == 'pixels' and isinstance(self.pixels, bytearray):
|
||||
return 'bytearray(...)'
|
||||
return super(MapPacket, self).field_string(field)
|
||||
|
||||
class MapIcon(MutableRecord):
|
||||
__slots__ = 'type', 'direction', 'location', 'display_name'
|
||||
|
||||
def __init__(self, type, direction, location, display_name=None):
|
||||
self.type = type
|
||||
self.direction = direction
|
||||
self.location = location
|
||||
self.display_name = display_name
|
||||
|
||||
class Map(MutableRecord):
|
||||
__slots__ = ('id', 'scale', 'icons', 'pixels', 'width', 'height',
|
||||
'is_tracking_position', 'is_locked')
|
||||
|
||||
def __init__(self, id=None, scale=None, width=128, height=128):
|
||||
self.id = id
|
||||
self.scale = scale
|
||||
self.icons = []
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.pixels = bytearray(0 for i in range(width*height))
|
||||
self.is_tracking_position = True
|
||||
self.is_locked = False
|
||||
|
||||
class MapSet(object):
|
||||
__slots__ = 'maps_by_id'
|
||||
|
||||
def __init__(self, *maps):
|
||||
self.maps_by_id = {map.id: map for map in maps}
|
||||
|
||||
def __repr__(self):
|
||||
maps = (repr(map) for map in self.maps_by_id.values())
|
||||
return 'MapSet(%s)' % ', '.join(maps)
|
||||
|
||||
def read(self, file_object):
|
||||
self.map_id = VarInt.read(file_object)
|
||||
self.scale = Byte.read(file_object)
|
||||
|
||||
if self.context.protocol_in_range(107, PRE | 6):
|
||||
self.is_tracking_position = Boolean.read(file_object)
|
||||
elif self.context.protocol_earlier(107):
|
||||
self.is_tracking_position = True
|
||||
|
||||
if self.context.protocol_later_eq(452):
|
||||
self.is_locked = Boolean.read(file_object)
|
||||
else:
|
||||
self.is_locked = False
|
||||
|
||||
if self.context.protocol_later_eq(PRE | 6):
|
||||
self.is_tracking_position = Boolean.read(file_object)
|
||||
|
||||
icon_count = VarInt.read(file_object)
|
||||
self.icons = []
|
||||
for i in range(icon_count):
|
||||
if self.context.protocol_later_eq(373):
|
||||
type = VarInt.read(file_object)
|
||||
else:
|
||||
type, direction = divmod(UnsignedByte.read(file_object), 16)
|
||||
x = Byte.read(file_object)
|
||||
z = Byte.read(file_object)
|
||||
if self.context.protocol_later_eq(373):
|
||||
direction = UnsignedByte.read(file_object)
|
||||
if self.context.protocol_later_eq(364):
|
||||
has_name = Boolean.read(file_object)
|
||||
display_name = String.read(file_object) if has_name else None
|
||||
else:
|
||||
display_name = None
|
||||
icon = MapPacket.MapIcon(type, direction, (x, z), display_name)
|
||||
self.icons.append(icon)
|
||||
|
||||
self.width = UnsignedByte.read(file_object)
|
||||
if self.width:
|
||||
self.height = UnsignedByte.read(file_object)
|
||||
x = Byte.read(file_object)
|
||||
z = Byte.read(file_object)
|
||||
self.offset = (x, z)
|
||||
self.pixels = VarIntPrefixedByteArray.read(file_object)
|
||||
else:
|
||||
self.height = 0
|
||||
self.offset = None
|
||||
self.pixels = None
|
||||
|
||||
def apply_to_map(self, map):
|
||||
map.id = self.map_id
|
||||
map.scale = self.scale
|
||||
map.icons[:] = self.icons
|
||||
if self.pixels is not None:
|
||||
for i in range(len(self.pixels)):
|
||||
x = self.offset[0] + i % self.width
|
||||
z = self.offset[1] + i // self.width
|
||||
map.pixels[x + map.width * z] = self.pixels[i]
|
||||
map.is_tracking_position = self.is_tracking_position
|
||||
map.is_locked = self.is_locked
|
||||
|
||||
def apply_to_map_set(self, map_set):
|
||||
map = map_set.maps_by_id.get(self.map_id)
|
||||
if map is None:
|
||||
map = MapPacket.Map(self.map_id)
|
||||
map_set.maps_by_id[self.map_id] = map
|
||||
self.apply_to_map(map)
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
VarInt.send(self.map_id, packet_buffer)
|
||||
Byte.send(self.scale, packet_buffer)
|
||||
if self.context.protocol_later_eq(107):
|
||||
Boolean.send(self.is_tracking_position, packet_buffer)
|
||||
|
||||
VarInt.send(len(self.icons), packet_buffer)
|
||||
for icon in self.icons:
|
||||
if self.context.protocol_later_eq(373):
|
||||
VarInt.send(icon.type, packet_buffer)
|
||||
else:
|
||||
type_and_direction = (icon.type << 4) & 0xF0
|
||||
type_and_direction |= (icon.direction & 0xF)
|
||||
UnsignedByte.send(type_and_direction, packet_buffer)
|
||||
Byte.send(icon.location[0], packet_buffer)
|
||||
Byte.send(icon.location[1], packet_buffer)
|
||||
if self.context.protocol_later_eq(373):
|
||||
UnsignedByte.send(icon.direction, packet_buffer)
|
||||
if self.context.protocol_later_eq(364):
|
||||
Boolean.send(icon.display_name is not None, packet_buffer)
|
||||
if icon.display_name is not None:
|
||||
String.send(icon.display_name, packet_buffer)
|
||||
|
||||
UnsignedByte.send(self.width, packet_buffer)
|
||||
if self.width:
|
||||
UnsignedByte.send(self.height, packet_buffer)
|
||||
UnsignedByte.send(self.offset[0], packet_buffer) # x
|
||||
UnsignedByte.send(self.offset[1], packet_buffer) # z
|
||||
VarIntPrefixedByteArray.send(self.pixels, packet_buffer)
|
|
@ -0,0 +1,219 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
String, Boolean, UUID, VarInt, MutableRecord,
|
||||
)
|
||||
|
||||
|
||||
# Player Info
|
||||
class PlayerListItemPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x36 if context.protocol_later_eq(755) else \
|
||||
0x32 if context.protocol_later_eq(741) else \
|
||||
0x33 if context.protocol_later_eq(721) else \
|
||||
0x34 if context.protocol_later_eq(550) else \
|
||||
0x33 if context.protocol_later_eq(471) else \
|
||||
0x31 if context.protocol_later_eq(451) else \
|
||||
0x30 if context.protocol_later_eq(389) else \
|
||||
0x2F if context.protocol_later_eq(345) else \
|
||||
0x2E if context.protocol_later_eq(336) else \
|
||||
0x2D if context.protocol_later_eq(332) else \
|
||||
0x2E if context.protocol_later_eq(318) else \
|
||||
0x2D if context.protocol_later_eq(107) else \
|
||||
0x38
|
||||
|
||||
packet_name = "player list item"
|
||||
|
||||
fields = 'action_type', 'actions'
|
||||
|
||||
def field_string(self, field):
|
||||
if field == 'action_type':
|
||||
return self.action_type.__name__
|
||||
return super(PlayerListItemPacket, self).field_string(field)
|
||||
|
||||
class PlayerList(object):
|
||||
__slots__ = 'players_by_uuid'
|
||||
|
||||
def __init__(self, *items):
|
||||
self.players_by_uuid = {item.uuid: item for item in items}
|
||||
|
||||
class PlayerListItem(MutableRecord):
|
||||
__slots__ = (
|
||||
'uuid', 'name', 'properties', 'gamemode', 'ping', 'display_name')
|
||||
|
||||
class PlayerProperty(MutableRecord):
|
||||
__slots__ = 'name', 'value', 'signature'
|
||||
|
||||
def read(self, file_object):
|
||||
self.name = String.read(file_object)
|
||||
self.value = String.read(file_object)
|
||||
is_signed = Boolean.read(file_object)
|
||||
if is_signed:
|
||||
self.signature = String.read(file_object)
|
||||
else:
|
||||
self.signature = None
|
||||
|
||||
def send(self, packet_buffer):
|
||||
String.send(self.name, packet_buffer)
|
||||
String.send(self.value, packet_buffer)
|
||||
if self.signature is not None:
|
||||
Boolean.send(True, packet_buffer)
|
||||
String.send(self.signature, packet_buffer)
|
||||
else:
|
||||
Boolean.send(False, packet_buffer)
|
||||
|
||||
class Action(MutableRecord):
|
||||
__slots__ = 'uuid',
|
||||
|
||||
def read(self, file_object):
|
||||
self.uuid = UUID.read(file_object)
|
||||
self._read(file_object)
|
||||
|
||||
def send(self, packet_buffer):
|
||||
UUID.send(self.uuid, packet_buffer)
|
||||
self._send(packet_buffer)
|
||||
|
||||
def _read(self, file_object):
|
||||
raise NotImplementedError(
|
||||
'This abstract method must be overridden in a subclass.')
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
raise NotImplementedError(
|
||||
'This abstract method must be overridden in a subclass.')
|
||||
|
||||
@classmethod
|
||||
def type_from_id(cls, action_id):
|
||||
for subcls in cls.__subclasses__():
|
||||
if subcls.action_id == action_id:
|
||||
return subcls
|
||||
raise ValueError("Unknown player list action ID: %s." % action_id)
|
||||
|
||||
class AddPlayerAction(Action):
|
||||
__slots__ = 'name', 'properties', 'gamemode', 'ping', 'display_name'
|
||||
action_id = 0
|
||||
|
||||
def _read(self, file_object):
|
||||
self.name = String.read(file_object)
|
||||
prop_count = VarInt.read(file_object)
|
||||
self.properties = []
|
||||
for i in range(prop_count):
|
||||
property = PlayerListItemPacket.PlayerProperty()
|
||||
property.read(file_object)
|
||||
self.properties.append(property)
|
||||
self.gamemode = VarInt.read(file_object)
|
||||
self.ping = VarInt.read(file_object)
|
||||
has_display_name = Boolean.read(file_object)
|
||||
if has_display_name:
|
||||
self.display_name = String.read(file_object)
|
||||
else:
|
||||
self.display_name = None
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
String.send(self.name, packet_buffer)
|
||||
VarInt.send(len(self.properties), packet_buffer)
|
||||
for property in self.properties:
|
||||
property.send(packet_buffer)
|
||||
VarInt.send(self.gamemode, packet_buffer)
|
||||
VarInt.send(self.ping, packet_buffer)
|
||||
if self.display_name is not None:
|
||||
Boolean.send(True, packet_buffer)
|
||||
String.send(self.display_name, packet_buffer)
|
||||
else:
|
||||
Boolean.send(False, packet_buffer)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = PlayerListItemPacket.PlayerListItem(
|
||||
uuid=self.uuid,
|
||||
name=self.name,
|
||||
properties=self.properties,
|
||||
gamemode=self.gamemode,
|
||||
ping=self.ping,
|
||||
display_name=self.display_name)
|
||||
player_list.players_by_uuid[self.uuid] = player
|
||||
|
||||
class UpdateGameModeAction(Action):
|
||||
__slots__ = 'gamemode'
|
||||
action_id = 1
|
||||
|
||||
def _read(self, file_object):
|
||||
self.gamemode = VarInt.read(file_object)
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
VarInt.send(self.gamemode, packet_buffer)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.gamemode = self.gamemode
|
||||
|
||||
class UpdateLatencyAction(Action):
|
||||
__slots__ = 'ping'
|
||||
action_id = 2
|
||||
|
||||
def _read(self, file_object):
|
||||
self.ping = VarInt.read(file_object)
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
VarInt.send(self.ping, packet_buffer)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.ping = self.ping
|
||||
|
||||
class UpdateDisplayNameAction(Action):
|
||||
__slots__ = 'display_name'
|
||||
action_id = 3
|
||||
|
||||
def _read(self, file_object):
|
||||
has_display_name = Boolean.read(file_object)
|
||||
if has_display_name:
|
||||
self.display_name = String.read(file_object)
|
||||
else:
|
||||
self.display_name = None
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
if self.display_name is not None:
|
||||
Boolean.send(True, packet_buffer)
|
||||
String.send(self.display_name, packet_buffer)
|
||||
else:
|
||||
Boolean.send(False, packet_buffer)
|
||||
|
||||
def apply(self, player_list):
|
||||
player = player_list.players_by_uuid.get(self.uuid)
|
||||
if player:
|
||||
player.display_name = self.display_name
|
||||
|
||||
class RemovePlayerAction(Action):
|
||||
action_id = 4
|
||||
|
||||
def _read(self, file_object):
|
||||
pass
|
||||
|
||||
def _send(self, packet_buffer):
|
||||
pass
|
||||
|
||||
def apply(self, player_list):
|
||||
if self.uuid in player_list.players_by_uuid:
|
||||
del player_list.players_by_uuid[self.uuid]
|
||||
|
||||
def read(self, file_object):
|
||||
action_id = VarInt.read(file_object)
|
||||
self.action_type = PlayerListItemPacket.Action.type_from_id(action_id)
|
||||
action_count = VarInt.read(file_object)
|
||||
self.actions = []
|
||||
for i in range(action_count):
|
||||
action = self.action_type()
|
||||
action.read(file_object)
|
||||
self.actions.append(action)
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
VarInt.send(self.action_type.action_id, packet_buffer)
|
||||
VarInt.send(len(self.actions), packet_buffer)
|
||||
for action in self.actions:
|
||||
action.send(packet_buffer)
|
||||
|
||||
def apply(self, player_list):
|
||||
for action in self.actions:
|
||||
action.apply(player_list)
|
|
@ -0,0 +1,94 @@
|
|||
from minecraft.networking.types.basic import Boolean
|
||||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
Double, Float, Byte, VarInt, BitFieldEnum, Vector, Direction,
|
||||
PositionAndLook, multi_attribute_alias,
|
||||
)
|
||||
|
||||
|
||||
class PlayerPositionAndLookPacket(Packet, BitFieldEnum):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x38 if context.protocol_later_eq(755) else \
|
||||
0x34 if context.protocol_later_eq(741) else \
|
||||
0x35 if context.protocol_later_eq(721) else \
|
||||
0x36 if context.protocol_later_eq(550) else \
|
||||
0x35 if context.protocol_later_eq(471) else \
|
||||
0x33 if context.protocol_later_eq(451) else \
|
||||
0x32 if context.protocol_later_eq(389) else \
|
||||
0x31 if context.protocol_later_eq(352) else \
|
||||
0x30 if context.protocol_later_eq(345) else \
|
||||
0x2F if context.protocol_later_eq(336) else \
|
||||
0x2E if context.protocol_later_eq(332) else \
|
||||
0x2F if context.protocol_later_eq(318) else \
|
||||
0x2E if context.protocol_later_eq(70) else \
|
||||
0x08
|
||||
|
||||
packet_name = "player position and look"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'x': Double},
|
||||
{'y': Double},
|
||||
{'z': Double},
|
||||
{'yaw': Float},
|
||||
{'pitch': Float},
|
||||
{'flags': Byte},
|
||||
{'teleport_id': VarInt} if context.protocol_later_eq(107) else {},
|
||||
{'dismount_vehicle': Boolean}
|
||||
if context.protocol_later_eq(755) else {},
|
||||
])
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector tuple.
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
# Access the 'yaw', 'pitch' fields as a Direction tuple.
|
||||
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
||||
|
||||
# Access the 'x', 'y', 'z', 'yaw', 'pitch' fields as a PositionAndLook.
|
||||
# NOTE: modifying the object retrieved from this property will not change
|
||||
# the packet; it can only be changed by attribute or property assignment.
|
||||
position_and_look = multi_attribute_alias(
|
||||
PositionAndLook, 'x', 'y', 'z', 'yaw', 'pitch')
|
||||
|
||||
field_enum = classmethod(
|
||||
lambda cls, field, context: cls if field == 'flags' else None)
|
||||
|
||||
FLAG_REL_X = 0x01
|
||||
FLAG_REL_Y = 0x02
|
||||
FLAG_REL_Z = 0x04
|
||||
FLAG_REL_YAW = 0x08
|
||||
FLAG_REL_PITCH = 0x10
|
||||
|
||||
# This alias is retained for backward compatibility.
|
||||
PositionAndLook = PositionAndLook
|
||||
|
||||
# Update a PositionAndLook instance using this packet.
|
||||
def apply(self, target):
|
||||
# pylint: disable=no-member
|
||||
if self.flags & self.FLAG_REL_X:
|
||||
target.x += self.x
|
||||
else:
|
||||
target.x = self.x
|
||||
|
||||
if self.flags & self.FLAG_REL_Y:
|
||||
target.y += self.y
|
||||
else:
|
||||
target.y = self.y
|
||||
|
||||
if self.flags & self.FLAG_REL_Z:
|
||||
target.z += self.z
|
||||
else:
|
||||
target.z = self.z
|
||||
|
||||
if self.flags & self.FLAG_REL_YAW:
|
||||
target.yaw += self.yaw
|
||||
else:
|
||||
target.yaw = self.yaw
|
||||
|
||||
if self.flags & self.FLAG_REL_PITCH:
|
||||
target.pitch += self.pitch
|
||||
else:
|
||||
target.pitch = self.pitch
|
||||
|
||||
target.yaw %= 360
|
||||
target.pitch %= 360
|
|
@ -0,0 +1,90 @@
|
|||
from minecraft import PRE
|
||||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types import (
|
||||
VarInt, String, Float, Byte, Type, Integer, Vector, Enum,
|
||||
)
|
||||
|
||||
__all__ = 'SoundEffectPacket',
|
||||
|
||||
|
||||
class SoundEffectPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x5D if context.protocol_later_eq(PRE | 48) else \
|
||||
0x5C if context.protocol_later_eq(755) else \
|
||||
0x51 if context.protocol_later_eq(721) else \
|
||||
0x52 if context.protocol_later_eq(550) else \
|
||||
0x51 if context.protocol_later_eq(471) else \
|
||||
0x4D if context.protocol_later_eq(461) else \
|
||||
0x4E if context.protocol_later_eq(451) else \
|
||||
0x4D if context.protocol_later_eq(389) else \
|
||||
0x4C if context.protocol_later_eq(352) else \
|
||||
0x4B if context.protocol_later_eq(345) else \
|
||||
0x4A if context.protocol_later_eq(343) else \
|
||||
0x49 if context.protocol_later_eq(336) else \
|
||||
0x48 if context.protocol_later_eq(318) else \
|
||||
0x46 if context.protocol_later_eq(110) else \
|
||||
0x47
|
||||
|
||||
packet_name = 'sound effect'
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
return [
|
||||
({'sound_category': VarInt}
|
||||
if context.protocol_later_eq(321)
|
||||
and context.protocol_earlier(326) else {}),
|
||||
{'sound_id': VarInt},
|
||||
({'sound_category': VarInt}
|
||||
if context.protocol_later_eq(95)
|
||||
and context.protocol_earlier(321)
|
||||
or context.protocol_later_eq(326) else {}),
|
||||
({'parroted_entity_type': String}
|
||||
if context.protocol_later_eq(321)
|
||||
and context.protocol_earlier(326) else {}),
|
||||
{'effect_position': SoundEffectPacket.EffectPosition},
|
||||
{'volume': Float},
|
||||
{'pitch': SoundEffectPacket.Pitch},
|
||||
]
|
||||
|
||||
class SoundCategory(Enum):
|
||||
MASTER = 0
|
||||
MUSIC = 1
|
||||
RECORDS = 2
|
||||
WEATHER = 3
|
||||
BLOCKS = 4
|
||||
HOSTILE = 5
|
||||
NEUTRAL = 6
|
||||
PLAYERS = 7
|
||||
AMBIENT = 8
|
||||
VOICE = 9
|
||||
|
||||
class EffectPosition(Type):
|
||||
@classmethod
|
||||
def read(cls, file_object):
|
||||
return Vector(*(Integer.read(file_object) / 8.0 for i in range(3)))
|
||||
|
||||
@classmethod
|
||||
def send(cls, value, socket):
|
||||
for coordinate in value:
|
||||
Integer.send(int(coordinate * 8), socket)
|
||||
|
||||
class Pitch(Type):
|
||||
@staticmethod
|
||||
def read_with_context(file_object, context):
|
||||
if context.protocol_later_eq(201):
|
||||
value = Float.read(file_object)
|
||||
else:
|
||||
value = Byte.read(file_object)
|
||||
if context.protocol_earlier(204):
|
||||
value /= 63.5
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def send_with_context(value, socket, context):
|
||||
if context.protocol_earlier(204):
|
||||
value *= 63.5
|
||||
if context.protocol_later_eq(201):
|
||||
Float.send(value, socket)
|
||||
else:
|
||||
Byte.send(int(value), socket)
|
|
@ -0,0 +1,174 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types.utility import descriptor
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, UUID, Byte, Double, Integer, Angle, Short, Enum, Vector,
|
||||
Direction, PositionAndLook, attribute_alias, multi_attribute_alias,
|
||||
)
|
||||
|
||||
|
||||
class SpawnObjectPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x00 if context.protocol_later_eq(67) else \
|
||||
0x0E
|
||||
|
||||
packet_name = 'spawn object'
|
||||
|
||||
fields = ('entity_id', 'object_uuid', 'type_id', 'x', 'y', 'z', 'pitch',
|
||||
'yaw', 'data', 'velocity_x', 'velocity_y', 'velocity_z')
|
||||
|
||||
@descriptor
|
||||
def EntityType(desc, self, cls): # pylint: disable=no-self-argument
|
||||
if self is None:
|
||||
# EntityType is being accessed as a class attribute.
|
||||
raise AttributeError(
|
||||
'This interface is deprecated:\n\n'
|
||||
'As of pyCraft\'s support for Minecraft 1.14, the nested '
|
||||
'class "SpawnObjectPacket.EntityType" cannot be accessed as a '
|
||||
'class attribute, because it depends on the protocol version. '
|
||||
'There are two ways to access the correct version of the '
|
||||
'class:\n\n'
|
||||
'1. Access the "EntityType" attribute of a '
|
||||
'"SpawnObjectPacket" instance with its "context" property '
|
||||
'set.\n\n'
|
||||
'2. Call "SpawnObjectPacket.field_enum(\'type_id\', '
|
||||
'context)".')
|
||||
else:
|
||||
# EntityType is being accessed as an instance attribute.
|
||||
return self.field_enum('type_id', self.context)
|
||||
|
||||
@classmethod
|
||||
def field_enum(cls, field, context):
|
||||
if field != 'type_id' or context is None:
|
||||
return
|
||||
|
||||
name = 'EntityType_%d' % context.protocol_version
|
||||
if hasattr(cls, name):
|
||||
return getattr(cls, name)
|
||||
|
||||
era = 0 if context.protocol_earlier(458) else 1
|
||||
|
||||
class EntityType(Enum):
|
||||
# XXX This has not been updated for >= v1.15
|
||||
ACTIVATED_TNT = (50, 55)[era] # PrimedTnt
|
||||
AREA_EFFECT_CLOUD = ( 3, 0)[era]
|
||||
ARMORSTAND = (78, 1)[era]
|
||||
ARROW = (60, 2)[era]
|
||||
BOAT = ( 1, 5)[era]
|
||||
DRAGON_FIREBALL = (93, 13)[era]
|
||||
EGG = (62, 74)[era] # ThrownEgg
|
||||
ENDERCRYSTAL = (51, 16)[era]
|
||||
ENDERPEARL = (65, 75)[era] # ThrownEnderpearl
|
||||
EVOCATION_FANGS = (79, 20)[era]
|
||||
EXP_BOTTLE = (75, 76)[era] # ThrownExpBottle
|
||||
EYE_OF_ENDER = (72, 23)[era] # EyeOfEnderSignal
|
||||
FALLING_OBJECT = (70, 24)[era] # FallingSand
|
||||
FIREBALL = (63, 34)[era] # Fireball (ghast)
|
||||
FIRECHARGE = (64, 65)[era] # SmallFireball (blaze)
|
||||
FIREWORK_ROCKET = (76, 25)[era] # FireworksRocketEntity
|
||||
FISHING_HOOK = (90, 93)[era] # Fishing bobber
|
||||
ITEM_FRAMES = (71, 33)[era] # ItemFrame
|
||||
ITEM_STACK = ( 2, 32)[era] # Item
|
||||
LEASH_KNOT = (77, 35)[era]
|
||||
LLAMA_SPIT = (68, 37)[era]
|
||||
MINECART = (10, 39)[era] # MinecartRideable
|
||||
POTION = (73, 77)[era] # ThrownPotion
|
||||
SHULKER_BULLET = (67, 60)[era]
|
||||
SNOWBALL = (61, 67)[era]
|
||||
SPECTRAL_ARROW = (91, 68)[era]
|
||||
WITHER_SKULL = (66, 85)[era]
|
||||
if context.protocol_later_eq(393):
|
||||
TRIDENT = 94
|
||||
if context.protocol_later_eq(458):
|
||||
MINECART_CHEST = 40
|
||||
MINECART_COMMAND_BLOCK = 41
|
||||
MINECART_FURNACE = 42
|
||||
MINECART_HOPPER = 43
|
||||
MINECART_SPAWNER = 44
|
||||
MINECART_TNT = 45
|
||||
|
||||
setattr(cls, name, EntityType)
|
||||
return EntityType
|
||||
|
||||
def read(self, file_object):
|
||||
self.entity_id = VarInt.read(file_object)
|
||||
if self.context.protocol_later_eq(49):
|
||||
self.object_uuid = UUID.read(file_object)
|
||||
|
||||
if self.context.protocol_later_eq(458):
|
||||
self.type_id = VarInt.read(file_object)
|
||||
else:
|
||||
self.type_id = Byte.read(file_object)
|
||||
|
||||
xyz_type = Double if self.context.protocol_later_eq(100) else Integer
|
||||
for attr in 'x', 'y', 'z':
|
||||
setattr(self, attr, xyz_type.read(file_object))
|
||||
for attr in 'pitch', 'yaw':
|
||||
setattr(self, attr, Angle.read(file_object))
|
||||
|
||||
self.data = Integer.read(file_object)
|
||||
if self.context.protocol_later_eq(49) or self.data > 0:
|
||||
for attr in 'velocity_x', 'velocity_y', 'velocity_z':
|
||||
setattr(self, attr, Short.read(file_object))
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
VarInt.send(self.entity_id, packet_buffer)
|
||||
if self.context.protocol_later_eq(49):
|
||||
UUID.send(self.object_uuid, packet_buffer)
|
||||
|
||||
if self.context.protocol_later_eq(458):
|
||||
VarInt.send(self.type_id, packet_buffer)
|
||||
else:
|
||||
Byte.send(self.type_id, packet_buffer)
|
||||
|
||||
# pylint: disable=no-member
|
||||
xyz_type = Double if self.context.protocol_later_eq(100) else Integer
|
||||
for coord in self.x, self.y, self.z:
|
||||
xyz_type.send(coord, packet_buffer)
|
||||
for coord in self.pitch, self.yaw:
|
||||
Angle.send(coord, packet_buffer)
|
||||
|
||||
Integer.send(self.data, packet_buffer)
|
||||
if self.context.protocol_later_eq(49) or self.data > 0:
|
||||
for coord in self.velocity_x, self.velocity_y, self.velocity_z:
|
||||
Short.send(coord, packet_buffer)
|
||||
|
||||
# Access the entity type as a string, according to the EntityType enum.
|
||||
@property
|
||||
def type(self):
|
||||
if self.context is None:
|
||||
raise ValueError('This packet must have a non-None "context" '
|
||||
'in order to read the "type" property.')
|
||||
# pylint: disable=no-member
|
||||
return self.EntityType.name_from_value(self.type_id)
|
||||
|
||||
@type.setter
|
||||
def type(self, type_name):
|
||||
if self.context is None:
|
||||
raise ValueError('This packet must have a non-None "context" '
|
||||
'in order to set the "type" property.')
|
||||
self.type_id = getattr(self.EntityType, type_name)
|
||||
|
||||
@type.deleter
|
||||
def type(self):
|
||||
del self.type_id
|
||||
|
||||
# Access the 'x', 'y', 'z' fields as a Vector.
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
# Access the 'yaw', 'pitch' fields as a Direction.
|
||||
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
||||
|
||||
# Access the 'x', 'y', 'z', 'pitch', 'yaw' fields as a PositionAndLook.
|
||||
# NOTE: modifying the object retrieved from this property will not change
|
||||
# the packet; it can only be changed by attribute or property assignment.
|
||||
position_and_look = multi_attribute_alias(
|
||||
PositionAndLook, x='x', y='y', z='z', yaw='yaw', pitch='pitch')
|
||||
|
||||
# Access the 'velocity_{x,y,z}' fields as a Vector.
|
||||
velocity = multi_attribute_alias(
|
||||
Vector, 'velocity_x', 'velocity_y', 'velocity_z')
|
||||
|
||||
# This alias is retained for backward compatibility.
|
||||
objectUUID = attribute_alias('object_uuid')
|
|
@ -0,0 +1,28 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
String, Long
|
||||
)
|
||||
|
||||
|
||||
# Formerly known as state_status_clientbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
ResponsePacket,
|
||||
PingResponsePacket,
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class ResponsePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "response"
|
||||
definition = [
|
||||
{'json_response': String}]
|
||||
|
||||
|
||||
class PingResponsePacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
|
@ -0,0 +1,17 @@
|
|||
from .packet import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Long
|
||||
)
|
||||
|
||||
|
||||
class AbstractKeepAlivePacket(Packet):
|
||||
packet_name = "keep alive"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'keep_alive_id': Long} if context.protocol_later_eq(339)
|
||||
else {'keep_alive_id': VarInt}
|
||||
])
|
||||
|
||||
|
||||
# This alias is retained for backward compatibility:
|
||||
KeepAlivePacket = AbstractKeepAlivePacket
|
|
@ -0,0 +1,156 @@
|
|||
from zlib import compress
|
||||
|
||||
from .packet_buffer import PacketBuffer
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Enum, overridable_property,
|
||||
)
|
||||
|
||||
|
||||
class Packet(object):
|
||||
packet_name = "base"
|
||||
|
||||
# To define the packet ID, either:
|
||||
# 1. Define the attribute `id', of type int, in a subclass; or
|
||||
# 2. Override `get_id' in a subclass and return the correct packet ID
|
||||
# for the given ConnectionContext. This is necessary if the packet ID
|
||||
# has changed across protocol versions, for example; or
|
||||
# 3. Define the attribute `id' in an instance of a class without either
|
||||
# of the above.
|
||||
@classmethod
|
||||
def get_id(cls, _context):
|
||||
return getattr(cls, 'id')
|
||||
|
||||
@overridable_property
|
||||
def id(self):
|
||||
return None if self.context is None else self.get_id(self.context)
|
||||
|
||||
# To define the network data layout of a packet, either:
|
||||
# 1. Define the attribute `definition', a list of fields, each of which
|
||||
# is a dict mapping attribute names to data types; or
|
||||
# 2. Override `get_definition' in a subclass and return the correct
|
||||
# definition for the given ConnectionContext. This may be necessary
|
||||
# if the layout has changed across protocol versions, for example; or
|
||||
# 3. Override the methods `read' and/or `write_fields' in a subclass.
|
||||
# This may be necessary if the packet layout cannot be described as a
|
||||
# simple list of fields.
|
||||
@classmethod
|
||||
def get_definition(cls, _context):
|
||||
return getattr(cls, 'definition')
|
||||
|
||||
@overridable_property
|
||||
def definition(self):
|
||||
return None if self.context is None else \
|
||||
self.get_definition(self.context)
|
||||
|
||||
# In general, a packet instance must have its 'context' attribute set to an
|
||||
# instance of 'ConnectionContext', for example to decide on version-
|
||||
# dependent behaviour. This can either be given as an argument to this
|
||||
# constructor (e.g. 'p = P(context=c)') or set later
|
||||
# (e.g. 'p.context = c').
|
||||
#
|
||||
# While a packet has no 'context' set, all attributes should *writable*
|
||||
# without errors, but some attributes may not be *readable*.
|
||||
#
|
||||
# When sending or receiving packets via 'Connection', it is generally not
|
||||
# necessary to set the 'context', as this will be done automatically by
|
||||
# 'Connection'.
|
||||
def __init__(self, context=None, **kwargs):
|
||||
self.context = context
|
||||
self.set_values(**kwargs)
|
||||
|
||||
def set_values(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
return self
|
||||
|
||||
def read(self, file_object):
|
||||
for field in self.definition: # pylint: disable=not-an-iterable
|
||||
for var_name, data_type in field.items():
|
||||
value = data_type.read_with_context(file_object, self.context)
|
||||
setattr(self, var_name, value)
|
||||
|
||||
# Writes a packet buffer to the socket with the appropriate headers
|
||||
# and compressing the data if necessary
|
||||
def _write_buffer(self, socket, packet_buffer, compression_threshold):
|
||||
# compression_threshold of None means compression is disabled
|
||||
if compression_threshold is not None:
|
||||
if len(packet_buffer.get_writable()) > compression_threshold != -1:
|
||||
# compress the current payload
|
||||
packet_data = packet_buffer.get_writable()
|
||||
compressed_data = compress(packet_data)
|
||||
packet_buffer.reset()
|
||||
# write out the length of the uncompressed payload
|
||||
VarInt.send(len(packet_data), packet_buffer)
|
||||
# write the compressed payload itself
|
||||
packet_buffer.send(compressed_data)
|
||||
else:
|
||||
# write out a 0 to indicate uncompressed data
|
||||
packet_data = packet_buffer.get_writable()
|
||||
packet_buffer.reset()
|
||||
VarInt.send(0, packet_buffer)
|
||||
packet_buffer.send(packet_data)
|
||||
|
||||
VarInt.send(len(packet_buffer.get_writable()), socket) # Packet Size
|
||||
socket.send(packet_buffer.get_writable()) # Packet Payload
|
||||
|
||||
def write(self, socket, compression_threshold=None):
|
||||
# buffer the data since we need to know the length of each packet's
|
||||
# payload
|
||||
packet_buffer = PacketBuffer()
|
||||
# write packet's id right off the bat in the header
|
||||
VarInt.send(self.id, packet_buffer)
|
||||
# write every individual field
|
||||
self.write_fields(packet_buffer)
|
||||
self._write_buffer(socket, packet_buffer, compression_threshold)
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
# Write the fields comprising the body of the packet (excluding the
|
||||
# length, packet ID, compression and encryption) into a PacketBuffer.
|
||||
for field in self.definition: # pylint: disable=not-an-iterable
|
||||
for var_name, data_type in field.items():
|
||||
data = getattr(self, var_name)
|
||||
data_type.send_with_context(data, packet_buffer, self.context)
|
||||
|
||||
def __repr__(self):
|
||||
str = type(self).__name__
|
||||
if self.id is not None:
|
||||
str = '0x%02X %s' % (self.id, str)
|
||||
fields = self.fields
|
||||
if fields is not None:
|
||||
inner_str = ', '.join('%s=%s' % (a, self.field_string(a))
|
||||
for a in fields if hasattr(self, a))
|
||||
str = '%s(%s)' % (str, inner_str)
|
||||
return str
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
""" An iterable of the names of the packet's fields, or None. """
|
||||
if self.definition is None:
|
||||
return None
|
||||
# pylint: disable=not-an-iterable
|
||||
return (field for defn in self.definition for field in defn)
|
||||
|
||||
def field_string(self, field):
|
||||
""" The string representation of the value of a the given named field
|
||||
of this packet. Override to customise field value representation.
|
||||
"""
|
||||
value = getattr(self, field, None)
|
||||
|
||||
enum_class = self.field_enum(field, self.context)
|
||||
if enum_class is not None:
|
||||
name = enum_class.name_from_value(value)
|
||||
if name is not None:
|
||||
return name
|
||||
|
||||
return repr(value)
|
||||
|
||||
@classmethod
|
||||
def field_enum(cls, field, context=None):
|
||||
""" The subclass of 'minecraft.networking.types.Enum' associated with
|
||||
this field, or None if there is no such class.
|
||||
"""
|
||||
enum_name = ''.join(s.capitalize() for s in field.split('_'))
|
||||
if hasattr(cls, enum_name):
|
||||
enum_class = getattr(cls, enum_name)
|
||||
if isinstance(enum_class, type) and issubclass(enum_class, Enum):
|
||||
return enum_class
|
|
@ -0,0 +1,28 @@
|
|||
from io import BytesIO
|
||||
|
||||
|
||||
class PacketBuffer(object):
|
||||
def __init__(self):
|
||||
self.bytes = BytesIO()
|
||||
|
||||
def send(self, value):
|
||||
"""
|
||||
Writes the given bytes to the buffer, designed to emulate socket.send
|
||||
:param value: The bytes to write
|
||||
"""
|
||||
self.bytes.write(value)
|
||||
|
||||
def read(self, length=None):
|
||||
return self.bytes.read(length)
|
||||
|
||||
def recv(self, length=None):
|
||||
return self.read(length)
|
||||
|
||||
def reset(self):
|
||||
self.bytes = BytesIO()
|
||||
|
||||
def reset_cursor(self):
|
||||
self.bytes.seek(0)
|
||||
|
||||
def get_writable(self):
|
||||
return self.bytes.getvalue()
|
|
@ -0,0 +1,17 @@
|
|||
from .packet import Packet
|
||||
|
||||
|
||||
class PacketListener(object):
|
||||
def __init__(self, callback, *args):
|
||||
self.callback = callback
|
||||
self.packets_to_listen = []
|
||||
for arg in args:
|
||||
if issubclass(arg, Packet):
|
||||
self.packets_to_listen.append(arg)
|
||||
|
||||
def call_packet(self, packet):
|
||||
for packet_type in self.packets_to_listen:
|
||||
if isinstance(packet, packet_type):
|
||||
self.callback(packet)
|
||||
return True
|
||||
return False
|
|
@ -0,0 +1,12 @@
|
|||
from .packet import Packet
|
||||
from minecraft.networking.types import String, TrailingByteArray
|
||||
|
||||
|
||||
class AbstractPluginMessagePacket(Packet):
|
||||
"""NOTE: Plugin channels were significantly changed, including changing the
|
||||
names of channels, between Minecraft 1.12 and 1.13 - see <http://wiki.vg
|
||||
/index.php?title=Pre-release_protocol&oldid=14132#Plugin_Channels>.
|
||||
"""
|
||||
definition = [
|
||||
{'channel': String},
|
||||
{'data': TrailingByteArray}]
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Contains the serverbound packets for `pyminecraft`.
|
||||
"""
|
|
@ -0,0 +1,23 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, String, UnsignedShort
|
||||
)
|
||||
|
||||
|
||||
# Formerly known as state_handshake_serverbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
HandShakePacket
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class HandShakePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "handshake"
|
||||
definition = [
|
||||
{'protocol_version': VarInt},
|
||||
{'server_address': String},
|
||||
{'server_port': UnsignedShort},
|
||||
{'next_state': VarInt}]
|
|
@ -0,0 +1,77 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Boolean, String, VarIntPrefixedByteArray, TrailingByteArray
|
||||
)
|
||||
|
||||
|
||||
# Formerly known as state_login_serverbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
LoginStartPacket,
|
||||
EncryptionResponsePacket
|
||||
}
|
||||
if context.protocol_later_eq(385):
|
||||
packets |= {
|
||||
PluginResponsePacket
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class LoginStartPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x00 if context.protocol_later_eq(391) else \
|
||||
0x01 if context.protocol_later_eq(385) else \
|
||||
0x00
|
||||
|
||||
packet_name = "login start"
|
||||
definition = [
|
||||
{'name': String}]
|
||||
|
||||
|
||||
class EncryptionResponsePacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x01 if context.protocol_later_eq(391) else \
|
||||
0x02 if context.protocol_later_eq(385) else \
|
||||
0x01
|
||||
|
||||
packet_name = "encryption response"
|
||||
definition = [
|
||||
{'shared_secret': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
|
||||
class PluginResponsePacket(Packet):
|
||||
""" NOTE: see comments on 'clientbound.login.PluginRequestPacket' for
|
||||
important information on the usage of this packet.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x02 if context.protocol_later_eq(391) else \
|
||||
0x00
|
||||
|
||||
packet_name = 'login plugin response'
|
||||
fields = (
|
||||
'message_id', # str
|
||||
'successful', # bool
|
||||
'data', # bytes, or None if 'successful' is False
|
||||
)
|
||||
|
||||
def read(self, file_object):
|
||||
self.message_id = VarInt.read(file_object)
|
||||
self.successful = Boolean.read(file_object)
|
||||
if self.successful:
|
||||
self.data = TrailingByteArray.read(file_object)
|
||||
else:
|
||||
self.data = None
|
||||
|
||||
def write_fields(self, packet_buffer):
|
||||
VarInt.send(self.message_id, packet_buffer)
|
||||
successful = getattr(self, 'data', None) is not None
|
||||
successful = getattr(self, 'successful', successful)
|
||||
Boolean.send(successful, packet_buffer)
|
||||
if successful:
|
||||
TrailingByteArray.send(self.data, packet_buffer)
|
|
@ -0,0 +1,279 @@
|
|||
from minecraft.networking.packets import (
|
||||
Packet, AbstractKeepAlivePacket, AbstractPluginMessagePacket
|
||||
)
|
||||
|
||||
from minecraft.networking.types import (
|
||||
Double, Float, Boolean, VarInt, String, Byte, Position, Enum,
|
||||
RelativeHand, BlockFace, Vector, Direction, PositionAndLook,
|
||||
multi_attribute_alias,
|
||||
)
|
||||
|
||||
from .client_settings_packet import ClientSettingsPacket
|
||||
|
||||
|
||||
# Formerly known as state_playing_serverbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
KeepAlivePacket,
|
||||
ChatPacket,
|
||||
PositionAndLookPacket,
|
||||
AnimationPacket,
|
||||
ClientStatusPacket,
|
||||
ClientSettingsPacket,
|
||||
PluginMessagePacket,
|
||||
PlayerBlockPlacementPacket,
|
||||
}
|
||||
if context.protocol_later_eq(69):
|
||||
packets |= {
|
||||
UseItemPacket,
|
||||
}
|
||||
if context.protocol_later_eq(107):
|
||||
packets |= {
|
||||
TeleportConfirmPacket,
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class KeepAlivePacket(AbstractKeepAlivePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0F if context.protocol_later_eq(755) else \
|
||||
0x10 if context.protocol_later_eq(712) else \
|
||||
0x0F if context.protocol_later_eq(471) else \
|
||||
0x10 if context.protocol_later_eq(464) else \
|
||||
0x0E if context.protocol_later_eq(389) else \
|
||||
0x0C if context.protocol_later_eq(386) else \
|
||||
0x0B if context.protocol_later_eq(345) else \
|
||||
0x0A if context.protocol_later_eq(343) else \
|
||||
0x0B if context.protocol_later_eq(336) else \
|
||||
0x0C if context.protocol_later_eq(318) else \
|
||||
0x0B if context.protocol_later_eq(107) else \
|
||||
0x00
|
||||
|
||||
|
||||
class ChatPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x03 if context.protocol_later_eq(755) else \
|
||||
0x03 if context.protocol_later_eq(464) else \
|
||||
0x02 if context.protocol_later_eq(389) else \
|
||||
0x01 if context.protocol_later_eq(343) else \
|
||||
0x02 if context.protocol_later_eq(336) else \
|
||||
0x03 if context.protocol_later_eq(318) else \
|
||||
0x02 if context.protocol_later_eq(107) else \
|
||||
0x01
|
||||
|
||||
@staticmethod
|
||||
def get_max_length(context):
|
||||
return 256 if context.protocol_later_eq(306) else \
|
||||
100
|
||||
|
||||
@property
|
||||
def max_length(self):
|
||||
if self.context is not None:
|
||||
return self.get_max_length(self.context)
|
||||
|
||||
packet_name = "chat"
|
||||
definition = [
|
||||
{'message': String}]
|
||||
|
||||
|
||||
class PositionAndLookPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x12 if context.protocol_later_eq(755) else \
|
||||
0x13 if context.protocol_later_eq(712) else \
|
||||
0x12 if context.protocol_later_eq(471) else \
|
||||
0x13 if context.protocol_later_eq(464) else \
|
||||
0x11 if context.protocol_later_eq(389) else \
|
||||
0x0F if context.protocol_later_eq(386) else \
|
||||
0x0E if context.protocol_later_eq(345) else \
|
||||
0x0D if context.protocol_later_eq(343) else \
|
||||
0x0E if context.protocol_later_eq(336) else \
|
||||
0x0F if context.protocol_later_eq(332) else \
|
||||
0x0E if context.protocol_later_eq(318) else \
|
||||
0x0D if context.protocol_later_eq(107) else \
|
||||
0x06
|
||||
|
||||
packet_name = "position and look"
|
||||
definition = [
|
||||
{'x': Double},
|
||||
{'feet_y': Double},
|
||||
{'z': Double},
|
||||
{'yaw': Float},
|
||||
{'pitch': Float},
|
||||
{'on_ground': Boolean}]
|
||||
|
||||
# Access the 'x', 'feet_y', 'z' fields as a Vector tuple.
|
||||
position = multi_attribute_alias(Vector, 'x', 'feet_y', 'z')
|
||||
|
||||
# Access the 'yaw', 'pitch' fields as a Direction tuple.
|
||||
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
||||
|
||||
# Access the 'x', 'feet_y', 'z', 'yaw', 'pitch' fields as a
|
||||
# PositionAndLook.
|
||||
# NOTE: modifying the object retrieved from this property will not change
|
||||
# the packet; it can only be changed by attribute or property assignment.
|
||||
position_and_look = multi_attribute_alias(
|
||||
PositionAndLook, 'x', 'feet_y', 'z', 'yaw', 'pitch')
|
||||
|
||||
|
||||
class TeleportConfirmPacket(Packet):
|
||||
# Note: added between protocol versions 47 and 107.
|
||||
id = 0x00
|
||||
packet_name = "teleport confirm"
|
||||
definition = [
|
||||
{'teleport_id': VarInt}]
|
||||
|
||||
|
||||
class AnimationPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2C if context.protocol_later_eq(755) else \
|
||||
0x2C if context.protocol_later_eq(738) else \
|
||||
0x2B if context.protocol_later_eq(712) else \
|
||||
0x2A if context.protocol_later_eq(468) else \
|
||||
0x29 if context.protocol_later_eq(464) else \
|
||||
0x27 if context.protocol_later_eq(389) else \
|
||||
0x25 if context.protocol_later_eq(386) else \
|
||||
0x1D if context.protocol_later_eq(345) else \
|
||||
0x1C if context.protocol_later_eq(343) else \
|
||||
0x1D if context.protocol_later_eq(332) else \
|
||||
0x1C if context.protocol_later_eq(318) else \
|
||||
0x1A if context.protocol_later_eq(107) else \
|
||||
0x0A
|
||||
|
||||
packet_name = "animation"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'hand': VarInt} if context.protocol_later_eq(107) else {}])
|
||||
|
||||
Hand = RelativeHand
|
||||
HAND_MAIN, HAND_OFF = Hand.MAIN, Hand.OFF # For backward compatibility.
|
||||
|
||||
|
||||
class ClientStatusPacket(Packet, Enum):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x04 if context.protocol_later_eq(755) else \
|
||||
0x04 if context.protocol_later_eq(464) else \
|
||||
0x03 if context.protocol_later_eq(389) else \
|
||||
0x02 if context.protocol_later_eq(343) else \
|
||||
0x03 if context.protocol_later_eq(336) else \
|
||||
0x04 if context.protocol_later_eq(318) else \
|
||||
0x03 if context.protocol_later_eq(80) else \
|
||||
0x02 if context.protocol_later_eq(67) else \
|
||||
0x17 if context.protocol_later_eq(49) else \
|
||||
0x16
|
||||
|
||||
packet_name = "client status"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'action_id': VarInt}])
|
||||
field_enum = classmethod(
|
||||
lambda cls, field, context: cls if field == 'action_id' else None)
|
||||
|
||||
RESPAWN = 0
|
||||
REQUEST_STATS = 1
|
||||
# Note: Open Inventory (id 2) was removed in protocol version 319
|
||||
OPEN_INVENTORY = 2
|
||||
|
||||
|
||||
class PluginMessagePacket(AbstractPluginMessagePacket):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x0A if context.protocol_later_eq(755) else \
|
||||
0x0B if context.protocol_later_eq(464) else \
|
||||
0x0A if context.protocol_later_eq(389) else \
|
||||
0x09 if context.protocol_later_eq(345) else \
|
||||
0x08 if context.protocol_later_eq(343) else \
|
||||
0x09 if context.protocol_later_eq(336) else \
|
||||
0x0A if context.protocol_later_eq(317) else \
|
||||
0x09 if context.protocol_later_eq(94) else \
|
||||
0x17
|
||||
|
||||
|
||||
class PlayerBlockPlacementPacket(Packet):
|
||||
"""Realizaton of http://wiki.vg/Protocol#Player_Block_Placement packet
|
||||
Usage:
|
||||
packet = PlayerBlockPlacementPacket()
|
||||
packet.location = Position(x=1200, y=65, z=-420)
|
||||
packet.face = packet.Face.TOP # See networking.types.BlockFace.
|
||||
packet.hand = packet.Hand.MAIN # See networking.types.RelativeHand.
|
||||
Next values are called in-block coordinates.
|
||||
They are calculated using raytracing. From 0 to 1 (from Minecraft 1.11)
|
||||
or integers from 0 to 15 or, in a special case, -1 (1.10.2 and earlier).
|
||||
packet.x = 0.725
|
||||
packet.y = 0.125
|
||||
packet.z = 0.555"""
|
||||
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2E if context.protocol_later_eq(755) else \
|
||||
0x2E if context.protocol_later_eq(738) else \
|
||||
0x2D if context.protocol_later_eq(712) else \
|
||||
0x2C if context.protocol_later_eq(468) else \
|
||||
0x2B if context.protocol_later_eq(464) else \
|
||||
0x29 if context.protocol_later_eq(389) else \
|
||||
0x27 if context.protocol_later_eq(386) else \
|
||||
0x1F if context.protocol_later_eq(345) else \
|
||||
0x1E if context.protocol_later_eq(343) else \
|
||||
0x1F if context.protocol_later_eq(332) else \
|
||||
0x1E if context.protocol_later_eq(318) else \
|
||||
0x1C if context.protocol_later_eq(94) else \
|
||||
0x08
|
||||
|
||||
packet_name = 'player block placement'
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
return [
|
||||
{'hand': VarInt} if context.protocol_later_eq(453) else {},
|
||||
{'location': Position},
|
||||
{'face': VarInt if context.protocol_later_eq(69) else Byte},
|
||||
{'hand': VarInt} if context.protocol_earlier(453) else {},
|
||||
{'x': Float if context.protocol_later_eq(309) else Byte},
|
||||
{'y': Float if context.protocol_later_eq(309) else Byte},
|
||||
{'z': Float if context.protocol_later_eq(309) else Byte},
|
||||
({'inside_block': Boolean}
|
||||
if context.protocol_later_eq(453) else {}),
|
||||
]
|
||||
|
||||
# PlayerBlockPlacementPacket.Hand is an alias for RelativeHand.
|
||||
Hand = RelativeHand
|
||||
|
||||
# PlayerBlockPlacementPacket.Face is an alias for BlockFace.
|
||||
Face = BlockFace
|
||||
|
||||
|
||||
class UseItemPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x2F if context.protocol_later_eq(755) else \
|
||||
0x2F if context.protocol_later_eq(738) else \
|
||||
0x2E if context.protocol_later_eq(712) else \
|
||||
0x2D if context.protocol_later_eq(468) else \
|
||||
0x2C if context.protocol_later_eq(464) else \
|
||||
0x2A if context.protocol_later_eq(389) else \
|
||||
0x28 if context.protocol_later_eq(386) else \
|
||||
0x20 if context.protocol_later_eq(345) else \
|
||||
0x1F if context.protocol_later_eq(343) else \
|
||||
0x20 if context.protocol_later_eq(332) else \
|
||||
0x1F if context.protocol_later_eq(318) else \
|
||||
0x1D if context.protocol_later_eq(94) else \
|
||||
0x1A if context.protocol_later_eq(70) else \
|
||||
0x08
|
||||
|
||||
packet_name = "use item"
|
||||
get_definition = staticmethod(lambda context: [
|
||||
{'hand': VarInt}])
|
||||
|
||||
Hand = RelativeHand
|
||||
|
||||
|
||||
class ResourcePackStatusPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x21
|
||||
packet_name = "resource pack status"
|
||||
definition = [
|
||||
{"result": VarInt}
|
||||
]
|
|
@ -0,0 +1,84 @@
|
|||
import operator
|
||||
|
||||
from minecraft.networking.packets import Packet
|
||||
from minecraft.networking.types import (
|
||||
String, Byte, VarInt, Boolean, UnsignedByte, Enum, BitFieldEnum,
|
||||
AbsoluteHand
|
||||
)
|
||||
from minecraft.utility import attribute_transform
|
||||
|
||||
|
||||
class ClientSettingsPacket(Packet):
|
||||
@staticmethod
|
||||
def get_id(context):
|
||||
return 0x05 if context.protocol_later_eq(464) else \
|
||||
0x04 if context.protocol_later_eq(389) else \
|
||||
0x03 if context.protocol_later_eq(343) else \
|
||||
0x04 if context.protocol_later_eq(336) else \
|
||||
0x05 if context.protocol_later_eq(318) else \
|
||||
0x04 if context.protocol_later_eq(94) else \
|
||||
0x15
|
||||
|
||||
packet_name = 'client settings'
|
||||
|
||||
@staticmethod
|
||||
def get_definition(context):
|
||||
return [
|
||||
{'locale': String},
|
||||
{'view_distance': Byte},
|
||||
{'chat_mode': VarInt if context.protocol_later(47) else Byte},
|
||||
{'chat_colors': Boolean},
|
||||
{'displayed_skin_parts': UnsignedByte},
|
||||
{'main_hand': VarInt} if context.protocol_later(49) else {},
|
||||
|
||||
{'enable_text_filtering': Boolean}
|
||||
if context.protocol_later_eq(757) else
|
||||
{'disable_text_filtering': Boolean}
|
||||
if context.protocol_later_eq(755) else {},
|
||||
|
||||
{'allow_server_listings': Boolean}
|
||||
if context.protocol_later_eq(755) else {},
|
||||
]
|
||||
|
||||
# Set a default value for 'enable_text_filtering', because most clients
|
||||
# will probably want this value, and to avoid breaking old code.
|
||||
enable_text_filtering = False
|
||||
|
||||
# To support the possibility of both 'enable_text_filtering' and
|
||||
# 'disable_text_filtering' fields existing, make 'disable_text_filtering'
|
||||
# (which is the less likely to be used of the two) into a property that
|
||||
# stores the negation of its value in the ordinary attribute
|
||||
# 'enable_text_filtering'.
|
||||
disable_text_filtering = attribute_transform(
|
||||
'enable_text_filtering', operator.not_, operator.not_)
|
||||
|
||||
# Set a default value for 'allow_server_listings', because most clients
|
||||
# will probably want this value, and to avoid breaking old code.
|
||||
allow_server_listings = False
|
||||
|
||||
field_enum = classmethod(
|
||||
lambda cls, field, context: {
|
||||
'chat_mode': cls.ChatMode,
|
||||
'displayed_skin_parts': cls.SkinParts,
|
||||
'main_hand': AbsoluteHand,
|
||||
}.get(field))
|
||||
|
||||
class ChatMode(Enum):
|
||||
FULL = 0 # Receive all types of chat messages.
|
||||
SYSTEM = 1 # Receive only command results and game information.
|
||||
NONE = 2 # Receive only game information.
|
||||
|
||||
class SkinParts(BitFieldEnum):
|
||||
CAPE = 0x01
|
||||
JACKET = 0x02
|
||||
LEFT_SLEEVE = 0x04
|
||||
RIGHT_SLEEVE = 0x08
|
||||
LEFT_PANTS_LEG = 0x10
|
||||
RIGHT_PANTS_LEG = 0x20
|
||||
HAT = 0x40
|
||||
|
||||
ALL = 0x7F
|
||||
NONE = 0x00
|
||||
|
||||
# This class alias is retained for backward compatibility.
|
||||
Hand = AbsoluteHand
|
|
@ -0,0 +1,27 @@
|
|||
from minecraft.networking.packets import Packet
|
||||
|
||||
from minecraft.networking.types import (
|
||||
Long
|
||||
)
|
||||
|
||||
|
||||
# Formerly known as state_status_serverbound.
|
||||
def get_packets(context):
|
||||
packets = {
|
||||
RequestPacket,
|
||||
PingPacket
|
||||
}
|
||||
return packets
|
||||
|
||||
|
||||
class RequestPacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "request"
|
||||
definition = []
|
||||
|
||||
|
||||
class PingPacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
|
@ -1,201 +0,0 @@
|
|||
"""Contains definitions for minecraft's different data types
|
||||
Each type has a method which is used to read and write it.
|
||||
These definitions and methods are used by the packet definitions
|
||||
"""
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
|
||||
class Type(object):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
raise NotImplementedError("Base data type not serializable")
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
raise NotImplementedError("Base data type not serializable")
|
||||
|
||||
|
||||
# =========================================================
|
||||
|
||||
|
||||
class Boolean(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('?', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('?', value))
|
||||
|
||||
|
||||
class UnsignedByte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>B', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>B', value))
|
||||
|
||||
|
||||
class Byte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>b', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>b', value))
|
||||
|
||||
|
||||
class Short(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>h', file_object.read(2))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>h', value))
|
||||
|
||||
|
||||
class UnsignedShort(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>H', file_object.read(2))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>H', value))
|
||||
|
||||
|
||||
class Integer(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>i', file_object.read(4))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>i', value))
|
||||
|
||||
|
||||
class VarInt(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
number = 0
|
||||
for i in range(5):
|
||||
byte = file_object.read(1)
|
||||
if len(byte) < 1:
|
||||
raise EOFError("Unexpected end of message.")
|
||||
byte = ord(byte)
|
||||
number |= (byte & 0x7F) << 7 * i
|
||||
if not byte & 0x80:
|
||||
break
|
||||
return number
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
out = bytes()
|
||||
while True:
|
||||
byte = value & 0x7F
|
||||
value >>= 7
|
||||
out += struct.pack("B", byte | (0x80 if value > 0 else 0))
|
||||
if value == 0:
|
||||
break
|
||||
socket.send(out)
|
||||
|
||||
@staticmethod
|
||||
def size(value):
|
||||
for max_value, size in VARINT_SIZE_TABLE.items():
|
||||
if value < max_value:
|
||||
return size
|
||||
|
||||
|
||||
# Maps (maximum integer value -> size of VarInt in bytes)
|
||||
VARINT_SIZE_TABLE = {
|
||||
2 ** 7: 1,
|
||||
2 ** 14: 2,
|
||||
2 ** 21: 3,
|
||||
2 ** 28: 4,
|
||||
2 ** 35: 5,
|
||||
2 ** 42: 6,
|
||||
2 ** 49: 7,
|
||||
2 ** 56: 8,
|
||||
2 ** 63: 9,
|
||||
2 ** 70: 10,
|
||||
2 ** 77: 11,
|
||||
2 ** 84: 12
|
||||
}
|
||||
|
||||
|
||||
class Long(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>q', file_object.read(8))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>q', value))
|
||||
|
||||
|
||||
class Float(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>f', file_object.read(4))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>f', value))
|
||||
|
||||
|
||||
class Double(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>d', file_object.read(8))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>d', value))
|
||||
|
||||
|
||||
class ShortPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = Short.read(file_object)
|
||||
return struct.unpack(str(length) + "s", file_object.read(length))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
Short.send(len(value), socket)
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class VarIntPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = VarInt.read(file_object)
|
||||
return struct.unpack(str(length) + "s", file_object.read(length))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
VarInt.send(len(value), socket)
|
||||
socket.send(struct.pack(str(len(value)) + "s", value))
|
||||
|
||||
|
||||
class String(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = VarInt.read(file_object)
|
||||
return file_object.read(length).decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
value = value.encode('utf-8')
|
||||
VarInt.send(len(value), socket)
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class UUID(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return str(uuid.UUID(bytes=file_object.read(16)))
|
|
@ -0,0 +1,3 @@
|
|||
from .basic import * # noqa: F401, F403
|
||||
from .enum import * # noqa: F401, F403
|
||||
from .utility import * # noqa: F401, F403
|
|
@ -0,0 +1,387 @@
|
|||
"""Contains definitions for minecraft's different data types
|
||||
Each type has a method which is used to read and write it.
|
||||
These definitions and methods are used by the packet definitions
|
||||
"""
|
||||
import struct
|
||||
import uuid
|
||||
import io
|
||||
|
||||
import pynbt
|
||||
|
||||
from .utility import Vector, class_and_instancemethod
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Type', 'Boolean', 'UnsignedByte', 'Byte', 'Short', 'UnsignedShort',
|
||||
'Integer', 'FixedPoint', 'FixedPointInteger', 'Angle', 'VarInt', 'VarLong',
|
||||
'Long', 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray',
|
||||
'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID',
|
||||
'Position', 'NBT', 'PrefixedArray',
|
||||
)
|
||||
|
||||
|
||||
class Type(object):
|
||||
# pylint: disable=no-self-argument
|
||||
__slots__ = ()
|
||||
|
||||
@class_and_instancemethod
|
||||
def read_with_context(cls_or_self, file_object, _context):
|
||||
return cls_or_self.read(file_object)
|
||||
|
||||
@class_and_instancemethod
|
||||
def send_with_context(cls_or_self, value, socket, _context):
|
||||
return cls_or_self.send(value, socket)
|
||||
|
||||
@classmethod
|
||||
def read(cls, file_object):
|
||||
if cls.read_with_context == Type.read_with_context:
|
||||
raise NotImplementedError('One of "read" or "read_with_context" '
|
||||
'must be overridden in a subclass.')
|
||||
else:
|
||||
raise TypeError('This type requires a ConnectionContext: '
|
||||
'call "read_with_context" instead of "read".')
|
||||
|
||||
@classmethod
|
||||
def send(cls, value, socket):
|
||||
if cls.send_with_context == Type.send_with_context:
|
||||
raise NotImplementedError('One of "send" or "send_with_context" '
|
||||
'must be overridden in a subclass.')
|
||||
else:
|
||||
raise TypeError('This type requires a ConnectionContext: '
|
||||
'call "send_with_context" instead of "send".')
|
||||
|
||||
|
||||
class Boolean(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('?', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('?', value))
|
||||
|
||||
|
||||
class UnsignedByte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>B', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>B', value))
|
||||
|
||||
|
||||
class Byte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>b', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>b', value))
|
||||
|
||||
|
||||
class Short(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>h', file_object.read(2))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>h', value))
|
||||
|
||||
|
||||
class UnsignedShort(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>H', file_object.read(2))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>H', value))
|
||||
|
||||
|
||||
class Integer(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>i', file_object.read(4))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>i', value))
|
||||
|
||||
|
||||
class FixedPoint(Type):
|
||||
__slots__ = 'integer_type', 'denominator'
|
||||
|
||||
def __init__(self, integer_type, fractional_bits=5):
|
||||
self.integer_type = integer_type
|
||||
self.denominator = 2**fractional_bits
|
||||
|
||||
def read(self, file_object):
|
||||
return self.integer_type.read(file_object) / self.denominator
|
||||
|
||||
def send(self, value, socket):
|
||||
self.integer_type.send(int(value * self.denominator))
|
||||
|
||||
|
||||
# This named instance is retained for backward compatibility:
|
||||
FixedPointInteger = FixedPoint(Integer)
|
||||
|
||||
|
||||
class Angle(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
# Linearly transform angle in steps of 1/256 into steps of 1/360
|
||||
return 360 * UnsignedByte.read(file_object) / 256
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
# Normalize angle between 0 and 255 and convert to int.
|
||||
UnsignedByte.send(round(256 * ((value % 360) / 360)), socket)
|
||||
|
||||
|
||||
class VarInt(Type):
|
||||
max_bytes = 5
|
||||
|
||||
@classmethod
|
||||
def read(cls, file_object):
|
||||
number = 0
|
||||
# Limit of 'cls.max_bytes' bytes, otherwise its possible to cause
|
||||
# a DOS attack by sending VarInts that just keep going
|
||||
bytes_encountered = 0
|
||||
while True:
|
||||
byte = file_object.read(1)
|
||||
if len(byte) < 1:
|
||||
raise EOFError("Unexpected end of message.")
|
||||
|
||||
byte = ord(byte)
|
||||
number |= (byte & 0x7F) << 7 * bytes_encountered
|
||||
if not byte & 0x80:
|
||||
break
|
||||
|
||||
bytes_encountered += 1
|
||||
if bytes_encountered > cls.max_bytes:
|
||||
raise ValueError("Tried to read too long of a VarInt")
|
||||
return number
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
out = bytes()
|
||||
while True:
|
||||
byte = value & 0x7F
|
||||
value >>= 7
|
||||
out += struct.pack("B", byte | (0x80 if value > 0 else 0))
|
||||
if value == 0:
|
||||
break
|
||||
socket.send(out)
|
||||
|
||||
@staticmethod
|
||||
def size(value):
|
||||
for max_value, size in VARINT_SIZE_TABLE.items():
|
||||
if value < max_value:
|
||||
return size
|
||||
raise ValueError("Integer too large")
|
||||
|
||||
|
||||
class VarLong(VarInt):
|
||||
max_bytes = 10
|
||||
|
||||
|
||||
# Maps (maximum integer value -> size of VarInt in bytes)
|
||||
VARINT_SIZE_TABLE = {
|
||||
2 ** 7: 1,
|
||||
2 ** 14: 2,
|
||||
2 ** 21: 3,
|
||||
2 ** 28: 4,
|
||||
2 ** 35: 5,
|
||||
2 ** 42: 6,
|
||||
2 ** 49: 7,
|
||||
2 ** 56: 8,
|
||||
2 ** 63: 9,
|
||||
2 ** 70: 10,
|
||||
2 ** 77: 11,
|
||||
2 ** 84: 12
|
||||
}
|
||||
|
||||
|
||||
class Long(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>q', file_object.read(8))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>q', value))
|
||||
|
||||
|
||||
class UnsignedLong(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>Q', file_object.read(8))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>Q', value))
|
||||
|
||||
|
||||
class Float(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>f', file_object.read(4))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>f', value))
|
||||
|
||||
|
||||
class Double(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>d', file_object.read(8))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>d', value))
|
||||
|
||||
|
||||
class ShortPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = Short.read(file_object)
|
||||
return struct.unpack(str(length) + "s", file_object.read(length))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
Short.send(len(value), socket)
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class VarIntPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = VarInt.read(file_object)
|
||||
return struct.unpack(str(length) + "s", file_object.read(length))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
VarInt.send(len(value), socket)
|
||||
socket.send(struct.pack(str(len(value)) + "s", value))
|
||||
|
||||
|
||||
class TrailingByteArray(Type):
|
||||
""" A byte array consisting of all remaining data. If present in a packet
|
||||
definition, this should only be the type of the last field. """
|
||||
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return file_object.read()
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class String(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
length = VarInt.read(file_object)
|
||||
return file_object.read(length).decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
value = value.encode('utf-8')
|
||||
VarInt.send(len(value), socket)
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class UUID(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return str(uuid.UUID(bytes=file_object.read(16)))
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(uuid.UUID(value).bytes)
|
||||
|
||||
|
||||
class Position(Type, Vector):
|
||||
"""3D position vectors with a specific, compact network representation."""
|
||||
__slots__ = ()
|
||||
|
||||
@staticmethod
|
||||
def read_with_context(file_object, context):
|
||||
location = UnsignedLong.read(file_object)
|
||||
x = int(location >> 38) # 26 most significant bits
|
||||
|
||||
if context.protocol_later_eq(443):
|
||||
z = int((location >> 12) & 0x3FFFFFF) # 26 intermediate bits
|
||||
y = int(location & 0xFFF) # 12 least signficant bits
|
||||
else:
|
||||
y = int((location >> 26) & 0xFFF) # 12 intermediate bits
|
||||
z = int(location & 0x3FFFFFF) # 26 least significant bits
|
||||
|
||||
if x >= pow(2, 25):
|
||||
x -= pow(2, 26)
|
||||
|
||||
if y >= pow(2, 11):
|
||||
y -= pow(2, 12)
|
||||
|
||||
if z >= pow(2, 25):
|
||||
z -= pow(2, 26)
|
||||
|
||||
return Position(x=x, y=y, z=z)
|
||||
|
||||
@staticmethod
|
||||
def send_with_context(position, socket, context):
|
||||
# 'position' can be either a tuple or Position object.
|
||||
x, y, z = position
|
||||
value = ((x & 0x3FFFFFF) << 38 | (z & 0x3FFFFFF) << 12 | (y & 0xFFF)
|
||||
if context.protocol_later_eq(443) else
|
||||
(x & 0x3FFFFFF) << 38 | (y & 0xFFF) << 26 | (z & 0x3FFFFFF))
|
||||
UnsignedLong.send(value, socket)
|
||||
|
||||
|
||||
class NBT(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return pynbt.NBTFile(io=file_object)
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
buffer = io.BytesIO()
|
||||
pynbt.NBTFile(value=value).save(buffer)
|
||||
socket.send(buffer.getvalue())
|
||||
|
||||
|
||||
class PrefixedArray(Type):
|
||||
__slots__ = 'length_type', 'element_type'
|
||||
|
||||
def __init__(self, length_type, element_type):
|
||||
self.length_type = length_type
|
||||
self.element_type = element_type
|
||||
|
||||
def read(self, file_object):
|
||||
return self.__read(file_object, self.element_type.read)
|
||||
|
||||
def send(self, value, socket):
|
||||
return self.__send(value, socket, self.element_type.send)
|
||||
|
||||
def read_with_context(self, file_object, context):
|
||||
def element_read(file_object):
|
||||
return self.element_type.read_with_context(file_object, context)
|
||||
return self.__read(file_object, element_read)
|
||||
|
||||
def send_with_context(self, value, socket, context):
|
||||
def element_send(value, socket):
|
||||
return self.element_type.send_with_context(value, socket, context)
|
||||
return self.__send(value, socket, element_send)
|
||||
|
||||
def __read(self, file_object, element_read):
|
||||
length = self.length_type.read(file_object)
|
||||
return [element_read(file_object) for i in range(length)]
|
||||
|
||||
def __send(self, value, socket, element_send):
|
||||
self.length_type.send(len(value), socket)
|
||||
for element in value:
|
||||
element_send(element, socket)
|
|
@ -0,0 +1,124 @@
|
|||
"""Types for enumerations of values occurring in packets, including operations
|
||||
for working with these values.
|
||||
|
||||
The values in an enum are given as class attributes with UPPERCASE names.
|
||||
|
||||
These classes are usually not supposed to be instantiated, but sometimes an
|
||||
instantiatable class may subclass Enum to provide class enum attributes in
|
||||
addition to other functionality.
|
||||
"""
|
||||
from .utility import Vector
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Enum', 'BitFieldEnum', 'AbsoluteHand', 'RelativeHand', 'BlockFace',
|
||||
'Difficulty', 'Dimension', 'GameMode', 'OriginPoint'
|
||||
)
|
||||
|
||||
|
||||
class Enum(object):
|
||||
# Return a human-readable string representation of an enum value.
|
||||
@classmethod
|
||||
def name_from_value(cls, value):
|
||||
for name, name_value in cls.__dict__.items():
|
||||
if name.isupper() and name_value == value:
|
||||
return name
|
||||
|
||||
|
||||
class BitFieldEnum(Enum):
|
||||
@classmethod
|
||||
def name_from_value(cls, value):
|
||||
if not isinstance(value, int):
|
||||
return
|
||||
ret_names = []
|
||||
ret_value = 0
|
||||
for cls_name, cls_value in sorted(
|
||||
[(n, v) for (n, v) in cls.__dict__.items()
|
||||
if isinstance(v, int) and n.isupper() and v | value == value],
|
||||
reverse=True, key=lambda p: p[1]
|
||||
):
|
||||
if ret_value | cls_value != ret_value or cls_value == value:
|
||||
ret_names.append(cls_name)
|
||||
ret_value |= cls_value
|
||||
if ret_value == value:
|
||||
return '|'.join(reversed(ret_names)) if ret_names else '0'
|
||||
|
||||
|
||||
# Designation of one of a player's hands, in absolute terms.
|
||||
class AbsoluteHand(Enum):
|
||||
LEFT = 0
|
||||
RIGHT = 1
|
||||
|
||||
|
||||
# Designation of one a player's hands, relative to a choice of main/off hand.
|
||||
class RelativeHand(Enum):
|
||||
MAIN = 0
|
||||
OFF = 1
|
||||
|
||||
|
||||
# Designation of one of a block's 6 faces.
|
||||
class BlockFace(Enum):
|
||||
BOTTOM = 0 # -Y
|
||||
TOP = 1 # +Y
|
||||
NORTH = 2 # -Z
|
||||
SOUTH = 3 # +Z
|
||||
WEST = 4 # -X
|
||||
EAST = 5 # +X
|
||||
|
||||
# A dict mapping Vector tuples to the corresponding BlockFace values.
|
||||
# When accessing this dict, plain tuples also match. For example:
|
||||
# >>> BlockFace.from_vector[0, 0, -1] == BlockFace.NORTH
|
||||
# True
|
||||
from_vector = {
|
||||
Vector(0, -1, 0): BOTTOM,
|
||||
Vector(0, +1, 0): TOP,
|
||||
Vector(0, 0, -1): NORTH,
|
||||
Vector(0, 0, +1): SOUTH,
|
||||
Vector(-1, 0, 0): WEST,
|
||||
Vector(+1, 0, 0): EAST,
|
||||
}
|
||||
|
||||
# A dict mapping BlockFace values to unit Position tuples.
|
||||
# This is the inverse mapping of face_by_position. For example:
|
||||
# >>> BlockFace.to_vector[BlockFace.NORTH]
|
||||
# Position(x=0, y=0, z=-1)
|
||||
to_vector = {fce: pos for (pos, fce) in from_vector.items()}
|
||||
|
||||
|
||||
# Designation of a world's difficulty.
|
||||
class Difficulty(Enum):
|
||||
PEACEFUL = 0
|
||||
EASY = 1
|
||||
NORMAL = 2
|
||||
HARD = 3
|
||||
|
||||
|
||||
# Designation of a world's dimension.
|
||||
class Dimension(Enum):
|
||||
NETHER = -1
|
||||
OVERWORLD = 0
|
||||
END = 1
|
||||
|
||||
from_identifier_dict = {
|
||||
'minecraft:the_nether': NETHER,
|
||||
'minecraft:overworld': OVERWORLD,
|
||||
'minecraft:the_end': END,
|
||||
}
|
||||
|
||||
to_identifier_dict = {e: i for (i, e) in from_identifier_dict.items()}
|
||||
|
||||
|
||||
# Designation of a player's gamemode.
|
||||
class GameMode(BitFieldEnum):
|
||||
SURVIVAL = 0
|
||||
CREATIVE = 1
|
||||
ADVENTURE = 2
|
||||
SPECTATOR = 3
|
||||
HARDCORE = 8 # Only used prior to protocol 738.
|
||||
|
||||
|
||||
# Currently designates an entity's feet or eyes.
|
||||
# Used in the Face Player Packet
|
||||
class OriginPoint(Enum):
|
||||
FEET = 0
|
||||
EYES = 1
|
|
@ -0,0 +1,102 @@
|
|||
"""Minecraft data types that are used by packets, but don't have a specific
|
||||
network representation.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
|
||||
# These aliases are retained for backward compatibility
|
||||
from minecraft.utility import ( # noqa: F401
|
||||
descriptor, overridable_descriptor, overridable_property, attribute_alias,
|
||||
multi_attribute_alias, attribute_transform, class_and_instancemethod,
|
||||
)
|
||||
|
||||
|
||||
class Vector(namedtuple('BaseVector', ('x', 'y', 'z'))):
|
||||
"""An immutable type usually used to represent 3D spatial coordinates,
|
||||
supporting elementwise vector addition, subtraction, and negation; and
|
||||
scalar multiplication and (right) division.
|
||||
|
||||
NOTE: subclasses of 'Vector' should have '__slots__ = ()' to avoid the
|
||||
creation of a '__dict__' attribute, which would waste space.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __add__(self, other):
|
||||
return NotImplemented if not isinstance(other, Vector) else \
|
||||
type(self)(self.x + other.x, self.y + other.y, self.z + other.z)
|
||||
|
||||
def __sub__(self, other):
|
||||
return NotImplemented if not isinstance(other, Vector) else \
|
||||
type(self)(self.x - other.x, self.y - other.y, self.z - other.z)
|
||||
|
||||
def __neg__(self):
|
||||
return type(self)(-self.x, -self.y, -self.z)
|
||||
|
||||
def __mul__(self, other):
|
||||
return type(self)(self.x*other, self.y*other, self.z*other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return type(self)(other*self.x, other*self.y, other*self.z)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return type(self)(self.x/other, self.y/other, self.z/other)
|
||||
|
||||
def __floordiv__(self, other):
|
||||
return type(self)(self.x//other, self.y//other, self.z//other)
|
||||
|
||||
__div__ = __floordiv__
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, %r, %r)' % (type(self).__name__, self.x, self.y, self.z)
|
||||
|
||||
|
||||
class MutableRecord(object):
|
||||
"""An abstract base class providing namedtuple-like repr(), ==, hash(), and
|
||||
iter(), implementations for types containing mutable fields given by
|
||||
__slots__.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwds):
|
||||
for attr, value in kwds.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (type(self).__name__, ', '.join(
|
||||
'%s=%r' % (a, getattr(self, a)) for a in self._all_slots()
|
||||
if hasattr(self, a)))
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) is type(other) and all(
|
||||
getattr(self, a) == getattr(other, a) for a in self._all_slots())
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __hash__(self):
|
||||
values = tuple(getattr(self, a, None) for a in self._all_slots())
|
||||
return hash((type(self), values))
|
||||
|
||||
def __iter__(self):
|
||||
return iter(getattr(self, a) for a in self._all_slots())
|
||||
|
||||
@classmethod
|
||||
def _all_slots(cls):
|
||||
for supcls in reversed(cls.__mro__):
|
||||
slots = supcls.__dict__.get('__slots__', ())
|
||||
slots = (slots,) if isinstance(slots, str) else slots
|
||||
for slot in slots:
|
||||
yield slot
|
||||
|
||||
|
||||
Direction = namedtuple('Direction', ('yaw', 'pitch'))
|
||||
|
||||
|
||||
class PositionAndLook(MutableRecord):
|
||||
"""A mutable record containing 3 spatial position coordinates
|
||||
and 2 rotational coordinates for a look direction.
|
||||
"""
|
||||
__slots__ = 'x', 'y', 'z', 'yaw', 'pitch'
|
||||
|
||||
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
|
||||
|
||||
look = multi_attribute_alias(Direction, 'yaw', 'pitch')
|
|
@ -0,0 +1,188 @@
|
|||
""" Miscellaneous general utilities.
|
||||
"""
|
||||
import types
|
||||
from itertools import chain
|
||||
|
||||
from . import PROTOCOL_VERSION_INDICES
|
||||
|
||||
|
||||
def protocol_earlier(pv1, pv2):
|
||||
""" Returns True if protocol version 'pv1' was published before 'pv2',
|
||||
or else returns False.
|
||||
"""
|
||||
return PROTOCOL_VERSION_INDICES[pv1] < PROTOCOL_VERSION_INDICES[pv2]
|
||||
|
||||
|
||||
def protocol_earlier_eq(pv1, pv2):
|
||||
""" Returns True if protocol versions 'pv1' and 'pv2' are the same or if
|
||||
'pv1' was published before 'pv2', or else returns False.
|
||||
"""
|
||||
return PROTOCOL_VERSION_INDICES[pv1] <= PROTOCOL_VERSION_INDICES[pv2]
|
||||
|
||||
|
||||
def attribute_transform(name, from_orig, to_orig):
|
||||
"""An attribute descriptor that provides a view of a different attribute
|
||||
with a given name via a given transformation and its given inverse."""
|
||||
return property(
|
||||
fget=(lambda self: from_orig(getattr(self, name))),
|
||||
fset=(lambda self, value: setattr(self, name, to_orig(value))),
|
||||
fdel=(lambda self: delattr(self, name)))
|
||||
|
||||
|
||||
def attribute_alias(name):
|
||||
"""An attribute descriptor that redirects access to a different attribute
|
||||
with a given name.
|
||||
"""
|
||||
return property(
|
||||
fget=(lambda self: getattr(self, name)),
|
||||
fset=(lambda self, value: setattr(self, name, value)),
|
||||
fdel=(lambda self: delattr(self, name)))
|
||||
|
||||
|
||||
def partial_attribute_alias(name, part):
|
||||
"""An attribute descriptor that redirects access to a particular named
|
||||
attribute, 'part', on a different attribute with a given name.
|
||||
"""
|
||||
return property(
|
||||
fget=(lambda self: getattr(getattr(self, name), part)),
|
||||
fset=(lambda self, value: setattr(getattr(self, name), part, value)),
|
||||
fdel=(lambda self: delattr(getattr(self, name), part)))
|
||||
|
||||
|
||||
def multi_attribute_alias(container, *arg_names, **kwd_names):
|
||||
"""A descriptor for an attribute whose value is a container of a given type
|
||||
with several fields, each of which is aliased to a different attribute
|
||||
of the parent object.
|
||||
|
||||
The 'n'th name in 'arg_names' is the parent attribute that will be
|
||||
aliased to the field of 'container' settable by the 'n'th positional
|
||||
argument to its constructor, and accessible as its 'n'th iterable
|
||||
element.
|
||||
|
||||
As a special case, 'tuple' may be given as the 'container' when there
|
||||
are positional arguments, and (even though the tuple constructor does
|
||||
not take positional arguments), the arguments will be aliased to the
|
||||
corresponding positions in a tuple.
|
||||
|
||||
The name in 'kwd_names' mapped to by the key 'k' is the parent attribute
|
||||
that will be aliased to the field of 'container' settable by the keyword
|
||||
argument 'k' to its constructor, and accessible as its 'k' attribute.
|
||||
"""
|
||||
if container is tuple:
|
||||
container = lambda *args: args # noqa: E731
|
||||
|
||||
@property
|
||||
def alias(self):
|
||||
return container(
|
||||
*(getattr(self, name) for name in arg_names),
|
||||
**{kwd: getattr(self, name) for (kwd, name) in kwd_names.items()})
|
||||
|
||||
@alias.setter
|
||||
def alias(self, values):
|
||||
if arg_names:
|
||||
for name, value in zip(arg_names, values):
|
||||
setattr(self, name, value)
|
||||
for kwd, name in kwd_names.items():
|
||||
setattr(self, name, getattr(values, kwd))
|
||||
|
||||
@alias.deleter
|
||||
def alias(self):
|
||||
for name in chain(arg_names, kwd_names.values()):
|
||||
delattr(self, name)
|
||||
|
||||
return alias
|
||||
|
||||
|
||||
class overridable_descriptor:
|
||||
"""As 'descriptor' (defined below), except that only a getter can be
|
||||
defined, and the resulting descriptor has no '__set__' or '__delete__'
|
||||
methods defined; hence, attributes defined via this class can be
|
||||
overridden by attributes of instances of the class in which it occurs.
|
||||
"""
|
||||
__slots__ = '_fget',
|
||||
|
||||
def __init__(self, fget=None):
|
||||
self._fget = fget if fget is not None else self._default_get
|
||||
|
||||
def getter(self, fget):
|
||||
self._fget = fget
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _default_get(instance, owner):
|
||||
raise AttributeError('unreadable attribute')
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self._fget(self, instance, owner)
|
||||
|
||||
|
||||
class overridable_property(overridable_descriptor):
|
||||
"""As the builtin 'property' decorator of Python, except that only
|
||||
a getter is defined and the resulting descriptor is a non-data
|
||||
descriptor, overridable by attributes of instances of the class
|
||||
in which the property occurs. See also 'overridable_descriptor' above.
|
||||
"""
|
||||
def __get__(self, instance, _owner):
|
||||
return self._fget(instance)
|
||||
|
||||
|
||||
class descriptor(overridable_descriptor):
|
||||
"""Behaves identically to the builtin 'property' decorator of Python,
|
||||
except that the getter, setter and deleter functions given by the
|
||||
user are used as the raw __get__, __set__ and __delete__ functions
|
||||
as defined in Python's descriptor protocol.
|
||||
|
||||
Since an instance of this class always havs '__set__' and '__delete__'
|
||||
defined, it is a "data descriptor", so its binding behaviour cannot be
|
||||
overridden in instances of the class in which it occurs. See
|
||||
https://docs.python.org/3/reference/datamodel.html#descriptor-invocation
|
||||
for more information. See also 'overridable_descriptor' above.
|
||||
"""
|
||||
__slots__ = '_fset', '_fdel'
|
||||
|
||||
def __init__(self, fget=None, fset=None, fdel=None):
|
||||
super(descriptor, self).__init__(fget=fget)
|
||||
self._fset = fset if fset is not None else self._default_set
|
||||
self._fdel = fdel if fdel is not None else self._default_del
|
||||
|
||||
def setter(self, fset):
|
||||
self._fset = fset
|
||||
return self
|
||||
|
||||
def deleter(self, fdel):
|
||||
self._fdel = fdel
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _default_set(instance, value):
|
||||
raise AttributeError("can't set attribute")
|
||||
|
||||
@staticmethod
|
||||
def _default_del(instance):
|
||||
raise AttributeError("can't delete attribute")
|
||||
|
||||
def __set__(self, instance, value):
|
||||
return self._fset(self, instance, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
return self._fdel(self, instance)
|
||||
|
||||
|
||||
class class_and_instancemethod:
|
||||
""" A decorator for functions defined in a class namespace which are to be
|
||||
accessed as both class and instance methods: retrieving the method from
|
||||
a class will return a bound class method (like the built-in
|
||||
'classmethod' decorator), but retrieving the method from an instance
|
||||
will return a bound instance method (as if the function were not
|
||||
decorated). Therefore, the first argument of the decorated function may
|
||||
be either a class or an instance, depending on how it was called.
|
||||
"""
|
||||
|
||||
__slots__ = '_func',
|
||||
|
||||
def __init__(self, func):
|
||||
self._func = func
|
||||
|
||||
def __get__(self, inst, owner=None):
|
||||
bind_to = owner if inst is None else inst
|
||||
return types.MethodType(self._func, bind_to)
|
|
@ -1,3 +1,3 @@
|
|||
cryptography
|
||||
requests
|
||||
future
|
||||
# Package dependencies are stored in setup.py.
|
||||
# For more information, see <https://github.com/ammaraskar/pyCraft/pull/156>.
|
||||
-e .
|
||||
|
|
26
setup.py
26
setup.py
|
@ -1,4 +1,4 @@
|
|||
from distutils.core import setup
|
||||
from setuptools import setup
|
||||
from minecraft import __version__
|
||||
|
||||
|
||||
|
@ -15,14 +15,32 @@ MAIN_AUTHORS = ["Ammar Askar <ammar@ammaraskar.com>",
|
|||
|
||||
URL = "https://github.com/ammaraskar/pyCraft"
|
||||
|
||||
setup(name="minecraft",
|
||||
setup(name="pyCraft",
|
||||
version=__version__,
|
||||
description="Python MineCraft library",
|
||||
long_description=read("README.rst"),
|
||||
url=URL,
|
||||
download_url=URL + "/tarball/" + __version__,
|
||||
author=", ".join(MAIN_AUTHORS),
|
||||
packages=["minecraft", "minecraft.networking"],
|
||||
install_requires=["cryptography>=1.5",
|
||||
"requests",
|
||||
"pynbt",
|
||||
],
|
||||
packages=["minecraft",
|
||||
"minecraft.networking",
|
||||
"minecraft.networking.packets",
|
||||
"minecraft.networking.packets.clientbound",
|
||||
"minecraft.networking.packets.clientbound.status",
|
||||
"minecraft.networking.packets.clientbound.handshake",
|
||||
"minecraft.networking.packets.clientbound.login",
|
||||
"minecraft.networking.packets.clientbound.play",
|
||||
"minecraft.networking.packets.serverbound",
|
||||
"minecraft.networking.packets.serverbound.status",
|
||||
"minecraft.networking.packets.serverbound.handshake",
|
||||
"minecraft.networking.packets.serverbound.login",
|
||||
"minecraft.networking.packets.serverbound.play",
|
||||
"minecraft.networking.types",
|
||||
],
|
||||
keywords=["MineCraft", "networking", "pyCraft", "minecraftdev", "mc"],
|
||||
classifiers=["Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
|
@ -35,6 +53,8 @@ setup(name="minecraft",
|
|||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Games/Entertainment",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Utilities"
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import getpass
|
||||
import sys
|
||||
import re
|
||||
from optparse import OptionParser
|
||||
|
||||
from minecraft import authentication
|
||||
from minecraft.exceptions import YggdrasilError
|
||||
from minecraft.networking.connection import Connection
|
||||
from minecraft.networking.packets import ChatMessagePacket, ChatPacket
|
||||
from minecraft.compat import input
|
||||
from minecraft.networking.packets import Packet, clientbound, serverbound
|
||||
|
||||
|
||||
def get_options():
|
||||
|
@ -19,10 +21,20 @@ def get_options():
|
|||
help="password to log in with")
|
||||
|
||||
parser.add_option("-s", "--server", dest="server", default=None,
|
||||
help="server to connect to")
|
||||
help="server host or host:port "
|
||||
"(enclose IPv6 addresses in square brackets)")
|
||||
|
||||
parser.add_option("-o", "--offline", dest="offline", action="store_true",
|
||||
help="connect to a server in offline mode")
|
||||
help="connect to a server in offline mode "
|
||||
"(no password required)")
|
||||
|
||||
parser.add_option("-d", "--dump-packets", dest="dump_packets",
|
||||
action="store_true",
|
||||
help="print sent and received packets to standard error")
|
||||
|
||||
parser.add_option("-v", "--dump-unknown-packets", dest="dump_unknown",
|
||||
action="store_true",
|
||||
help="include unknown packets in --dump-packets output")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
|
@ -30,19 +42,20 @@ def get_options():
|
|||
options.username = input("Enter your username: ")
|
||||
|
||||
if not options.password and not options.offline:
|
||||
options.password = getpass.getpass("Enter your password: ")
|
||||
options.password = getpass.getpass("Enter your password (leave "
|
||||
"blank for offline mode): ")
|
||||
options.offline = options.offline or (options.password == "")
|
||||
|
||||
if not options.server:
|
||||
options.server = input("Please enter server address"
|
||||
" (including port): ")
|
||||
options.server = input("Enter server host or host:port "
|
||||
"(enclose IPv6 addresses in square brackets): ")
|
||||
# Try to split out port and address
|
||||
if ':' in options.server:
|
||||
server = options.server.split(":")
|
||||
options.address = server[0]
|
||||
options.port = int(server[1])
|
||||
else:
|
||||
options.address = options.server
|
||||
options.port = 25565
|
||||
match = re.match(r"((?P<host>[^\[\]:]+)|\[(?P<addr>[^\[\]]+)\])"
|
||||
r"(:(?P<port>\d+))?$", options.server)
|
||||
if match is None:
|
||||
raise ValueError("Invalid server address: '%s'." % options.server)
|
||||
options.address = match.group("host") or match.group("addr")
|
||||
options.port = int(match.group("port") or 25565)
|
||||
|
||||
return options
|
||||
|
||||
|
@ -51,7 +64,7 @@ def main():
|
|||
options = get_options()
|
||||
|
||||
if options.offline:
|
||||
print("Connecting in offline mode")
|
||||
print("Connecting in offline mode...")
|
||||
connection = Connection(
|
||||
options.address, options.port, username=options.username)
|
||||
else:
|
||||
|
@ -61,23 +74,56 @@ def main():
|
|||
except YggdrasilError as e:
|
||||
print(e)
|
||||
sys.exit()
|
||||
print("Logged in as " + auth_token.username)
|
||||
print("Logged in as %s..." % auth_token.username)
|
||||
connection = Connection(
|
||||
options.address, options.port, auth_token=auth_token)
|
||||
|
||||
connection.connect()
|
||||
if options.dump_packets:
|
||||
def print_incoming(packet):
|
||||
if type(packet) is Packet:
|
||||
# This is a direct instance of the base Packet type, meaning
|
||||
# that it is a packet of unknown type, so we do not print it
|
||||
# unless explicitly requested by the user.
|
||||
if options.dump_unknown:
|
||||
print('--> [unknown packet] %s' % packet, file=sys.stderr)
|
||||
else:
|
||||
print('--> %s' % packet, file=sys.stderr)
|
||||
|
||||
def print_outgoing(packet):
|
||||
print('<-- %s' % packet, file=sys.stderr)
|
||||
|
||||
connection.register_packet_listener(
|
||||
print_incoming, Packet, early=True)
|
||||
connection.register_packet_listener(
|
||||
print_outgoing, Packet, outgoing=True)
|
||||
|
||||
def handle_join_game(join_game_packet):
|
||||
print('Connected.')
|
||||
|
||||
connection.register_packet_listener(
|
||||
handle_join_game, clientbound.play.JoinGamePacket)
|
||||
|
||||
def print_chat(chat_packet):
|
||||
print("Position: " + str(chat_packet.position))
|
||||
print("Data: " + chat_packet.json_data)
|
||||
print("Message (%s): %s" % (
|
||||
chat_packet.field_string('position'), chat_packet.json_data))
|
||||
|
||||
connection.register_packet_listener(
|
||||
print_chat, clientbound.play.ChatMessagePacket)
|
||||
|
||||
connection.connect()
|
||||
|
||||
connection.register_packet_listener(print_chat, ChatMessagePacket)
|
||||
while True:
|
||||
try:
|
||||
text = input()
|
||||
packet = ChatPacket()
|
||||
packet.message = text
|
||||
connection.write_packet(packet)
|
||||
if text == "/respawn":
|
||||
print("respawning...")
|
||||
packet = serverbound.play.ClientStatusPacket()
|
||||
packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
|
||||
connection.write_packet(packet)
|
||||
else:
|
||||
packet = serverbound.play.ChatPacket()
|
||||
packet.message = text
|
||||
connection.write_packet(packet)
|
||||
except KeyboardInterrupt:
|
||||
print("Bye!")
|
||||
sys.exit()
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import platform
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
if StrictVersion(platform.python_version()) < StrictVersion("3.3.0"):
|
||||
import mock # noqa
|
||||
else:
|
||||
from unittest import mock # noqa
|
|
@ -0,0 +1,640 @@
|
|||
import pynbt
|
||||
|
||||
from minecraft import SUPPORTED_MINECRAFT_VERSIONS
|
||||
from minecraft.networking import connection
|
||||
from minecraft.networking import types
|
||||
from minecraft.networking import packets
|
||||
from minecraft.networking.packets import clientbound
|
||||
from minecraft.networking.packets import serverbound
|
||||
from minecraft.networking.encryption import (
|
||||
create_AES_cipher, EncryptedFileObjectWrapper, EncryptedSocketWrapper
|
||||
)
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
|
||||
from numbers import Integral
|
||||
import unittest
|
||||
import threading
|
||||
import logging
|
||||
import socket
|
||||
import json
|
||||
import sys
|
||||
import zlib
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
|
||||
THREAD_TIMEOUT_S = 2
|
||||
|
||||
|
||||
class FakeClientDisconnect(Exception):
|
||||
""" Raised by 'FakeClientHandler.read_packet' if the client has cleanly
|
||||
disconnected prior to the call.
|
||||
"""
|
||||
|
||||
|
||||
class FakeServerDisconnect(Exception):
|
||||
""" May be raised within 'FakeClientHandler.handle_*' in order to terminate
|
||||
the client's connection. 'message' is provided as an argument to
|
||||
'handle_play_server_disconnect' or 'handle_login_server_disconnect'.
|
||||
"""
|
||||
def __init__(self, message=None):
|
||||
self.message = message
|
||||
|
||||
|
||||
class FakeServerTestSuccess(Exception):
|
||||
""" May be raised from within 'FakeClientHandler.handle_*' or from a
|
||||
'Connection' packet listener in order to terminate a 'FakeServerTest'
|
||||
successfully.
|
||||
"""
|
||||
|
||||
|
||||
class FakeClientHandler(object):
|
||||
""" Represents a single client connection being handled by a 'FakeServer'.
|
||||
The methods of the form 'handle_*' may be overridden by subclasses to
|
||||
customise the behaviour of the server.
|
||||
"""
|
||||
|
||||
__slots__ = 'server', 'socket', 'socket_file', 'packets', \
|
||||
'compression_enabled', 'user_uuid', 'user_name'
|
||||
|
||||
def __init__(self, server, socket):
|
||||
self.server = server
|
||||
self.socket = socket
|
||||
self.socket_file = socket.makefile('rb', 0)
|
||||
self.compression_enabled = False
|
||||
self.user_uuid = None
|
||||
self.user_name = None
|
||||
|
||||
def run(self):
|
||||
# Communicate with the client until disconnected.
|
||||
try:
|
||||
self._run_handshake()
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except IOError:
|
||||
pass
|
||||
except (FakeClientDisconnect, BrokenPipeError) as exc:
|
||||
if not self.handle_abnormal_disconnect(exc):
|
||||
raise
|
||||
finally:
|
||||
self.socket.close()
|
||||
self.socket_file.close()
|
||||
|
||||
def handle_abnormal_disconnect(self, exc):
|
||||
# Called when the client disconnects in an abnormal fashion. If this
|
||||
# handler returns True, the error is ignored and is treated as a normal
|
||||
# disconnection.
|
||||
return False
|
||||
|
||||
def handle_connection(self):
|
||||
# Called in the handshake state, just after the client connects,
|
||||
# before any packets have been exchanged.
|
||||
pass
|
||||
|
||||
def handle_handshake(self, handshake_packet):
|
||||
# Called in the handshake state, after receiving the client's
|
||||
# Handshake packet, which is provided as an argument.
|
||||
pass
|
||||
|
||||
def handle_login(self, login_start_packet):
|
||||
# Called to transition from the login state to the play state, after
|
||||
# compression and encryption, if applicable, have been set up. The
|
||||
# client's LoginStartPacket is given as an argument.
|
||||
self.user_name = login_start_packet.name
|
||||
self.user_uuid = uuid.UUID(bytes=hashlib.md5(
|
||||
('OfflinePlayer:%s' % self.user_name).encode('utf8')).digest())
|
||||
self.write_packet(clientbound.login.LoginSuccessPacket(
|
||||
UUID=str(self.user_uuid), Username=self.user_name))
|
||||
|
||||
def handle_play_start(self):
|
||||
# Called upon entering the play state.
|
||||
packet = clientbound.play.JoinGamePacket(
|
||||
entity_id=0, is_hardcore=False, game_mode=0, previous_game_mode=0,
|
||||
world_names=['minecraft:overworld'],
|
||||
world_name='minecraft:overworld',
|
||||
hashed_seed=12345, difficulty=2, max_players=1,
|
||||
level_type='default', reduced_debug_info=False, render_distance=9,
|
||||
simulation_distance=9, respawn_screen=False, is_debug=False,
|
||||
is_flat=False)
|
||||
|
||||
if self.server.context.protocol_later_eq(748):
|
||||
packet.dimension = pynbt.TAG_Compound({
|
||||
'natural': pynbt.TAG_Byte(1),
|
||||
'effects': pynbt.TAG_String('minecraft:overworld'),
|
||||
}, '')
|
||||
packet.dimension_codec = pynbt.TAG_Compound({
|
||||
'minecraft:dimension_type': pynbt.TAG_Compound({
|
||||
'type': pynbt.TAG_String('minecraft:dimension_type'),
|
||||
'value': pynbt.TAG_List(pynbt.TAG_Compound, [
|
||||
pynbt.TAG_Compound(packet.dimension),
|
||||
]),
|
||||
}),
|
||||
'minecraft:worldgen/biome': pynbt.TAG_Compound({
|
||||
'type': pynbt.TAG_String('minecraft:worldgen/biome'),
|
||||
'value': pynbt.TAG_List(pynbt.TAG_Compound, [
|
||||
pynbt.TAG_Compound({
|
||||
'id': pynbt.TAG_Int(1),
|
||||
'name': pynbt.TAG_String('minecraft:plains'),
|
||||
}),
|
||||
pynbt.TAG_Compound({
|
||||
'id': pynbt.TAG_Int(2),
|
||||
'name': pynbt.TAG_String('minecraft:desert'),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}, '')
|
||||
elif self.server.context.protocol_later_eq(718):
|
||||
packet.dimension = 'minecraft:overworld'
|
||||
else:
|
||||
packet.dimension = types.Dimension.OVERWORLD
|
||||
|
||||
self.write_packet(packet)
|
||||
|
||||
def handle_play_packet(self, packet):
|
||||
# Called upon each packet received after handle_play_start() returns.
|
||||
if isinstance(packet, serverbound.play.ChatPacket):
|
||||
assert len(packet.message) <= packet.max_length
|
||||
self.write_packet(clientbound.play.ChatMessagePacket(json.dumps({
|
||||
'translate': 'chat.type.text',
|
||||
'with': [self.username, packet.message],
|
||||
})))
|
||||
|
||||
def handle_status(self, request_packet):
|
||||
# Called in the first phase of the status state, to send the Response
|
||||
# packet. The client's Request packet is provided as an argument.
|
||||
packet = clientbound.status.ResponsePacket()
|
||||
packet.json_response = json.dumps({
|
||||
'version': {
|
||||
'name': self.server.minecraft_version,
|
||||
'protocol': self.server.context.protocol_version},
|
||||
'players': {
|
||||
'max': 1,
|
||||
'online': 0,
|
||||
'sample': []},
|
||||
'description': {
|
||||
'text': 'FakeServer'}})
|
||||
self.write_packet(packet)
|
||||
|
||||
def handle_ping(self, ping_packet):
|
||||
# Called in the second phase of the status state, to respond to a Ping
|
||||
# packet, which is provided as an argument.
|
||||
packet = clientbound.status.PingResponsePacket(time=ping_packet.time)
|
||||
self.write_packet(packet)
|
||||
|
||||
def handle_login_server_disconnect(self, message):
|
||||
# Called when the server cleanly terminates the connection during
|
||||
# login, i.e. by raising FakeServerDisconnect from a handler.
|
||||
message = 'Connection denied.' if message is None else message
|
||||
self.write_packet(clientbound.login.DisconnectPacket(
|
||||
json_data=json.dumps({'text': message})))
|
||||
|
||||
def handle_play_server_disconnect(self, message):
|
||||
# As 'handle_login_server_disconnect', but for the play state.
|
||||
message = 'Disconnected.' if message is None else message
|
||||
self.write_packet(clientbound.play.DisconnectPacket(
|
||||
json_data=json.dumps({'text': message})))
|
||||
|
||||
def handle_play_client_disconnect(self):
|
||||
# Called when the client cleanly terminates the connection during play.
|
||||
pass
|
||||
|
||||
def write_packet(self, packet):
|
||||
# Send and log a clientbound packet.
|
||||
packet.context = self.server.context
|
||||
logging.debug('[S-> ] %s' % packet)
|
||||
packet.write(self.socket, **(
|
||||
{'compression_threshold': self.server.compression_threshold}
|
||||
if self.compression_enabled else {}))
|
||||
|
||||
def read_packet(self):
|
||||
# Read and log a serverbound packet from the client, or raises
|
||||
# FakeClientDisconnect if the client has cleanly disconnected.
|
||||
buffer = self._read_packet_buffer()
|
||||
packet_id = types.VarInt.read(buffer)
|
||||
if packet_id in self.packets:
|
||||
packet = self.packets[packet_id](self.server.context)
|
||||
packet.read(buffer)
|
||||
else:
|
||||
packet = packets.Packet(self.server.context, id=packet_id)
|
||||
logging.debug('[ ->S] %s' % packet)
|
||||
return packet
|
||||
|
||||
def _run_handshake(self):
|
||||
# Enter the initial (i.e. handshaking) state of the connection.
|
||||
self.packets = self.server.packets_handshake
|
||||
try:
|
||||
self.handle_connection()
|
||||
packet = self.read_packet()
|
||||
assert isinstance(packet, serverbound.handshake.HandShakePacket), \
|
||||
type(packet)
|
||||
self.handle_handshake(packet)
|
||||
if packet.next_state == 1:
|
||||
self._run_status()
|
||||
elif packet.next_state == 2:
|
||||
self._run_handshake_play(packet)
|
||||
else:
|
||||
raise AssertionError('Unknown state: %s' % packet.next_state)
|
||||
except FakeServerDisconnect:
|
||||
pass
|
||||
|
||||
def _run_handshake_play(self, packet):
|
||||
# Prepare to transition from handshaking to play state (via login),
|
||||
# using the given serverbound HandShakePacket to perform play-specific
|
||||
# processing.
|
||||
if self.server.context.protocol_version == packet.protocol_version:
|
||||
return self._run_login()
|
||||
elif self.server.context.protocol_earlier(packet.protocol_version):
|
||||
msg = "Outdated server! I'm still on %s" \
|
||||
% self.server.minecraft_version
|
||||
else:
|
||||
msg = 'Outdated client! Please use %s' \
|
||||
% self.server.minecraft_version
|
||||
self.handle_login_server_disconnect(msg)
|
||||
|
||||
def _run_login(self):
|
||||
# Enter the login state of the connection.
|
||||
self.packets = self.server.packets_login
|
||||
packet = self.read_packet()
|
||||
assert isinstance(packet, serverbound.login.LoginStartPacket)
|
||||
|
||||
if self.server.private_key is not None:
|
||||
self._run_login_encryption()
|
||||
|
||||
if self.server.compression_threshold is not None:
|
||||
self.write_packet(clientbound.login.SetCompressionPacket(
|
||||
threshold=self.server.compression_threshold))
|
||||
self.compression_enabled = True
|
||||
|
||||
try:
|
||||
self.handle_login(packet)
|
||||
except FakeServerDisconnect as e:
|
||||
self.handle_login_server_disconnect(message=e.message)
|
||||
else:
|
||||
self._run_playing()
|
||||
|
||||
def _run_login_encryption(self):
|
||||
# Set up protocol encryption with the client, then return.
|
||||
server_token = b'\x89\x82\x9a\x01' # Guaranteed to be random.
|
||||
self.write_packet(clientbound.login.EncryptionRequestPacket(
|
||||
server_id='', verify_token=server_token,
|
||||
public_key=self.server.public_key_bytes))
|
||||
|
||||
packet = self.read_packet()
|
||||
assert isinstance(packet, serverbound.login.EncryptionResponsePacket)
|
||||
private_key = self.server.private_key
|
||||
client_token = private_key.decrypt(packet.verify_token, PKCS1v15())
|
||||
assert client_token == server_token
|
||||
shared_secret = private_key.decrypt(packet.shared_secret, PKCS1v15())
|
||||
|
||||
cipher = create_AES_cipher(shared_secret)
|
||||
enc, dec = cipher.encryptor(), cipher.decryptor()
|
||||
self.socket = EncryptedSocketWrapper(self.socket, enc, dec)
|
||||
self.socket_file = EncryptedFileObjectWrapper(self.socket_file, dec)
|
||||
|
||||
def _run_playing(self):
|
||||
# Enter the playing state of the connection.
|
||||
self.packets = self.server.packets_playing
|
||||
client_disconnected = False
|
||||
try:
|
||||
self.handle_play_start()
|
||||
try:
|
||||
while True:
|
||||
self.handle_play_packet(self.read_packet())
|
||||
except FakeClientDisconnect:
|
||||
client_disconnected = True
|
||||
self.handle_play_client_disconnect()
|
||||
except FakeServerDisconnect as e:
|
||||
if not client_disconnected:
|
||||
self.handle_play_server_disconnect(message=e.message)
|
||||
|
||||
def _run_status(self):
|
||||
# Enter the status state of the connection.
|
||||
self.packets = self.server.packets_status
|
||||
|
||||
packet = self.read_packet()
|
||||
assert isinstance(packet, serverbound.status.RequestPacket)
|
||||
try:
|
||||
self.handle_status(packet)
|
||||
try:
|
||||
packet = self.read_packet()
|
||||
except FakeClientDisconnect:
|
||||
return
|
||||
assert isinstance(packet, serverbound.status.PingPacket)
|
||||
self.handle_ping(packet)
|
||||
except FakeServerDisconnect:
|
||||
pass
|
||||
|
||||
def _read_packet_buffer(self):
|
||||
# Read a serverbound packet in the form of a raw buffer, or raises
|
||||
# FakeClientDisconnect if the client has cleanly disconnected.
|
||||
try:
|
||||
length = types.VarInt.read(self.socket_file)
|
||||
except EOFError:
|
||||
raise FakeClientDisconnect
|
||||
buffer = packets.PacketBuffer()
|
||||
while len(buffer.get_writable()) < length:
|
||||
data = self.socket_file.read(length - len(buffer.get_writable()))
|
||||
buffer.send(data)
|
||||
buffer.reset_cursor()
|
||||
if self.compression_enabled:
|
||||
data_length = types.VarInt.read(buffer)
|
||||
if data_length > 0:
|
||||
data = zlib.decompress(buffer.read())
|
||||
assert len(data) == data_length, \
|
||||
'%s != %s' % (len(data), data_length)
|
||||
buffer.reset()
|
||||
buffer.send(data)
|
||||
buffer.reset_cursor()
|
||||
return buffer
|
||||
|
||||
|
||||
class FakeServer(object):
|
||||
"""
|
||||
A rudimentary implementation of a Minecraft server, suitable for
|
||||
testing features of minecraft.networking.connection.Connection that
|
||||
require a full connection to be established.
|
||||
|
||||
The server listens on a local TCP socket and accepts client connections
|
||||
in serial, in a single-threaded manner. It responds to status queries,
|
||||
performs handshake and login, and, by default, echoes any chat messages
|
||||
back to the client until it disconnects.
|
||||
|
||||
The behaviour of the server can be customised by writing subclasses of
|
||||
FakeClientHandler, overriding its public methods of the form
|
||||
'handle_*', and providing the class to the FakeServer as its
|
||||
'client_handler_type'.
|
||||
|
||||
If 'private_key' is not None, it must be an instance of
|
||||
'cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey',
|
||||
'public_key_bytes' must be the corresponding public key serialised in
|
||||
DER format with PKCS1 encoding, and encryption will be enabled for all
|
||||
client sessions; otherwise, if it is None, encryption is disabled.
|
||||
"""
|
||||
|
||||
__slots__ = 'listen_socket', 'compression_threshold', 'context', \
|
||||
'minecraft_version', 'client_handler_type', 'server_type', \
|
||||
'packets_handshake', 'packets_login', 'packets_playing', \
|
||||
'packets_status', 'lock', 'stopping', 'private_key', \
|
||||
'public_key_bytes', 'test_case'
|
||||
|
||||
def __init__(self, minecraft_version=None, compression_threshold=None,
|
||||
client_handler_type=FakeClientHandler, private_key=None,
|
||||
public_key_bytes=None, test_case=None):
|
||||
if minecraft_version is None:
|
||||
minecraft_version = list(SUPPORTED_MINECRAFT_VERSIONS.keys())[-1]
|
||||
|
||||
if isinstance(minecraft_version, Integral):
|
||||
proto = minecraft_version
|
||||
minecraft_version = 'FakeVersion%d' % proto
|
||||
for ver, ver_proto in SUPPORTED_MINECRAFT_VERSIONS.items():
|
||||
if ver_proto == proto:
|
||||
minecraft_version = ver
|
||||
else:
|
||||
proto = SUPPORTED_MINECRAFT_VERSIONS[minecraft_version]
|
||||
self.context = connection.ConnectionContext(protocol_version=proto)
|
||||
|
||||
self.minecraft_version = minecraft_version
|
||||
self.compression_threshold = compression_threshold
|
||||
self.client_handler_type = client_handler_type
|
||||
self.private_key = private_key
|
||||
self.public_key_bytes = public_key_bytes
|
||||
self.test_case = test_case
|
||||
|
||||
self.packets_handshake = {
|
||||
p.get_id(self.context): p for p in
|
||||
serverbound.handshake.get_packets(self.context)}
|
||||
|
||||
self.packets_login = {
|
||||
p.get_id(self.context): p for p in
|
||||
serverbound.login.get_packets(self.context)}
|
||||
|
||||
self.packets_playing = {
|
||||
p.get_id(self.context): p for p in
|
||||
serverbound.play.get_packets(self.context)}
|
||||
|
||||
self.packets_status = {
|
||||
p.get_id(self.context): p for p in
|
||||
serverbound.status.get_packets(self.context)}
|
||||
|
||||
self.listen_socket = socket.socket()
|
||||
self.listen_socket.settimeout(0.1)
|
||||
self.listen_socket.bind(('localhost', 0))
|
||||
self.listen_socket.listen(1)
|
||||
|
||||
self.lock = threading.Lock()
|
||||
self.stopping = False
|
||||
|
||||
super(FakeServer, self).__init__()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
client_socket, addr = self.listen_socket.accept()
|
||||
logging.debug('[ ++ ] Client %s connected.' % (addr,))
|
||||
self.client_handler_type(self, client_socket).run()
|
||||
logging.debug('[ -- ] Client %s disconnected.' % (addr,))
|
||||
except socket.timeout:
|
||||
pass
|
||||
with self.lock:
|
||||
if self.stopping:
|
||||
logging.debug('[ ** ] Server stopped normally.')
|
||||
break
|
||||
finally:
|
||||
self.listen_socket.close()
|
||||
|
||||
def stop(self):
|
||||
with self.lock:
|
||||
self.stopping = True
|
||||
|
||||
|
||||
class _FakeServerTest(unittest.TestCase):
|
||||
"""
|
||||
A template for test cases involving a single client connecting to a
|
||||
single 'FakeServer'. The default behaviour causes the client to connect
|
||||
to the server, join the game, then disconnect, considering it a success
|
||||
if a 'JoinGamePacket' is received before a 'DisconnectPacket'.
|
||||
|
||||
Customise by making subclasses that:
|
||||
1. Override the attributes present in this class, where desired, so
|
||||
that they will apply to all tests; and
|
||||
2. Define tests (or override 'runTest') to call '_test_connect' with
|
||||
the necessary arguments to override class attributes; and
|
||||
3. Override '_start_client' in order to set event listeners and
|
||||
change the connection mode, if necessary.
|
||||
To terminate the test and indicate that it finished successfully, a
|
||||
client packet handler or a handler method of the 'FakeClientHandler'
|
||||
must raise a 'FakeServerTestSuccess' exception.
|
||||
"""
|
||||
|
||||
server_version = None
|
||||
# The Minecraft version ID that the server will support.
|
||||
# If None, the latest supported version will be used.
|
||||
|
||||
client_versions = None
|
||||
# The set of Minecraft version IDs or protocol version numbers that the
|
||||
# client will support. If None, the client supports all possible versions.
|
||||
|
||||
server_type = FakeServer
|
||||
# A subclass of FakeServer to be used in tests.
|
||||
|
||||
client_handler_type = FakeClientHandler
|
||||
# A subclass of FakeClientHandler to be used in tests.
|
||||
|
||||
connection_type = connection.Connection
|
||||
# The constructor of the Connection instance to be used.
|
||||
|
||||
compression_threshold = None
|
||||
# The compression threshold that the server will dictate.
|
||||
# If None, compression is disabled.
|
||||
|
||||
private_key = None
|
||||
# The RSA private key used by the server: see 'FakeServer'.
|
||||
|
||||
public_key_bytes = None
|
||||
# The serialised RSA public key used by the server: see 'FakeServer'.
|
||||
|
||||
ignore_extra_exceptions = False
|
||||
# If True, any occurrence of the 'FakeServerTestSuccess' exception is
|
||||
# considered a success, even if other exceptions are raised.
|
||||
|
||||
def _start_client(self, client):
|
||||
game_joined = [False]
|
||||
|
||||
def handle_join_game(packet):
|
||||
game_joined[0] = True
|
||||
client.register_packet_listener(
|
||||
handle_join_game, clientbound.play.JoinGamePacket)
|
||||
|
||||
def handle_disconnect(packet):
|
||||
assert game_joined[0], 'JoinGamePacket not received.'
|
||||
raise FakeServerTestSuccess
|
||||
client.register_packet_listener(
|
||||
handle_disconnect, clientbound.play.DisconnectPacket)
|
||||
|
||||
client.connect()
|
||||
|
||||
def _test_connect(self, client_versions=None, server_version=None,
|
||||
server_type=None, client_handler_type=None,
|
||||
connection_type=None, compression_threshold=None,
|
||||
private_key=None, public_key_bytes=None,
|
||||
ignore_extra_exceptions=None):
|
||||
if client_versions is None:
|
||||
client_versions = self.client_versions
|
||||
if server_version is None:
|
||||
server_version = self.server_version
|
||||
if server_type is None:
|
||||
server_type = self.server_type
|
||||
if client_handler_type is None:
|
||||
client_handler_type = self.client_handler_type
|
||||
if connection_type is None:
|
||||
connection_type = self.connection_type
|
||||
if compression_threshold is None:
|
||||
compression_threshold = self.compression_threshold
|
||||
if private_key is None:
|
||||
private_key = self.private_key
|
||||
if public_key_bytes is None:
|
||||
public_key_bytes = self.public_key_bytes
|
||||
if ignore_extra_exceptions is None:
|
||||
ignore_extra_exceptions = self.ignore_extra_exceptions
|
||||
|
||||
server = server_type(minecraft_version=server_version,
|
||||
compression_threshold=compression_threshold,
|
||||
client_handler_type=client_handler_type,
|
||||
private_key=private_key,
|
||||
public_key_bytes=public_key_bytes,
|
||||
test_case=self)
|
||||
addr = "localhost"
|
||||
port = server.listen_socket.getsockname()[1]
|
||||
|
||||
cond = threading.Condition()
|
||||
server_lock = threading.Lock()
|
||||
server_exc_info = [None]
|
||||
client_lock = threading.Lock()
|
||||
client_exc_info = [None]
|
||||
|
||||
client = connection_type(
|
||||
addr, port, username='TestUser', allowed_versions=client_versions)
|
||||
|
||||
@client.exception_handler()
|
||||
def handle_client_exception(exc, exc_info):
|
||||
with client_lock:
|
||||
client_exc_info[0] = exc_info
|
||||
with cond:
|
||||
cond.notify_all()
|
||||
|
||||
@client.listener(packets.Packet, early=True)
|
||||
def handle_incoming_packet(packet):
|
||||
logging.debug('[ ->C] %s' % packet)
|
||||
|
||||
@client.listener(packets.Packet, early=True, outgoing=True)
|
||||
def handle_outgoing_packet(packet):
|
||||
logging.debug('[C-> ] %s' % packet)
|
||||
|
||||
server_thread = threading.Thread(
|
||||
name='FakeServer',
|
||||
target=self._test_connect_server,
|
||||
args=(server, cond, server_lock, server_exc_info))
|
||||
server_thread.daemon = True
|
||||
|
||||
errors = []
|
||||
try:
|
||||
try:
|
||||
with cond:
|
||||
server_thread.start()
|
||||
self._start_client(client)
|
||||
cond.wait(THREAD_TIMEOUT_S)
|
||||
finally:
|
||||
# Wait for all threads to exit.
|
||||
server.stop()
|
||||
for thread in server_thread, client.networking_thread:
|
||||
if thread is not None and thread.is_alive():
|
||||
thread.join(THREAD_TIMEOUT_S)
|
||||
if thread is not None and thread.is_alive():
|
||||
errors.append({
|
||||
'msg': 'Thread "%s" timed out.' % thread.name})
|
||||
except Exception:
|
||||
errors.insert(0, {
|
||||
'msg': 'Exception in main thread',
|
||||
'exc_info': sys.exc_info()})
|
||||
else:
|
||||
timeout = True
|
||||
for lock, [exc_info], thread_name in (
|
||||
(client_lock, client_exc_info, 'client thread'),
|
||||
(server_lock, server_exc_info, 'server thread')
|
||||
):
|
||||
with lock:
|
||||
if exc_info is None:
|
||||
continue
|
||||
timeout = False
|
||||
if not issubclass(exc_info[0], FakeServerTestSuccess):
|
||||
errors.insert(0, {
|
||||
'msg': 'Exception in %s:' % thread_name,
|
||||
'exc_info': exc_info})
|
||||
elif ignore_extra_exceptions:
|
||||
del errors[:]
|
||||
break
|
||||
if timeout:
|
||||
errors.insert(0, {'msg': 'Test timed out.'})
|
||||
|
||||
if len(errors) > 1:
|
||||
for error in errors:
|
||||
logging.error(**error)
|
||||
self.fail('Multiple errors: see logging output.')
|
||||
elif errors and 'exc_info' in errors[0]:
|
||||
exc_value, exc_tb = errors[0]['exc_info'][1:]
|
||||
raise exc_value.with_traceback(exc_tb)
|
||||
elif errors:
|
||||
self.fail(errors[0]['msg'])
|
||||
|
||||
def _test_connect_server(self, server, cond, server_lock, server_exc_info):
|
||||
exc_info = None
|
||||
try:
|
||||
server.run()
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
with server_lock:
|
||||
server_exc_info[0] = exc_info
|
||||
with cond:
|
||||
cond.notify_all()
|
|
@ -1,13 +1,14 @@
|
|||
from minecraft.authentication import Profile
|
||||
from minecraft.authentication import AuthenticationToken
|
||||
from minecraft.authentication import _make_request
|
||||
from minecraft.authentication import _raise_from_request
|
||||
from minecraft.authentication import _raise_from_response
|
||||
from minecraft.exceptions import YggdrasilError
|
||||
|
||||
from unittest import mock
|
||||
import unittest
|
||||
import requests
|
||||
import json
|
||||
import unittest
|
||||
from .compat import mock
|
||||
import os
|
||||
|
||||
FAKE_DATA = {
|
||||
"id_": "85e2c12b9eab4a7dabf61babc11354c2",
|
||||
|
@ -46,14 +47,14 @@ def get_mc_credentials():
|
|||
username, password = get_mc_credentials()
|
||||
|
||||
|
||||
def should_skip_cred_test():
|
||||
"""
|
||||
Returns `True` if a test requiring credentials should be skipped.
|
||||
Otherwise returns `False`
|
||||
"""
|
||||
if username is None or password is None:
|
||||
return True
|
||||
return False
|
||||
skipIfNoCredentials = unittest.skipIf(
|
||||
username is None or password is None,
|
||||
"Need credentials to perform test.")
|
||||
|
||||
|
||||
skipUnlessInternetTestsEnabled = unittest.skipUnless(
|
||||
os.environ.get('PYCRAFT_RUN_INTERNET_TESTS'),
|
||||
"Tests involving Internet access are disabled.")
|
||||
|
||||
|
||||
class InitProfile(unittest.TestCase):
|
||||
|
@ -175,18 +176,21 @@ class AuthenticateAuthenticationToken(unittest.TestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
a.authenticate("username")
|
||||
|
||||
@skipUnlessInternetTestsEnabled
|
||||
def test_authenticate_wrong_credentials(self):
|
||||
a = AuthenticationToken()
|
||||
|
||||
# We assume these aren't actual, valid credentials.
|
||||
with self.assertRaises(YggdrasilError) as e:
|
||||
with self.assertRaises(YggdrasilError) as cm:
|
||||
a.authenticate("Billy", "The Goat")
|
||||
|
||||
err = "Invalid Credentials. Invalid username or password."
|
||||
self.assertEqual(e.error, err)
|
||||
err = "[403] ForbiddenOperationException: " \
|
||||
"'Invalid credentials. Invalid username or password.'"
|
||||
self.maxDiff = 5000
|
||||
self.assertEqual(str(cm.exception), err)
|
||||
|
||||
@unittest.skipIf(should_skip_cred_test(),
|
||||
"Need credentials to perform test.")
|
||||
@skipIfNoCredentials
|
||||
@skipUnlessInternetTestsEnabled
|
||||
def test_authenticate_good_credentials(self):
|
||||
a = AuthenticationToken()
|
||||
|
||||
|
@ -194,35 +198,11 @@ class AuthenticateAuthenticationToken(unittest.TestCase):
|
|||
self.assertTrue(resp)
|
||||
|
||||
|
||||
class RefreshAuthenticationToken(unittest.TestCase):
|
||||
# TODO: Make me!
|
||||
pass
|
||||
|
||||
|
||||
class ValidateAuthenticationToken(unittest.TestCase):
|
||||
# TODO: Make me!
|
||||
pass
|
||||
|
||||
|
||||
class SignOutAuthenticationToken(unittest.TestCase):
|
||||
# TODO: Make me!
|
||||
pass
|
||||
|
||||
|
||||
class InvalidateAuthenticationToken(unittest.TestCase):
|
||||
# TODO: Make me!
|
||||
pass
|
||||
|
||||
|
||||
class JoinAuthenticationToken(unittest.TestCase):
|
||||
# TODO: Make me!
|
||||
pass
|
||||
|
||||
|
||||
@skipUnlessInternetTestsEnabled
|
||||
class MakeRequest(unittest.TestCase):
|
||||
def test_make_request_http_method(self):
|
||||
req = _make_request(AUTHSERVER, "authenticate", {"Billy": "Bob"})
|
||||
self.assertEqual(req.request.method, "POST")
|
||||
res = _make_request(AUTHSERVER, "authenticate", {"Billy": "Bob"})
|
||||
self.assertEqual(res.request.method, "POST")
|
||||
|
||||
def test_make_request_json_dump(self):
|
||||
data = {"Marie": "McGee",
|
||||
|
@ -233,40 +213,185 @@ class MakeRequest(unittest.TestCase):
|
|||
"Listly": ["listling1", 2, "listling 3"]
|
||||
}
|
||||
|
||||
req = _make_request(AUTHSERVER, "authenticate", data)
|
||||
self.assertEqual(req.request.body, json.dumps(data))
|
||||
res = _make_request(AUTHSERVER, "authenticate", data)
|
||||
self.assertEqual(res.request.body, json.dumps(data))
|
||||
|
||||
def test_make_request_url(self):
|
||||
URL = "https://authserver.mojang.com/authenticate"
|
||||
req = _make_request(AUTHSERVER, "authenticate", {"Darling": "Diary"})
|
||||
self.assertEqual(req.request.url, URL)
|
||||
res = _make_request(AUTHSERVER, "authenticate", {"Darling": "Diary"})
|
||||
self.assertEqual(res.request.url, URL)
|
||||
|
||||
|
||||
class RaiseFromRequest(unittest.TestCase):
|
||||
def test_raise_from_erroneous_request(self):
|
||||
err_req = requests.Request()
|
||||
err_req.status_code = 401
|
||||
err_req.json = mock.MagicMock(
|
||||
err_res = mock.NonCallableMock(requests.Response)
|
||||
err_res.status_code = 401
|
||||
err_res.json = mock.MagicMock(
|
||||
return_value={"error": "ThisIsAnException",
|
||||
"errorMessage": "Went wrong."})
|
||||
err_res.text = json.dumps(err_res.json())
|
||||
|
||||
with self.assertRaises(YggdrasilError) as e:
|
||||
_raise_from_request(err_req)
|
||||
self.assertEqual(e, "[401]) ThisIsAnException: Went Wrong.")
|
||||
with self.assertRaises(YggdrasilError) as cm:
|
||||
_raise_from_response(err_res)
|
||||
|
||||
def test_raise_from_erroneous_request_without_error(self):
|
||||
err_req = requests.Request()
|
||||
err_req.status_code = 401
|
||||
err_req.json = mock.MagicMock(return_value={"goldfish": "are pretty."})
|
||||
message = "[401] ThisIsAnException: 'Went wrong.'"
|
||||
self.assertEqual(str(cm.exception), message)
|
||||
|
||||
with self.assertRaises(YggdrasilError) as e:
|
||||
_raise_from_request(err_req)
|
||||
def test_raise_invalid_json(self):
|
||||
err_res = mock.NonCallableMock(requests.Response)
|
||||
err_res.status_code = 401
|
||||
err_res.json = mock.MagicMock(
|
||||
side_effect=ValueError("no json could be decoded")
|
||||
)
|
||||
err_res.text = "{sample invalid json}"
|
||||
|
||||
self.assertEqual(e, "Malformed error message.")
|
||||
with self.assertRaises(YggdrasilError) as cm:
|
||||
_raise_from_response(err_res)
|
||||
|
||||
def test_raise_from_healthy_request(self):
|
||||
req = requests.Request()
|
||||
req.status_code = 200
|
||||
req.json = mock.MagicMock(return_value={"vegetables": "are healthy."})
|
||||
message_start = "[401] Malformed error message"
|
||||
self.assertTrue(str(cm.exception).startswith(message_start))
|
||||
|
||||
self.assertIs(_raise_from_request(req), None)
|
||||
def test_raise_from_erroneous_response_without_error(self):
|
||||
err_res = mock.NonCallableMock(requests.Response)
|
||||
err_res.status_code = 401
|
||||
err_res.json = mock.MagicMock(return_value={"goldfish": "are pretty."})
|
||||
err_res.text = json.dumps(err_res.json())
|
||||
|
||||
with self.assertRaises(YggdrasilError) as cm:
|
||||
_raise_from_response(err_res)
|
||||
|
||||
message_start = "[401] Malformed error message"
|
||||
self.assertTrue(str(cm.exception).startswith(message_start))
|
||||
|
||||
def test_raise_from_healthy_response(self):
|
||||
res = mock.NonCallableMock(requests.Response)
|
||||
res.status_code = 200
|
||||
res.json = mock.MagicMock(return_value={"vegetables": "are healthy."})
|
||||
res.text = json.dumps(res.json())
|
||||
|
||||
self.assertIs(_raise_from_response(res), None)
|
||||
|
||||
|
||||
class NormalConnectionProcedure(unittest.TestCase):
|
||||
def test_login_connect_and_logout(self):
|
||||
a = AuthenticationToken()
|
||||
|
||||
successful_res = mock.NonCallableMock(requests.Response)
|
||||
successful_res.status_code = 200
|
||||
successful_res.json = mock.MagicMock(
|
||||
return_value={"accessToken": "token",
|
||||
"clientToken": "token",
|
||||
"selectedProfile": {
|
||||
"id": "1",
|
||||
"name": "asdf"
|
||||
}}
|
||||
)
|
||||
successful_res.text = json.dumps(successful_res.json())
|
||||
|
||||
error_res = mock.NonCallableMock(requests.Response)
|
||||
error_res.status_code = 400
|
||||
error_res.json = mock.MagicMock(
|
||||
return_value={
|
||||
"error": "invalid request",
|
||||
"errorMessage": "invalid request"
|
||||
}
|
||||
)
|
||||
error_res.text = json.dumps(error_res.json())
|
||||
|
||||
def mocked_make_request(server, endpoint, data):
|
||||
if endpoint == "authenticate":
|
||||
if "accessToken" in data:
|
||||
response = successful_res.copy()
|
||||
response.json["accessToken"] = data["accessToken"]
|
||||
return response
|
||||
return successful_res
|
||||
if endpoint == "refresh" and data["accessToken"] == "token":
|
||||
return successful_res
|
||||
if (endpoint == "validate" and data["accessToken"] == "token") \
|
||||
or endpoint == "join":
|
||||
r = requests.Response()
|
||||
r.status_code = 204
|
||||
r.raise_for_status = mock.MagicMock(return_value=None)
|
||||
return r
|
||||
if endpoint == "signout":
|
||||
return successful_res
|
||||
if endpoint == "invalidate":
|
||||
return successful_res
|
||||
|
||||
return error_res
|
||||
|
||||
# Test a successful sequence of events
|
||||
with mock.patch("minecraft.authentication._make_request",
|
||||
side_effect=mocked_make_request) as _make_request_mock:
|
||||
|
||||
self.assertFalse(a.authenticated)
|
||||
self.assertTrue(a.authenticate("username", "password"))
|
||||
|
||||
self.assertEqual(_make_request_mock.call_count, 1)
|
||||
self.assertIn("clientToken", _make_request_mock.call_args[0][2])
|
||||
|
||||
self.assertTrue(a.authenticated)
|
||||
|
||||
self.assertTrue(a.refresh())
|
||||
self.assertTrue(a.validate())
|
||||
|
||||
self.assertTrue(a.authenticated)
|
||||
|
||||
self.assertTrue(a.join(123))
|
||||
self.assertTrue(a.sign_out("username", "password"))
|
||||
|
||||
self.assertTrue(a.invalidate())
|
||||
|
||||
self.assertEqual(_make_request_mock.call_count, 6)
|
||||
|
||||
# Test that we send a provided clientToken if the authenticationToken
|
||||
# is initialized with one
|
||||
with mock.patch("minecraft.authentication._make_request",
|
||||
side_effect=mocked_make_request) as _make_request_mock:
|
||||
a = AuthenticationToken(client_token="existing_token")
|
||||
|
||||
self.assertTrue(a.authenticate("username", "password",
|
||||
invalidate_previous=False))
|
||||
|
||||
self.assertEqual(_make_request_mock.call_count, 1)
|
||||
self.assertEqual(
|
||||
"existing_token",
|
||||
_make_request_mock.call_args[0][2]["clientToken"]
|
||||
)
|
||||
|
||||
# Test that we invalidate previous tokens properly
|
||||
with mock.patch("minecraft.authentication._make_request",
|
||||
side_effect=mocked_make_request) as _make_request_mock:
|
||||
a = AuthenticationToken()
|
||||
|
||||
self.assertFalse(a.authenticated)
|
||||
self.assertTrue(a.authenticate("username", "password",
|
||||
invalidate_previous=True))
|
||||
|
||||
self.assertTrue(a.authenticated)
|
||||
self.assertEqual(a.access_token, "token")
|
||||
self.assertEqual(_make_request_mock.call_count, 1)
|
||||
self.assertNotIn("clientToken", _make_request_mock.call_args[0][2])
|
||||
|
||||
a = AuthenticationToken(username="username",
|
||||
access_token="token",
|
||||
client_token="token")
|
||||
|
||||
# Failures
|
||||
with mock.patch("minecraft.authentication._make_request",
|
||||
return_value=error_res) as _make_request_mock:
|
||||
self.assertFalse(a.authenticated)
|
||||
|
||||
a.client_token = "token"
|
||||
a.access_token = None
|
||||
self.assertRaises(ValueError, a.refresh)
|
||||
|
||||
a.client_token = None
|
||||
a.access_token = "token"
|
||||
self.assertRaises(ValueError, a.refresh)
|
||||
|
||||
a.access_token = None
|
||||
self.assertRaises(ValueError, a.validate)
|
||||
|
||||
self.assertRaises(YggdrasilError, a.join, 123)
|
||||
self.assertRaises(YggdrasilError, a.invalidate)
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
import unittest
|
||||
|
||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
|
||||
from minecraft import utility
|
||||
from minecraft.networking.connection import ConnectionContext
|
||||
from minecraft.networking import packets
|
||||
from minecraft.networking import types
|
||||
from minecraft.networking.packets import clientbound
|
||||
from minecraft.networking.packets import serverbound
|
||||
|
||||
|
||||
class LegacyPacketNamesTest(unittest.TestCase):
|
||||
def test_legacy_packets_equal_current_packets(self):
|
||||
self.assertEqual(packets.KeepAlivePacket,
|
||||
packets.AbstractKeepAlivePacket)
|
||||
|
||||
self.assertEqual(packets.state_handshake_clientbound,
|
||||
clientbound.handshake.get_packets)
|
||||
self.assertEqual(packets.HandShakePacket,
|
||||
serverbound.handshake.HandShakePacket)
|
||||
self.assertEqual(packets.state_handshake_serverbound,
|
||||
serverbound.handshake.get_packets)
|
||||
|
||||
self.assertEqual(packets.ResponsePacket,
|
||||
clientbound.status.ResponsePacket)
|
||||
self.assertEqual(packets.PingPacketResponse,
|
||||
clientbound.status.PingResponsePacket)
|
||||
self.assertEqual(packets.state_status_clientbound,
|
||||
clientbound.status.get_packets)
|
||||
self.assertEqual(packets.RequestPacket,
|
||||
serverbound.status.RequestPacket)
|
||||
self.assertEqual(packets.PingPacket,
|
||||
serverbound.status.PingPacket)
|
||||
self.assertEqual(packets.state_status_serverbound,
|
||||
serverbound.status.get_packets)
|
||||
|
||||
self.assertEqual(packets.DisconnectPacket,
|
||||
clientbound.login.DisconnectPacket)
|
||||
self.assertEqual(packets.EncryptionRequestPacket,
|
||||
clientbound.login.EncryptionRequestPacket)
|
||||
self.assertEqual(packets.LoginSuccessPacket,
|
||||
clientbound.login.LoginSuccessPacket)
|
||||
self.assertEqual(packets.SetCompressionPacket,
|
||||
clientbound.login.SetCompressionPacket)
|
||||
self.assertEqual(packets.state_login_clientbound,
|
||||
clientbound.login.get_packets)
|
||||
self.assertEqual(packets.LoginStartPacket,
|
||||
serverbound.login.LoginStartPacket)
|
||||
self.assertEqual(packets.EncryptionResponsePacket,
|
||||
serverbound.login.EncryptionResponsePacket)
|
||||
self.assertEqual(packets.state_login_serverbound,
|
||||
serverbound.login.get_packets)
|
||||
|
||||
self.assertEqual(packets.KeepAlivePacketClientbound,
|
||||
clientbound.play.KeepAlivePacket)
|
||||
self.assertEqual(packets.KeepAlivePacketServerbound,
|
||||
serverbound.play.KeepAlivePacket)
|
||||
self.assertEqual(packets.JoinGamePacket,
|
||||
clientbound.play.JoinGamePacket)
|
||||
self.assertEqual(packets.ChatMessagePacket,
|
||||
clientbound.play.ChatMessagePacket)
|
||||
self.assertEqual(packets.PlayerPositionAndLookPacket,
|
||||
clientbound.play.PlayerPositionAndLookPacket)
|
||||
self.assertEqual(packets.DisconnectPacketPlayState,
|
||||
clientbound.play.DisconnectPacket)
|
||||
self.assertEqual(packets.SetCompressionPacketPlayState,
|
||||
clientbound.play.SetCompressionPacket)
|
||||
self.assertEqual(packets.PlayerListItemPacket,
|
||||
clientbound.play.PlayerListItemPacket)
|
||||
self.assertEqual(packets.MapPacket,
|
||||
clientbound.play.MapPacket)
|
||||
self.assertEqual(packets.state_playing_clientbound,
|
||||
clientbound.play.get_packets)
|
||||
self.assertEqual(packets.ChatPacket,
|
||||
serverbound.play.ChatPacket)
|
||||
self.assertEqual(packets.PositionAndLookPacket,
|
||||
serverbound.play.PositionAndLookPacket)
|
||||
self.assertEqual(packets.TeleportConfirmPacket,
|
||||
serverbound.play.TeleportConfirmPacket)
|
||||
self.assertEqual(packets.AnimationPacketServerbound,
|
||||
serverbound.play.AnimationPacket)
|
||||
self.assertEqual(packets.state_playing_serverbound,
|
||||
serverbound.play.get_packets)
|
||||
|
||||
|
||||
class LegacyTypesTest(unittest.TestCase):
|
||||
def test_legacy_types(self):
|
||||
self.assertIsInstance(types.FixedPointInteger, types.FixedPoint)
|
||||
self.assertEqual(types.FixedPointInteger.denominator, 32)
|
||||
|
||||
for attr in ('descriptor', 'overridable_descriptor',
|
||||
'overridable_property', 'attribute_alias',
|
||||
'multi_attribute_alias', 'attribute_transform',
|
||||
'class_and_instancemethod'):
|
||||
self.assertEqual(getattr(types, attr), getattr(utility, attr))
|
||||
|
||||
|
||||
class ClassMemberAliasesTest(unittest.TestCase):
|
||||
def test_alias_values(self):
|
||||
self.assertEqual(serverbound.play.AnimationPacket.HAND_MAIN,
|
||||
types.RelativeHand.MAIN)
|
||||
self.assertEqual(serverbound.play.AnimationPacket.HAND_OFF,
|
||||
types.RelativeHand.OFF)
|
||||
|
||||
self.assertEqual(serverbound.play.ClientSettingsPacket.Hand.LEFT,
|
||||
types.AbsoluteHand.LEFT)
|
||||
self.assertEqual(serverbound.play.ClientSettingsPacket.Hand.RIGHT,
|
||||
types.AbsoluteHand.RIGHT)
|
||||
|
||||
def test_block_change_packet(self):
|
||||
context = ConnectionContext()
|
||||
context.protocol_version = SUPPORTED_PROTOCOL_VERSIONS[-1]
|
||||
bi, bm = 358, 9
|
||||
packet = clientbound.play.BlockChangePacket(blockId=bi, blockMeta=bm)
|
||||
self.assertEqual((packet.blockId, packet.blockMeta), (bi, bm))
|
||||
self.assertEqual(packet.blockStateId, packet.block_state_id)
|
||||
|
||||
def test_join_game_packet(self):
|
||||
GameMode = types.GameMode
|
||||
context = ConnectionContext()
|
||||
for pure_game_mode in (GameMode.SURVIVAL, GameMode.CREATIVE,
|
||||
GameMode.ADVENTURE, GameMode.SPECTATOR):
|
||||
for is_hardcore in (False, True):
|
||||
context.protocol_version = 70
|
||||
game_mode = \
|
||||
pure_game_mode | GameMode.HARDCORE \
|
||||
if is_hardcore else pure_game_mode
|
||||
|
||||
packet = clientbound.play.JoinGamePacket()
|
||||
packet.game_mode = game_mode
|
||||
packet.context = context
|
||||
self.assertEqual(packet.pure_game_mode, pure_game_mode)
|
||||
self.assertEqual(packet.is_hardcore, is_hardcore)
|
||||
|
||||
del packet.context
|
||||
del packet.is_hardcore
|
||||
packet.context = context
|
||||
self.assertEqual(packet.game_mode, packet.pure_game_mode)
|
||||
|
||||
del packet.context
|
||||
del packet.game_mode
|
||||
packet.context = context
|
||||
self.assertFalse(hasattr(packet, 'is_hardcore'))
|
||||
|
||||
packet = clientbound.play.JoinGamePacket()
|
||||
packet.pure_game_mode = game_mode
|
||||
packet.is_hardcore = is_hardcore
|
||||
packet.context = context
|
||||
self.assertEqual(packet.game_mode, game_mode)
|
||||
|
||||
context.protocol_version = 738
|
||||
game_mode = pure_game_mode | GameMode.HARDCORE
|
||||
|
||||
packet = clientbound.play.JoinGamePacket()
|
||||
packet.game_mode = game_mode
|
||||
packet.is_hardcore = is_hardcore
|
||||
packet.context = context
|
||||
self.assertEqual(packet.game_mode, game_mode)
|
||||
self.assertEqual(packet.pure_game_mode, game_mode)
|
||||
self.assertEqual(packet.is_hardcore, is_hardcore)
|
||||
|
||||
del packet.context
|
||||
packet.is_hardcore = is_hardcore
|
||||
packet.context = context
|
||||
self.assertEqual(packet.game_mode, game_mode)
|
||||
self.assertEqual(packet.pure_game_mode, game_mode)
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
del packet.pure_game_mode
|
||||
|
||||
def test_entity_position_delta_packet(self):
|
||||
packet = clientbound.play.EntityPositionDeltaPacket()
|
||||
packet.delta_x = -32768
|
||||
packet.delta_y = 33
|
||||
packet.delta_z = 32767
|
||||
self.assertEqual(packet.delta_x_float, -8.0)
|
||||
self.assertEqual(packet.delta_y_float, 0.008056640625)
|
||||
self.assertEqual(packet.delta_z_float, 7.999755859375)
|
||||
self.assertEqual(packet.delta_x, -32768)
|
||||
self.assertEqual(packet.delta_y, 33)
|
||||
self.assertEqual(packet.delta_z, 32767)
|
|
@ -1,321 +1,480 @@
|
|||
from __future__ import print_function
|
||||
from minecraft import (
|
||||
SUPPORTED_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS,
|
||||
PROTOCOL_VERSION_INDICES,
|
||||
)
|
||||
from minecraft.networking.packets import clientbound, serverbound
|
||||
from minecraft.networking.connection import Connection
|
||||
from minecraft.exceptions import (
|
||||
VersionMismatch, LoginDisconnect, InvalidState, IgnorePacket
|
||||
)
|
||||
|
||||
from minecraft import SUPPORTED_MINECRAFT_VERSIONS
|
||||
from minecraft.networking import connection
|
||||
from minecraft.networking import types
|
||||
from minecraft.networking import packets
|
||||
from . import fake_server
|
||||
|
||||
from future.utils import raise_
|
||||
|
||||
import unittest
|
||||
import threading
|
||||
import logging
|
||||
import socket
|
||||
import json
|
||||
import sys
|
||||
|
||||
VERSIONS = sorted(SUPPORTED_MINECRAFT_VERSIONS.items(), key=lambda i: i[1])
|
||||
THREAD_TIMEOUT_S = 5
|
||||
import re
|
||||
import io
|
||||
|
||||
|
||||
class _ConnectTest(unittest.TestCase):
|
||||
def _test_connect(self, client_version=None, server_version=None):
|
||||
server = FakeServer(minecraft_version=server_version)
|
||||
addr, port = server.listen_socket.getsockname()
|
||||
|
||||
cond = threading.Condition()
|
||||
|
||||
def handle_client_exception(exc, exc_info):
|
||||
with cond:
|
||||
cond.exc_info = exc_info
|
||||
cond.notify_all()
|
||||
|
||||
def client_write(packet, *args, **kwds):
|
||||
def packet_write(*args, **kwds):
|
||||
logging.debug('[C-> ] %s' % packet)
|
||||
return real_packet_write(*args, **kwds)
|
||||
real_packet_write = packet.write
|
||||
packet.write = packet_write
|
||||
return real_client_write(packet, *args, **kwds)
|
||||
|
||||
def client_react(packet, *args, **kwds):
|
||||
logging.debug('[ ->C] %s' % packet)
|
||||
return real_client_react(packet, *args, **kwds)
|
||||
|
||||
client = connection.Connection(
|
||||
addr, port, username='User', initial_version=client_version,
|
||||
handle_exception=handle_client_exception)
|
||||
real_client_react = client._react
|
||||
real_client_write = client.write_packet
|
||||
client.write_packet = client_write
|
||||
client._react = client_react
|
||||
|
||||
try:
|
||||
with cond:
|
||||
server_thread = threading.Thread(
|
||||
name='_ConnectTest server',
|
||||
target=self._test_connect_server,
|
||||
args=(server, cond))
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
self._test_connect_client(client, cond)
|
||||
|
||||
cond.exc_info = Ellipsis
|
||||
cond.wait(THREAD_TIMEOUT_S)
|
||||
if cond.exc_info is Ellipsis:
|
||||
self.fail('Timed out.')
|
||||
elif cond.exc_info is not None:
|
||||
raise_(*cond.exc_info)
|
||||
finally:
|
||||
# Wait for all threads to exit.
|
||||
for thread in server_thread, client.networking_thread:
|
||||
if thread is not None and thread.is_alive():
|
||||
thread.join(THREAD_TIMEOUT_S)
|
||||
if thread is not None and thread.is_alive():
|
||||
if cond.exc_info is None:
|
||||
self.fail('Thread "%s" timed out.' % thread.name)
|
||||
else:
|
||||
# Keep the earlier exception, if there is one.
|
||||
break
|
||||
|
||||
def _test_connect_client(self, client, cond):
|
||||
client.connect()
|
||||
|
||||
def _test_connect_server(self, server, cond):
|
||||
try:
|
||||
server.run()
|
||||
exc_info = None
|
||||
except:
|
||||
exc_info = sys.exc_info()
|
||||
with cond:
|
||||
cond.exc_info = exc_info
|
||||
cond.notify_all()
|
||||
|
||||
|
||||
class ConnectOldToOldTest(_ConnectTest):
|
||||
def runTest(self):
|
||||
self._test_connect(VERSIONS[0][1], VERSIONS[0][0])
|
||||
|
||||
|
||||
class ConnectOldToNewTest(_ConnectTest):
|
||||
def runTest(self):
|
||||
self._test_connect(VERSIONS[0][1], VERSIONS[-1][0])
|
||||
|
||||
|
||||
class ConnectNewToOldTest(_ConnectTest):
|
||||
def runTest(self):
|
||||
self._test_connect(VERSIONS[-1][1], VERSIONS[0][0])
|
||||
|
||||
|
||||
class ConnectNewToNewTest(_ConnectTest):
|
||||
def runTest(self):
|
||||
self._test_connect(VERSIONS[-1][1], VERSIONS[-1][0])
|
||||
|
||||
|
||||
class PingTest(_ConnectTest):
|
||||
def runTest(self):
|
||||
class ConnectTest(fake_server._FakeServerTest):
|
||||
def test_connect(self):
|
||||
self._test_connect()
|
||||
|
||||
def _test_connect_client(self, client, cond):
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
def handle_play_start(self):
|
||||
super(ConnectTest.client_handler_type, self).handle_play_start()
|
||||
self.write_packet(clientbound.play.KeepAlivePacket(
|
||||
keep_alive_id=1223334444))
|
||||
|
||||
def handle_play_packet(self, packet):
|
||||
super(ConnectTest.client_handler_type, self) \
|
||||
.handle_play_packet(packet)
|
||||
if isinstance(packet, serverbound.play.KeepAlivePacket):
|
||||
assert packet.keep_alive_id == 1223334444
|
||||
raise fake_server.FakeServerDisconnect
|
||||
|
||||
|
||||
class ReconnectTest(ConnectTest):
|
||||
phase = 0
|
||||
|
||||
def _start_client(self, client):
|
||||
def handle_login_disconnect(packet):
|
||||
if 'Please reconnect' in packet.json_data:
|
||||
# Override the default behaviour of raising a fatal exception.
|
||||
client.disconnect()
|
||||
client.connect()
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_login_disconnect, clientbound.login.DisconnectPacket,
|
||||
early=True)
|
||||
|
||||
def handle_play_disconnect(packet):
|
||||
if 'Please reconnect' in packet.json_data:
|
||||
client.connect()
|
||||
elif 'Test successful' in packet.json_data:
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
client.register_packet_listener(
|
||||
handle_play_disconnect, clientbound.play.DisconnectPacket)
|
||||
|
||||
client.connect()
|
||||
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
def handle_login(self, packet):
|
||||
if self.server.test_case.phase == 0:
|
||||
self.server.test_case.phase = 1
|
||||
raise fake_server.FakeServerDisconnect('Please reconnect (0).')
|
||||
super(ReconnectTest.client_handler_type, self).handle_login(packet)
|
||||
|
||||
def handle_play_start(self):
|
||||
if self.server.test_case.phase == 1:
|
||||
self.server.test_case.phase = 2
|
||||
raise fake_server.FakeServerDisconnect('Please reconnect (1).')
|
||||
else:
|
||||
assert self.server.test_case.phase == 2
|
||||
raise fake_server.FakeServerDisconnect('Test successful (2).')
|
||||
|
||||
|
||||
class PingTest(ConnectTest):
|
||||
def _start_client(self, client):
|
||||
def handle_ping(latency_ms):
|
||||
assert 0 <= latency_ms < 60000
|
||||
with cond:
|
||||
cond.exc_info = None
|
||||
cond.notify_all()
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
client.status(handle_status=False, handle_ping=handle_ping)
|
||||
|
||||
def _test_connect_server(self, server, cond):
|
||||
try:
|
||||
server.continue_after_status = False
|
||||
server.run()
|
||||
except:
|
||||
with cond:
|
||||
cond.exc_info = sys.exc_info()
|
||||
cond.notify_all()
|
||||
|
||||
class StatusTest(ConnectTest):
|
||||
def _start_client(self, client):
|
||||
def handle_status(status_dict):
|
||||
assert status_dict['description'] == {'text': 'FakeServer'}
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
client.status(handle_status=handle_status, handle_ping=False)
|
||||
|
||||
|
||||
class FakeServer(threading.Thread):
|
||||
__slots__ = 'context', 'minecraft_version', 'listen_socket', \
|
||||
'packets_login', 'packets_playing', 'packets_status', \
|
||||
'packets',
|
||||
class DefaultStatusTest(ConnectTest):
|
||||
def setUp(self):
|
||||
class FakeStdOut(io.BytesIO):
|
||||
def write(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf8')
|
||||
super(FakeStdOut, self).write(data)
|
||||
sys.stdout, self.old_stdout = FakeStdOut(), sys.stdout
|
||||
|
||||
def __init__(self, minecraft_version=None, continue_after_status=True):
|
||||
if minecraft_version is None:
|
||||
minecraft_version = VERSIONS[-1][0]
|
||||
self.minecraft_version = minecraft_version
|
||||
self.continue_after_status = continue_after_status
|
||||
protocol_version = SUPPORTED_MINECRAFT_VERSIONS[minecraft_version]
|
||||
self.context = connection.ConnectionContext(
|
||||
protocol_version=protocol_version)
|
||||
def tearDown(self):
|
||||
sys.stdout, self.old_stdout = self.old_stdout, None
|
||||
|
||||
self.packets_handshake = {
|
||||
p.get_id(self.context): p for p in
|
||||
packets.state_handshake_serverbound(self.context)}
|
||||
def _start_client(self, client):
|
||||
def handle_exit():
|
||||
output = sys.stdout.getvalue()
|
||||
assert re.match(b'{.*}\\nPing: \\d+ ms\\n$', output), \
|
||||
'Invalid stdout contents: %r.' % output
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
client.handle_exit = handle_exit
|
||||
|
||||
self.packets_login = {
|
||||
p.get_id(self.context): p for p in
|
||||
packets.state_login_serverbound(self.context)}
|
||||
client.status(handle_status=None, handle_ping=None)
|
||||
|
||||
self.packets_playing = {
|
||||
p.get_id(self.context): p for p in
|
||||
packets.state_playing_serverbound(self.context)}
|
||||
|
||||
self.packets_status = {
|
||||
p.get_id(self.context): p for p in
|
||||
packets.state_status_serverbound(self.context)}
|
||||
class ConnectCompressionLowTest(ConnectTest):
|
||||
compression_threshold = 0
|
||||
|
||||
self.listen_socket = socket.socket()
|
||||
self.listen_socket.bind(('0.0.0.0', 0))
|
||||
self.listen_socket.listen(0)
|
||||
|
||||
super(FakeServer, self).__init__()
|
||||
class ConnectCompressionHighTest(ConnectTest):
|
||||
compression_threshold = 256
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.run_accept()
|
||||
finally:
|
||||
self.listen_socket.close()
|
||||
|
||||
def run_accept(self):
|
||||
running = True
|
||||
while running:
|
||||
client_socket, addr = self.listen_socket.accept()
|
||||
logging.debug('[ ++ ] Client %s connected to server.' % (addr,))
|
||||
client_file = client_socket.makefile('rb', 0)
|
||||
try:
|
||||
running = self.run_handshake(client_socket, client_file)
|
||||
except:
|
||||
raise
|
||||
class AllowedVersionsTest(fake_server._FakeServerTest):
|
||||
versions = list(SUPPORTED_MINECRAFT_VERSIONS.items())
|
||||
test_indices = (0, len(versions) // 2, len(versions) - 1)
|
||||
|
||||
client_handler_type = ConnectTest.client_handler_type
|
||||
|
||||
def test_with_version_names(self):
|
||||
for index in self.test_indices:
|
||||
self._test_connect(
|
||||
server_version=self.versions[index][0],
|
||||
client_versions={v[0] for v in self.versions[:index+1]})
|
||||
|
||||
def test_with_protocol_numbers(self):
|
||||
for index in self.test_indices:
|
||||
self._test_connect(
|
||||
server_version=self.versions[index][0],
|
||||
client_versions={v[1] for v in self.versions[:index+1]})
|
||||
|
||||
|
||||
class LoginDisconnectTest(fake_server._FakeServerTest):
|
||||
def test_login_disconnect(self):
|
||||
with self.assertRaisesRegexp(LoginDisconnect, r'You are banned'):
|
||||
self._test_connect()
|
||||
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
def handle_login(self, login_start_packet):
|
||||
raise fake_server.FakeServerDisconnect('You are banned.')
|
||||
|
||||
|
||||
class ConnectTwiceTest(fake_server._FakeServerTest):
|
||||
def test_connect(self):
|
||||
with self.assertRaisesRegexp(InvalidState, 'existing connection'):
|
||||
self._test_connect()
|
||||
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
def handle_play_start(self):
|
||||
super(ConnectTwiceTest.client_handler_type, self) \
|
||||
.handle_play_start()
|
||||
raise fake_server.FakeServerDisconnect('Test complete.')
|
||||
|
||||
def _start_client(self, client):
|
||||
client.connect()
|
||||
client.connect()
|
||||
|
||||
|
||||
class ConnectStatusTest(ConnectTwiceTest):
|
||||
def _start_client(self, client):
|
||||
client.connect()
|
||||
client.status()
|
||||
|
||||
|
||||
class LoginPluginTest(fake_server._FakeServerTest):
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
def handle_login(self, login_start_packet):
|
||||
request = clientbound.login.PluginRequestPacket(
|
||||
message_id=1, channel='pyCraft:tests/fail', data=b'ignored')
|
||||
self.write_packet(request)
|
||||
response = self.read_packet()
|
||||
assert isinstance(response, serverbound.login.PluginResponsePacket)
|
||||
assert response.message_id == request.message_id
|
||||
assert response.successful is False
|
||||
assert response.data is None
|
||||
|
||||
request = clientbound.login.PluginRequestPacket(
|
||||
message_id=2, channel='pyCraft:tests/echo', data=b'hello')
|
||||
self.write_packet(request)
|
||||
response = self.read_packet()
|
||||
assert isinstance(response, serverbound.login.PluginResponsePacket)
|
||||
assert response.message_id == request.message_id
|
||||
assert response.successful is True
|
||||
assert response.data == request.data
|
||||
|
||||
super(LoginPluginTest.client_handler_type, self) \
|
||||
.handle_login(login_start_packet)
|
||||
|
||||
def handle_play_start(self):
|
||||
super(LoginPluginTest.client_handler_type, self) \
|
||||
.handle_play_start()
|
||||
raise fake_server.FakeServerDisconnect
|
||||
|
||||
def _start_client(self, client):
|
||||
def handle_plugin_request(packet):
|
||||
if packet.channel == 'pyCraft:tests/echo':
|
||||
client.write_packet(serverbound.login.PluginResponsePacket(
|
||||
message_id=packet.message_id, data=packet.data))
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_plugin_request, clientbound.login.PluginRequestPacket,
|
||||
early=True)
|
||||
|
||||
super(LoginPluginTest, self)._start_client(client)
|
||||
|
||||
def test_login_plugin_messages(self):
|
||||
self._test_connect()
|
||||
|
||||
|
||||
class EarlyPacketListenerTest(ConnectTest):
|
||||
""" Early packet listeners should be called before ordinary ones, even when
|
||||
the early packet listener is registered afterwards.
|
||||
"""
|
||||
def _start_client(self, client):
|
||||
@client.listener(clientbound.play.JoinGamePacket)
|
||||
def handle_join(packet):
|
||||
assert early_handle_join.called, \
|
||||
'Ordinary listener called before early listener.'
|
||||
handle_join.called = True
|
||||
handle_join.called = False
|
||||
|
||||
@client.listener(clientbound.play.JoinGamePacket, early=True)
|
||||
def early_handle_join(packet):
|
||||
early_handle_join.called = True
|
||||
early_handle_join.called = False
|
||||
|
||||
@client.listener(clientbound.play.DisconnectPacket)
|
||||
def handle_disconnect(packet):
|
||||
assert early_handle_join.called, 'Early listener not called.'
|
||||
assert handle_join.called, 'Ordinary listener not called.'
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
|
||||
client.connect()
|
||||
|
||||
|
||||
class IgnorePacketTest(ConnectTest):
|
||||
""" Raising 'minecraft.networking.connection.IgnorePacket' from within a
|
||||
packet listener should prevent any subsequent packet listeners from
|
||||
being called, and, if the listener is early, should prevent the default
|
||||
behaviour from being triggered.
|
||||
"""
|
||||
|
||||
def _start_client(self, client):
|
||||
keep_alive_ids_incoming = []
|
||||
keep_alive_ids_outgoing = []
|
||||
|
||||
def handle_keep_alive_1(packet):
|
||||
keep_alive_ids_incoming.append(packet.keep_alive_id)
|
||||
if packet.keep_alive_id == 1:
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_keep_alive_1, clientbound.play.KeepAlivePacket, early=True)
|
||||
|
||||
def handle_keep_alive_2(packet):
|
||||
keep_alive_ids_incoming.append(packet.keep_alive_id)
|
||||
assert packet.keep_alive_id > 1
|
||||
if packet.keep_alive_id == 2:
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_keep_alive_2, clientbound.play.KeepAlivePacket)
|
||||
|
||||
def handle_keep_alive_3(packet):
|
||||
keep_alive_ids_incoming.append(packet.keep_alive_id)
|
||||
assert packet.keep_alive_id == 3
|
||||
client.register_packet_listener(
|
||||
handle_keep_alive_3, clientbound.play.KeepAlivePacket)
|
||||
|
||||
def handle_outgoing_keep_alive_2(packet):
|
||||
keep_alive_ids_outgoing.append(packet.keep_alive_id)
|
||||
assert 2 <= packet.keep_alive_id <= 3
|
||||
if packet.keep_alive_id == 2:
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_outgoing_keep_alive_2, serverbound.play.KeepAlivePacket,
|
||||
outgoing=True, early=True)
|
||||
|
||||
def handle_outgoing_keep_alive_3(packet):
|
||||
keep_alive_ids_outgoing.append(packet.keep_alive_id)
|
||||
assert packet.keep_alive_id == 3
|
||||
raise IgnorePacket
|
||||
client.register_packet_listener(
|
||||
handle_outgoing_keep_alive_3, serverbound.play.KeepAlivePacket,
|
||||
outgoing=True)
|
||||
|
||||
def handle_outgoing_keep_alive_none(packet):
|
||||
keep_alive_ids_outgoing.append(packet.keep_alive_id)
|
||||
assert False
|
||||
client.register_packet_listener(
|
||||
handle_outgoing_keep_alive_none, serverbound.play.KeepAlivePacket,
|
||||
outgoing=True)
|
||||
|
||||
def handle_disconnect(packet):
|
||||
assert keep_alive_ids_incoming == [1, 2, 2, 3, 3, 3], \
|
||||
'Incoming keep-alive IDs %r != %r' % \
|
||||
(keep_alive_ids_incoming, [1, 2, 2, 3, 3, 3])
|
||||
assert keep_alive_ids_outgoing == [2, 3, 3], \
|
||||
'Outgoing keep-alive IDs %r != %r' % \
|
||||
(keep_alive_ids_incoming, [2, 3, 3])
|
||||
client.register_packet_listener(
|
||||
handle_disconnect, clientbound.play.DisconnectPacket)
|
||||
|
||||
client.connect()
|
||||
|
||||
class client_handler_type(fake_server.FakeClientHandler):
|
||||
__slots__ = '_keep_alive_ids_returned'
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
super(IgnorePacketTest.client_handler_type, self).__init__(
|
||||
*args, **kwds)
|
||||
self._keep_alive_ids_returned = []
|
||||
|
||||
def handle_play_start(self):
|
||||
super(IgnorePacketTest.client_handler_type, self)\
|
||||
.handle_play_start()
|
||||
self.write_packet(clientbound.play.KeepAlivePacket(
|
||||
keep_alive_id=1))
|
||||
self.write_packet(clientbound.play.KeepAlivePacket(
|
||||
keep_alive_id=2))
|
||||
self.write_packet(clientbound.play.KeepAlivePacket(
|
||||
keep_alive_id=3))
|
||||
self.handle_play_server_disconnect('Test complete.')
|
||||
|
||||
def handle_play_packet(self, packet):
|
||||
super(IgnorePacketTest.client_handler_type, self) \
|
||||
.handle_play_packet(packet)
|
||||
if isinstance(packet, serverbound.play.KeepAlivePacket):
|
||||
self._keep_alive_ids_returned.append(packet.keep_alive_id)
|
||||
|
||||
def handle_play_client_disconnect(self):
|
||||
assert self._keep_alive_ids_returned == [3], \
|
||||
'Returned keep-alive IDs %r != %r' % \
|
||||
(self._keep_alive_ids_returned, [3])
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
|
||||
|
||||
class HandleExceptionTest(ConnectTest):
|
||||
ignore_extra_exceptions = True
|
||||
|
||||
def _start_client(self, client):
|
||||
message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
|
||||
|
||||
@client.listener(clientbound.login.LoginSuccessPacket)
|
||||
def handle_login_success(_packet):
|
||||
raise Exception(message)
|
||||
|
||||
@client.exception_handler(early=True)
|
||||
def handle_exception(exc, _exc_info):
|
||||
assert isinstance(exc, Exception) and exc.args == (message,)
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
|
||||
client.connect()
|
||||
|
||||
|
||||
class ExceptionReconnectTest(ConnectTest):
|
||||
class CustomException(Exception):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
self.phase = 0
|
||||
|
||||
def _start_client(self, client):
|
||||
@client.listener(clientbound.play.JoinGamePacket)
|
||||
def handle_join_game(packet):
|
||||
if self.phase == 0:
|
||||
self.phase += 1
|
||||
raise self.CustomException
|
||||
else:
|
||||
client_socket.shutdown(socket.SHUT_RDWR)
|
||||
logging.debug('[ -- ] Client %s disconnected.' % (addr,))
|
||||
finally:
|
||||
client_socket.close()
|
||||
client_file.close()
|
||||
raise fake_server.FakeServerTestSuccess
|
||||
|
||||
def run_handshake(self, client_socket, client_file):
|
||||
self.packets = self.packets_handshake
|
||||
packet = self.read_packet_filtered(client_file)
|
||||
assert isinstance(packet, packets.HandShakePacket)
|
||||
if packet.next_state == 1:
|
||||
return self.run_handshake_status(
|
||||
packet, client_socket, client_file)
|
||||
elif packet.next_state == 2:
|
||||
return self.run_handshake_play(
|
||||
packet, client_socket, client_file)
|
||||
else:
|
||||
raise AssertionError('Unknown state: %s' % packet.next_state)
|
||||
@client.exception_handler(self.CustomException, early=True)
|
||||
def handle_custom_exception(exc, exc_info):
|
||||
client.disconnect(immediate=True)
|
||||
client.connect()
|
||||
|
||||
def run_handshake_status(self, packet, client_socket, client_file):
|
||||
self.run_status(client_socket, client_file)
|
||||
return self.continue_after_status
|
||||
client.connect()
|
||||
|
||||
def run_handshake_play(self, packet, client_socket, client_file):
|
||||
if packet.protocol_version == self.context.protocol_version:
|
||||
self.run_login(client_socket, client_file)
|
||||
else:
|
||||
if packet.protocol_version < self.context.protocol_version:
|
||||
msg = 'Outdated client! Please use %s' \
|
||||
% self.minecraft_version
|
||||
else:
|
||||
msg = "Outdated server! I'm still on %s" \
|
||||
% self.minecraft_version
|
||||
packet = packets.DisconnectPacket(
|
||||
self.context, json_data=json.dumps({'text': msg}))
|
||||
self.write_packet(packet, client_socket)
|
||||
class client_handler_type(ConnectTest.client_handler_type):
|
||||
def handle_abnormal_disconnect(self, exc):
|
||||
return True
|
||||
|
||||
def run_login(self, client_socket, client_file):
|
||||
self.packets = self.packets_login
|
||||
packet = self.read_packet_filtered(client_file)
|
||||
assert isinstance(packet, packets.LoginStartPacket)
|
||||
|
||||
packet = packets.LoginSuccessPacket(
|
||||
self.context, UUID='{fake uuid}', Username=packet.name)
|
||||
self.write_packet(packet, client_socket)
|
||||
self.run_playing(client_socket, client_file)
|
||||
class VersionNegotiationEdgeCases(fake_server._FakeServerTest):
|
||||
earliest_version = SUPPORTED_PROTOCOL_VERSIONS[0]
|
||||
latest_version = SUPPORTED_PROTOCOL_VERSIONS[-1]
|
||||
|
||||
def run_playing(self, client_socket, client_file):
|
||||
self.packets = self.packets_playing
|
||||
fake_version = max(PROTOCOL_VERSION_INDICES.keys()) + 1
|
||||
fake_version_index = max(PROTOCOL_VERSION_INDICES.values()) + 1
|
||||
|
||||
packet = packets.JoinGamePacket(
|
||||
self.context, entity_id=0, game_mode=0, dimension=0, difficulty=2,
|
||||
max_players=1, level_type='default', reduced_debug_info=False)
|
||||
self.write_packet(packet, client_socket)
|
||||
def setUp(self):
|
||||
PROTOCOL_VERSION_INDICES[self.fake_version] = self.fake_version_index
|
||||
super(VersionNegotiationEdgeCases, self).setUp()
|
||||
|
||||
keep_alive_id = 1076048782
|
||||
packet = packets.KeepAlivePacketClientbound(
|
||||
self.context, keep_alive_id=keep_alive_id)
|
||||
self.write_packet(packet, client_socket)
|
||||
def tearDown(self):
|
||||
super(VersionNegotiationEdgeCases, self).tearDown()
|
||||
del PROTOCOL_VERSION_INDICES[self.fake_version]
|
||||
|
||||
packet = self.read_packet_filtered(client_file)
|
||||
assert isinstance(packet, packets.KeepAlivePacketServerbound)
|
||||
assert packet.keep_alive_id == keep_alive_id
|
||||
def test_client_protocol_unsupported(self):
|
||||
self._test_client_protocol(version=self.fake_version)
|
||||
|
||||
packet = packets.DisconnectPacketPlayState(
|
||||
self.context, json_data=json.dumps({'text': 'Test complete.'}))
|
||||
self.write_packet(packet, client_socket)
|
||||
return False
|
||||
def test_client_protocol_unknown(self):
|
||||
self._test_client_protocol(version='surprise me!')
|
||||
|
||||
def run_status(self, client_socket, client_file):
|
||||
self.packets = self.packets_status
|
||||
def test_client_protocol_invalid(self):
|
||||
self._test_client_protocol(version=object())
|
||||
|
||||
packet = self.read_packet(client_file)
|
||||
assert isinstance(packet, packets.RequestPacket)
|
||||
def _test_client_protocol(self, version):
|
||||
with self.assertRaisesRegexp(ValueError, 'Unsupported version'):
|
||||
self._test_connect(client_versions={version})
|
||||
|
||||
packet = packets.ResponsePacket(self.context)
|
||||
packet.json_response = json.dumps({
|
||||
'version': {
|
||||
'name': self.minecraft_version,
|
||||
'protocol': self.context.protocol_version},
|
||||
'players': {
|
||||
'max': 1,
|
||||
'online': 0,
|
||||
'sample': []},
|
||||
'description': {
|
||||
'text': 'FakeServer'}})
|
||||
self.write_packet(packet, client_socket)
|
||||
def test_server_protocol_unsupported(self, client_versions=None):
|
||||
with self.assertRaisesRegexp(VersionMismatch, 'not supported'):
|
||||
self._test_connect(client_versions=client_versions,
|
||||
server_version=self.fake_version)
|
||||
|
||||
try:
|
||||
packet = self.read_packet(client_file)
|
||||
except EOFError:
|
||||
return False
|
||||
assert isinstance(packet, packets.PingPacket)
|
||||
def test_server_protocol_unsupported_direct(self):
|
||||
self.test_server_protocol_unsupported({self.latest_version})
|
||||
|
||||
res_packet = packets.PingPacketResponse(self.context)
|
||||
res_packet.time = packet.time
|
||||
self.write_packet(res_packet, client_socket)
|
||||
return False
|
||||
def test_server_protocol_disallowed(self, client_versions=None):
|
||||
if client_versions is None:
|
||||
client_versions = set(SUPPORTED_PROTOCOL_VERSIONS) \
|
||||
- {self.latest_version}
|
||||
with self.assertRaisesRegexp(VersionMismatch, 'not allowed'):
|
||||
self._test_connect(client_versions={self.earliest_version},
|
||||
server_version=self.latest_version)
|
||||
|
||||
def read_packet_filtered(self, client_file):
|
||||
while True:
|
||||
packet = self.read_packet(client_file)
|
||||
if isinstance(packet, packets.PositionAndLookPacket):
|
||||
continue
|
||||
if isinstance(packet, packets.AnimationPacketServerbound):
|
||||
continue
|
||||
return packet
|
||||
def test_server_protocol_disallowed_direct(self):
|
||||
self.test_server_protocol_disallowed({self.earliest_version})
|
||||
|
||||
def read_packet(self, client_file):
|
||||
buffer = self.read_packet_buffer(client_file)
|
||||
packet_id = types.VarInt.read(buffer)
|
||||
if packet_id in self.packets:
|
||||
packet = self.packets[packet_id](self.context)
|
||||
packet.read(buffer)
|
||||
else:
|
||||
packet = packets.Packet(self.context, id=packet_id)
|
||||
logging.debug('[ ->S] %s' % packet)
|
||||
return packet
|
||||
def test_default_protocol_version(self, status_response=None):
|
||||
if status_response is None:
|
||||
status_response = '{"description": {"text": "FakeServer"}}'
|
||||
|
||||
def read_packet_buffer(self, client_file):
|
||||
length = types.VarInt.read(client_file)
|
||||
buffer = packets.PacketBuffer()
|
||||
while len(buffer.get_writable()) < length:
|
||||
buffer.send(client_file.read(length - len(buffer.get_writable())))
|
||||
buffer.reset_cursor()
|
||||
return buffer
|
||||
class ClientHandler(fake_server.FakeClientHandler):
|
||||
def handle_status(self, request_packet):
|
||||
packet = clientbound.status.ResponsePacket()
|
||||
packet.json_response = status_response
|
||||
self.write_packet(packet)
|
||||
|
||||
def write_packet(self, packet, client_socket):
|
||||
packet.write(client_socket)
|
||||
logging.debug('[S-> ] %s' % packet)
|
||||
def handle_play_start(self):
|
||||
super(ClientHandler, self).handle_play_start()
|
||||
raise fake_server.FakeServerDisconnect('Test complete.')
|
||||
|
||||
def make_connection(*args, **kwds):
|
||||
kwds['initial_version'] = self.earliest_version
|
||||
return Connection(*args, **kwds)
|
||||
|
||||
self._test_connect(server_version=self.earliest_version,
|
||||
client_handler_type=ClientHandler,
|
||||
connection_type=make_connection)
|
||||
|
||||
def test_default_protocol_version_empty(self):
|
||||
with self.assertRaisesRegexp(IOError, 'Invalid server status'):
|
||||
self.test_default_protocol_version(status_response='{}')
|
||||
|
||||
def test_default_protocol_version_eof(self):
|
||||
class ClientHandler(fake_server.FakeClientHandler):
|
||||
def handle_status(self, request_packet):
|
||||
raise fake_server.FakeServerDisconnect(
|
||||
'Refusing to handle status request, for test purposes.')
|
||||
|
||||
def handle_play_start(self):
|
||||
super(ClientHandler, self).handle_play_start()
|
||||
raise fake_server.FakeServerDisconnect('Test complete.')
|
||||
|
||||
def make_connection(*args, **kwds):
|
||||
kwds['initial_version'] = self.earliest_version
|
||||
return Connection(*args, **kwds)
|
||||
|
||||
self._test_connect(server_version=self.earliest_version,
|
||||
client_handler_type=ClientHandler,
|
||||
connection_type=make_connection)
|
||||
|
|
|
@ -11,6 +11,8 @@ from minecraft.networking.encryption import (
|
|||
EncryptedFileObjectWrapper,
|
||||
EncryptedSocketWrapper
|
||||
)
|
||||
from minecraft.networking.packets import clientbound
|
||||
from tests import test_connection
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
|
@ -20,6 +22,24 @@ KEY_LOCATION = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
|||
"encryption")
|
||||
|
||||
|
||||
def setUpModule():
|
||||
global private_key, public_key, token
|
||||
|
||||
with open(os.path.join(KEY_LOCATION, "priv_key.bin"), "rb") as f:
|
||||
private_key = f.read()
|
||||
private_key = load_der_private_key(private_key, None, default_backend())
|
||||
|
||||
with open(os.path.join(KEY_LOCATION, "pub_key.bin"), "rb") as f:
|
||||
public_key = f.read()
|
||||
|
||||
token = generate_shared_secret()
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
global private_key, public_key, token
|
||||
del private_key, public_key, token
|
||||
|
||||
|
||||
class Hashing(unittest.TestCase):
|
||||
test_data = {'Notch': '4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48',
|
||||
'jeb_': '-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1',
|
||||
|
@ -34,31 +54,19 @@ class Hashing(unittest.TestCase):
|
|||
|
||||
class Encryption(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
with open(os.path.join(KEY_LOCATION, "priv_key.bin"), "rb") as f:
|
||||
self.private_key = f.read()
|
||||
self.private_key = load_der_private_key(self.private_key, None,
|
||||
default_backend())
|
||||
|
||||
with open(os.path.join(KEY_LOCATION, "pub_key.bin"), "rb") as f:
|
||||
self.public_key = f.read()
|
||||
self.token = generate_shared_secret()
|
||||
|
||||
def test_token_secret_encryption(self):
|
||||
secret = generate_shared_secret()
|
||||
token, encrypted_secret = encrypt_token_and_secret(self.public_key,
|
||||
self.token, secret)
|
||||
decrypted_token = self.private_key.decrypt(token,
|
||||
PKCS1v15())
|
||||
decrypted_secret = self.private_key.decrypt(encrypted_secret,
|
||||
PKCS1v15())
|
||||
encrypted_token, encrypted_secret = \
|
||||
encrypt_token_and_secret(public_key, token, secret)
|
||||
decrypted_token = private_key.decrypt(encrypted_token, PKCS1v15())
|
||||
decrypted_secret = private_key.decrypt(encrypted_secret, PKCS1v15())
|
||||
|
||||
self.assertEquals(self.token, decrypted_token)
|
||||
self.assertEquals(token, decrypted_token)
|
||||
self.assertEquals(secret, decrypted_secret)
|
||||
|
||||
def test_generate_hash(self):
|
||||
verification_hash = generate_verification_hash(
|
||||
u"", "secret".encode('utf-8'), self.public_key)
|
||||
u"", "secret".encode('utf-8'), public_key)
|
||||
self.assertEquals("1f142e737a84a974a5f2a22f6174a78d80fd97f5",
|
||||
verification_hash)
|
||||
|
||||
|
@ -104,6 +112,31 @@ class Encryption(unittest.TestCase):
|
|||
self.assertEqual(test_data, mock_socket.received)
|
||||
|
||||
|
||||
class EncryptedConnection(test_connection.ConnectTest):
|
||||
def test_connect(self):
|
||||
self._test_connect(private_key=private_key,
|
||||
public_key_bytes=public_key)
|
||||
|
||||
def _start_client(self, client):
|
||||
def handle_login_success(_packet):
|
||||
assert isinstance(client.socket, EncryptedSocketWrapper)
|
||||
assert isinstance(client.file_object, EncryptedFileObjectWrapper)
|
||||
client.register_packet_listener(
|
||||
handle_login_success, clientbound.login.LoginSuccessPacket)
|
||||
super(EncryptedConnection, self)._start_client(client)
|
||||
|
||||
|
||||
class EncryptedCompressedConnection(EncryptedConnection,
|
||||
test_connection.ConnectCompressionLowTest):
|
||||
pass
|
||||
|
||||
|
||||
# Regression test for <https://github.com/ammaraskar/pyCraft/issues/109>.
|
||||
class EncryptedCompressedReconnect(test_connection.ReconnectTest,
|
||||
EncryptedCompressedConnection):
|
||||
pass
|
||||
|
||||
|
||||
class MockSocket(object):
|
||||
|
||||
def __init__(self, encryptor, decryptor):
|
||||
|
|
|
@ -9,7 +9,7 @@ class RaiseYggdrasilError(unittest.TestCase):
|
|||
raise YggdrasilError
|
||||
|
||||
def test_raise_yggdrasil_error_message(self):
|
||||
with self.assertRaises(YggdrasilError) as e:
|
||||
with self.assertRaises(YggdrasilError) as cm:
|
||||
raise YggdrasilError("Error!")
|
||||
|
||||
self.assertEqual(str(e.exception), "Error!")
|
||||
self.assertEqual(str(cm.exception), "Error!")
|
||||
|
|
|
@ -1,23 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import string
|
||||
import logging
|
||||
import struct
|
||||
from zlib import decompress
|
||||
from random import choice
|
||||
|
||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
|
||||
from minecraft.utility import protocol_earlier
|
||||
from minecraft import (
|
||||
PRE, SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS,
|
||||
)
|
||||
from minecraft.networking.connection import ConnectionContext
|
||||
from minecraft.networking.types import VarInt
|
||||
from minecraft.networking.types import (
|
||||
VarInt, Enum, Vector, PositionAndLook, OriginPoint,
|
||||
)
|
||||
from minecraft.networking.packets import (
|
||||
PacketBuffer, ChatPacket, KeepAlivePacket, PacketListener)
|
||||
Packet, PacketBuffer, PacketListener, KeepAlivePacket, serverbound,
|
||||
clientbound
|
||||
)
|
||||
|
||||
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
|
||||
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
|
||||
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
|
||||
|
||||
|
||||
class PacketSerializatonTest(unittest.TestCase):
|
||||
class PacketBufferTest(unittest.TestCase):
|
||||
def test_basic_read_write(self):
|
||||
message = b"hello"
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
packet_buffer.send(message)
|
||||
|
||||
packet_buffer.reset_cursor()
|
||||
self.assertEqual(packet_buffer.read(), message)
|
||||
packet_buffer.reset_cursor()
|
||||
self.assertEqual(packet_buffer.recv(), message)
|
||||
|
||||
packet_buffer.reset()
|
||||
self.assertNotEqual(packet_buffer.read(), message)
|
||||
|
||||
def test_get_writable(self):
|
||||
message = b"hello"
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
packet_buffer.send(message)
|
||||
|
||||
self.assertEqual(packet_buffer.get_writable(), message)
|
||||
|
||||
|
||||
class PacketSerializationTest(unittest.TestCase):
|
||||
|
||||
def test_packet(self):
|
||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
logging.debug('protocol_version = %r' % protocol_version)
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
packet = ChatPacket(context)
|
||||
packet = serverbound.play.ChatPacket(context)
|
||||
packet.message = u"κόσμε"
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
|
@ -29,24 +67,26 @@ class PacketSerializatonTest(unittest.TestCase):
|
|||
packet_id = VarInt.read(packet_buffer)
|
||||
self.assertEqual(packet_id, packet.id)
|
||||
|
||||
deserialized = ChatPacket(context)
|
||||
deserialized = serverbound.play.ChatPacket(context)
|
||||
deserialized.read(packet_buffer)
|
||||
|
||||
self.assertEqual(packet.message, deserialized.message)
|
||||
|
||||
def test_compressed_packet(self):
|
||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
logging.debug('protocol_version = %r' % protocol_version)
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
msg = ''.join(choice(string.ascii_lowercase) for i in range(500))
|
||||
packet = ChatPacket(context)
|
||||
packet = serverbound.play.ChatPacket(context)
|
||||
packet.message = msg
|
||||
|
||||
self.write_read_packet(packet, 20)
|
||||
self.write_read_packet(packet, -1)
|
||||
|
||||
def write_read_packet(self, packet, compression_threshold):
|
||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
logging.debug('protocol_version = %r' % protocol_version)
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
|
@ -66,7 +106,7 @@ class PacketSerializatonTest(unittest.TestCase):
|
|||
packet_id = VarInt.read(packet_buffer)
|
||||
self.assertEqual(packet_id, packet.id)
|
||||
|
||||
deserialized = ChatPacket(context)
|
||||
deserialized = serverbound.play.ChatPacket(context)
|
||||
deserialized.read(packet_buffer)
|
||||
|
||||
self.assertEqual(packet.message, deserialized.message)
|
||||
|
@ -80,13 +120,301 @@ class PacketListenerTest(unittest.TestCase):
|
|||
def test_packet(chat_packet):
|
||||
self.assertEqual(chat_packet.message, message)
|
||||
|
||||
for protocol_version in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
logging.debug('protocol_version = %r' % protocol_version)
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
listener = PacketListener(test_packet, ChatPacket)
|
||||
listener = PacketListener(test_packet, serverbound.play.ChatPacket)
|
||||
|
||||
packet = ChatPacket(context).set_values(message=message)
|
||||
packet = serverbound.play.ChatPacket(context).set_values(
|
||||
message=message)
|
||||
uncalled_packet = KeepAlivePacket().set_values(keep_alive_id=0)
|
||||
|
||||
listener.call_packet(packet)
|
||||
listener.call_packet(uncalled_packet)
|
||||
|
||||
|
||||
class PacketEnumTest(unittest.TestCase):
|
||||
def test_packet_str(self):
|
||||
class ExamplePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = 'example'
|
||||
definition = [
|
||||
{'alpha': VarInt},
|
||||
{'beta': VarInt},
|
||||
{'gamma': VarInt}]
|
||||
|
||||
class Alpha(Enum):
|
||||
ZERO = 0
|
||||
|
||||
class Beta(Enum):
|
||||
ONE = 1
|
||||
|
||||
self.assertEqual(
|
||||
str(ExamplePacket(ConnectionContext(), alpha=0, beta=0, gamma=0)),
|
||||
'0x00 ExamplePacket(alpha=ZERO, beta=0, gamma=0)'
|
||||
)
|
||||
|
||||
|
||||
class TestReadWritePackets(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def test_explosion_packet(self):
|
||||
context = ConnectionContext(protocol_version=TEST_VERSIONS[-1])
|
||||
Record = clientbound.play.ExplosionPacket.Record
|
||||
packet = clientbound.play.ExplosionPacket(
|
||||
position=Vector(787, -37, 0), radius=15,
|
||||
records=[Record(-14, -116, -5), Record(-77, 34, -36),
|
||||
Record(-35, -127, 95), Record(11, 113, -8)],
|
||||
player_motion=Vector(4, 5, 0), context=context)
|
||||
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
'0x%02X ExplosionPacket(x=787, y=-37, z=0, radius=15, records=['
|
||||
'Record(-14, -116, -5), Record(-77, 34, -36), '
|
||||
'Record(-35, -127, 95), Record(11, 113, -8)], '
|
||||
'player_motion_x=4, player_motion_y=5, player_motion_z=0)'
|
||||
% packet.id
|
||||
)
|
||||
|
||||
self._test_read_write_packet(packet)
|
||||
|
||||
def test_combat_event_packet(self):
|
||||
packet = clientbound.play.CombatEventPacket(
|
||||
event=clientbound.play.CombatEventPacket.EnterCombatEvent())
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
'CombatEventPacket(event=EnterCombatEvent())'
|
||||
)
|
||||
self._test_read_write_packet(packet, vmax=PRE | 14)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self._test_read_write_packet(packet, vmin=PRE | 15)
|
||||
|
||||
specialised_packet = clientbound.play.EnterCombatEventPacket()
|
||||
self.assertIsInstance(specialised_packet.event, type(packet.event))
|
||||
for field in specialised_packet.fields:
|
||||
value = getattr(packet.event, field)
|
||||
setattr(specialised_packet, field, value)
|
||||
self.assertEqual(getattr(specialised_packet.event, field), value)
|
||||
|
||||
packet = clientbound.play.CombatEventPacket(
|
||||
event=clientbound.play.CombatEventPacket.EndCombatEvent(
|
||||
duration=415, entity_id=91063502))
|
||||
self.assertEqual(str(packet),
|
||||
'CombatEventPacket(event=EndCombatEvent('
|
||||
'duration=415, entity_id=91063502))')
|
||||
self._test_read_write_packet(packet, vmax=PRE | 14)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self._test_read_write_packet(packet, vmin=PRE | 15)
|
||||
|
||||
specialised_packet = clientbound.play.EndCombatEventPacket()
|
||||
self.assertIsInstance(specialised_packet.event, type(packet.event))
|
||||
for field in specialised_packet.fields:
|
||||
value = getattr(packet.event, field)
|
||||
setattr(specialised_packet, field, value)
|
||||
self.assertEqual(getattr(specialised_packet.event, field), value)
|
||||
|
||||
packet = clientbound.play.CombatEventPacket(
|
||||
event=clientbound.play.CombatEventPacket.EntityDeadEvent(
|
||||
player_id=178, entity_id=36, message='RIP'))
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
"CombatEventPacket(event=EntityDeadEvent("
|
||||
"player_id=178, entity_id=36, message='RIP'))"
|
||||
)
|
||||
self._test_read_write_packet(packet, vmax=PRE | 14)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self._test_read_write_packet(packet, vmin=PRE | 15)
|
||||
|
||||
specialised_packet = clientbound.play.DeathCombatEventPacket()
|
||||
self.assertIsInstance(specialised_packet.event, type(packet.event))
|
||||
for field in specialised_packet.fields:
|
||||
value = getattr(packet.event, field)
|
||||
setattr(specialised_packet, field, value)
|
||||
self.assertEqual(getattr(specialised_packet.event, field), value)
|
||||
|
||||
def test_multi_block_change_packet(self):
|
||||
Record = clientbound.play.MultiBlockChangePacket.Record
|
||||
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext()
|
||||
context.protocol_version = protocol_version
|
||||
packet = clientbound.play.MultiBlockChangePacket(context)
|
||||
|
||||
if context.protocol_later_eq(741):
|
||||
packet.chunk_section_pos = Vector(167, 17, 33)
|
||||
packet.invert_trust_edges = False
|
||||
else:
|
||||
packet.chunk_x, packet.chunk_z = 167, 17
|
||||
self.assertEqual(packet.chunk_pos, (167, 17))
|
||||
|
||||
packet.records = [
|
||||
Record(x=1, y=2, z=3, blockId=56, blockMeta=13),
|
||||
Record(position=Vector(1, 2, 3), block_state_id=909),
|
||||
Record(position=(1, 2, 3), blockStateId=909),
|
||||
]
|
||||
|
||||
for i in range(3):
|
||||
self.assertEqual(packet.records[i].blockId, 56)
|
||||
self.assertEqual(packet.records[i].blockMeta, 13)
|
||||
self.assertEqual(packet.records[i].blockStateId, 909)
|
||||
self.assertEqual(packet.records[i].position, Vector(1, 2, 3))
|
||||
|
||||
self._test_read_write_packet(packet, context)
|
||||
|
||||
def test_spawn_object_packet(self):
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
logging.debug('protocol_version = %r' % protocol_version)
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
EntityType = clientbound.play.SpawnObjectPacket.field_enum(
|
||||
'type_id', context)
|
||||
|
||||
pos_look = PositionAndLook(
|
||||
position=(Vector(68.0, 38.0, 76.0)
|
||||
if context.protocol_later_eq(100) else
|
||||
Vector(68, 38, 76)),
|
||||
yaw=263.494, pitch=180)
|
||||
velocity = Vector(21, 55, 41)
|
||||
entity_id, type_name, type_id = 49846, 'EGG', EntityType.EGG
|
||||
|
||||
packet = clientbound.play.SpawnObjectPacket(
|
||||
context=context,
|
||||
x=pos_look.x, y=pos_look.y, z=pos_look.z,
|
||||
yaw=pos_look.yaw, pitch=pos_look.pitch,
|
||||
velocity_x=velocity.x, velocity_y=velocity.y,
|
||||
velocity_z=velocity.z,
|
||||
entity_id=entity_id, type_id=type_id, data=1)
|
||||
if context.protocol_later_eq(49):
|
||||
object_uuid = 'd9568851-85bc-4a10-8d6a-261d130626fa'
|
||||
packet.object_uuid = object_uuid
|
||||
self.assertEqual(packet.objectUUID, object_uuid)
|
||||
self.assertEqual(packet.position_and_look, pos_look)
|
||||
self.assertEqual(packet.position, pos_look.position)
|
||||
self.assertEqual(packet.velocity, velocity)
|
||||
self.assertEqual(packet.type, type_name)
|
||||
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
"0x%02X SpawnObjectPacket(entity_id=49846, "
|
||||
"object_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', "
|
||||
"type_id=EGG, x=68.0, y=38.0, z=76.0, pitch=180, yaw=263.494, "
|
||||
"data=1, velocity_x=21, velocity_y=55, velocity_z=41)"
|
||||
% packet.id if context.protocol_later_eq(100) else
|
||||
"0x%02X SpawnObjectPacket(entity_id=49846, "
|
||||
"object_uuid='d9568851-85bc-4a10-8d6a-261d130626fa', "
|
||||
"type_id=EGG, x=68, y=38, z=76, pitch=180, yaw=263.494, "
|
||||
"data=1, velocity_x=21, velocity_y=55, velocity_z=41)"
|
||||
% packet.id if context.protocol_later_eq(49) else
|
||||
"0x%02X SpawnObjectPacket(entity_id=49846, type_id=EGG, "
|
||||
"x=68, y=38, z=76, pitch=180, yaw=263.494, data=1, "
|
||||
"velocity_x=21, velocity_y=55, velocity_z=41)" % packet.id
|
||||
)
|
||||
|
||||
packet2 = clientbound.play.SpawnObjectPacket(
|
||||
context=context, position_and_look=pos_look,
|
||||
velocity=velocity, type=type_name,
|
||||
entity_id=entity_id, data=1)
|
||||
if context.protocol_later_eq(49):
|
||||
packet2.object_uuid = object_uuid
|
||||
self.assertEqual(packet.__dict__, packet2.__dict__)
|
||||
|
||||
packet2.position = pos_look.position
|
||||
self.assertEqual(packet.position, packet2.position)
|
||||
|
||||
packet2.data = 0
|
||||
if context.protocol_earlier(49):
|
||||
del packet2.velocity
|
||||
self._test_read_write_packet(packet, context,
|
||||
yaw=360/256, pitch=360/256)
|
||||
self._test_read_write_packet(packet2, context,
|
||||
yaw=360/256, pitch=360/256)
|
||||
|
||||
def test_sound_effect_packet(self):
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
packet = clientbound.play.SoundEffectPacket(
|
||||
sound_id=545, effect_position=Vector(0.125, 300.0, 50.5),
|
||||
volume=0.75)
|
||||
if context.protocol_later_eq(201):
|
||||
packet.pitch = struct.unpack('f', struct.pack('f', 1.5))[0]
|
||||
else:
|
||||
packet.pitch = int(1.5 / 63.5) * 63.5
|
||||
if context.protocol_later_eq(95):
|
||||
packet.sound_category = \
|
||||
clientbound.play.SoundEffectPacket.SoundCategory.NEUTRAL
|
||||
|
||||
self._test_read_write_packet(packet, context)
|
||||
|
||||
def test_face_player_packet(self):
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
packet = clientbound.play.FacePlayerPacket(context)
|
||||
packet.target = 1.0, -2.0, 3.5
|
||||
packet.entity_id = None
|
||||
if context.protocol_later_eq(353):
|
||||
packet.origin = OriginPoint.EYES
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
"0x%02X FacePlayerPacket(origin=EYES, x=1.0, y=-2.0, z=3.5, "
|
||||
"entity_id=None)" % packet.id
|
||||
if context.protocol_later_eq(353) else
|
||||
"0x%02X FacePlayerPacket(entity_id=None, x=1.0, y=-2.0, z=3.5)"
|
||||
% packet.id
|
||||
)
|
||||
self._test_read_write_packet(packet, context)
|
||||
|
||||
packet.entity_id = 123
|
||||
if context.protocol_later_eq(353):
|
||||
packet.entity_origin = OriginPoint.FEET
|
||||
else:
|
||||
del packet.target
|
||||
self.assertEqual(
|
||||
str(packet),
|
||||
"0x%02X FacePlayerPacket(origin=EYES, x=1.0, y=-2.0, z=3.5, "
|
||||
"entity_id=123, entity_origin=FEET)" % packet.id
|
||||
if context.protocol_later_eq(353) else
|
||||
"0x%02X FacePlayerPacket(entity_id=123)" % packet.id
|
||||
)
|
||||
self._test_read_write_packet(packet, context)
|
||||
|
||||
def _test_read_write_packet(
|
||||
self, packet_in, context=None, vmin=None, vmax=None, **kwargs
|
||||
):
|
||||
"""
|
||||
If kwargs are specified, the key will be tested against the
|
||||
respective delta value. Useful for testing FixedPointNumbers
|
||||
where there is precision lost in the resulting value.
|
||||
"""
|
||||
if context is None:
|
||||
for pv in TEST_VERSIONS:
|
||||
if vmin is not None and protocol_earlier(pv, vmin):
|
||||
continue
|
||||
if vmax is not None and protocol_earlier(vmax, pv):
|
||||
continue
|
||||
logging.debug('protocol_version = %r' % pv)
|
||||
context = ConnectionContext(protocol_version=pv)
|
||||
self._test_read_write_packet(packet_in, context)
|
||||
else:
|
||||
packet_in.context = context
|
||||
packet_buffer = PacketBuffer()
|
||||
packet_in.write(packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
VarInt.read(packet_buffer)
|
||||
packet_id = VarInt.read(packet_buffer)
|
||||
self.assertEqual(packet_id, packet_in.id)
|
||||
|
||||
packet_out = type(packet_in)(context=context)
|
||||
packet_out.read(packet_buffer)
|
||||
self.assertIs(type(packet_in), type(packet_out))
|
||||
|
||||
for packet_attr, precision in kwargs.items():
|
||||
packet_attribute_in = packet_in.__dict__.pop(packet_attr)
|
||||
packet_attribute_out = packet_out.__dict__.pop(packet_attr)
|
||||
self.assertAlmostEqual(packet_attribute_in,
|
||||
packet_attribute_out,
|
||||
delta=precision)
|
||||
|
||||
self.assertEqual(packet_in.__dict__, packet_out.__dict__)
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
import unittest
|
||||
from minecraft.networking.types import (UUID, VarInt)
|
||||
from minecraft.networking.packets import PacketBuffer
|
||||
from minecraft.networking.packets.clientbound.play import (
|
||||
PlayerPositionAndLookPacket, PlayerListItemPacket, MapPacket
|
||||
)
|
||||
from minecraft.networking.packets import serverbound
|
||||
from minecraft.networking.connection import ConnectionContext
|
||||
|
||||
from tests.test_packets import TEST_VERSIONS
|
||||
|
||||
|
||||
class PlayerPositionAndLookTest(unittest.TestCase):
|
||||
|
||||
def test_position_and_look(self):
|
||||
current_position = PlayerPositionAndLookPacket.PositionAndLook(
|
||||
x=999, y=999, z=999, yaw=999, pitch=999)
|
||||
|
||||
packet = PlayerPositionAndLookPacket()
|
||||
packet.x = 1.0
|
||||
packet.y = 2.0
|
||||
packet.z = 3.0
|
||||
packet.yaw = 4.0
|
||||
packet.pitch = 5.0
|
||||
# First do an absolute move to these cordinates
|
||||
packet.flags = 0
|
||||
|
||||
packet.apply(current_position)
|
||||
self.assertEqual(current_position.x, 1.0)
|
||||
self.assertEqual(current_position.y, 2.0)
|
||||
self.assertEqual(current_position.z, 3.0)
|
||||
self.assertEqual(current_position.yaw, 4.0)
|
||||
self.assertEqual(current_position.pitch, 5.0)
|
||||
|
||||
# Now a relative move
|
||||
packet.flags = 0b11111
|
||||
|
||||
packet.apply(current_position)
|
||||
self.assertEqual(current_position.x, 2.0)
|
||||
self.assertEqual(current_position.y, 4.0)
|
||||
self.assertEqual(current_position.z, 6.0)
|
||||
self.assertEqual(current_position.yaw, 8.0)
|
||||
self.assertEqual(current_position.pitch, 10.0)
|
||||
|
||||
|
||||
class MapPacketTest(unittest.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def make_map_packet(
|
||||
context, width=2, height=2, offset=(2, 2), pixels=b"this"):
|
||||
|
||||
packet = MapPacket(context)
|
||||
|
||||
packet.map_id = 1
|
||||
packet.scale = 42
|
||||
packet.is_tracking_position = True
|
||||
packet.is_locked = False
|
||||
packet.icons = []
|
||||
d_name = u'Marshmallow' if context.protocol_later_eq(364) else None
|
||||
packet.icons.append(MapPacket.MapIcon(
|
||||
type=2, direction=2, location=(1, 1), display_name=d_name
|
||||
))
|
||||
packet.icons.append(MapPacket.MapIcon(
|
||||
type=3, direction=3, location=(3, 3)
|
||||
))
|
||||
packet.width = width
|
||||
packet.height = height if width else 0
|
||||
packet.offset = offset if width else None
|
||||
packet.pixels = pixels if width else None
|
||||
return packet
|
||||
|
||||
def packet_roundtrip(self, context, **kwds):
|
||||
packet = self.make_map_packet(context, **kwds)
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
packet.write(packet_buffer)
|
||||
|
||||
packet_buffer.reset_cursor()
|
||||
|
||||
# Read the length and packet id
|
||||
VarInt.read(packet_buffer)
|
||||
packet_id = VarInt.read(packet_buffer)
|
||||
self.assertEqual(packet_id, packet.id)
|
||||
|
||||
p = MapPacket(context)
|
||||
p.read(packet_buffer)
|
||||
|
||||
self.assertEqual(p.map_id, packet.map_id)
|
||||
self.assertEqual(p.scale, packet.scale)
|
||||
self.assertEqual(p.is_tracking_position, packet.is_tracking_position)
|
||||
self.assertEqual(p.width, packet.width)
|
||||
self.assertEqual(p.height, packet.height)
|
||||
self.assertEqual(p.offset, packet.offset)
|
||||
self.assertEqual(p.pixels, packet.pixels)
|
||||
self.assertEqual(str(p.icons[0]), str(packet.icons[0]))
|
||||
self.assertEqual(str(p.icons[1]), str(packet.icons[1]))
|
||||
self.assertEqual(str(p), str(packet))
|
||||
|
||||
def test_packet_roundtrip(self):
|
||||
self.packet_roundtrip(ConnectionContext(protocol_version=106))
|
||||
self.packet_roundtrip(ConnectionContext(protocol_version=107))
|
||||
self.packet_roundtrip(ConnectionContext(protocol_version=379))
|
||||
self.packet_roundtrip(ConnectionContext(protocol_version=379),
|
||||
width=0)
|
||||
|
||||
def test_map_set(self):
|
||||
map_set = MapPacket.MapSet()
|
||||
|
||||
context = ConnectionContext(protocol_version=107)
|
||||
packet = self.make_map_packet(context)
|
||||
|
||||
packet.apply_to_map_set(map_set)
|
||||
self.assertEqual(len(map_set.maps_by_id), 1)
|
||||
|
||||
packet = self.make_map_packet(
|
||||
context, width=1, height=0, offset=(2, 2), pixels=b"x"
|
||||
)
|
||||
packet.apply_to_map_set(map_set)
|
||||
map = map_set.maps_by_id[1]
|
||||
self.assertIn(b"xh", map.pixels)
|
||||
self.assertIn(b"is", map.pixels)
|
||||
self.assertIsNotNone(str(map_set))
|
||||
|
||||
|
||||
fake_uuid = "12345678-1234-5678-1234-567812345678"
|
||||
|
||||
|
||||
class PlayerListItemTest(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def test_base_action(self):
|
||||
packet_buffer = PacketBuffer()
|
||||
UUID.send(fake_uuid, packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
action = PlayerListItemPacket.Action()
|
||||
action.read(packet_buffer)
|
||||
|
||||
def test_invalid_action(self):
|
||||
packet_buffer = PacketBuffer()
|
||||
VarInt.send(200, packet_buffer) # action_id
|
||||
packet_buffer.reset_cursor()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
PlayerListItemPacket().read(packet_buffer)
|
||||
|
||||
def make_add_player_packet(self, context, display_name=True):
|
||||
packet_buffer = PacketBuffer()
|
||||
packet = PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.AddPlayerAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.AddPlayerAction(
|
||||
uuid=fake_uuid,
|
||||
name='goodmonson',
|
||||
properties=[
|
||||
PlayerListItemPacket.PlayerProperty(
|
||||
name='property1', value='value1', signature=None),
|
||||
PlayerListItemPacket.PlayerProperty(
|
||||
name='property2', value='value2', signature='gm')
|
||||
],
|
||||
gamemode=42,
|
||||
ping=69,
|
||||
display_name='Goodmonson' if display_name else None
|
||||
),
|
||||
],
|
||||
)
|
||||
if display_name:
|
||||
self.assertEqual(
|
||||
str(packet), "0x%02X PlayerListItemPacket("
|
||||
"action_type=AddPlayerAction, actions=[AddPlayerAction("
|
||||
"uuid=%r, name='goodmonson', properties=[PlayerProperty("
|
||||
"name='property1', value='value1', signature=None), "
|
||||
"PlayerProperty(name='property2', value='value2', "
|
||||
"signature='gm')], gamemode=42, ping=69, "
|
||||
"display_name='Goodmonson')])" % (packet.id, fake_uuid))
|
||||
packet.write_fields(packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
return packet_buffer
|
||||
|
||||
def test_add_player_action(self):
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
player_list = PlayerListItemPacket.PlayerList()
|
||||
packet_buffer = self.make_add_player_packet(context)
|
||||
|
||||
packet = PlayerListItemPacket(context)
|
||||
packet.read(packet_buffer)
|
||||
packet.apply(player_list)
|
||||
|
||||
self.assertIn(fake_uuid, player_list.players_by_uuid)
|
||||
player = player_list.players_by_uuid[fake_uuid]
|
||||
|
||||
self.assertEqual(player.name, 'goodmonson')
|
||||
self.assertEqual(player.properties[0].name, 'property1')
|
||||
self.assertIsNone(player.properties[0].signature)
|
||||
self.assertEqual(player.properties[1].value, 'value2')
|
||||
self.assertEqual(player.properties[1].signature, 'gm')
|
||||
self.assertEqual(player.gamemode, 42)
|
||||
self.assertEqual(player.ping, 69)
|
||||
self.assertEqual(player.display_name, 'Goodmonson')
|
||||
|
||||
def read_and_apply(self, context, packet_buffer, player_list):
|
||||
packet_buffer.reset_cursor()
|
||||
packet = PlayerListItemPacket(context)
|
||||
packet.read(packet_buffer)
|
||||
packet.apply(player_list)
|
||||
|
||||
def test_add_and_others(self):
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
player_list = PlayerListItemPacket.PlayerList()
|
||||
by_uuid = player_list.players_by_uuid
|
||||
|
||||
packet_buffer = self.make_add_player_packet(
|
||||
context, display_name=False)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertEqual(by_uuid[fake_uuid].gamemode, 42)
|
||||
self.assertEqual(by_uuid[fake_uuid].ping, 69)
|
||||
self.assertIsNone(by_uuid[fake_uuid].display_name)
|
||||
|
||||
# Change the game mode
|
||||
packet_buffer = PacketBuffer()
|
||||
packet = PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.UpdateGameModeAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.UpdateGameModeAction(
|
||||
uuid=fake_uuid, gamemode=43),
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
str(packet), "0x%02X PlayerListItemPacket("
|
||||
"action_type=UpdateGameModeAction, actions=["
|
||||
"UpdateGameModeAction(uuid=%r, gamemode=43)])"
|
||||
% (packet.id, fake_uuid))
|
||||
packet.write_fields(packet_buffer)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertEqual(by_uuid[fake_uuid].gamemode, 43)
|
||||
|
||||
# Change the ping
|
||||
packet_buffer = PacketBuffer()
|
||||
packet = PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.UpdateLatencyAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.UpdateLatencyAction(
|
||||
uuid=fake_uuid, ping=70),
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
str(packet), "0x%02X PlayerListItemPacket("
|
||||
"action_type=UpdateLatencyAction, actions=["
|
||||
"UpdateLatencyAction(uuid=%r, ping=70)])"
|
||||
% (packet.id, fake_uuid))
|
||||
packet.write_fields(packet_buffer)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertEqual(by_uuid[fake_uuid].ping, 70)
|
||||
|
||||
# Change the display name
|
||||
packet_buffer = PacketBuffer()
|
||||
packet = PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.UpdateDisplayNameAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.UpdateDisplayNameAction(
|
||||
uuid=fake_uuid, display_name='Badmonson'),
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
str(packet), "0x%02X PlayerListItemPacket("
|
||||
"action_type=UpdateDisplayNameAction, actions=["
|
||||
"UpdateDisplayNameAction(uuid=%r, display_name='Badmonson')])"
|
||||
% (packet.id, fake_uuid))
|
||||
packet.write_fields(packet_buffer)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertEqual(by_uuid[fake_uuid].display_name, 'Badmonson')
|
||||
|
||||
# Remove the display name
|
||||
packet_buffer = PacketBuffer()
|
||||
PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.UpdateDisplayNameAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.UpdateDisplayNameAction(
|
||||
uuid=fake_uuid, display_name=None),
|
||||
],
|
||||
).write_fields(packet_buffer)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertIsNone(by_uuid[fake_uuid].display_name)
|
||||
|
||||
# Remove the player
|
||||
packet_buffer = PacketBuffer()
|
||||
packet = PlayerListItemPacket(
|
||||
context=context,
|
||||
action_type=PlayerListItemPacket.RemovePlayerAction,
|
||||
actions=[
|
||||
PlayerListItemPacket.RemovePlayerAction(uuid=fake_uuid),
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
str(packet), "0x%02X PlayerListItemPacket("
|
||||
"action_type=RemovePlayerAction, actions=[RemovePlayerAction("
|
||||
"uuid=%r)])" % (packet.id, fake_uuid))
|
||||
packet.write_fields(packet_buffer)
|
||||
self.read_and_apply(context, packet_buffer, player_list)
|
||||
self.assertNotIn(fake_uuid, player_list.players_by_uuid)
|
||||
|
||||
|
||||
class ClientSettingsTest(unittest.TestCase):
|
||||
def test_enable_disable_text_filtering(self):
|
||||
packet = serverbound.play.ClientSettingsPacket()
|
||||
self.assertEqual(packet.enable_text_filtering, False)
|
||||
self.assertEqual(packet.disable_text_filtering, True)
|
||||
packet.enable_text_filtering = True
|
||||
self.assertEqual(packet.enable_text_filtering, True)
|
||||
self.assertEqual(packet.disable_text_filtering, False)
|
||||
packet.disable_text_filtering = True
|
||||
self.assertEqual(packet.enable_text_filtering, False)
|
||||
self.assertEqual(packet.disable_text_filtering, True)
|
|
@ -0,0 +1,110 @@
|
|||
import unittest
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS
|
||||
from minecraft.networking.connection import (
|
||||
LoginReactor, PlayingReactor, ConnectionContext
|
||||
)
|
||||
from minecraft.networking.packets import clientbound
|
||||
|
||||
|
||||
latest_proto = SUPPORTED_PROTOCOL_VERSIONS[-1]
|
||||
|
||||
|
||||
class LoginReactorTest(unittest.TestCase):
|
||||
|
||||
@mock.patch('minecraft.networking.connection.encryption')
|
||||
def test_encryption_online_server(self, encrypt):
|
||||
connection = mock.MagicMock()
|
||||
connection.context = ConnectionContext(protocol_version=latest_proto)
|
||||
reactor = LoginReactor(connection)
|
||||
|
||||
packet = clientbound.login.EncryptionRequestPacket()
|
||||
packet.server_id = "123"
|
||||
packet.public_key = b"asdf"
|
||||
packet.verify_token = b"23"
|
||||
|
||||
secret = b"secret"
|
||||
encrypt.generate_shared_secret.return_value = secret
|
||||
encrypt.encrypt_token_and_secret.return_value = (b"a", b"b")
|
||||
encrypt.generate_verification_hash.return_value = b"hash"
|
||||
|
||||
reactor.react(packet)
|
||||
|
||||
encrypt.encrypt_token_and_secret.assert_called_once_with(
|
||||
packet.public_key, packet.verify_token, secret
|
||||
)
|
||||
connection.auth_token.join.assert_called_once_with(b"hash")
|
||||
self.assertEqual(connection.write_packet.call_count, 1)
|
||||
|
||||
@mock.patch('minecraft.networking.connection.encryption')
|
||||
def test_encryption_offline_server(self, encrypt):
|
||||
connection = mock.MagicMock()
|
||||
connection.context = ConnectionContext(protocol_version=latest_proto)
|
||||
reactor = LoginReactor(connection)
|
||||
|
||||
packet = clientbound.login.EncryptionRequestPacket()
|
||||
packet.server_id = "-"
|
||||
packet.public_key = b"asdf"
|
||||
packet.verify_token = b"23"
|
||||
|
||||
secret = b"secret"
|
||||
encrypt.generate_shared_secret.return_value = secret
|
||||
encrypt.encrypt_token_and_secret.return_value = (b"a", b"b")
|
||||
|
||||
reactor.react(packet)
|
||||
|
||||
encrypt.encrypt_token_and_secret.assert_called_once_with(
|
||||
packet.public_key, packet.verify_token, secret
|
||||
)
|
||||
self.assertEqual(connection.auth_token.join.call_count, 0)
|
||||
self.assertEqual(connection.write_packet.call_count, 1)
|
||||
|
||||
|
||||
class PlayingReactorTest(unittest.TestCase):
|
||||
|
||||
def get_position_packet(self):
|
||||
packet = clientbound.play.PlayerPositionAndLookPacket()
|
||||
packet.x = 1.0
|
||||
packet.y = 2.0
|
||||
packet.z = 3.0
|
||||
packet.yaw = 4.0
|
||||
packet.pitch = 5.0
|
||||
|
||||
packet.teleport_id = 42
|
||||
|
||||
return packet
|
||||
|
||||
def test_teleport_confirmation_old(self):
|
||||
connection = mock.MagicMock()
|
||||
connection.context = ConnectionContext(protocol_version=106)
|
||||
reactor = PlayingReactor(connection)
|
||||
|
||||
packet = self.get_position_packet()
|
||||
reactor.react(packet)
|
||||
|
||||
self.assertEqual(connection.write_packet.call_count, 1)
|
||||
response_packet = connection.write_packet.call_args[0][0]
|
||||
|
||||
self.assertEqual(response_packet.x, 1.0)
|
||||
self.assertEqual(response_packet.feet_y, 2.0)
|
||||
self.assertEqual(response_packet.z, 3.0)
|
||||
self.assertEqual(response_packet.yaw, 4.0)
|
||||
self.assertEqual(response_packet.pitch, 5.0)
|
||||
self.assertTrue(response_packet.on_ground)
|
||||
|
||||
def test_teleport_confirmation_new(self):
|
||||
connection = mock.MagicMock()
|
||||
connection.context = ConnectionContext(protocol_version=107)
|
||||
reactor = PlayingReactor(connection)
|
||||
|
||||
packet = self.get_position_packet()
|
||||
reactor.react(packet)
|
||||
|
||||
self.assertEqual(connection.write_packet.call_count, 1)
|
||||
|
||||
response_packet = connection.write_packet.call_args[0][0]
|
||||
self.assertEqual(response_packet.teleport_id, 42)
|
|
@ -3,55 +3,100 @@
|
|||
import unittest
|
||||
from minecraft.networking.types import (
|
||||
Type, Boolean, UnsignedByte, Byte, Short, UnsignedShort,
|
||||
Integer, VarInt, Long, Float, Double, ShortPrefixedByteArray,
|
||||
VarIntPrefixedByteArray, String as StringType
|
||||
Integer, FixedPointInteger, Angle, VarInt, Long, Float, Double,
|
||||
ShortPrefixedByteArray, VarIntPrefixedByteArray, UUID,
|
||||
String as StringType, Position, TrailingByteArray, UnsignedLong,
|
||||
)
|
||||
from minecraft.networking.packets.clientbound.play import (
|
||||
MultiBlockChangePacket
|
||||
)
|
||||
from minecraft.networking.packets import PacketBuffer
|
||||
from minecraft.networking.connection import ConnectionContext
|
||||
from minecraft import SUPPORTED_PROTOCOL_VERSIONS, RELEASE_PROTOCOL_VERSIONS
|
||||
|
||||
|
||||
TEST_VERSIONS = list(RELEASE_PROTOCOL_VERSIONS)
|
||||
if SUPPORTED_PROTOCOL_VERSIONS[-1] not in TEST_VERSIONS:
|
||||
TEST_VERSIONS.append(SUPPORTED_PROTOCOL_VERSIONS[-1])
|
||||
|
||||
TEST_DATA = {
|
||||
Boolean: [True, False],
|
||||
UnsignedByte: [0, 125],
|
||||
Byte: [-22, 22],
|
||||
Short: [-340, 22, 350],
|
||||
UnsignedShort: [0, 400],
|
||||
UnsignedLong: [0, 400],
|
||||
Integer: [-1000, 1000],
|
||||
FixedPointInteger: [float(-13098.3435), float(-0.83), float(1000)],
|
||||
Angle: [0, 360.0, 720, 47.12947238973, -108.7],
|
||||
VarInt: [1, 250, 50000, 10000000],
|
||||
Long: [50000000],
|
||||
Float: [21.000301],
|
||||
Double: [36.004002],
|
||||
ShortPrefixedByteArray: [bytes(245)],
|
||||
VarIntPrefixedByteArray: [bytes(1234)],
|
||||
StringType: ["hello world"]
|
||||
TrailingByteArray: [b'Q^jO<5*|+o LGc('],
|
||||
UUID: ["12345678-1234-5678-1234-567812345678"],
|
||||
StringType: ["hello world"],
|
||||
Position: [(758, 0, 691), (-500, -12, -684)],
|
||||
MultiBlockChangePacket.ChunkSectionPos: [
|
||||
(x, y, z)
|
||||
for x in [-0x200000, -123, -1, 0, 123, 0x1FFFFF]
|
||||
for z in [-0x200000, -456, -1, 0, 456, 0x1FFFFF]
|
||||
for y in [-0x80000, -789, -1, 0, 789, 0x7FFFF]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class SerializationTest(unittest.TestCase):
|
||||
|
||||
def test_serialization(self):
|
||||
for data_type in Type.__subclasses__():
|
||||
if data_type in TEST_DATA:
|
||||
test_cases = TEST_DATA[data_type]
|
||||
for protocol_version in TEST_VERSIONS:
|
||||
context = ConnectionContext(protocol_version=protocol_version)
|
||||
|
||||
for test_data in test_cases:
|
||||
packet_buffer = PacketBuffer()
|
||||
data_type.send(test_data, packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
for data_type in Type.__subclasses__():
|
||||
if data_type in TEST_DATA:
|
||||
test_cases = TEST_DATA[data_type]
|
||||
|
||||
deserialized = data_type.read(packet_buffer)
|
||||
if data_type is Float or data_type is Double:
|
||||
self.assertAlmostEquals(test_data, deserialized, 3)
|
||||
else:
|
||||
self.assertEqual(test_data, deserialized)
|
||||
for test_data in test_cases:
|
||||
packet_buffer = PacketBuffer()
|
||||
data_type.send_with_context(
|
||||
test_data, packet_buffer, context)
|
||||
packet_buffer.reset_cursor()
|
||||
|
||||
deserialized = data_type.read_with_context(
|
||||
packet_buffer, context)
|
||||
if data_type is FixedPointInteger:
|
||||
self.assertAlmostEqual(
|
||||
test_data, deserialized, delta=1.0/32.0)
|
||||
elif data_type is Angle:
|
||||
self.assertAlmostEqual(test_data % 360,
|
||||
deserialized,
|
||||
delta=360/256)
|
||||
elif data_type is Float or data_type is Double:
|
||||
self.assertAlmostEqual(test_data, deserialized, 3)
|
||||
else:
|
||||
self.assertEqual(test_data, deserialized)
|
||||
|
||||
def test_exceptions(self):
|
||||
base_type = Type()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
base_type.read(None)
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
base_type.read_with_context(None, None)
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
base_type.send(None, None)
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
base_type.send_with_context(None, None, None)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
Position.read(None)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
Position.send(None, None)
|
||||
|
||||
empty_socket = PacketBuffer()
|
||||
with self.assertRaises(Exception):
|
||||
VarInt.read(empty_socket)
|
||||
|
@ -60,6 +105,15 @@ class SerializationTest(unittest.TestCase):
|
|||
self.assertEqual(VarInt.size(2), 1)
|
||||
self.assertEqual(VarInt.size(1250), 2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
VarInt.size(2 ** 90)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
packet_buffer = PacketBuffer()
|
||||
VarInt.send(2 ** 49, packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
VarInt.read(packet_buffer)
|
||||
|
||||
packet_buffer = PacketBuffer()
|
||||
VarInt.send(50000, packet_buffer)
|
||||
packet_buffer.reset_cursor()
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import unittest
|
||||
|
||||
from minecraft.networking.types import (
|
||||
Enum, BitFieldEnum, Vector, Position, PositionAndLook
|
||||
)
|
||||
|
||||
|
||||
class EnumTest(unittest.TestCase):
|
||||
def test_enum(self):
|
||||
class Example(Enum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
|
||||
self.assertEqual(
|
||||
list(map(Example.name_from_value, range(5))),
|
||||
[None, 'ONE', 'TWO', 'THREE', None])
|
||||
|
||||
|
||||
class BitFieldEnumTest(unittest.TestCase):
|
||||
def test_name_from_value(self):
|
||||
class Example1(BitFieldEnum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
FOUR = 4
|
||||
ALL = 7
|
||||
NONE = 0
|
||||
|
||||
self.assertEqual(
|
||||
list(map(Example1.name_from_value, range(9))),
|
||||
['NONE', 'ONE', 'TWO', 'ONE|TWO', 'FOUR',
|
||||
'ONE|FOUR', 'TWO|FOUR', 'ALL', None])
|
||||
|
||||
class Example2(BitFieldEnum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
FOUR = 4
|
||||
|
||||
self.assertEqual(
|
||||
list(map(Example2.name_from_value, range(9))),
|
||||
['0', 'ONE', 'TWO', 'ONE|TWO', 'FOUR',
|
||||
'ONE|FOUR', 'TWO|FOUR', 'ONE|TWO|FOUR', None])
|
||||
|
||||
|
||||
class VectorTest(unittest.TestCase):
|
||||
def test_operators(self):
|
||||
self.assertEqual(Vector(1, -2, 0) + Vector(0, 1, 2), Vector(1, -1, 2))
|
||||
self.assertEqual(Vector(1, -2, 0) - Vector(0, 1, 2), Vector(1, -3, -2))
|
||||
self.assertEqual(-Vector(1, -2, 0), Vector(-1, 2, 0))
|
||||
self.assertEqual(Vector(1, -2, 0) * 2, Vector(2, -4, 0))
|
||||
self.assertEqual(2 * Vector(1, -2, 0), Vector(2, -4, 0))
|
||||
self.assertEqual(Vector(1, -2, 0) / 2, Vector(1/2, -2/2, 0/2))
|
||||
self.assertEqual(Vector(1, -2, 0) // 2, Vector(0, -1, 0))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(str(Vector(1, 2, 3)), 'Vector(1, 2, 3)')
|
||||
self.assertEqual(str(Position(1, 2, 3)), 'Position(1, 2, 3)')
|
||||
|
||||
|
||||
class PositionAndLookTest(unittest.TestCase):
|
||||
""" This also tests the MutableRecord base type. """
|
||||
def test_properties(self):
|
||||
pos_look_1 = PositionAndLook(position=(1, 2, 3), look=(4, 5))
|
||||
pos_look_2 = PositionAndLook(x=1, y=2, z=3, yaw=4, pitch=5)
|
||||
string_repr = 'PositionAndLook(x=1, y=2, z=3, yaw=4, pitch=5)'
|
||||
|
||||
self.assertEqual(pos_look_1, pos_look_2)
|
||||
self.assertEqual(pos_look_1.position, pos_look_1.position)
|
||||
self.assertEqual(pos_look_1.look, pos_look_2.look)
|
||||
self.assertEqual(hash(pos_look_1), hash(pos_look_2))
|
||||
self.assertEqual(str(pos_look_1), string_repr)
|
||||
|
||||
self.assertFalse(pos_look_1 != pos_look_2)
|
||||
pos_look_1.position += Vector(1, 1, 1)
|
||||
self.assertTrue(pos_look_1 != pos_look_2)
|
56
tox.ini
56
tox.ini
|
@ -4,31 +4,18 @@
|
|||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = py27, py33, py34, py35, pypy, cover, flake8, pylint-errors, pylint-full, verify-manifest
|
||||
envlist = py35, py36, py37, py38, py39, pypy, flake8, pylint-errors, pylint-full, verify-manifest
|
||||
|
||||
[testenv]
|
||||
commands = nosetests
|
||||
commands = nosetests --with-timer
|
||||
install_command = pip install --prefer-binary {opts} {packages}
|
||||
|
||||
deps =
|
||||
nose
|
||||
requests
|
||||
cryptography
|
||||
future
|
||||
nose-timer
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
[testenv:py27]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
mock
|
||||
|
||||
[testenv:pypy]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
mock
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3.5
|
||||
commands =
|
||||
nosetests --with-xunit --with-xcoverage --cover-package=minecraft --nocapture --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
coverage
|
||||
|
@ -36,31 +23,46 @@ deps =
|
|||
|
||||
[testenv:coveralls]
|
||||
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
|
||||
basepython = {[testenv:cover]basepython}
|
||||
commands =
|
||||
{[testenv:cover]commands}
|
||||
coveralls
|
||||
deps =
|
||||
{[testenv:cover]deps}
|
||||
coveralls
|
||||
|
||||
[testenv:py39]
|
||||
setenv =
|
||||
PYCRAFT_RUN_INTERNET_TESTS=1
|
||||
commands =
|
||||
{[testenv]commands} --with-xunit --with-xcoverage --cover-package=minecraft --cover-erase --cover-inclusive --cover-tests --cover-branches --cover-min-percentage=60
|
||||
deps =
|
||||
{[testenv:cover]deps}
|
||||
|
||||
[testenv:pypy]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
mock
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python3.5
|
||||
basepython = python3.9
|
||||
commands =
|
||||
flake8 minecraft tests setup.py start.py bin/generate_travis_yml.py
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
flake8
|
||||
|
||||
[flake8]
|
||||
per-file-ignores =
|
||||
*/clientbound/play/spawn_object_packet.py:E221,E222,E271,E272,E201
|
||||
minecraft/networking/packets/__init__.py:F401
|
||||
|
||||
[testenv:pylint-errors]
|
||||
basepython = python3.5
|
||||
basepython = python3.9
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pylint
|
||||
commands = pylint minecraft -E
|
||||
|
||||
[testenv:pylint-full]
|
||||
basepython = python3.5
|
||||
basepython = python3.9
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pylint
|
||||
|
@ -68,7 +70,7 @@ commands =
|
|||
- pylint minecraft --disable=E
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3.5
|
||||
basepython = python3.9
|
||||
deps =
|
||||
{[testenv:cover]deps}
|
||||
sphinx
|
||||
|
@ -77,9 +79,9 @@ commands =
|
|||
{toxinidir}/bin/build_docs
|
||||
|
||||
[testenv:verify-manifest]
|
||||
basepython = python3.5
|
||||
basepython = python3.9
|
||||
deps =
|
||||
check-manifest
|
||||
commands =
|
||||
check-manifest
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue