diff --git a/poetry.lock b/poetry.lock index 93f5f0c..60989c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -43,6 +43,17 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.0.4" @@ -63,6 +74,25 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "cryptography" +version = "36.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "flake8" version = "4.0.1" @@ -204,6 +234,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pyflakes" version = "2.4.0" @@ -245,6 +283,21 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.18.1" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] + [[package]] name = "tomli" version = "2.0.1" @@ -284,7 +337,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "8efb788c265532dfdab7851959f459d35b1e58baf5aafbb9066c614730e9f670" +content-hash = "3750687b24a352b625441ebaf1d20726351e3ebfc592fb386e4491455582ae49" [metadata.files] atomicwrites = [ @@ -320,6 +373,58 @@ black = [ {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, ] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] click = [ {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, @@ -328,6 +433,28 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +cryptography = [ + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -391,6 +518,10 @@ pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, @@ -403,6 +534,10 @@ pytest = [ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, ] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, + {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, diff --git a/pymine_net/__init__.py b/pymine_net/__init__.py index 50dd981..ad07e28 100644 --- a/pymine_net/__init__.py +++ b/pymine_net/__init__.py @@ -1,4 +1,5 @@ import pymine_net.types.nbt as nbt # noqa: F401 +from pymine_net.net.asyncio.client import AsyncTCPClient # noqa: F401 from pymine_net.packets import load_packet_map # noqa: F401 from pymine_net.types.buffer import Buffer # noqa: F401 from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket # noqa: F401 diff --git a/pymine_net/net/asyncio/__init__.py b/pymine_net/net/asyncio/__init__.py new file mode 100644 index 0000000..9a0fe40 --- /dev/null +++ b/pymine_net/net/asyncio/__init__.py @@ -0,0 +1,3 @@ +from .client import AsyncTCPClient # noqa: F401 +from .server import AsyncProtocolServer, AsyncProtocolServerClient # noqa: F401 +from .stream import AsyncTCPStream, EncryptedAsyncTCPStream # noqa: F401 diff --git a/pymine_net/net/asyncio/client.py b/pymine_net/net/asyncio/client.py new file mode 100644 index 0000000..78defee --- /dev/null +++ b/pymine_net/net/asyncio/client.py @@ -0,0 +1,34 @@ +import asyncio +from typing import Union + +from pymine_net.net.asyncio.stream import AsyncTCPStream +from pymine_net.net.client import AbstractTCPClient +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + +__all__ = ("AsyncTCPClient",) + + +class AsyncTCPClient(AbstractTCPClient): + """An async connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.stream: AsyncTCPStream = None + + async def connect(self) -> None: + _, writer = await asyncio.open_connection(self.host, self.port) + self.stream = AsyncTCPStream(writer) + + async def close(self) -> None: + self.stream.close() + await self.stream.wait_closed() + + async def read_packet(self) -> ClientBoundPacket: + packet_length = await self.stream.read_varint() + return self._decode_packet(await self.stream.readexactly(packet_length)) + + async def write_packet(self, packet: ServerBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) + await self.stream.drain() diff --git a/pymine_net/net/asyncio/server.py b/pymine_net/net/asyncio/server.py new file mode 100644 index 0000000..e9f6e29 --- /dev/null +++ b/pymine_net/net/asyncio/server.py @@ -0,0 +1,53 @@ +import asyncio +from typing import Dict, Tuple, Union + +from pymine_net.net.asyncio.stream import AsyncTCPStream +from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient +from pymine_net.strict_abc import abstract +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + +__all__ = ("AsyncProtocolServerClient", "AsyncProtocolServer") + + +class AsyncProtocolServerClient(AbstractProtocolServerClient): + def __init__(self, stream: AsyncTCPStream, packet_map: PacketMap): + super().__init__(stream, packet_map) + self.stream = stream # redefine this cause typehints + + async def read_packet(self) -> ServerBoundPacket: + length = await self.stream.read_varint() + return self._decode_packet(await self.stream.readexactly(length)) + + async def write_packet(self, packet: ClientBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) + await self.stream.drain() + + +class AsyncProtocolServer(AbstractProtocolServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], AsyncProtocolServerClient] = {} + + self.server: asyncio.AbstractServer = None + + async def run(self) -> None: + self.server = await asyncio.start_server(self._client_connected_cb, self.host, self.port) + + async def close(self) -> None: + self.server.close() + await self.server.wait_closed() + + async def _client_connected_cb( + self, _: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + client = AsyncProtocolServerClient(AsyncTCPStream(writer), self.packet_map) + + self.connected_clients[client.stream.remote] = client + + await self.new_client_connected(client) + + @abstract + async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: + pass diff --git a/pymine_net/net/asyncio/stream.py b/pymine_net/net/asyncio/stream.py new file mode 100644 index 0000000..b136968 --- /dev/null +++ b/pymine_net/net/asyncio/stream.py @@ -0,0 +1,93 @@ +import struct +from asyncio import StreamWriter +from typing import Tuple, Union + +from cryptography.hazmat.primitives.ciphers import Cipher + +from pymine_net.net.stream import AbstractTCPStream +from pymine_net.types.buffer import Buffer + +__all__ = ("AsyncTCPStream", "EncryptedAsyncTCPStream") + + +class AsyncTCPStream(AbstractTCPStream, StreamWriter): + """Used for reading and writing from/to a connected client, merges functions of a StreamReader and StreamWriter. + + :param StreamReader reader: An asyncio.StreamReader instance. + :param StreamWriter writer: An asyncio.StreamWriter instance. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + """ + + def __init__(self, writer: StreamWriter): + super().__init__(writer._transport, writer._protocol, writer._reader, writer._loop) + + self.remote: Tuple[str, int] = self.get_extra_info("peername") + + async def read(self, length: int = -1) -> Buffer: + return Buffer(await self._reader.read(length)) + + async def readline(self) -> Buffer: + return Buffer(await self._reader.readline()) + + async def readexactly(self, length: int) -> Buffer: + return Buffer(await self._reader.readexactly(length)) + + async def readuntil(self, separator: bytes = b"\n") -> Buffer: + return Buffer(await self._reader.readuntil(separator)) + + async def read_varint(self) -> int: + value = 0 + + for i in range(10): + (byte,) = struct.unpack(">B", await self.readexactly(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + + +class EncryptedAsyncTCPStream(AsyncTCPStream): + """An encrypted version of an AsyncTCPStream, automatically encrypts and decrypts outgoing and incoming data. + + :param AsyncTCPStream stream: The original, stream-compatible object. + :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. + :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. + :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. + """ + + def __init__(self, stream: AsyncTCPStream, cipher: Cipher): + super().__init__(stream) + + self.decryptor = cipher.decryptor() + self.encryptor = cipher.encryptor() + + async def read(self, length: int = -1) -> Buffer: + return Buffer(self.decryptor.update(await super().read(length))) + + async def readline(self) -> Buffer: + return Buffer(self.decryptor.update(await super().readline())) + + async def readexactly(self, length: int) -> Buffer: + return Buffer(self.decryptor.update(await super().readexactly(length))) + + async def readuntil(self, separator: bytes = b"\n") -> Buffer: + return Buffer(self.decryptor.update(await super().readuntil(separator))) + + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().write(self.encryptor.update(data)) + + def writelines(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().writelines(self.encryptor.update(data)) diff --git a/pymine_net/net/client.py b/pymine_net/net/client.py new file mode 100644 index 0000000..195204a --- /dev/null +++ b/pymine_net/net/client.py @@ -0,0 +1,72 @@ +import zlib +from typing import Type, Union + +from pymine_net.enums import GameState, PacketDirection +from pymine_net.errors import UnknownPacketIdError +from pymine_net.strict_abc import StrictABC, abstract +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class AbstractTCPClient(StrictABC): + """Abstract class for a connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + self.host = host + self.port = port + self.protocol = protocol + self.packet_map = packet_map + + self.state = GameState.HANDSHAKING + self.compression_threshold = -1 + + @abstract + def connect(self) -> None: + pass + + @abstract + def close(self) -> None: + pass + + @staticmethod + def _encode_packet(packet: ServerBoundPacket, compression_threshold: int = -1) -> Buffer: + """Encodes and (if necessary) compresses a ServerBoundPacket.""" + + buf = Buffer().write_varint(packet.id).extend(packet.pack()) + + if compression_threshold >= 1: + if len(buf) >= compression_threshold: + buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) + else: + buf = Buffer().write_varint(0).extend(buf) + + return Buffer().write_varint(len(buf)).extend(buf) + + def _decode_packet(self, buf: Buffer) -> ClientBoundPacket: + # decompress packet if necessary + if self.compression_threshold >= 0: + uncompressed_length = buf.read_varint() + + if uncompressed_length > 0: + buf = Buffer(zlib.decompress(buf.read_bytes())) + + packet_id = buf.read_varint() + + # attempt to get packet class from given state and packet id + try: + packet_class: Type[ClientBoundPacket] = self.packet_map[ + PacketDirection.CLIENTBOUND, self.state, packet_id + ] + except KeyError: + raise UnknownPacketIdError(None, self.state, packet_id, PacketDirection.CLIENTBOUND) + + return packet_class.unpack(buf) + + @abstract + def read_packet(self) -> ClientBoundPacket: + pass + + @abstract + def write_packet(self, packet: ServerBoundPacket) -> None: + pass diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py new file mode 100644 index 0000000..3429e8e --- /dev/null +++ b/pymine_net/net/server.py @@ -0,0 +1,84 @@ +import zlib +from typing import Dict, Tuple, Type, Union + +from pymine_net.enums import GameState, PacketDirection +from pymine_net.errors import UnknownPacketIdError +from pymine_net.net.stream import AbstractTCPStream +from pymine_net.strict_abc import StrictABC, abstract +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class AbstractProtocolServerClient(StrictABC): + __slots__ = ("stream", "packet_map", "state", "compression_threshold") + + def __init__(self, stream: AbstractTCPStream, packet_map: PacketMap): + self.stream = stream + self.packet_map = packet_map + self.state = GameState.HANDSHAKING + self.compression_threshold = -1 + + def _encode_packet(self, packet: ClientBoundPacket) -> Buffer: + """Encodes and (if necessary) compresses a ClientBoundPacket.""" + + buf = Buffer().write_varint(packet.id).extend(packet.pack()) + + if self.compression_threshold >= 1: + if len(buf) >= self.compression_threshold: + buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) + else: + buf = Buffer().write_varint(0).extend(buf) + + buf = Buffer().write_varint(len(buf)).extend(buf) + return buf + + def _decode_packet(self, buf: Buffer) -> ServerBoundPacket: + # decompress packet if necessary + if self.compression_threshold >= 0: + uncompressed_length = buf.read_varint() + + if uncompressed_length > 0: + buf = Buffer(zlib.decompress(buf.read_bytes())) + + packet_id = buf.read_varint() + + # attempt to get packet class from given state and packet id + try: + packet_class: Type[ClientBoundPacket] = self.packet_map[ + PacketDirection.SERVERBOUND, self.state, packet_id + ] + except KeyError: + raise UnknownPacketIdError( + self.packet_map.protocol, self.state, packet_id, PacketDirection.SERVERBOUND + ) + + return packet_class.unpack(buf) + + @abstract + def read_packet(self) -> ServerBoundPacket: + pass + + @abstract + def write_packet(self, packet: ClientBoundPacket) -> None: + pass + + +class AbstractProtocolServer(StrictABC): + """Abstract class for a TCP server that handles Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + self.host = host + self.port = port + self.protocol = protocol + self.packet_map = packet_map + + self.connected_clients: Dict[Tuple[str, int], AbstractProtocolServerClient] = {} + + @abstract + def run(self) -> None: + pass + + @abstract + def close(self) -> None: + pass diff --git a/pymine_net/net/socket/__init__.py b/pymine_net/net/socket/__init__.py new file mode 100644 index 0000000..275e020 --- /dev/null +++ b/pymine_net/net/socket/__init__.py @@ -0,0 +1,3 @@ +from .client import SocketTCPClient # noqa: F401 +from .server import SocketProtocolServer, SocketProtocolServerClient # noqa: F401 +from .stream import EncryptedSocketTCPStream, SocketTCPStream # noqa: F401 diff --git a/pymine_net/net/socket/client.py b/pymine_net/net/socket/client.py new file mode 100644 index 0000000..35e52f6 --- /dev/null +++ b/pymine_net/net/socket/client.py @@ -0,0 +1,34 @@ +import socket +from typing import Union + +from pymine_net.net.client import AbstractTCPClient +from pymine_net.net.socket.stream import SocketTCPStream +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + +__all__ = ("SocketTCPClient",) + + +class SocketTCPClient(AbstractTCPClient): + """A connection over a TCP socket for reading + writing Minecraft packets.""" + + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.stream: SocketTCPStream = None + + def connect(self) -> None: + sock = socket.create_connection((self.host, self.port)) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + self.stream = SocketTCPStream(sock) + + def close(self) -> None: + self.stream.close() + + def read_packet(self) -> ClientBoundPacket: + packet_length = self.stream.read_varint() + return self._decode_packet(self.stream.read(packet_length)) + + def write_packet(self, packet: ServerBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) diff --git a/pymine_net/net/socket/server.py b/pymine_net/net/socket/server.py new file mode 100644 index 0000000..a87be35 --- /dev/null +++ b/pymine_net/net/socket/server.py @@ -0,0 +1,65 @@ +import socket +import threading +from typing import Dict, List, Tuple, Union + +from pymine_net.net.server import AbstractProtocolServer, AbstractProtocolServerClient +from pymine_net.net.socket.stream import SocketTCPStream +from pymine_net.strict_abc import abstract +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket +from pymine_net.types.packet_map import PacketMap + + +class SocketProtocolServerClient(AbstractProtocolServerClient): + def __init__(self, stream: SocketTCPStream, packet_map: PacketMap): + super().__init__(stream, packet_map) + self.stream = stream # redefine this cause typehints + + def read_packet(self) -> ServerBoundPacket: + length = self.stream.read_varint() + return self._decode_packet(self.stream.read(length)) + + def write_packet(self, packet: ClientBoundPacket) -> None: + self.stream.write(self._encode_packet(packet)) + + +class SocketProtocolServer(AbstractProtocolServer): + def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): + super().__init__(host, port, protocol, packet_map) + + self.connected_clients: Dict[Tuple[str, int], SocketProtocolServerClient] = {} + + self.sock: socket.socket = None + self.threads: List[threading.Thread] = [] + self.running = False + + def run(self) -> None: + self.running = True + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as self.sock: + self.sock.bind((self.host, self.port)) + self.sock.listen(20) + + while self.running: + connection, _ = self.sock.accept() + self._client_connected_cb(connection) + + def close(self) -> None: + self.sock.close() + + for thread in self.threads: + thread.join(0.1) + + def _client_connected_cb(self, sock: socket.socket) -> None: + client = SocketProtocolServerClient(SocketTCPStream(sock), self.packet_map) + self.connected_clients[client.stream.remote] = client + thread = threading.Thread(target=self._new_client_connected, args=(client,)) + self.threads.append(thread) + thread.start() + + def _new_client_connected(self, client: SocketProtocolServerClient) -> None: + with client.stream.sock: + self.new_client_connected(client) + + @abstract + def new_client_connected(self, client: SocketProtocolServerClient) -> None: + pass diff --git a/pymine_net/net/socket/stream.py b/pymine_net/net/socket/stream.py new file mode 100644 index 0000000..19e75ac --- /dev/null +++ b/pymine_net/net/socket/stream.py @@ -0,0 +1,92 @@ +import socket +import struct +from typing import Tuple, Union + +from cryptography.hazmat.primitives.ciphers import Cipher + +from pymine_net.net.stream import AbstractTCPStream +from pymine_net.types.buffer import Buffer + +__all__ = ("SocketTCPStream", "EncryptedSocketTCPStream") + + +class SocketTCPStream(AbstractTCPStream, socket.socket): + """Used for reading and writing from/to a connected client, wraps a socket.socket. + + :param socket.socket sock: A socket.socket instance. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + :ivar sock: + """ + + __slots__ = ("sock",) + + def __init__(self, sock: socket.socket): + self.sock = sock + + self.remote: Tuple[str, int] = sock.getsockname() + + def read(self, length: int) -> Buffer: + result = Buffer() + + while len(result) < length: + read_bytes = self.sock.recv(length - len(result)) + + if len(read_bytes) == 0: + raise IOError("Server didn't respond with information!") + + result.extend(read_bytes) + + return result + + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: + self.sock.sendall(data) + + def close(self) -> None: + self.sock.close() + + def read_varint(self) -> int: + value = 0 + + for i in range(10): + (byte,) = struct.unpack(">B", self.read(1)) + value |= (byte & 0x7F) << 7 * i + + if not byte & 0x80: + break + + if value & (1 << 31): + value -= 1 << 32 + + value_max = (1 << (32 - 1)) - 1 + value_min = -1 << (32 - 1) + + if not (value_min <= value <= value_max): + raise ValueError( + f"Value doesn't fit in given range: {value_min} <= {value} < {value_max}" + ) + + return value + + +class EncryptedSocketTCPStream(SocketTCPStream): + """Used for reading and writing from/to a connected client, wraps a socket.socket. + + :param socket.socket sock: A socket.socket instance. + :param Cipher cipher: The cipher instance, used to encrypt + decrypt data. + :ivar Tuple[str, int] remote: A tuple which stores the remote client's address and port. + :ivar sock: + :ivar _CipherContext decryptor: Description of parameter `_CipherContext`. + :ivar _CipherContext encryptor: Description of parameter `_CipherContext`. + """ + + def __init__(self, sock: socket.socket, cipher: Cipher): + super().__init__(sock) + + self.decryptor = cipher.decryptor() + self.encryptor = cipher.encryptor() + + def read(self, length: int) -> Buffer: + return Buffer(self.decryptor.update(super().read(length))) + + def write(self, data: Union[Buffer, bytes, bytearray]) -> None: + super().write(self.encryptor.update(data)) diff --git a/pymine_net/net/stream.py b/pymine_net/net/stream.py new file mode 100644 index 0000000..217401b --- /dev/null +++ b/pymine_net/net/stream.py @@ -0,0 +1,9 @@ +from pymine_net.strict_abc import abstract + + +class AbstractTCPStream: + """Abstract class for a TCP stream.""" + + @abstract + def read_varint(self) -> int: + pass diff --git a/pymine_net/packets/757/play/explosion.py b/pymine_net/packets/757/play/explosion.py deleted file mode 100644 index e3196d0..0000000 --- a/pymine_net/packets/757/play/explosion.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Contains packets related to entities.""" - -from __future__ import annotations - -from pymine_net.types.buffer import Buffer -from pymine_net.types.packet import ClientBoundPacket - -__all__ = ("PlayExplosion",) - - -class PlayExplosion(ClientBoundPacket): - """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). Each block in Records is set to air. Coordinates for each axis in record is int(X) + record. (Server -> Client)""" - - id = 0x1C - - def __init__( - self, - x: int, - y: int, - z: int, - strength: int, - record_count: int, - records: list, - pmx: int, - pmy: int, - pmz: int, - ) -> None: - super().__init__() - - self.x, self.y, self.z = x, y, z - self.strength = strength - self.record_count = record_count - self.records = records - self.pmx = pmx - self.pmy = pmy - self.pmz = pmz - - def pack(self) -> Buffer: - return ( - Buffer.write("f", self.x) - + Buffer.write("f", self.y) - + Buffer.write("f", self.z) - + Buffer.write("f", self.strength) - + Buffer.write("i", self.record_count) - + b"".join([Buffer.write("b", r) for r in self.records]) - + Buffer.write("f", self.pmx) - + Buffer.write("f", self.pmy) - + Buffer.write("f", self.pmz) - ) diff --git a/pymine_net/packets/__init__.py b/pymine_net/packets/__init__.py index a1a160e..6905d3d 100644 --- a/pymine_net/packets/__init__.py +++ b/pymine_net/packets/__init__.py @@ -19,8 +19,11 @@ # the directory this file is contained in FILE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +PROTOCOL_MAP = {757: "v_1_18_1"} + def load_packet_map(protocol: Union[int, str], *, debug: bool = False) -> PacketMap: + protocol = PROTOCOL_MAP.get(protocol, protocol) packets: Dict[GameState, StatePacketMap] = {} for state, state_name in GAME_STATES.items(): diff --git a/pymine_net/packets/757/handshaking/handshake.py b/pymine_net/packets/v_1_18_1/handshaking/handshake.py similarity index 82% rename from pymine_net/packets/757/handshaking/handshake.py rename to pymine_net/packets/v_1_18_1/handshaking/handshake.py index a049d51..6156eb0 100644 --- a/pymine_net/packets/757/handshaking/handshake.py +++ b/pymine_net/packets/v_1_18_1/handshaking/handshake.py @@ -30,6 +30,15 @@ def __init__(self, protocol: int, address: str, port: int, next_state: int): self.port = port self.next_state = next_state + def pack(self) -> Buffer: + return ( + Buffer() + .write_varint(self.protocol) + .write_string(self.address) + .write("H", self.port) + .write_varint(self.next_state) + ) + @classmethod def unpack(cls, buf: Buffer) -> HandshakeHandshake: return cls(buf.read_varint(), buf.read_string(), buf.read("H"), buf.read_varint()) diff --git a/pymine_net/packets/757/login/compression.py b/pymine_net/packets/v_1_18_1/login/compression.py similarity index 88% rename from pymine_net/packets/757/login/compression.py rename to pymine_net/packets/v_1_18_1/login/compression.py index 62dfb26..2f90bc6 100644 --- a/pymine_net/packets/757/login/compression.py +++ b/pymine_net/packets/v_1_18_1/login/compression.py @@ -25,3 +25,7 @@ def __init__(self, compression_threshold: int = -1): def pack(self) -> Buffer: return Buffer().write_varint(self.compression_threshold) + + @classmethod + def unpack(cls, buf: Buffer) -> LoginSetCompression: + return cls(buf.read_varint()) diff --git a/pymine_net/packets/757/login/login.py b/pymine_net/packets/v_1_18_1/login/login.py similarity index 77% rename from pymine_net/packets/757/login/login.py rename to pymine_net/packets/v_1_18_1/login/login.py index 512f03b..8ebc983 100644 --- a/pymine_net/packets/757/login/login.py +++ b/pymine_net/packets/v_1_18_1/login/login.py @@ -32,6 +32,9 @@ def __init__(self, username: str): self.username = username + def pack(self) -> Buffer: + return Buffer().write_string(self.username) + @classmethod def unpack(cls, buf: Buffer) -> LoginStart: return cls(buf.read_string()) @@ -64,6 +67,12 @@ def pack(self) -> Buffer: .write_bytes(self.verify_token) ) + @classmethod + def unpack(cls, buf: Buffer) -> LoginEncryptionRequest: + buf.read_string() + + return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) + class LoginEncryptionResponse(ServerBoundPacket): """Response from the client to a LoginEncryptionRequest. (Client -> Server) @@ -83,6 +92,15 @@ def __init__(self, shared_key: bytes, verify_token: bytes): self.shared_key = shared_key self.verify_token = verify_token + def pack(self) -> Buffer: + return ( + Buffer() + .write_varint(len(self.shared_key)) + .write_bytes(self.shared_key) + .write_varint(len(self.verify_token)) + .write_bytes(self.verify_token) + ) + @classmethod def unpack(cls, buf: Buffer) -> LoginEncryptionResponse: return cls(buf.read_bytes(buf.read_varint()), buf.read_bytes(buf.read_varint())) @@ -109,6 +127,10 @@ def __init__(self, uuid: UUID, username: str): def pack(self) -> Buffer: return Buffer().write_uuid(self.uuid).write_string(self.username) + @classmethod + def unpack(cls, buf: Buffer) -> LoginSuccess: + return cls(buf.read_uuid(), buf.read_string()) + class LoginDisconnect(ClientBoundPacket): """Sent by the server to kick a player while in the login state. (Server -> Client) @@ -127,6 +149,10 @@ def __init__(self, reason: Chat): def pack(self) -> Buffer: return Buffer().write_chat(self.reason) + @classmethod + def unpack(cls, buf: Buffer) -> LoginDisconnect: + return cls(buf.read_chat()) + class LoginPluginRequest(ClientBoundPacket): """Sent by server to implement a custom handshaking flow. @@ -149,29 +175,29 @@ def pack(self) -> Buffer: Buffer().write_varint(self.message_id).write_string(self.channel).write_bytes(self.data) ) + @classmethod + def unpack(cls, buf: Buffer) -> LoginPluginRequest: + return cls(buf.read_varint(), buf.read_string(), buf.read_bytes()) + class LoginPluginResponse(ServerBoundPacket): """Response to LoginPluginRequest from client. :param int message_id: Message id, generated by the server, should be unique to the connection. - :param bool successful: True if the client understands the request, False otherwise. When false, no data follows. - :param Optional[bytes] data: Optional response data. + :param Optional[bytes] data: Optional response data, present if client understood request. :ivar int id: Unique packet ID. """ id = 0x02 - def __init__(self, message_id: int, successful: bool, data: bytes = None): + def __init__(self, message_id: int, data: bytes = None): self.message_id = message_id - self.successful = successful self.data = data + def pack(self) -> Buffer: + buf = Buffer().write_varint(self.message_id) + return buf.write_optional(buf.write_bytes, self.data) + @classmethod def unpack(cls, buf: Buffer) -> LoginPluginResponse: - instance = cls(buf.read_varint(), buf.read("?")) - - # assumes that buf can be .read() to completion because data must be inferred from the packet length. - if instance.successful: - instance.data = buf.read() - - return instance + return cls(buf.read_varint(), buf.read_optional(buf.read_bytes)) diff --git a/pymine_net/packets/757/play/advancement.py b/pymine_net/packets/v_1_18_1/play/advancement.py similarity index 95% rename from pymine_net/packets/757/play/advancement.py rename to pymine_net/packets/v_1_18_1/play/advancement.py index d416213..aaa0db8 100644 --- a/pymine_net/packets/757/play/advancement.py +++ b/pymine_net/packets/v_1_18_1/play/advancement.py @@ -5,10 +5,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket -__all__ = ( - "PlayAdvancementTab", - "PlaySelectAdvancementTab", -) +__all__ = ("PlayAdvancementTab", "PlaySelectAdvancementTab") class PlayAdvancementTab(ServerBoundPacket): diff --git a/pymine_net/packets/757/play/animations.py b/pymine_net/packets/v_1_18_1/play/animations.py similarity index 100% rename from pymine_net/packets/757/play/animations.py rename to pymine_net/packets/v_1_18_1/play/animations.py diff --git a/pymine_net/packets/757/play/beacon.py b/pymine_net/packets/v_1_18_1/play/beacon.py similarity index 100% rename from pymine_net/packets/757/play/beacon.py rename to pymine_net/packets/v_1_18_1/play/beacon.py diff --git a/pymine_net/packets/757/play/block.py b/pymine_net/packets/v_1_18_1/play/block.py similarity index 99% rename from pymine_net/packets/757/play/block.py rename to pymine_net/packets/v_1_18_1/play/block.py index a493068..d7c149c 100644 --- a/pymine_net/packets/757/play/block.py +++ b/pymine_net/packets/v_1_18_1/play/block.py @@ -176,6 +176,8 @@ class PlayNBTQueryResponse(ClientBoundPacket): :param int transaction_id: :param nbt.TAG nbt_tag: :ivar int id: Unique packet ID. + :ivar transaction_id: + :ivar nbt_tag: """ id = 0x60 diff --git a/pymine_net/packets/757/play/boss.py b/pymine_net/packets/v_1_18_1/play/boss.py similarity index 100% rename from pymine_net/packets/757/play/boss.py rename to pymine_net/packets/v_1_18_1/play/boss.py diff --git a/pymine_net/packets/757/play/chat.py b/pymine_net/packets/v_1_18_1/play/chat.py similarity index 98% rename from pymine_net/packets/757/play/chat.py rename to pymine_net/packets/v_1_18_1/play/chat.py index 1b9a244..9c36293 100644 --- a/pymine_net/packets/757/play/chat.py +++ b/pymine_net/packets/v_1_18_1/play/chat.py @@ -74,7 +74,7 @@ class PlayTabCompleteServerBound(ServerBoundPacket): id = 0x06 - def __init__(self, transaction_id: int, text: str) -> None: + def __init__(self, transaction_id: int, text: str): super().__init__() self.transaction_id = transaction_id diff --git a/pymine_net/packets/757/play/command.py b/pymine_net/packets/v_1_18_1/play/command.py similarity index 93% rename from pymine_net/packets/757/play/command.py rename to pymine_net/packets/v_1_18_1/play/command.py index ba0a635..b737f6d 100644 --- a/pymine_net/packets/757/play/command.py +++ b/pymine_net/packets/v_1_18_1/play/command.py @@ -14,8 +14,8 @@ class PlayDeclareCommands(ClientBoundPacket): """Tells the clients what commands there are. (Server -> Client) :param List[dict] nodes: The command nodes, a list of dictionaries. The first item is assumed to be the root node. - :attr int id: Unique packet ID. - :attr nodes: + :ivar int id: Unique packet ID. + :ivar nodes: """ id = 0x12 diff --git a/pymine_net/packets/757/play/command_block.py b/pymine_net/packets/v_1_18_1/play/command_block.py similarity index 100% rename from pymine_net/packets/757/play/command_block.py rename to pymine_net/packets/v_1_18_1/play/command_block.py diff --git a/pymine_net/packets/757/play/cooldown.py b/pymine_net/packets/v_1_18_1/play/cooldown.py similarity index 95% rename from pymine_net/packets/757/play/cooldown.py rename to pymine_net/packets/v_1_18_1/play/cooldown.py index 5e62acf..f7e31a5 100644 --- a/pymine_net/packets/757/play/cooldown.py +++ b/pymine_net/packets/v_1_18_1/play/cooldown.py @@ -15,6 +15,8 @@ class PlaySetCooldown(ClientBoundPacket): :param int item_id: The unique id of the type of affected items. :param int cooldown_ticks: The length of the cooldown in in-game ticks. :ivar int id: The unique ID of the packet. + :ivar item_id: + :ivar cooldown_ticks: """ id = 0x17 diff --git a/pymine_net/packets/757/play/crafting.py b/pymine_net/packets/v_1_18_1/play/crafting.py similarity index 100% rename from pymine_net/packets/757/play/crafting.py rename to pymine_net/packets/v_1_18_1/play/crafting.py diff --git a/pymine_net/packets/757/play/difficulty.py b/pymine_net/packets/v_1_18_1/play/difficulty.py similarity index 95% rename from pymine_net/packets/757/play/difficulty.py rename to pymine_net/packets/v_1_18_1/play/difficulty.py index 1f0f0ef..378893b 100644 --- a/pymine_net/packets/757/play/difficulty.py +++ b/pymine_net/packets/v_1_18_1/play/difficulty.py @@ -51,7 +51,7 @@ def __init__(self, new_difficulty: int) -> None: @classmethod def unpack(cls, buf: Buffer) -> PlaySetDifficulty: - return cls(buf.read("b")) + return cls(buf.read_byte()) class PlayLockDifficulty(ServerBoundPacket): @@ -64,7 +64,7 @@ class PlayLockDifficulty(ServerBoundPacket): id = 0x10 - def __init__(self, locked: bool) -> None: + def __init__(self, locked: bool): super().__init__() self.locked = locked diff --git a/pymine_net/packets/757/play/effect.py b/pymine_net/packets/v_1_18_1/play/effect.py similarity index 95% rename from pymine_net/packets/757/play/effect.py rename to pymine_net/packets/v_1_18_1/play/effect.py index 32320dd..a5530ef 100644 --- a/pymine_net/packets/757/play/effect.py +++ b/pymine_net/packets/v_1_18_1/play/effect.py @@ -5,11 +5,7 @@ from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket -__all__ = ( - "PlayEffect", - "PlayEntityEffect", - "PlaySoundEffect", -) +__all__ = ("PlayEffect", "PlayEntityEffect", "PlaySoundEffect") class PlayEffect(ClientBoundPacket): @@ -38,7 +34,9 @@ def __init__( super().__init__() self.effect_id = effect_id - self.x, self.y, self.z = x, y, z + self.x = x + self.y = y + self.z = z self.data = data self.disable_relative_volume = disable_relative_volume @@ -117,7 +115,9 @@ def __init__( self.sound_id = sound_id self.category = category - self.x, self.y, self.z = x, y, z + self.x = x + self.y = y + self.z = z self.volume = volume self.pitch = pitch diff --git a/pymine_net/packets/757/play/entity.py b/pymine_net/packets/v_1_18_1/play/entity.py similarity index 100% rename from pymine_net/packets/757/play/entity.py rename to pymine_net/packets/v_1_18_1/play/entity.py diff --git a/pymine_net/packets/v_1_18_1/play/explosion.py b/pymine_net/packets/v_1_18_1/play/explosion.py new file mode 100644 index 0000000..b550c79 --- /dev/null +++ b/pymine_net/packets/v_1_18_1/play/explosion.py @@ -0,0 +1,72 @@ +"""Contains packets related to entities.""" + +from __future__ import annotations + +from typing import List, Tuple + +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket + +__all__ = ("PlayExplosion",) + + +class PlayExplosion(ClientBoundPacket): + """Sent when an explosion occurs (creepers, TNT, and ghast fireballs). (Server -> Client) + + :param float x: The x coordinate of the explosion. + :param float y: The y coordinate of the explosion. + :param float z: The z coordinate of the explosion. + :param float strength: Strength of the explosion, will summon a minecraft:explosion_emitter particle if >=2.0 else a minecraft:explosion particle. + :param List[Tuple[int, int, int]] records: Array of bytes containing the coordinates of the blocks to destroy relative to the explosion's coordinates. + :param float pmx: Velocity to add to the player's motion in the x axis due to the explosion. + :param float pmy: Velocity to add to the player's motion in the y axis due to the explosion. + :param float pmz: Velocity to add to the player's motion in the z axis due to the explosion. + :ivar int id: + :ivar x: + :ivar y: + :ivar z: + :ivar strength: + :ivar records: + :ivar pmx: + :ivar pmy: + :ivar pmz: + """ + + id = 0x1C + + def __init__( + self, + x: float, + y: float, + z: float, + strength: float, + records: List[Tuple[int, int, int]], + pmx: float, + pmy: float, + pmz: float, + ): + super().__init__() + + self.x = x + self.y = y + self.z = z + self.strength = strength + self.records = records + self.pmx = pmx + self.pmy = pmy + self.pmz = pmz + + def pack(self) -> Buffer: + buf = ( + Buffer() + .write("f", self.x) + .write("f", self.y) + .write("f", self.z) + .write("f", self.strength) + .write("i", len(self.records)) + ) + + for rx, ry, rz in self.records: + buf.write_byte(rx).write_byte(ry).write_byte(rz) + + return buf.write("f", self.pmx).write("f", self.pmy).write("f", self.pmz) diff --git a/pymine_net/packets/v_1_18_1/play/keep_alive.py b/pymine_net/packets/v_1_18_1/play/keep_alive.py new file mode 100644 index 0000000..47ddd96 --- /dev/null +++ b/pymine_net/packets/v_1_18_1/play/keep_alive.py @@ -0,0 +1,47 @@ +"""Contains packets for maintaining the connection between client and server.""" + +from __future__ import annotations + +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket + +__all__ = ("PlayKeepAliveClientBound", "PlayKeepAliveServerBound") + + +class PlayKeepAliveClientBound(ClientBoundPacket): + """Sent by the server in order to maintain connection with the client. (Server -> Client) + + :param int keep_alive_id: A randomly generated (by the server) integer/long. + :ivar int id: Unique packet ID. + :ivar keep_alive_id: + """ + + id = 0x21 + + def __init__(self, keep_alive_id: int): + super().__init__() + + self.keep_alive_id = keep_alive_id + + def pack(self) -> Buffer: + return Buffer().write("q", self.keep_alive_id) + + +class PlayKeepAliveServerBound(ServerBoundPacket): + """Sent by client in order to maintain connection with server. (Client -> Server) + + :param int keep_alive_id: A randomly generated (by the server) integer/long. + :ivar int id: Unique packet ID. + :ivar keep_alive_id: + """ + + id = 0x0F + + def __init__(self, keep_alive_id: int): + super().__init__() + + self.keep_alive_id = keep_alive_id + + @classmethod + def unpack(cls, buf: Buffer) -> PlayKeepAliveServerBound: + return cls(buf.read("q")) diff --git a/pymine_net/packets/v_1_18_1/play/map.py b/pymine_net/packets/v_1_18_1/play/map.py new file mode 100644 index 0000000..89d5565 --- /dev/null +++ b/pymine_net/packets/v_1_18_1/play/map.py @@ -0,0 +1,98 @@ +"""Contains packets related to the in-game map item.""" + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from pymine_net.types.buffer import Buffer +from pymine_net.types.chat import Chat +from pymine_net.types.packet import ClientBoundPacket + +__all__ = ("PlayMapData",) + + +class PlayMapData(ClientBoundPacket): + """Updates a rectangular area on a map item. (Server -> Client) + + :param int map_id: ID of the map to be modified. + :param int scale: Zoom of the map (0 fully zoomed in - 4 fully zoomed out). + :param bool locked: Whether the map has been locked in a cartography table or not. + :param bool tracking_pos: Whether the player and other icons are shown on the map. + :param List[Tuple[int, int, int, int, bool, Optional[Chat]]] icons: List of icons shown on the map. + :param int columns: Number of columns being updated. + :param Optional[int] rows: Number of rows being updated, only if columns > 0. + :param Optional[int] x: X offset of the westernmost column, only if columns > 0. + :param Optional[int] z: Z offset of the northernmost row, only if columns > 0. + :param bytes data: The map data, see https://minecraft.fandom.com/wiki/Map_item_format. + :ivar int id: + :ivar map_id: + :ivar scale: + :ivar locked: + :ivar tracking_pos: + :ivar icons: + :ivar columns: + :ivar rows: + :ivar x: + :ivar z: + :ivar data: + """ + + id = 0x27 + + def __init__( + self, + map_id: int, + scale: int, + locked: bool, + tracking_pos: bool, + icons: List[Tuple[int, int, int, int, bool, Optional[Chat]]], + columns: int, + rows: int = None, + x: int = None, + z: int = None, + data: bytes = None, + ): + super().__init__() + + self.map_id = map_id + self.scale = scale + self.tracking_pos = tracking_pos + self.locked = locked + self.icons = icons + self.columns = columns + self.rows = rows + self.x = x + self.z = z + self.data = data + + def pack(self) -> Buffer: + buf = ( + Buffer() + .write_varint(self.map_id) + .write_byte(self.scale) + .write("?", self.locked) + .write("?", self.tracking_pos) + .write_varint(len(self.icons)) + ) + + for (icon_type, x, z, direction, display_name) in self.icons: + ( + buf.write_varint(icon_type) + .write_byte(x) + .write_byte(z) + .write_byte(direction) + .write_optional(buf.write_chat, display_name) + ) + + buf.write("B", self.columns) + + if len(self.columns) < 1: + return buf + + return ( + buf.write("B", self.rows) + .write_byte(self.x) + .write_byte(self.y) + .write_varint(len(self.data)) + .write_bytes(self.data) + ) diff --git a/pymine_net/packets/v_1_18_1/play/particle.py b/pymine_net/packets/v_1_18_1/play/particle.py new file mode 100644 index 0000000..e5aa490 --- /dev/null +++ b/pymine_net/packets/v_1_18_1/play/particle.py @@ -0,0 +1,77 @@ +"""Contains packets that are related to particles.""" + +from __future__ import annotations + +from pymine_net.types.buffer import Buffer +from pymine_net.types.packet import ClientBoundPacket + +__all__ = ("PlayParticle",) + + +class PlayParticle(ClientBoundPacket): + """Sent by server to make the client display particles. (Server -> Client) + + :param int particle_id: ID of the particle. + :param bool long_distance: If true, particle distance increases to 65536 from 256. + :param int x: X coordinate of the particle. + :param int y: Y coordinate of the particle. + :param int z: Z coordinate of the particle. + :param float particle_data: Particle data. + :param int particle_count: How many particles to display. + :param dict data: More particle data. + :ivar int id: Unique packet ID. + :ivar particle_id: + :ivar long_distance: + :ivar x: + :ivar y: + :ivar z: + :ivar particle_data: + :ivar particle_count: + :ivar data: + """ + + id = 0x24 + + def __init__( + self, + particle_id: int, + long_distance: bool, + x: float, + y: float, + z: float, + offset_x: float, + offset_y: float, + offset_z: float, + particle_data: float, + particle_count: int, + data: dict, + ): + super().__init__() + + self.part_id = particle_id + self.long_dist = long_distance + self.x = x + self.y = y + self.z = z + self.offset_x = offset_x + self.offset_y = offset_y + self.offset_z = offset_z + self.particle_data = particle_data + self.particle_count = particle_count + self.data = data + + def pack(self) -> Buffer: + return ( + Buffer() + .write("i", self.part_id) + .write("?", self.long_dist) + .write("d", self.x) + .write("d", self.y) + .write("d", self.z) + .write("f", self.offset_x) + .write("f", self.offset_y) + .write("f", self.offset_z) + .write("f", self.particle_data) + .write("i", self.particle_count) + .write_particle(self.data) + ) diff --git a/pymine_net/packets/v_1_18_1/play/player_list.py b/pymine_net/packets/v_1_18_1/play/player_list.py new file mode 100644 index 0000000..e926ecf --- /dev/null +++ b/pymine_net/packets/v_1_18_1/play/player_list.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from pymine_net.types.buffer import Buffer +from pymine_net.types.chat import Chat +from pymine_net.types.packet import ClientBoundPacket + +__all__ = ("PlayPlayerListHeaderAndFooter",) + + +class PlayPlayerListHeaderAndFooter(ClientBoundPacket): + """Sent to display additional information above/below the client's player list. (Server -> Client) + + :param Chat header: Content to display above player list. + :param Chat footer: Content to display below player list. + :ivar int id: Unique packet ID. + :ivar header: + :ivar footer: + """ + + id = 0x5F + + def __init__(self, header: Chat, footer: Chat): + super().__init__() + + self.header = header + self.footer = footer + + def pack(self) -> Buffer: + return Buffer().write_chat(self.header).write_chat(self.footer) diff --git a/pymine_net/packets/757/status/status.py b/pymine_net/packets/v_1_18_1/status/status.py similarity index 79% rename from pymine_net/packets/757/status/status.py rename to pymine_net/packets/v_1_18_1/status/status.py index 4aab519..fa3808b 100644 --- a/pymine_net/packets/757/status/status.py +++ b/pymine_net/packets/v_1_18_1/status/status.py @@ -17,6 +17,9 @@ class StatusStatusRequest(ServerBoundPacket): def __init__(self): super().__init__() + def pack(self) -> Buffer: + return Buffer() + @classmethod def unpack(cls, buf: Buffer) -> StatusStatusRequest: return cls() @@ -25,20 +28,24 @@ def unpack(cls, buf: Buffer) -> StatusStatusRequest: class StatusStatusResponse(ClientBoundPacket): """Returns server status data back to the requesting client. (Server -> Client) - :param dict response_data: JSON response data sent back to the client. + :param dict data: JSON response data sent back to the client. :ivar int id: Unique packet ID. - :ivar response_data: + :ivar data: """ id = 0x00 - def __init__(self, response_data: dict): + def __init__(self, data: dict): super().__init__() - self.response_data = response_data + self.data = data def pack(self) -> Buffer: - return Buffer().write_json(self.response_data) + return Buffer().write_json(self.data) + + @classmethod + def unpack(cls, buf: Buffer) -> StatusStatusResponse: + return cls(buf.read_json()) class StatusStatusPingPong(ServerBoundPacket, ClientBoundPacket): diff --git a/pymine_net/strict_abc.py b/pymine_net/strict_abc.py index 20d4186..422ce47 100644 --- a/pymine_net/strict_abc.py +++ b/pymine_net/strict_abc.py @@ -43,8 +43,12 @@ def check_annotations(a: dict, b: dict) -> bool: if type(v) is str: return type(b[k]) is str and v == b[k] - if not issubclass(b[k], v): - return False + try: + if not issubclass(b[k], v): + return False + except TypeError: + if b[k] is not v: + return False return True diff --git a/pymine_net/types/buffer.py b/pymine_net/types/buffer.py index c08a049..f5bc6d0 100644 --- a/pymine_net/types/buffer.py +++ b/pymine_net/types/buffer.py @@ -21,9 +21,7 @@ def __init__(self, *args, **kwargs): def write_bytes(self, data: Union[bytes, bytearray]) -> Buffer: """Writes bytes to the buffer.""" - self.extend(data) - - return self + return self.extend(data) def read_bytes(self, length: int = None) -> bytearray: """Reads bytes from the buffer, if length is None then all bytes are read.""" @@ -47,6 +45,10 @@ def reset(self) -> None: self.pos = 0 + def extend(self, data: Union[Buffer, bytes, bytearray]) -> Buffer: + super().extend(data) + return self + def read_byte(self) -> int: """Reads a singular byte as an integer from the buffer.""" @@ -57,8 +59,7 @@ def read_byte(self) -> int: def write_byte(self, value: int) -> Buffer: """Writes a singular byte to the buffer.""" - self.extend(struct.pack(">b", value)) - return self + return self.extend(struct.pack(">b", value)) def read(self, fmt: str) -> Union[object, Tuple[object]]: """Using the given format, reads from the buffer and returns the unpacked value.""" diff --git a/pymine_net/types/packet.py b/pymine_net/types/packet.py index e06f7e5..594882c 100644 --- a/pymine_net/types/packet.py +++ b/pymine_net/types/packet.py @@ -47,5 +47,5 @@ def pack(self) -> Buffer: @classmethod @optionalabstract - def unpack(cls, buf: Buffer) -> ServerBoundPacket: + def unpack(cls, buf: Buffer) -> ClientBoundPacket: raise NotImplementedError diff --git a/pymine_net/types/packet_map.py b/pymine_net/types/packet_map.py index 7ca6531..8856d08 100644 --- a/pymine_net/types/packet_map.py +++ b/pymine_net/types/packet_map.py @@ -1,11 +1,9 @@ from __future__ import annotations -import zlib -from typing import Dict, List, Type, Union +from typing import Dict, List, Tuple, Type, Union from pymine_net.enums import GameState, PacketDirection -from pymine_net.errors import DuplicatePacketIdError, UnknownPacketIdError -from pymine_net.types.buffer import Buffer +from pymine_net.errors import DuplicatePacketIdError from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket @@ -63,37 +61,10 @@ def __init__(self, protocol: Union[str, int], packets: Dict[GameState, StatePack self.protocol = protocol self.packets = packets - def encode_packet(self, packet: ClientBoundPacket, compression_threshold: int = -1) -> Buffer: - """Encodes and (if necessary) compresses a ClientBoundPacket.""" + def __getitem__(self, key: Tuple[PacketDirection, int, int]) -> Packet: + direction, state, packet_id = key - buf = Buffer().write_varint(packet.id).extend(packet.pack()) + if direction == PacketDirection.CLIENTBOUND: + return self.packets[state].client_bound[packet_id] - if compression_threshold >= 1: - if len(buf) >= compression_threshold: - buf = Buffer().write_varint(len(buf)).extend(zlib.compress(buf)) - else: - buf = Buffer().write_varint(0).extend(buf) - - return Buffer().write_varint(len(buf)).extend(buf) - - def decode_packet( - self, buf: Buffer, state: GameState, compression_threshold: int = -1 - ) -> ServerBoundPacket: - """Decodes and (if necessary) decompresses a ServerBoundPacket.""" - - # decompress packet if necessary - if compression_threshold >= 0: - uncompressed_length = buf.read_varint() - - if uncompressed_length > 0: - buf = Buffer(zlib.decompress(buf.read_bytes())) - - packet_id = buf.read_varint() - - # attempt to get packet class from given state and packet id - try: - packet_class = self.packets[state].server_bound[packet_id] - except KeyError: - raise UnknownPacketIdError(self.protocol, state, packet_id, PacketDirection.SERVERBOUND) - - return packet_class.unpack(buf) + return self.packets[state].server_bound[packet_id] diff --git a/pyproject.toml b/pyproject.toml index 917d45f..34eace0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ include = '\.pyi?$' [tool.pytest.ini_options] addopts = "--full-trace -rxP" testpaths = ["tests"] +asyncio_mode = "auto" [tool.isort] profile="black" line_length=100 - [tool.poetry] name = "pymine-net" version = "0.1.1" @@ -25,6 +25,7 @@ keywords = ["Minecraft", "protocol", "networking"] [tool.poetry.dependencies] python = "^3.7" mutf8 = "^1.0.6" +cryptography = "^36.0.1" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" @@ -32,6 +33,7 @@ black = "^22.1.0" pytest = "^7.0.1" colorama = "^0.4.4" isort = "^5.10.1" +pytest-asyncio = "^0.18.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 95b4d19..fdfd9ef 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -7,7 +7,6 @@ # fix path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -import pymine_net.types.nbt as nbt from pymine_net.types.buffer import Buffer VAR_INT_ERR_MSG = "Value doesn't fit in given range" diff --git a/tests/test_net_asyncio.py b/tests/test_net_asyncio.py new file mode 100644 index 0000000..bb89219 --- /dev/null +++ b/tests/test_net_asyncio.py @@ -0,0 +1,88 @@ +import asyncio + +import pytest + +from pymine_net.enums import GameState +from pymine_net.net.asyncio import AsyncProtocolServer, AsyncProtocolServerClient, AsyncTCPClient +from pymine_net.packets import load_packet_map +from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake +from pymine_net.packets.v_1_18_1.status.status import ( + StatusStatusPingPong, + StatusStatusRequest, + StatusStatusResponse, +) + +TESTING_PROTOCOL = 757 +TESTING_HOST = "localhost" +TESTING_PORT = 12345 +TESTING_RANDOM_LONG = 1234567890 +TESTING_STATUS_JSON = { + "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, + "players": { + "max": 20, + "online": 0, + "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], + }, + "description": {"text": "Hello world"}, + "favicon": None, +} + + +# proactor event loop vomits an error on exit on windows due to a Python bug +@pytest.fixture +def event_loop(): + asyncio.set_event_loop(asyncio.SelectorEventLoop()) + yield asyncio.get_event_loop() + + +@pytest.mark.asyncio +async def test_asyncio_net_status(): + class TestAsyncTCPServer(AsyncProtocolServer): + async def new_client_connected(self, client: AsyncProtocolServerClient) -> None: + packet = await client.read_packet() + assert isinstance(packet, HandshakeHandshake) + assert packet.protocol == TESTING_PROTOCOL + assert packet.address == "localhost" + assert packet.port == TESTING_PORT + assert packet.next_state == GameState.STATUS + + client.state = packet.next_state + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusRequest) + + await client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + await client.write_packet(packet) + + packet_map = load_packet_map(TESTING_PROTOCOL) + + server = TestAsyncTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + server_task = asyncio.create_task(server.run()) + + client = AsyncTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + await client.connect() + + await client.write_packet( + HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) + ) + client.state = GameState.STATUS + + await client.write_packet(StatusStatusRequest()) + + packet = await client.read_packet() + assert isinstance(packet, StatusStatusResponse) + assert packet.data == TESTING_STATUS_JSON + + await client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) + packet = await client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + + await client.close() + + server_task.cancel() + await server.close() diff --git a/tests/test_net_socket.py b/tests/test_net_socket.py new file mode 100644 index 0000000..7c973ba --- /dev/null +++ b/tests/test_net_socket.py @@ -0,0 +1,79 @@ +from concurrent.futures import ThreadPoolExecutor + +from pymine_net.enums import GameState +from pymine_net.net.socket import SocketProtocolServer, SocketProtocolServerClient, SocketTCPClient +from pymine_net.packets import load_packet_map +from pymine_net.packets.v_1_18_1.handshaking.handshake import HandshakeHandshake +from pymine_net.packets.v_1_18_1.status.status import ( + StatusStatusPingPong, + StatusStatusRequest, + StatusStatusResponse, +) + +TESTING_PROTOCOL = 757 +TESTING_HOST = "localhost" +TESTING_PORT = 12345 +TESTING_RANDOM_LONG = 1234567890 +TESTING_STATUS_JSON = { + "version": {"name": "1.18.1", "protocol": TESTING_PROTOCOL}, + "players": { + "max": 20, + "online": 0, + "sample": [{"name": "Iapetus11", "id": "cbcfa252-867d-4bda-a214-776c881cf370"}], + }, + "description": {"text": "Hello world"}, + "favicon": None, +} + + +def test_socket_net_status(): + class TestSocketTCPServer(SocketProtocolServer): + def new_client_connected(self, client: SocketProtocolServerClient) -> None: + packet = client.read_packet() + assert isinstance(packet, HandshakeHandshake) + assert packet.protocol == TESTING_PROTOCOL + assert packet.address == "localhost" + assert packet.port == TESTING_PORT + assert packet.next_state == GameState.STATUS + + client.state = packet.next_state + + packet = client.read_packet() + assert isinstance(packet, StatusStatusRequest) + + client.write_packet(StatusStatusResponse(TESTING_STATUS_JSON)) + + packet = client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + client.write_packet(packet) + + packet_map = load_packet_map(TESTING_PROTOCOL) + + server = TestSocketTCPServer(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + threadpool = ThreadPoolExecutor() + threadpool.submit(server.run) + + client = SocketTCPClient(TESTING_HOST, TESTING_PORT, TESTING_PROTOCOL, packet_map) + client.connect() + + client.write_packet( + HandshakeHandshake(TESTING_PROTOCOL, TESTING_HOST, TESTING_PORT, GameState.STATUS) + ) + client.state = GameState.STATUS + + client.write_packet(StatusStatusRequest()) + + packet = client.read_packet() + assert isinstance(packet, StatusStatusResponse) + assert packet.data == TESTING_STATUS_JSON + + client.write_packet(StatusStatusPingPong(TESTING_RANDOM_LONG)) + packet = client.read_packet() + assert isinstance(packet, StatusStatusPingPong) + assert packet.payload == TESTING_RANDOM_LONG + + client.close() + + threadpool.shutdown(wait=False, cancel_futures=True) + server.close()