From 9061abb8557f3c9fcb69bdb0cd4ea080e3b72f98 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 26 Oct 2024 13:34:40 +0200 Subject: [PATCH] Move usage guides from readme to docs --- README.md | 332 +---------------------------- docs/usage/first-steps.md | 249 ++++++++++++++++++++++ docs/usage/packet-communication.md | 187 ++++++++++++++++ mkdocs.yml | 2 + 4 files changed, 446 insertions(+), 324 deletions(-) create mode 100644 docs/usage/first-steps.md create mode 100644 docs/usage/packet-communication.md diff --git a/README.md b/README.md index ac129f89..8155532d 100644 --- a/README.md +++ b/README.md @@ -5,330 +5,14 @@ [![current PyPI version](https://img.shields.io/pypi/v/mcproto.svg)](https://pypi.org/project/mcproto/) [![Validation](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/validation.yml) [![Unit tests](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ItsDrike/mcproto/actions/workflows/unit-tests.yml) -[![Docs](https://img.shields.io/readthedocs/mcproto?label=Docs)](https://mcproto.readthedocs.io/) +[![Docs](https://github.com/py-mine/mcproto/actions/workflows/mkdocs.yml/badge.svg))](https://py-mine.github.io/mcproto) -This is a heavily Work-In-Progress library, which attempts to be a full wrapper around the minecraft protocol, allowing -for simple interactions with minecraft servers, and perhaps even for use as a base to a full minecraft server -implementation in python (though the speed will very likely be quite terrible, making it probably unusable as any -real-world playable server). +Mcproto is a python library that provides various low level interactions with the Minecraft protocol. It attempts to be +a full wrapper around the Minecraft protocol, which means it could be used as a basis for Minecraft bots written in +python, or even full python server implementations. -Currently, the library is very limited and doesn't yet have any documentation, so while contributions are welcome, fair -warning that there is a lot to comprehend in the code-base and it may be challenging to understand it all. +> [!WARNING] +> Currently, the library is still work in progress and very incomplete, so while contributions are welcome, fair warning +> that using mcproto in production isn't recommended. -## Examples - -Since there is no documentation, to satisfy some curious minds that really want to use this library even in this -heavily unfinished state, here's a few simple snippets of it in practice: - -### Manual communication with the server - -As sending entire packets is still being worked on, the best solution to communicate with a server is to send the data -manually, using our connection reader/writer, and buffers (being readers/writers, but only from bytearrays as opposed -to using an actual connection). - -Fair warning: This example is pretty long, but that's because it aims to explain the minecraft protocol to people that -see it for the first time, and so a lot of explanation comments are included. But the code itself is actually quite -simple, due to a bunch of helpful read/write methods the library already provides. - -```python -import json -import asyncio - -from mcproto.buffer import Buffer -from mcproto.connection import TCPAsyncConnection -from mcproto.protocol.base_io import StructFormat - - -async def handshake(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> None: - # As a simple example, let's request status info from a server. - # (this is what you see in the multiplayer server list, i.e. the server's motd, icon, info - # about how many players are connected, etc.) - - # To do this, we first need to understand how are minecraft packets composed, and take a look - # at the specific packets that we're interested in. Thankfully, there's an amazing community - # made wiki that documents all of this! You can find it at https://wiki.vg/ - - # Alright then, let's take a look at the (uncompressed) packet format specification: - # https://wiki.vg/Protocol#Packet_format - # From the wiki, we can see that a packet is composed of 3 fields: - # - Packet length (in bytes), sent as a variable length integer - # combined length of the 2 fields below - # - Packet ID, also sent as varint - # each packet has a unique number, that we use to find out which packet it is - # - Data, specific to the individual packet - # every packet can hold different kind of data, this will be shown in the packet's - # specification (you can find these in wiki.vg) - - # Ok then, with this knowledge, let's establish a connection with our server, and request - # status. To do this, we fist need to send a handshake packet. Let's do it: - - # Let's take a look at what data the Handshake packet should contain: - # https://wiki.vg/Protocol#Handshake - handshake = Buffer() - # We use 47 for the protocol version, as it's quite old, and will work with almost any server - handshake.write_varint(47) - handshake.write_utf(ip) - handshake.write_value(StructFormat.USHORT, port) - handshake.write_varint(1) # Intention to query status - - # Nice! Now that we have the packet data, let's follow the packet format and send it. - # Let's prepare another buffer that will contain the last 2 fields (packet id and data) - # combined. We do this since the first field will require us to know the size of these - # two combined, so let's just put them into 1 buffer. - packet = Buffer() - packet.write_varint(0) # Handshake packet has packet ID of 0 - packet.write(handshake) # Full data from our handshake packet - - # And now, it's time to send it! - await conn.write_varint(len(packet)) # First field (size of packet id + data) - await conn.write(packet) # Second + Third fields (packet id + data) - - -async def status(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> dict: - # This function will be called right after a handshake - # Sending this packet told the server recognize our connection, and since we've specified - # the intention to query status, it then moved us to STATUS game state. - - # Different game states have different packets that we can send out, for example there is a - # game state for login, that we're put into while joining the server, and from it, we tell - # the server our username player UUID, etc. - - # The packet IDs are unique to each game state, so since we're now in status state, a packet - # with ID of 0 is no longer the handshake packet, but rather the status request packet - # (precisely what we need). - # https://wiki.vg/Protocol#Status_Request - - # The status request packet is empty, and doesn't contain any data, it just instructs the - # server to send us back a status response packet. Let's send it! - packet = Buffer() - packet.write_varint(0) # Status request packet ID - - await conn.write_varint(len(packet)) - await conn.write(packet) - - # Now, let's receive the response packet from the server - # Remember, the packet format states that we first receive a length, then packet id, then data - _response_len = await conn.read_varint() - _response = await conn.read(_response_len) # will give us a bytearray - - # Amazing, we've just received data from the server! But it's just bytes, let's turn it into - # a Buffer object, which includes helpful methods that allow us to read from it - response = Buffer(_response) - packet_id = response.read_varint() # Remember, 2nd field is the packet ID - - # Let's see it then, what packet did we get? - print(packet_id) # 0 - - # Interesting, this packet has an ID of 0, but wasn't that the status request packet? We wanted - # a response tho. Well, actually, if we take a look at the status response packet at the wiki, - # it really has an ID of 0: - # https://wiki.vg/Protocol#Status_Response - # Aha, so not only are packet ids unique between game states, they're also unique between the - # direction a server bound packet (sent by client, with server as the destination) can have an - # id of 0, while a client bound packet (sent by server, with client as the destination) can - # have the same id, and mean something else. - - # Alright then, so we know what we got is a status response packet, let's read the wiki a bit - # further and see what data it actually contains, and see how we can get it out. Hmmm, it - # contains a UTF-8 encoded string that represents JSON data, ok, so let's get that string, it's - # still in our buffer. - received_string = response.read_utf() - - # Now, let's just use the json module, convert the string into some json object (in this case, - # a dict) - data = json.loads(received_string) - return data - -async def main(): - # That's it, all that's left is actually calling our functions now - - ip = "mc.hypixel.net" - port = 25565 - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - await handshake(connection, ip, port) - data = await status(connection, ip, port) - - # Wohoo, we got the status data! Let's see it - print(data["players"]["max"]) # This is the server's max player amount (slots) - print(data["players"]["online"]) # This is how many people are currently online - print(data["description"]) # And here's the motd - - # There's a bunch of other things in this data, try it out, see what you can find! - -def start(): - # Just some boilerplate code that can run our asynchronous main function - asyncio.run(main()) -``` - -### Using packet classes for communication - -The first thing you'll need to understand about packet classes in mcproto is that they're generally going to support -the latest minecraft version, and while any the versions are usually mostly compatible, mcproto does NOT guarantee -support for any older protocol versions. - -#### Obtaining the packet map - -As we've already seen in the example before, packets follow certain format, and every packet has it's associated ID -number, direction (client->server or server->client), and game state (status/handshaking/login/play). The packet IDs -are unique to given direction and game state combination. - -For example in clientbound direction (packets sent from server to the client), when in the status game state, there -will always be unique ID numbers for the different packets. In this case, there would actually only be 2 packets here: -The Ping response packet, which has an ID of 1, and the Status response packet, with an ID of 0. - -To receive a packet, we therefore need to know both the game state, and the direction, as only then are we able to -figure out what the type of packet it is. In mcproto, packet receiving therefore requires a "packet map", which is a -mapping (dictionary) of packet id -> packet class. Here's an example of obtaining a packet map: - -```python -from mcproto.packets import generate_packet_map, GameState, PacketDirection - -STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) -``` - -Which, if you were to print it, would look like this: - -``` -{ - 0: - 1: , -} -``` - -Telling us that in the status gamestate, for the clientbound direction, these are the only packet we can receive, -and showing us the actual packet classes for every possible ID number. - -#### Building our own packets - -Our first packet will always have to be a Handshake, this is the only packet in the entire handshaking state, and it's -a "gateway", after which we get moved to a different state, specifically, either to STATUS (to obtain information about -the server, such as motd, amount of players, or other details you'd see in the multiplayer screen in your MC client). - -```python -from mcproto.packets.handshaking.handshake import Handshake, NextState - -my_handshake = Handshake( - # Once again, we use an old protocol version so that even older servers will respond - protocol_version=47, - server_address="mc.hypixel.net", - server_port=25565, - next_state=NextState.STATUS, -) -``` - -That's it! We've now constructed a full handshake packet with all of the data it should contain. You might remember -from the example above, that we originally had to look at the protocol specification, find the handshake packet and -construct it's data as a Buffer with all of these variables. - -With these packet classes, you can simply follow your editor's autocompletion to see what this packet requires, pass it -in and the data will be constructed for you from these attributes, without constantly cross-checking with the wiki. - -For completion, let's also construct the status request packet that we were sending to instruct the server to send us -back the status response packet. - -```python -from mcproto.packets.status.status import StatusRequest - -my_status_request = StatusRequest() -``` - -This one was even easier, as the status request packet alone doesn't contain any special data, it's just a request to -the server to send us some data back. - -#### Sending packets - -To actually send out a packet to the server, we'll need to create a connection, and use the custom functions -responsible for sending packets out. Let's see it: - -```python -from mcproto.packets import async_write_packet -from mcproto.connection import TCPAsyncConnection - -async def main(): - ip = "mc.hypixel.net" - port = 25565 - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - await async_write_packet(connection, my_handshake) - # my_handshake is a packet we've created in the example before -``` - -Much easier than the manual version, isn't it? - -#### Receiving packets - -Alright, we might now know how to send a packet, but how do we receive one? Let's see: - -```python -# Let's say we already have a connection at this moment, after all, how else would -# we've gotten into the STATUS game state. -# Also, let's do something different, let's say we have a synchronous connection, just for fun -from mcproto.connection import TCPSyncConnection -conn: TCPSyncConnection - -# With a synchronous connection, comes synchronous reader, so instead of using async_read_packet, -# we'll use sync_read_packet here -from mcproto.packets import sync_read_packet - -# But remember? To read a packet, we'll need to have that packet map, telling us which IDs represent -# which actual packet types. Let's pass in the one we've constructed before -packet = sync_read_packet(conn, STATUS_CLIENTBOUND_MAP) - -# Cool! We've got back a packet, let's see what kind of packet we got back -from mcproto.packets.status.status import StatusResponse -from mcproto.packets.status.ping import PingPong - -if isinstance(packet, StatusResponse): - ... -elif isinstance(packet, PingPong): - ... -else: - raise Exception("Impossible, there aren't other client bound packets in the STATUS game state") -``` - -#### Requesting status - -Alright, so let's actually try to put all of this knowledge together, and create something meaningful. Let's replicate -the status obtaining logic from the manual example, but with these new packet classes: - -```python -from mcproto.connection import TCPAsyncConnection -from mcproto.packets import async_write_packet, async_read_packet, generate_packet_map -from mcproto.packets.packet import PacketDirection, GameState -from mcproto.packets.handshaking.handshake import Handshake, NextState -from mcproto.packets.status.status import StatusRequest, StatusResponse -from mcproto.packets.status.ping import PingPong - -STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) - - -async def get_status(ip: str, port: int) -> dict: - handshake_packet = Handshake( - protocol_version=47, - server_address=ip, - server_port=port, - next_state=NextState.STATUS, - ) - status_req_packet = StatusRequest() - - async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: - # We start out at HANDSHAKING game state - await async_write_packet(connection, handshake_packet) - # After sending the handshake, we told the server to now move us into the STATUS game state - await async_write_packet(connection, status_req_packet) - # Since we're still in STATUS game state, we use the status packet map when reading - packet = await async_read_packet(connection, STATUS_CLIENTBOUND_MAP) - - # Now that we've got back the packet, we no longer need the connection, we won't be sending - # anything else, so let's get out of the context manager. - - # Now, we should always first make sure it really is the packet we expected - if not isinstance(packet, StatusResponse): - raise ValueError(f"We've got an unexpected packet back: {packet!r}") - - # Since we know we really are dealing with a status response, let's get out it's data, and return it - return packet.data -``` - -Well, that wasn't so hard, was it? +For more info, check our our [documentation](https://py-mine.github.io/mcproto). diff --git a/docs/usage/first-steps.md b/docs/usage/first-steps.md new file mode 100644 index 00000000..c0e8a816 --- /dev/null +++ b/docs/usage/first-steps.md @@ -0,0 +1,249 @@ +# Manual communication with the server + +This example demonstrates how to interact with a Minecraft server using mcproto at it's lowest-level interface. It +avoids the built-in packet classes to show how to manually handle data through mcproto's connection and buffer classes. +Although this isn’t the typical use case for mcproto, it provides insight into the underlying Minecraft protocol, which +is crucial to understand before transitioning to using the higher-level packet handling. + +In this example, we'll retrieve a server's status — information displayed in the multiplayer server list, such as the +server's MOTD, icon, and player count. + +## Step-by-step guide + +### Handshake with the server + +The first step when doing pretty much any kind of communication with the server is establishing a connection and +sending a "handshake" packet. + +??? question "What even is a packet?" + + A packet is a structured piece of data sent across a network to encode an action or message. In games, packets + allow different kinds of information — such as a player's movement, an item pickup, or a chat message — to be + communicated in a structured way, with each packet tailored for a specific purpose. + + Every packet has a set structure with fields that identify it and hold its data, making it clear what action or + event the packet is meant to represent. While packets may carry different types of information, they usually follow + a similar format, so the game’s client and server can read and respond to them easily. + +To do this, we first need to understand Minecraft packets structure in general, then focus on the specific handshake +packet format. To find this out, we recommend using [wiki.vg](https://wiki.vg), which is a fantastic resource, +detailing all of the Minecraft protocol logic. + +So, according to the [Packet Format](https://wiki.vg/Protocol#Packet_format) page, a Minecraft packet has three fields: + +- **Packet length**: the total size of the Packet ID and Data fields (in bytes). Sent in a variable length integer + format. +- **Packet ID**: uniquely identifies which packet this is. Also sent in the varint format. +- **Data**: the packet's actual content. This will differ depending on the packet type. + +Another important information to know is that Minecraft protocol operates in “states,” each with its own set of packets +and IDs. For example, the same packet ID in one state may represent a completely different packet in another state. +Upon establishing a connection with a Minecraft server, you'll begin in the "handshaking" state, with only one packet +available: the handshake packet. This packet tells the server which state to enter next. + +In our case, we’ll request to enter the "status" state, used for obtaining server information (in contrast, the "login" +state would be used to join the server). + +Next, let’s look at the specifics of the handshake packet on wiki.vg [here](https://wiki.vg/Protocol#Handshake). + +From here, we can see that the handshake packet has an ID of `0` and should contain the following data (fields): + +- **Protocol Version**: The version of minecraft protocol (for compatibility), sent as a varint. +- **Server Address**: The hostname or IP that was used to connect to the server, sent as a string with max length of + 255 characters. +- **Server Port**: The port number (usually 25565), sent as unsigned short. +- **Next State**: The desired state to transition to, sent as a varint. (1 for "status".) + +Armed with this information, we can start writing code to send the handshake: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection +from mcproto.protocol.base_io import StructFormat + + +async def handshake(conn: TCPAsyncConnection, ip: str, port: int = 25565) -> None: + handshake = Buffer() + # We use 47 for the protocol version, as which is quite old. We do that to make sure that this code + # will work with almost any server, including older ones. Using a newer protocol number may result + # in older servers refusing to respond. + handshake.write_varint(47) + handshake.write_utf(ip) + handshake.write_value(StructFormat.USHORT, port) + handshake.write_varint(1) # The next state should be "status" + + # Nice! Now we have the packet data, stored in a buffer object. + # This is the data field in the packet format specification. + + # Let's prepare another buffer that will contain the last 2 packet format fields (packet id and data). + # We do this since the first field will require us to know the size of these two combined, + # so let's put them into 1 buffer first: + packet = Buffer() + packet.write_varint(0) # Handshake packet ID + packet.write(handshake) # The entire handshake data, from our previous buffer. + + # And finally, it's time to send it! + await conn.write_varint(len(packet)) # First field (size of packet id + data) + await conn.write(packet) # Second + Third fields (packet id + data) +``` + +### Running the code + +Now, you may be wondering how to actually run this code, what is `TCPAsyncConnection`? Essentially, it's just a wrapper +around a socket connection, designed specifically for communication with Minecraft servers. + +To create an instance of this connection, you'll want to use an `async with` statement, like so: + +```python +import asyncio + +from mcproto.connection import TCPAsyncConnection + +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + +def start(): + # Just some boilerplate code that we can run our asynchronous main function + asyncio.run(main()) +``` + +Currently, this code only establishes a connection and requests a state transition to "status", so when running it you +won't see any meaningful result just yet. + +!!! tip "Synchronous handling" + + Even though we're using asynchronous connection in this example, mcproto does also provide a synchronous + version: `TCPSyncConnection`. + + While you can use this synchronous option, we recommend the asynchronous approach as it highlights blocking + operations with the `await` keyword and allows other tasks to run concurrently, while these blocking operations are + waiting. + +### Obtaining server status + +Now comes the interesting part, we'll request a status from the server, and read the response that it sends us. Since +we're already in the status game state by now, we'll want to take a look at the packets that are available in this +state. Once again, wiki.vg datails all of this for us [here](). + +We can notice that the packets are split into 2 categories: **client-bound** and **server-bound**. We'll first want to +look at the server-bound ones (i.e. packets targetted to the server, sent by the client - us). There are 2 packets +listed here: Ping Request and Status request. Ping is only here to check if the server is online, and allow us to +measure how long the response took, getting the latency, we're not that interested in doing this now, we want to see +some actual useful data from the server, so we'll choose the Status request packet. + +Since this packet just tells the server to send us the status, it actually doesn't contain any data fields for us to +add, so the packet itself will be empty: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def status_request(conn: TCPAsyncConnection) -> None: + # Let's construct a buffer with the packet ID & packet data (like we saw in the handshake example already) + # However, since the status request packet doesn't contain any data, we just need to set the packet id. + packet = Buffer() + packet.write_varint(0) # Status request packet ID + + await conn.write_varint(len(packet)) + await conn.write(packet) +``` + +After we send this request, the server should respond back to us. But what will it respond with? Well, let's find out: + +```python +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def read_status_response(conn: TCPAsyncConnection) -> None: + # Remember, the packet format states that we first receive a length, then packet id, then data + _response_len = await conn.read_varint() + _response = await conn.read(_response_len) # will give us a bytearray + + # Amazing, we've just received data from the server! But it's just bytes, let's turn it into + # a Buffer object, which includes helpful methods that allow us to read from it + response = Buffer(_response) + packet_id = response.read_varint() # Remember, 2nd field is the packet ID, encoded as a varint + + print(packet_id) +``` + +Adjusting our main function to run the new logic: + +```python +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + await status_request(connection) + await read_status_response(connection) +``` + +Running the code now, we can see it print `0`. Aha! That's our packet ID, so let's see what the server sent us. So, +looking through the list of **client-bound** packets in the wiki, this is the **Status Response Packet**! + +!!! note + + Interesting, this packet has an ID of 0, wasn't that the status request packet? + + Indeed, packets can have the same ID in different directions, so packet ID `0` for a client-bound response is + distinct from packet ID `0` for a server-bound request. + +Alright then, let's see what the status response packet contains: The wiki says it just has a single UTF-8 string +field, which contains JSON data. Let's adjust our function a bit, and read that data: + +```python +import json + +from mcproto.buffer import Buffer +from mcproto.connection import TCPAsyncConnection + +async def read_status_response(conn: TCPAsyncConnection) -> dict: # We're now returning a dict + _response_len = await conn.read_varint() + _response = await conn.read(_response_len) + + response = Buffer(_response) + packet_id = response.read_varint() + + # Let's always make sure we got the status response packet here. + assert packet_id == 0 + + # Let's now read that single UTF8 string field, it should still be in our buffer: + received_string = response.read_utf() + + # Now, let's just use the json built-in library, convert the JSON string into a python object + # (in this case, it will be a dict) + data = json.loads(received_string) + + # Cool, we now have the actual status data that the server has provided, we should return them + # from the function now. + # Before we do that though, let's just do a sanity-check and ensure that the buffer doesn't contain + # any more data. + assert response.remaining == 0 # 0 bytes (everything was read) + return data +``` + +Finally, we'll adjust the main function to show some of the status data that we obtained: + +```python +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + await handshake(connection, ip, port) + await status_request(connection) + data = await read_status_response(connection) + + # Wohoo, we got the status data! Let's see it + print(data["players"]["max"]) # This is the server's max player amount (slots) + print(data["players"]["online"]) # This is how many people are currently online + print(data["description"]) # And here's the motd + + # There's a bunch of other things in this data, try it out, see what you can find! +``` diff --git a/docs/usage/packet-communication.md b/docs/usage/packet-communication.md new file mode 100644 index 00000000..b35203a0 --- /dev/null +++ b/docs/usage/packet-communication.md @@ -0,0 +1,187 @@ +# Packet communication + +This guide explains how to communicate with the server using our packet classes. It will go over the same example from +[previous page](./first-steps.md), showing how to obtain the server status, but instead of using the low level +interactions, this guide will simplify a lot of that logic with the use of packet classes. + +!!! warning "Packets Target the Latest Minecraft Version" + + Mcproto's packet classes are designed to support the **latest Minecraft release**. While packets in the handshaking + and status game states usually remain compatible across versions, mcproto does NOT guarantee cross-version packet + compatibility. Using packets in the play game state, for example, will very likely lead to compatibility issues if + you're working with older Minecraft versions. + + Only the low level interactions are guaranteed to remain compatible across protocol updates, if you need support + for and older minecraft version, consider downgrading to an older version of mcproto, or using the low level + interactions. + +## Obtaining the packet map + +Every packet has a unique ID based on its direction (client to server or server to client) and game state (such as +status, handshaking, login, or play). This ID lets us recognize packet types in different situations, which is crucial +for correctly receiving packets. + +To make this process easier, mcproto provides a packet map—essentially a dictionary mapping packet IDs to packet +classes. Here’s how to generate a packet map: + +```python +from mcproto.packets import generate_packet_map, GameState, PacketDirection + +STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) +``` + +Printing `STATUS_CLIENTBOUND_MAP` would display something like this: + +``` +{ + 0: + 1: , +} +``` + +Telling us that in the STATUS gamestate, for the clientbound direction, these are the only packet we can receive, +and mapping the actual packet classes for every supported packet ID number. + +## Using packets + +The first packet we send to the server is always a **Handshake** packet. This is the only packet in the entire +handshaking state, and it's a "gateway", after which we get moved to a different state, in our case, that will be the +STATUS state. + +```python +from mcproto.packets.handshaking.handshake import Handshake, NextState + +my_handshake = Handshake( + # Once again, we use an old protocol version so that even older servers will respond + protocol_version=47, + server_address="mc.hypixel.net", + server_port=25565, + next_state=NextState.STATUS, +) +``` + +That's it! We've now constructed a full handshake packet with all of the data it should contain. You might remember +from the previous low-level example, that we originally had to look at the protocol specification, find the handshake +packet and construct it's data as a Buffer with all of these variables. + +With these packet classes, you can simply follow your editor's autocompletion to see what this packet requires, pass it +in and the data will be constructed for you from these attributes, without constantly cross-checking with the wiki. + +For completion, let's also construct the status request packet that we were sending to instruct the server to send us +back the status response packet. + +```python +from mcproto.packets.status.status import StatusRequest + +my_status_request = StatusRequest() +``` + +This one was even easier, as the status request packet alone doesn't contain any special data, it's just a request to +the server to send us some data back. + +## Sending packets + +To actually send out a packet to the server, we'll need to create a connection, and use mcproto's `async_write_packet` +function, responsible for sending packets. Let's see it: + +```python +from mcproto.packets import async_write_packet +from mcproto.connection import TCPAsyncConnection + +async def main(): + ip = "mc.hypixel.net" + port = 25565 + + async with (await TCPAsyncConnection.make_client((ip, port), timeout=2)) as connection: + # Let's send the handshake packet that we've created in the example before + await async_write_packet(connection, my_handshake) + # Followed by the status request + await async_write_packet(connection, my_status_request) +``` + +Much easier than the manual version, isn't it? + +## Receiving packets + +Alright, we might now know how to send a packet, but how do we receive one? + +Let's see, but this time, let's also try out using the synchronous connection, just for fun: + +```python +from mcproto.connection import TCPSyncConnection + +# With a synchronous connection, comes synchronous reader/writer functions +from mcproto.packets import sync_read_packet, sync_write_packet + +# We'll also need the packet classes from the status game-state +from mcproto.packets.status.status import StatusResponse +from mcproto.packets.status.ping import PingPong + +def main(): + ip = "mc.hypixel.net" + port = 25565 + + with TCPSyncConnection.make_client(("mc.hypixel.net", 25565), 2) as conn: + # First, send the handshake & status request, just like before, but synchronously + await sync_write_packet(connection, my_handshake) + await sync_write_packet(connection, my_status_request) + + # To read a packet, we'll also need to have the packet map, telling us which IDs represent + # which actual packet types. Let's pass in the map that we've constructed before: + packet = sync_read_packet(conn, STATUS_CLIENTBOUND_MAP) + + # Now that we've got back the packet, we no longer need the connection, we won't be sending + # anything else, so let's get out of the context manager. + + # Finally, let's handle the received packet: + if isinstance(packet, StatusResponse): + ... + elif isinstance(packet, PingPong): + ... + else: + raise Exception("Impossible, there are no other client bound packets in the STATUS game state") +``` + +## Requesting status + +Alright, so let's actually try to put all of this knowledge together, and create something meaningful. Let's replicate +the status obtaining logic from the manual example, but with these new packet classes: + +```python +from mcproto.connection import TCPAsyncConnection +from mcproto.packets import async_write_packet, async_read_packet, generate_packet_map +from mcproto.packets.packet import PacketDirection, GameState +from mcproto.packets.handshaking.handshake import Handshake, NextState +from mcproto.packets.status.status import StatusRequest, StatusResponse + +STATUS_CLIENTBOUND_MAP = generate_packet_map(PacketDirection.CLIENTBOUND, GameState.STATUS) + + +async def get_status(ip: str, port: int) -> dict: + handshake_packet = Handshake( + protocol_version=47, + server_address=ip, + server_port=port, + next_state=NextState.STATUS, + ) + status_req_packet = StatusRequest() + + async with (await TCPAsyncConnection.make_client((ip, port), 2)) as connection: + # We start out at HANDSHAKING game state + await async_write_packet(connection, handshake_packet) + # After sending the handshake, we told the server to now move us into the STATUS game state + await async_write_packet(connection, status_req_packet) + # Since we're still in STATUS game state, we use the status packet map when reading + packet = await async_read_packet(connection, STATUS_CLIENTBOUND_MAP) + + # Now, we should always first make sure it really is the packet we expected + if not isinstance(packet, StatusResponse): + raise ValueError(f"We've got an unexpected packet back: {packet!r}") + + # Since we know we really are dealing with a status response, let's get out it's data, and return it + # this is the same JSON data that we obtained from the first example with the manual interactions + return packet.data +``` + +As you can see, this approach is more convenient and eliminates much of the manual packet handling, letting you focus +on higher-level logic! diff --git a/mkdocs.yml b/mkdocs.yml index a9850b5b..26c2c308 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,8 @@ nav: - Changelog: installation/changelog.md - Usage: - usage/index.md + - First steps: usage/first-steps.md + - Packet Communication: usage/packet-communication.md - Authentication: usage/authentication.md - Community: - Code of Conduct: community/code-of-conduct.md