Compare commits

...

273 Commits
1.11 ... master

Author SHA1 Message Date
joodicator bcd156e83b Fix: 1.16.5 is erroneously marked as unsupported 2022-01-02 15:41:36 +01:00
joodicator 33293a2395 Fix: Travis pypy test environment fails to install 2021-12-16 20:51:46 +01:00
joodicator bf49006553 Add support for Minecraft 1.18 and 1.18.1. 2021-12-16 19:39:13 +01:00
joodicator 1ae9a2b48a Merge pull request #238 into pull request #230 2021-12-16 19:36:41 +01:00
Andrew So c04e831e19 update to 1.17.1 and minor fixes 2021-12-10 14:53:18 -08:00
Mike Shlanta 63b8e614cd Move virtualenv upgrade to before_install 2021-12-05 13:13:17 -05:00
Mike Shlanta c58e809dbf Address missing rustc for py36 in TravisCI 2021-12-05 13:08:06 -05:00
Mike Shlanta f01584ec01 Flake8: E502, E131 2021-12-05 12:28:37 -05:00
Mike Shlanta fad399d5d7 E501 2021-12-05 12:21:06 -05:00
Andrew So 99a97df753
Merge pull request #1 from jyooru/pull230
fix linting issues
2021-07-07 20:52:34 -07:00
jyooru 37bd22172c
lint: E231 2021-07-05 15:57:25 +10:00
jyooru e40fb466ae
lint: W293 2021-07-05 15:56:37 +10:00
jyooru 317f3e62b2
lint: E302 2021-07-05 15:56:07 +10:00
jyooru 550886b414
lint: W292 2021-07-05 15:55:22 +10:00
Andrew So 3db741ad52 got all basic requires for it work for 1.17 2021-06-13 00:22:34 -07:00
Andrew So 0169e0aa75 updated all other packets that needed updating 2021-06-12 10:38:15 -07:00
Andrew So dd5c77545b fixed compression typo 2021-06-12 09:08:56 -07:00
Andrew So 6400602625 initial 1.17 update changes 2021-06-11 10:34:27 -07:00
joo 2813d02ae7 Fix: Connection.file_object is not closed on disconnection 2020-12-07 23:59:10 +01:00
joo 73728957e7 Allow reconnection from within exception handlers
This implements the changes suggested in
<https://github.com/ammaraskar/pyCraft/issues/146#issuecomment-738914064>,
i.e.:

1. `minecraft.networking.Connection.disconnect' now correctly terminates the
   new networking thread if it is still waiting to replace the old one, and

2. `minecraft.networking.Connectoin._handle_exception' no longer calls
   `disconnect' if any exception handler has initiated a new connection.
2020-12-07 23:47:48 +01:00
joo 93db454cb5 Add test case reproducing #146 (fixed to behave consistently) 2020-12-04 17:06:39 +01:00
joo 7693961fb9 Let fake server tests override the default client exception handler
This can by done by adding an exception handler with `early=True'
in the `_start_client' method of a subclass of
`fake_server._FakeServerTest'.
2020-12-04 15:37:49 +01:00
joo 252e60a6e2 Add support for Minecraft 20w45a to 20w48a (protocol 2^30+5 to 2^30+7). 2020-12-02 16:27:28 +01:00
joo 969419da3f Fix: non-monotonic protocol versions are not correctly handled
After 1.16.3, Mojang started publishing snapshot, pre-release and release
candidate versions of Minecraft with protocol version numbers of the form
`(1 << 30) | n' where 'n' is a small non-negative integer increasing with each
such version; the release versions continued to use the old format. For
example, these are the last 8 published Minecraft versions as of this commit:

release           1.16.3      uses protocol version 753
pre-release       1.16.4-pre1 uses protocol version 1073741825 == (1 << 30) | 1
pre-release       1.16.4-pre2 uses protocol version 1073741826 == (1 << 30) | 2
release candidate 1.16.4-rc1  uses protocol version 1073741827 == (1 << 30) | 3
release           1.16.4      uses protocol version 754
snapshot          20w45a      uses protocol version 1073741829 == (1 << 30) | 5
snapshot          20w46a      uses protocol version 1073741830 == (1 << 30) | 6
snapshot          20w48a      uses protocol version 1073741831 == (1 << 30) | 7

This means that protocol versions no longer increase monotonically with respect
to publication history, a property that was assumed to hold in much of
pyCraft's code relating to support of multiple protocol versions. This commit
rectifies the issue by replacing any comparison of protocol versions by their
numerical value with a comparison based on their publication time.

Newly defined is the dictionary `minecraft.PROTOCOL_VERSION_INDICES', which
maps each known protocol version to its index in the protocol chronology. As
such, the bound method `minecraft.PROTOCOL_VERSION_INDICES.get` can be used as
a key function for the built-in `sorted`, `min` and `max` functions to collate
protocol versions chronologically.

Two utility functions are provided for direct comparison of protocol versions:
    `minecraft.utility.protocol_earlier` and
    `minecraft.utility.protocol_earlier_eq`.

Additionally, four methods are added to the `ConnectionContext` type to ease
the most common cases where the protocol of a given context must be compared to
a given version number:
    `minecraft.connection.ConnectionContext.protocol_earlier`,
    `minecraft.connection.ConnectionContext.protocol_earlier_eq`,
    `minecraft.connection.ConnectionContext.protocol_later` and
    `minecraft.connection.ConnectionContext.protocol_later_eq`.
2020-12-02 15:11:39 +01:00
joo 4052136d30 List support for Minecraft 1.16.4 in README.rst 2020-12-02 14:28:59 +01:00
Vesek 4a4b699b85 Add support for Minecraft 1.16.4-pre1, 1.16.4-pre2, 1.16.4-rc1 and 1.16.4 (protocols 1073741825, 1073741826, 1073741827 and 754) 2020-12-01 05:20:30 +01:00
BD103 2e77cf5f77
Update README.rst (#198)
Added 1.16.3 to the list of supported versions
2020-12-01 02:46:53 +01:00
Tristan 4e01fdd310
Update travis distro to focal (#203)
Fixes OpenSSL version no longer being supported
2020-11-11 20:59:28 -05:00
joo 903c20f9e2 Add support for Minecraft 1.16.3-pre1 and 1.16.3 (protocols 752 and 753) 2020-09-12 07:02:22 +02:00
joo f37feeca18 Fix: MultiBlockChangePacket.ChunkSectionPos reads/writes incorrectly 2020-09-10 16:35:53 +02:00
joo cf93923acc Fix: EntityPositionDeltaPacket.delta_{x,y,z} use wrong format 2020-09-01 00:49:35 +02:00
joo eae6e5c9cd Fix EntityPositionDeltaPacket format for 1.8.9; closes #190 2020-09-01 00:39:49 +02:00
joo e434497dc7 Add type for general fixed-point numbers 2020-09-01 00:37:48 +02:00
joo 3c84c2a429 Merge branch 'v1.16' into master 2020-08-19 20:27:11 +02:00
Radon Rosborough 095191a77c
Change package name from 'minecraft' to 'pyCraft'
* Use setuptools instead of distutils
* Fix package name
2020-08-17 18:32:16 +02:00
joo 51fa0bd2d1 Change setup.py to use setuptools instead of distutils 2020-08-17 18:25:50 +02:00
Amund Eggen Svandal d01dfd4812 Requirements moved to setup.py 2020-08-17 18:25:50 +02:00
Amund Eggen Svandal 1f42f61620 Install minecraft in editable mode 2020-08-17 18:25:50 +02:00
Amund Eggen Svandal 4a8fab1138 Add requirements to setup.py 2020-08-17 18:25:50 +02:00
laundmo 2947aa6619
Added listener decorator documentation (#161)
* added decorator example to connection.rst

since decorators are more pythonic, it was put in front of the register method.

* Expanded listener decorator docstring

* changed autofunction to autodecorator

* removed whitespace in empty line

* remvoed trailing whitespace

i didn't even edit there WTF
2020-08-17 17:49:25 +02:00
joo 2d9479cc12 Change Travis config to use pypy3 instead of pypy 2020-08-17 12:01:26 +02:00
joo fcacb8abf8 Remove trailing space from join_game_and_respawn_packets.py 2020-08-17 12:01:03 +02:00
joo 3f4a5d46a6 Update README with supported versions; increment package version 2020-08-17 11:31:02 +02:00
joo 4c35517157 Fix support for Minecraft 20w06a to 1.16.2 (protocols 701 to 751) 2020-08-17 11:25:30 +02:00
joo b79f8b30eb Remove support for Python 2.7 2020-08-17 07:10:10 +02:00
joo 3723655fa3 Add .vscode to gitignore 2020-08-17 05:40:40 +02:00
joo 84df884ca4 Remove 'Packet.packet_id' attribute, replacing with 'id' 2020-08-17 05:39:48 +02:00
joo c6afe25429 Remove 'UUIDIntegerArray' type, as 'UUID' already exists 2020-08-16 05:40:13 +02:00
Sillyfrog 8a098b399b Support for v1.16.2 2020-08-13 22:19:04 +10:00
Sillyfrog 3dcefae645 v1.16.1 support 2020-06-26 17:46:33 +10:00
Sillyfrog 0d28271c96 v1.16 release support 2020-06-24 13:50:29 +10:00
Sillyfrog b582029099 Support for 1.16-rc1 2020-06-19 09:18:29 +10:00
Tristan Gosselin-Hane 7d9ffb8836 Fix line length 2020-05-22 14:05:45 -04:00
Tristan Gosselin-Hane e61cfffab1 Attempt to fix test? 2020-05-22 13:28:02 -04:00
Tristan Gosselin-Hane 1b714e6449 Update tests to use new join game packet definiton 2020-05-22 12:49:12 -04:00
Tristan Gosselin-Hane 0343df918c Fixed packet types 2020-05-22 12:49:02 -04:00
Tristan Gosselin-Hane 9c08c6c9f5 Added support for snapshots 20w20a and 20w20b 2020-05-14 21:37:25 -04:00
Tristan Gosselin-Hane d26aacec28 Added support for snapshot 20w19a 2020-05-07 11:58:08 -04:00
Tristan Gosselin-Hane 8cb02e7f7f Added support for snapshot 20w18a 2020-05-04 12:04:04 -04:00
Tristan Gosselin-Hane 180c698ce1 Added support for snapshot 20w17a 2020-04-22 16:36:19 -04:00
Tristan Gosselin-Hane 7380bc2c61 Added support for snapshots 20w13b, 20w14a, 20w15a, 20w16a 2020-04-17 00:07:41 -04:00
Tristan Gosselin-Hane 428a599f40 Added support for snapshot 20w13a 2020-04-16 20:54:51 -04:00
Tristan Gosselin-Hane 76f7b4bdc9 Added support for snapshot 20w12a 2020-03-20 15:00:31 -04:00
Tristan Gosselin-Hane 5c6edf5e44 Added support for snapshot versions up to 20w11a 2020-03-17 00:23:35 -04:00
joo ff9a0813b6 Add support for 1.15.2-pre1 to 1.15.2 (protocols 576 to 578) 2020-01-23 18:02:10 +01:00
joo b38adc1aa1 Add pre-release versions between 1.14.4 and 1.15.1; update test config 2020-01-08 16:15:42 +01:00
Sillyfrog 51c618aeb5 Support v1.15.1 2019-12-23 21:53:16 -05:00
Sillyfrog e1afabcba5 Add support for v1.15 2019-12-23 21:53:10 -05:00
joo 6f2f25656c
Merge pull request #135 from jimchen5209/1.14.4
Upgrade to 1.14.4 (Same Packet Format)
2019-08-16 02:03:42 +02:00
joodicator bbbd3fb195 Add pre-release versions for 1.14.3 and 1.14.4 2019-08-16 00:16:41 +02:00
Jim Chen 6f54e852d4
Update readme supported list 2019-07-26 15:09:47 +08:00
Jim Chen c80cfd50fe
Upgrade to 1.14.4(Same Packet Format) 2019-07-26 09:10:35 +08:00
joodicator 997f813a6c Add tests for string representation of PlayerListItemPacket (issue #133) 2019-07-03 19:54:15 +02:00
joodicator a03e1a7c47 Fix: MutableRecord interprets __slots__ incorrectly (issue #133) 2019-07-03 19:53:30 +02:00
joo e97458c970
Update README.rst 2019-06-25 15:14:11 +02:00
Sillyfrog d90f08c503 Update to v1.14.3 (same packet format) 2019-06-25 15:13:17 +02:00
joodicator e3d2b1a368 Improve metadata and auxiliary methods of existing packets.
* Add multi-attribute aliases to some packets, for user convenience.
* Add support for writing PlayerListItemPacket.
* Add 'fields' attributes to manually-read/written packet classes,
  implementing 'field_string' where appropriate to allow enable the
  default __repr__ implementation.
* Modify data constructors where appropriate so that __repr__
  implementations match their constructor protocols.
* Improve comments on type aliases within packet classes.
* Add/modify tests to cover the new functionality.
2019-06-08 15:39:24 +02:00
joodicator 234e57716c Increment package version to 0.6.0 2019-05-29 18:47:14 +02:00
joodicator d1e1da85c8 Declare support for 1.14.2 pre-releases 1-4 (481-484); update README 2019-05-29 18:26:06 +02:00
Sillyfrog 6d9d15845a Support for 1.14.2 (same packet format) 2019-05-29 15:37:25 +10:00
joo b83b33f8df
Merge pull request #125 from SirGhostal/new-packets
* Add Angle type for byte-encoded rotation angles.
* Fix: SpawnPlayerPacket and SpawnObjectPacket use wrong type for pitch/yaw.
* Add FacePlayerPacket and EntityLookPacket.
2019-05-18 03:04:59 +02:00
joodicator 1012ee8640 Revert: Add tests for Angle (were already present) 2019-05-18 02:54:21 +02:00
joodicator a3357762d7 Add tests for FacePlayerPacket, Angle; fix bugs; misc. changes
* Add alias FacePlayerPacket.target for x, y, z fields.
* Replace FacePlayerPacket.OriginPoint type alias with Origin and
  EntityOrigin aliases.
2019-05-18 02:36:36 +02:00
Zachy a6c11bbb34 Flake8: Correct E501 line too long (81 > 79 characters) 2019-05-17 21:56:32 +01:00
Zachy b0a9a3693c Remove white space 2019-05-17 21:45:21 +01:00
Zachy e8a0e34aef Feedback: Replace FeetEyes convention with OriginPoint for forward compat 2019-05-17 21:38:57 +01:00
Zachy 0b127da0ca Feedback: Correct Fixed Point conversion & take mod of angle send value 2019-05-17 21:31:33 +01:00
Zachy a60b513e74 Fix import order 2019-05-17 02:15:01 +01:00
Zachy 93f6d269da Fix tests to work with new Angle type@ 2019-05-17 02:09:27 +01:00
Zachy 22008c5c5c New line at end of file 2019-05-15 14:18:49 +01:00
Zachy 3c594a1386 Remove debugging print statement and make send static 2019-05-15 13:59:23 +01:00
Zachy 0fc8c3bbfe Add tests for new Angle type. 2019-05-15 13:52:13 +01:00
Zachy 7361f761f5 Implement type 'Angle', packet 'EntityLookPacket' and fix Packet Types
Remarks: I chose to implement an angle between 0-360 degrees as opposed to -180 - 180. linear transformation, the maths was far simpler converting an UnsignedByte into positive values instead of a Byte into negative and positive
2019-05-15 13:51:31 +01:00
Zachy d7b560a9f4 Implement FacePlayerPacket
Called when using the teleport chat command and you specify a facing parameter. `/teleport [<targets>] <x> <y> <z> facing`
2019-05-15 01:28:56 +01:00
joodicator 7b1567c352 Improve test coverage wrt protocol versions; other fixes/improvements
Improvements to the test suite:
* List release version names and numbers in minecraft/__init__.py.
* Make some tests, which previously ran for *all* protocol versions,
  run only for release protocol versions (to improve test performance).
* Make some tests, which previously ran only for the latest protocol
  version, run for all release protocol versions (to improve coverage).
* Print each protocol version being tested to the debug log, to help
  identify sources of errors.
* Use the `nose-timer' plugin to show the run time of each test.

Fix errors revealed by increased test coverage:
* Fix: SoundEffectPacket.Pitch is not serialised correctly for early
  protocol versions.
* Fix: handleExceptionTest finishes later than necessary because
  the test overrode an exception handler used internally by
  `_FakeServerTest', causing the server thread to time out after 4s.
* Add support for multiple exception handlers in `Connection'
  (required for the above).

Improvements to data descriptors:
* Make syntax of property declarations more consistent/Pythonic.
* Factor the definition of several aliasing properties into the
  utility methods `attribute_alias' and `multi_attribute_alias',
  which construct suitable data descriptors.
* Define and use the named tuple `Direction' for (pitch, yaw) values.
2019-05-14 18:41:58 +02:00
joodicator 6ef868bc5b Update README.rst 2019-05-13 22:36:36 +02:00
joodicator b3cf00a856 Add support for Minecraft 1.14.1 Pre Release 1 to 1.14.1 (protocols 478 to 480) 2019-05-13 22:25:40 +02:00
joo 6d62d3956a
Merge pull request #116 from Amund211/packets
Implements the sound effect and use item packets
2019-05-13 22:05:40 +02:00
joodicator d24b6eaded Update SoundEffectPacket and UseItemPacket to 1.14; misc improvements 2019-05-13 21:58:59 +02:00
joodicator faf02acf62 Merge branch 'master' into packets 2019-05-13 21:28:02 +02:00
joodicator e4f8b5583a Fix: networking.types.utility.__all__ is incorrect. 2019-05-13 21:24:41 +02:00
joo 4956d5e70d
Merge pull request #103 from SirGhostal/patch-2 2019-05-13 19:14:37 +02:00
joodicator 41ea36c642 Add test coverage for @listener. 2019-05-13 19:04:35 +02:00
joodicator 24ca96accb Merge branch 'master' into patch-2 2019-05-13 19:02:23 +02:00
joo 7ae6d599fb
Merge pull request #102 from SirGhostal/patch-1
Add RespawnPacket and ServerDifficultyPacket.
2019-05-13 18:39:20 +02:00
joodicator bf719611ec Update RespawnPacket and ServerDifficultyPacket to 1.14. 2019-05-13 18:23:05 +02:00
joo d627423949 Merge branch 'master' into patch-1 2019-05-13 18:02:01 +02:00
joodicator f248006b86 Fix: doc build fails due to unused 'sphinx.ext.pngmath'. 2019-05-11 09:28:57 +02:00
joodicator 612fa8e324 Add support for Minecraft 18w43a to 1.14 (protocols 441 to 477)
This commit introduces two backward-incompatible changes which may break
existing code:

(1) `networking.packets.clientbound.play.SpawnObjectPacket.EntityType'
is no longer accessible as an attribute of the the `SpawnObjectPacket'
class: the values now depend on a `ConnectionContext`, and must be
accessed through an instance, or using `SpawnObjectPacket.field_enum'.
See the text of the `AttributeError` raised from the descriptor for
`SpawnObjectPacket.EntityType` for the full details.

(2) For some subclasses of `networking.types.Type', it is necessary to
call the methods `read_with_context' and `send_with_context' instead of
`read' and `send', supplying a `ConnectionContext' for those data types
- currently only `Position` - whose layout depends on it.
2019-05-11 08:43:51 +02:00
Amund Eggen Svandal 1a1b9803f8 Edit definition of `SoundEffectPacket` 2019-01-13 23:05:51 +00:00
Amund Eggen Svandal 56d1300db1 Updated id of UseItemPacket 2019-01-13 22:06:39 +00:00
L1LxHa 9b43d6f004 Fix hanging indefinitely while making auth-related requests (#117) 2019-01-04 20:22:42 -05:00
Ammar Askar b4c58477f4 Fixes for flake8 2019-01-04 20:12:07 -05:00
Ammar Askar 6adefa8c75 Add test for new invalidate_previous functionality 2019-01-04 19:59:45 -05:00
Amund Eggen Svandal bea661860d Add Use Item packet 2019-01-02 01:38:53 +01:00
Amund Eggen Svandal e21c0d877f Implement the Sound Effect packet
Information gathered from https://wiki.vg/Protocol_version_numbers.
Due to some difficulties the change from "sound_id" to "sound_name" and
the re-implementation of "sound_category" in the packet may be off by
some protocol versions.
2019-01-02 01:38:14 +01:00
Amund Eggen Svandal c67652d7e8 Add option to invalidate previous `access_token`s to `authenticate`
This changes the default behaviour to include `self.client_token`
when using `authenticate`. If `self.client_token` is `None`, a new
token is generated using uuid4 (like the vanilla client does).
2019-01-02 01:16:02 +01:00
Tristan Gosselin-Hane 316ea4d63d Implemented Player List Header And Footer Packet 2018-11-12 21:22:55 +01:00
joo 527f3d3146 Add support for Minecraft 1.13.2-pre1, 1.13.2-pre2 and 1.13.2 (protocols 402 to 404). 2018-10-26 19:58:20 +01:00
joo 48e1003f42 Fix issue #109 and add regression test. 2018-10-12 17:07:04 +01:00
Billy SU eb302094aa Fix typo of arbitary to arbitrary 2018-10-08 18:46:15 +02:00
joo 0eec179f48 Add support for Minecraft 1.13.1 and 1.13.1-pre2 (protocols 400 to 401). 2018-08-23 07:42:28 +01:00
joo 720868fab7 Add support for Minecraft 18w80a to 1.13.1-pre1 (protocols 394 to 399). 2018-08-19 18:11:12 +01:00
Zachy24 103b53a97a Change case on GameMode 2018-08-15 22:29:18 +01:00
Zachy 409c619eb0
return method 2018-08-15 20:53:13 +01:00
Zachy 6d6a592f07
Add decorator for register_packet_listener() 2018-08-13 01:57:16 +01:00
Zachy24 da103c6d3c Oops 2018-08-13 01:35:34 +01:00
Zachy24 4ba6a40df6 Add aliases for Enums in Packet Definitions 2018-08-13 00:41:21 +01:00
Zachy aeaf7b5bcb
Import new enums into Packet Definition 2018-08-12 23:12:45 +01:00
Zachy ed85cb793a
Implement Enums for Difficulty/Dimension/Gamemode 2018-08-12 23:07:07 +01:00
Zachy 0198476fa9
Fix packet id for protocol versions 47 and 69. 2018-08-12 22:56:16 +01:00
Zachy e840fab267
Update __init__.py 2018-08-12 11:04:40 +01:00
Zachy 1a114c1b95
Implement clientbound.play.ServerDifficultyPacket 2018-08-12 10:47:50 +01:00
Zachy d20344cac1
Implement clientbound.play.RespawnPacket 2018-08-12 10:39:11 +01:00
joo f6f6511788 Remove dead code from connection.py. 2018-07-19 12:21:49 +01:00
joo adc8d15ddc Add support for Minecraft 1.13 and 1.13-pre3 to pre10 (protocols 385 to 393).
Add clientbound.login.PluginRequestPacket and serverbound.login.PluginResponsePacket.
2018-07-19 09:50:13 +01:00
joo 745aa054b0 Add minecraft.networking.types to setup.py to fix #97. 2018-06-25 15:20:05 +01:00
joo bea2222c58 Fix: MutableRecord.__ne__ misspelt as '__neq__'.
Add tests for MutableRecord and Position.
2018-06-21 07:06:45 +01:00
joo 4b6feda1cb Various improvements to utility types:
- Add operations for Vector.
- Move some tests into test_utility_types.py.
- Add tests for PositionAndLook and Vector.
2018-06-21 06:39:55 +01:00
joo 61598eba75 Divide minecraft.networking.types into multiple modules. 2018-06-20 09:54:17 +01:00
joo 201e075591 Add support for Minecraft pre-release 1.13-pre2 (protocol 384). 2018-06-20 09:50:20 +01:00
Zachy d3a8cc8dfb Implement New Type. FixedPointInteger. (#93)
Fix: SpawnPlayerPacket coordinates read wrongly before protocol 100. Add types.FixedPointInteger.
2018-06-20 05:32:35 +01:00
joo 0a1776f97a Add support for Minecraft versions 18w22a to 1.13-pre1 (protocols 380 to 383). 2018-06-05 01:14:19 +01:00
joo d36a4170ed Add tests for various Connection edge cases. 2018-05-29 01:14:46 +01:00
joo d36b652b69 Fix: reconnecting from an exception handler does not work. 2018-05-29 01:14:23 +01:00
joo c01f194d06 Raise exception on login disconnect instead of silently stopping. 2018-05-28 17:42:08 +01:00
joo db714f9490 Fix: MapPacket.write_fields() is incorrect. 2018-05-27 17:12:50 +01:00
joo 8578326c2f Add serialisation and tests for SpawnObjectPacket. 2018-05-27 15:36:13 +01:00
joo 709b80b539 Add serialisation and tests for Explosion, {Multi,}BlockChange, and CombatEvent packets. 2018-05-27 13:28:01 +01:00
joo 92f2eff681 Add several tests for the Connection class. 2018-05-27 07:40:13 +01:00
joo ab9ca6dfee Add full connection tests with encryption enabled. 2018-05-27 03:30:43 +01:00
joo ebee077303 Update README.rst. 2018-05-26 20:49:01 +01:00
joo f22447b97a Add support for Minecraft snapshot 18w21b (protocol 379). 2018-05-25 19:35:32 +01:00
joo bbf7200220 Add support for Minecraft snapshot 18w21a (protocol 378). 2018-05-24 12:59:06 +01:00
joo 52c0671f4f Add support for Minecraft snapshots 18w03a-18w20c (protocol 354-377). 2018-05-20 06:58:23 +01:00
joo bca783115c start.py: allow IPv6 addresses to be given in square brackets. 2018-05-18 12:27:06 +01:00
joo 67344f2ceb Add IPv6 support to Connnection. 2018-05-18 12:26:47 +01:00
joo 19a82f51ef Add serverbound.play.PlayerBlockPlacementPacket. 2018-05-18 06:01:31 +01:00
gurland c584f29154 Position type fix. Add new PlayerBlockPlacementPacket 2018-05-18 08:50:59 +01:00
joo da46c4553d Remove support for Python 3.3. Add Python 3.6 to autotests as default version. 2018-05-18 06:00:35 +01:00
joo c90afe4424 Fix: networking.types.Position.send() doesn't accept a Position. 2018-05-17 06:08:54 +01:00
joo 38fa39a236 Extract Hand enum classes to minecraft.networking.types. 2018-03-02 02:07:25 +00:00
joo ae0a3b3989
Merge pull request #76 from TheSnoozer/packet-name-issue
fix AttributeError: 'Packet' object has no attribute 'name'
2018-01-21 17:40:24 +00:00
TheSnoozer 5c0c95068f fix AttributeError: 'Packet' object has no attribute 'name' (should be 'packet_name' - see https://github.com/ammaraskar/pyCraft/blob/master/minecraft/networking/packets/packet.py#L9) 2018-01-20 19:45:41 -05:00
joo ece90fcd9d Fix: MultiBlockChangePacket reads y_coordinate wrongly. 2018-01-13 17:04:39 +00:00
joo 0ec2398fb4 Fix: test_authenticate_wrong_credentials is not marked as an Internet test. 2018-01-13 02:37:28 +00:00
joo bfaabcad58 Increase maxDiff for test_authenticate_wrong_credentials. 2018-01-13 01:57:59 +00:00
joo f492adfeff Add support for Minecraft snapshots 17w43a-18w02a (protocol 341-353).
Add support declaration for Minecraft version 1.8.9 (protocol 47).
2018-01-13 01:12:28 +00:00
joo 258c1f2566 Fix: some subpackages are missing from setup.py. 2018-01-13 01:02:47 +00:00
joo da7c13076f Fix: client.handshake.get_packets returns a dict instead of a set. 2018-01-13 01:02:00 +00:00
joo 1766b30983 Remove duplicates from SUPPORTED_PROTOCOL_VERSIONS. 2018-01-07 23:56:31 +00:00
joo ec4d04c530
Update README.rst 2018-01-06 19:51:10 +00:00
joo 8301f714d6
Merge pull request #74 from TheSnoozer/master
merging branch issue70 into master + minor fix for ClientSettingsPacket
2018-01-06 19:46:16 +00:00
joo 979468b4f1 test_authenticate_wrong_credentials: compare exception string instead of yggdrasil_message, so failure message is more useful in case the latter is None. 2018-01-06 19:25:55 +00:00
joo 53312f997b tests/fake_server.py: use "except Exception" instead of bare except clauses. 2018-01-06 19:24:22 +00:00
joo 3fb922b0d1 Require cryptography<2.0 for Python 3.3, as >=2.0 only supports 3.4+. 2018-01-06 19:22:55 +00:00
TheSnoozer 821dad72ca Merge remote-tracking branch 'upstream/testing' 2018-01-03 23:53:58 -05:00
TheSnoozer 860628f64b the main hand attribute for ClientSettingsPacket was added in 15w31a [Protocol Version is now 49] and causes the client to send more data than the server expects which result in a instant disconnect while connecting to a 1.8.8 server 2017-11-19 23:22:00 -05:00
joo af559e181a Remove limits on number of packets read/written per tick.
This addresses possible memory leaks or crashes caused by overflowing packet backlogs.
2017-10-10 04:47:50 +01:00
joo 88a5fdc637 Merge branch 'master' into testing 2017-09-24 06:36:52 +01:00
joo ca100a5b1f start.py: cosmetic improvements. 2017-09-24 05:51:28 +01:00
joo c5bd055fa0 Merge pull request #72 from TheSnoozer/master
support 1.12.2
2017-09-20 19:49:26 +01:00
TheSnoozer 61d9695226 support 1.12.2 2017-09-19 18:30:51 -04:00
joo e9f095de42 Add ClientSettingsPacket and PluginMessagePacket.
Improve Packet string representation.
2017-08-24 05:49:32 +01:00
joo 3269a022a8 Add KeepAlivePacket test to ConnectTest and derived tests. 2017-08-22 18:16:07 +01:00
joo b79e7b5f28 Add tests for early and outgoing packet listeners. 2017-08-22 17:50:16 +01:00
joo 9497aae8fa Make FakeServer class more reusable, and extract it into its own module. 2017-08-22 14:22:12 +01:00
joo f1d04e6610 start.py: add --dump-packets option. 2017-08-21 21:06:39 +01:00
joo 593c98f168 Add support for early and outgoing packet listeners. 2017-08-21 21:06:28 +01:00
joo 9765e936c9 Fix incorrect packet IDs for PlayerPositionAndLookPacket for old protocol versions. 2017-08-20 07:35:14 +01:00
joo 46e058dd08 Update all tests, docs and code to use new packet names. 2017-08-09 20:53:54 +01:00
joo f1ae765458 Add docstring about legacy packet names to packets module. 2017-08-09 19:32:07 +01:00
Ammar Askar 4a7d06c3cf
Add proper matrix for travis 2017-08-09 12:40:09 -04:00
Ammar Askar 997a59efb0
Revert "List out the python versions we need for travis"
This reverts commit ef790d2f08.
2017-08-09 12:33:05 -04:00
Ammar Askar ef790d2f08 List out the python versions we need for travis 2017-08-09 12:31:34 -04:00
joo 304f08bf8c Configure Travis to use "dist: precise" to allay build errors. 2017-08-09 17:04:03 +01:00
TheSnoozer 61b07f52f2 better packet names to match new packet structure 2017-08-09 16:33:41 +01:00
TheSnoozer 64cf23436b the class 'Type' needs to have '__slots__ = ()', otherwise every instance of 'Position' will have an unnecessary empty '__dict__' allocated 2017-08-09 16:33:41 +01:00
TheSnoozer 2f0dbf5cbb fix flake error as suggested 2017-08-09 16:33:41 +01:00
TheSnoozer 7eef61bfde use 'zlib.decompressobj' to handle the 'faulty' compression correctly as suggested in https://github.com/ammaraskar/pyCraft/pull/68 2017-08-09 16:33:41 +01:00
TheSnoozer 0c64623696 fix wrong import; note we don't need to consider legacy import inside the packets module since this packet just got added before the structure changed 2017-08-09 16:33:41 +01:00
TheSnoozer ab71aeeb7d Revert "deal with faulty compression's"
This reverts commit 357340e6dc.
2017-08-09 16:33:41 +01:00
TheSnoozer 346b3081ec fix broken tests 2017-08-09 16:33:41 +01:00
TheSnoozer 4a508f935b use namedtuple for position type and use it as subclass for ClientExplosion.Record 2017-08-09 16:33:41 +01:00
TheSnoozer 8552c6efe5 regorganize packet structure as outlined in https://github.com/ammaraskar/pyCraft/pull/68 2017-08-09 16:33:41 +01:00
TheSnoozer f8781c19c8 The packet IDs of Entity Velocity (0x3E), Update Health (0x41), Combat Event (0x2D), and Client Status (0x03) changed in [protocol 336 (snapshot 17w31a)](http://wiki.vg/index.php?title=Pre-release_protocol&oldid=13265) 2017-08-09 16:33:41 +01:00
TheSnoozer 6137436d03 feedback: add write method 2017-08-09 16:33:41 +01:00
TheSnoozer 5349ff2730 feedback: we can use enum since subclass packets do no vary depending on the type id 2017-08-09 16:33:41 +01:00
TheSnoozer d8fc742862 feedback: pass x, y, z as arguments since types classes are supposed to abstract away the python types and sending them over the network and calling an encode method before sending breaks that a little. 2017-08-09 16:33:41 +01:00
TheSnoozer 9ab2e1ae69 fix tox -e pylint-errors 2017-08-09 16:33:41 +01:00
TheSnoozer fdb5a0bb72 fix tox -e flake 2017-08-09 16:33:41 +01:00
TheSnoozer cc466bb0ea fixed an issue with mc 1.8.8 where velocity is not being sent and self.data is negative 2017-08-09 16:33:41 +01:00
TheSnoozer 6fbf75203c add Multi Block Change (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer 93227e26fa add Block Change Packet (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer ca30ff2e74 add spawn object (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer 3ad5d1abd5 add Explosion Packet (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer 81f2ae4070 add Combat Event Packet (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer fecb1d10e9 add Client Status Packet and allow client to respawn when issuing '/respawn' 2017-08-09 16:33:41 +01:00
TheSnoozer 0dc333237b add Update Health Packet (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer 6f52ceac0d add Entity Velocity Packet (client bound) 2017-08-09 16:33:41 +01:00
TheSnoozer 9caff502ca add ClientSpawnPlayer packet 2017-08-09 16:33:41 +01:00
TheSnoozer 42ede3f83d deal with faulty compression's 2017-08-09 16:33:41 +01:00
joo 89a1bfb796 Update README.rst 2017-08-04 16:15:55 +01:00
joo 9e7e75f9a7 Increment package version to 0.5.0. 2017-08-03 19:08:36 +01:00
joo 33cd42848e Add version data for Minecraft 1.12.1 (protocol 338). 2017-08-03 18:48:36 +01:00
joo 5aa2d3df59 Add support for Minecraft 17w31a and 1.12.1-pre1 (protocols 336 and 337). 2017-08-03 18:31:00 +01:00
joo cf464d2da2 Add compression tests to test_connection. 2017-08-03 13:04:47 +01:00
joo cab8d56746 Revert "Remove unnecessary fileno method from FileObjectWrapper"
This partially addresses issue #65.

This reverts commit c87d7bc6f3.
2017-07-18 13:39:56 +01:00
Ammar Askar f450ef5ff4
Add test for reactors 2017-07-16 20:02:50 -07:00
Ammar Askar d686b6487f
Add testing for MapPacket 2017-07-16 15:18:09 -07:00
Ammar Askar 5b261b840e
Refactor out the action of writing out the packet header.
This allows subclasses of Packet to just call the new
method instead of having to duplicate the header writing
and compression code.
2017-07-16 13:40:00 -07:00
Ammar Askar 4ce8c7f6ca
Fix flake errors in test file 2017-07-16 02:53:52 -07:00
Ammar Askar 8859e0f7bf
Add test coverage for PlayerList packet 2017-07-16 02:42:16 -07:00
Ammar Askar da967a4e56
Minor coverage improvement for packets 2017-07-16 01:20:08 -07:00
Ammar Askar c87d7bc6f3
Remove unnecessary fileno method from FileObjectWrapper 2017-07-16 01:03:47 -07:00
Ammar Askar d8226d266f
Improve tests of types.py 2017-07-16 01:00:14 -07:00
Ammar Askar ca4fd6680e
Connect to localhost instead of the socket's binding address.
The bound address is 0.0.0.0 which usually implies all
available interfaces, which makes sense when listening
for something. However, when connecting to an address,
a specific address needs to be targeted. Hopefully, any
properly configured computer should have `localhost`
pointing to its loopback interface. Fixes #64
2017-07-16 00:19:30 -07:00
joo 2cf1d3cb03 Fix incorrect packet ID for PlayerPositionAndLookPacket.
Fix: PlayerPositionAndLookPacket.apply() does not correctly restrict angles.
2017-07-03 11:32:52 +01:00
joo 991f0b3da6 Increment package version to 0.4.0. 2017-06-09 09:25:56 +01:00
joo 5b5f36048c Update README.rst. 2017-06-08 06:52:46 +01:00
joo ece5fd903d Fix incorrect packet ID for MapPacket. 2017-06-08 06:10:40 +01:00
joo 8d1dcec3e2 Add version data for Minecraft 1.12 (protocol 335). 2017-06-07 22:06:01 +01:00
joo bcf22b8312 Add version data for Minecraft pre-release 1.12-pre7 (protocol 334). 2017-06-02 21:32:11 +01:00
joo 0cc96f7dc5 Add version data for Minecraft pre-release 1.12-pre6 (protocol 333). 2017-05-29 17:31:04 +01:00
joo b7290cf327 Add support for Minecraft pre-release 1.12-pre5 (protocol 332). 2017-05-20 05:27:23 +01:00
joo a1570bd3a9 Add version data for Minecraft pre-releases 1.12-pre3 and 1.12-pre4 (protocols 330 and 331). 2017-05-19 12:22:42 +01:00
joo 3f4571d9e9 Update testing configuration:
(1) Add py35 job to Travis.
(2) To address issue #57: run tests that connect to Mojang's auth server exactly once, during the py35 job.
(3) Measure coverage exactly once, during the py35 job; always submit the result to coveralls.
(4) Fix miscellaneous errors in generate_travis_yml.py.
2017-05-19 11:58:14 +01:00
joo 19cdf80952 Require cryptography>=1.5 -- see issue #60. 2017-05-19 10:43:51 +01:00
joo 028ef3f802 Add version data for Minecraft pre-release 1.12-pre2 (protocol 329). 2017-05-12 00:52:31 +01:00
joo 0d42c18211 Add version data for Minecraft pre-release 1.12-pre1 (protocol 328). 2017-05-11 10:16:11 +01:00
Nigel Todman 0ffb08327a Add version data for Minecraft snapshot 17w18b (protocol 327).
(Squash and merge pull request #59: Added 17w18b)
2017-05-05 08:11:59 +01:00
joo 623d2f00c9 Merge pull request #58 from Veritas83/patch-1
Added 17w18a
2017-05-04 06:06:33 +01:00
Nigel Todman 5805d6e476 Added 17w18a
Added 17w18a
2017-05-03 23:23:19 -04:00
joo e2c4c97ea5 Add version data for Minecraft snapshots 17w16a-17w17b (protocols 322-325). 2017-04-28 20:00:26 +01:00
joo a77092572c Update README.rst 2017-04-17 11:07:22 +01:00
joo 9e369cb938 Update README.rst. 2017-04-16 05:06:12 +01:00
joo b1edff913b Fix comment spacing in previous commit. 2017-04-16 04:19:42 +01:00
joo df9171edd1 Suppress erroneous Pylint not-context-manager errors.
See: https://github.com/PyCQA/pylint/issues/782
2017-04-16 03:58:35 +01:00
joo 3981c46569 Add version data for Minecraft snapshot 17w15a (protocol 321). 2017-04-16 01:47:48 +01:00
joo f27689f729 Add version data for Minecraft snapshots 17w13b and 17w14a (protocols 319 and 320). 2017-04-06 17:25:25 +01:00
joo 8eb1cdeee7 start.py: add hashbang and make executable. 2017-04-06 13:45:26 +01:00
joo e99d2a4ef5 Add documentation for YggdrasilError. 2017-03-31 14:33:51 +01:00
joo bc260b0a91 Add Python 3.6 to supported Python versions. 2017-03-31 13:16:35 +01:00
joo 66a0603acf Fix various problems in minecraft.authentication and its tests:
- Return value of _make_request() is treated as a requests.Request, when it is in fact a requests.Response.
- Some tests in test_authentication use assertRaises() incorrectly, resulting in testing code that never gets run.
- Other miscellaneous errors exposed by the above changes.

Additionally:
- YggdrasilError instances now have fields with specific error information, and _raise_from_response() populates them. (This will be useful for later changes.)
2017-03-31 12:59:43 +01:00
joo 73672401ef Add support for Minecraft snapshot 17w13a (protocol 318). 2017-03-31 08:59:14 +01:00
joo b0f15ed5a2 Add version data for Minecraft snapshot 17w06a (protocol 317). 2017-03-31 08:27:04 +01:00
joo 7fd37a79f2 Add version data for Minecraft 1.11.2 (protocol 316). 2016-12-21 17:06:35 +00:00
joo 00ab1b4209 Add version data for Minecraft 1.11.1 (protocol 316). 2016-12-20 16:10:58 +00:00
Ammar Askar 89ca73532a
Fully cover authentication module 2016-12-19 06:57:45 -05:00
Ammar Askar 10fb291752
Add some additional tests for the authentication module 2016-12-19 06:39:01 -05:00
Ammar Askar 0c31e748e8
Fix flake error caused by doc change.
Love that 79 character line limit >.>
2016-12-19 05:41:28 -05:00
Ammar Askar 9aa369c7da
Fix outdated documentation 2016-12-19 05:26:12 -05:00
joo f560f73df8 Add support for Minecraft snapshot 16w50a (protocol 316). 2016-12-16 08:01:26 +00:00
64 changed files with 7394 additions and 1795 deletions

3
.gitignore vendored
View File

@ -81,5 +81,8 @@ target/
# sftp configuration file
sftp-config.json
### Visual Studio
.vscode
### pyCraft ###
credentials

View File

@ -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

View File

@ -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.

14
bin/generate_travis_yml.py Normal file → Executable file
View File

@ -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")

View File

@ -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)

View File

@ -34,7 +34,6 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode',
]

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.
"""

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
"""
Contains the clientbound packets for `pyminecraft`.
"""

View File

@ -0,0 +1,3 @@
# Formerly known as state_handshake_clientbound.
def get_packets(context):
return set()

View File

@ -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}]

View File

@ -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 {},
]

View File

@ -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')

View File

@ -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}]

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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}]

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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}]

View File

@ -0,0 +1,3 @@
"""
Contains the serverbound packets for `pyminecraft`.
"""

View File

@ -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}]

View File

@ -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)

View File

@ -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}
]

View File

@ -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

View File

@ -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}]

View File

@ -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)))

View File

@ -0,0 +1,3 @@
from .basic import * # noqa: F401, F403
from .enum import * # noqa: F401, F403
from .utility import * # noqa: F401, F403

View File

@ -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)

View File

@ -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

View File

@ -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')

188
minecraft/utility.py Normal file
View File

@ -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)

View File

@ -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 .

View File

@ -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"

92
start.py Normal file → Executable file
View File

@ -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()

View File

@ -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

640
tests/fake_server.py Normal file
View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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!")

View File

@ -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__)

View File

@ -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)

110
tests/test_reactors.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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
View File

@ -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