commit 2e29696

shrub  ·  2026-07-30 23:00:12 +0000 UTC
parent 42d3dc4
implement protocol basics offline mode
6 files changed,  +875, -2
+40, -0
 1@@ -0,0 +1,40 @@
 2+#ifndef MCPROTO_H
 3+#define MCPROTO_H
 4+
 5+#include <stddef.h>
 6+
 7+#include "types.h"
 8+
 9+#define MC_PROTOCOL_1_8_9 47
10+
11+typedef struct MCClient MCClient;
12+
13+typedef struct {
14+	const char* username;
15+	const char* profile_uuid;
16+	const char* access_token;
17+} MCAccount;
18+
19+typedef struct {
20+	i32 id;
21+	u8* data;
22+	size_t size;
23+} MCPacket;
24+
25+int mc_client_login(
26+    MCClient** out,
27+    const char* address,
28+    const MCAccount* account,
29+    char* error,
30+    size_t error_size);
31+void mc_client_interrupt(MCClient* client);
32+void mc_client_destroy(MCClient* client);
33+
34+int mc_client_read(MCClient* client, MCPacket* packet, char* error, size_t error_size);
35+int mc_client_write(MCClient* client, i32 packet_id, const void* data, size_t size);
36+void mc_packet_free(MCPacket* packet);
37+
38+const char* mc_client_username(const MCClient* client);
39+const char* mc_client_uuid(const MCClient* client);
40+
41+#endif /* MCPROTO_H */
+4, -0
 1@@ -10,6 +10,7 @@ project(
 2 cc = meson.get_compiler('c')
 3 math_dep = cc.find_library('m', required: false)
 4 sdl_dep = dependency('sdl2')
 5+zlib_dep = dependency('zlib')
 6 renderer_backend = get_option('renderer')
 7 
 8 sources = [
 9@@ -17,6 +18,8 @@ sources = [
10 	'source/util.c',
11 	'source/world/chunk.c',
12 	'source/client/camera.c',
13+	'source/mcproto/buffer.c',
14+	'source/mcproto/client.c',
15 	'source/platform/platform.c',
16         'vendor/stb_image_c89.c',
17 ]
18@@ -95,6 +98,7 @@ renderer_dep = declare_dependency(
19 
20 deps = [
21 	sdl_dep,
22+	zlib_dep,
23 	mgmath_dep,
24 	renderer_dep,
25 ]
+52, -2
 1@@ -1,7 +1,9 @@
 2 #include <SDL2/SDL.h>
 3+#include <stdio.h>
 4 
 5 #include "SDL_keyboard.h"
 6 #include "SDL_timer.h"
 7+#include "mcproto/mcproto.h"
 8 #include "util.h"
 9 #include "platform/platform.h"
10 #include "renderer/renderer.h"
11@@ -50,14 +52,62 @@ static void input(void)
12 	if (keys[SDL_SCANCODE_LCTRL])
13 		move.y -= 1.0f;
14 
15-	move = MG_MulV3F(move, 4.0f * dt); /* arbritrary movement speed */
16+	move = MG_MulV3F(move, 8.0f * dt); /* arbritrary movement speed */
17 	camera_move(&cam, move);
18 	SDL_GetRelativeMouseState(&mx, &my);
19 	camera_rotate(&cam, mx * sensitivity, -my * sensitivity);
20 }
21 
22-int main(void)
23+static int protocol_poc(const char* address, const char* username)
24 {
25+	MCAccount account;
26+	MCClient* client;
27+	char error[1024];
28+
29+	account.username = username;
30+	account.profile_uuid = NULL;
31+	account.access_token = NULL;
32+	if (!mc_client_login(&client, address, &account, error, sizeof(error))) {
33+		fprintf(stderr, "%s\n", error);
34+		return MG_ERR_EXIT_FAILURE;
35+	}
36+	/*
37+	 * offline mode protocol test: 
38+	 * packet keep alive is play/clientbound 0x00 in protocol 47
39+	 *https://prismarinejs.github.io/minecraft-data/protocol/pc/1.8/#play.toClient.types.packet_keep_alive
40+	 */
41+	puts("entered play state: printing packet ids and echoing keepalives");
42+	for (;;) {
43+		MCPacket packet;
44+		if (!mc_client_read(client, &packet, error, sizeof(error))) {
45+			fprintf(stderr, "%s\n", error);
46+			mc_client_destroy(client);
47+			return MG_ERR_EXIT_FAILURE;
48+		}
49+		printf("play packet 0x%02x size %lu\n",
50+		    packet.id, (unsigned long)packet.size);
51+		if (packet.id == 0x00) {
52+			if (!mc_client_write(client, 0x00, packet.data, packet.size)) {
53+				fprintf(stderr, "failed to echo keepalive\n");
54+				mc_packet_free(&packet);
55+				mc_client_destroy(client);
56+				return MG_ERR_EXIT_FAILURE;
57+			}
58+			puts("echoed keepalive");
59+		}
60+		mc_packet_free(&packet);
61+	}
62+}
63+
64+int main(int argc, char** argv)
65+{
66+	if (argc != 1 && argc != 3) {
67+		fprintf(stderr, "usage: %s [server[:port] username]\n", argv[0]);
68+		return MG_ERR_EXIT_FAILURE;
69+	}
70+	if (argc == 3)
71+		return protocol_poc(argv[1], argv[2]);
72+
73 	platform_init(&ctx);
74 	platform_window_create(&ctx);
75 	renderer_create(&ctx);
+259, -0
  1@@ -0,0 +1,259 @@
  2+#include <stdarg.h>
  3+#include <stdio.h>
  4+#include <stdlib.h>
  5+#include <string.h>
  6+
  7+#include "internal.h"
  8+
  9+void mc_set_error(char* error, size_t error_size, const char* fmt, ...)
 10+{
 11+	va_list args;
 12+
 13+	if (!error || error_size == 0)
 14+		return;
 15+	va_start(args, fmt);
 16+	vsnprintf(error, error_size, fmt, args);
 17+	va_end(args);
 18+}
 19+
 20+void bufinit(MCBuffer* buffer)
 21+{
 22+	memset(buffer, 0, sizeof(*buffer));
 23+}
 24+
 25+void buffree(MCBuffer* buffer)
 26+{
 27+	free(buffer->data);
 28+	memset(buffer, 0, sizeof(*buffer));
 29+}
 30+
 31+int bufreserve(MCBuffer* buffer, size_t extra)
 32+{
 33+	size_t needed;
 34+	size_t capacity;
 35+	u8* data;
 36+
 37+	if (extra > MC_MAX_PACKET_SIZE || buffer->size > MC_MAX_PACKET_SIZE - extra)
 38+		return 0;
 39+	needed = buffer->size + extra;
 40+	if (needed <= buffer->capacity)
 41+		return 1;
 42+	capacity = buffer->capacity ? buffer->capacity : 128;
 43+	while (capacity < needed) {
 44+		if (capacity > MC_MAX_PACKET_SIZE / 2) {
 45+			capacity = MC_MAX_PACKET_SIZE;
 46+			break;
 47+		}
 48+		capacity *= 2;
 49+	}
 50+	data = (u8*)realloc(buffer->data, capacity);
 51+	if (!data)
 52+		return 0;
 53+	buffer->data = data;
 54+	buffer->capacity = capacity;
 55+	return 1;
 56+}
 57+
 58+int bufwrite(MCBuffer* buffer, const void* data, size_t size)
 59+{
 60+	if (!bufreserve(buffer, size))
 61+		return 0;
 62+	if (size)
 63+		memcpy(buffer->data + buffer->size, data, size);
 64+	buffer->size += size;
 65+	return 1;
 66+}
 67+
 68+int bufwrite_u8(MCBuffer* buffer, u8 value)
 69+{
 70+	return bufwrite(buffer, &value, 1);
 71+}
 72+
 73+int bufwrite_u16(MCBuffer* buffer, u16 value)
 74+{
 75+	u8 data[2];
 76+	/*
 77+	 * minecraft protocol integers are big-endian except VarInt
 78+	 * https://wikivg.booky.dev/Protocol#Data_types
 79+	 */
 80+	data[0] = (u8)(value >> 8);
 81+	data[1] = (u8)value;
 82+	return bufwrite(buffer, data, sizeof(data));
 83+}
 84+
 85+int bufwrite_i32(MCBuffer* buffer, i32 value)
 86+{
 87+	u32 v = (u32)value;
 88+	u8 data[4];
 89+	data[0] = (u8)(v >> 24);
 90+	data[1] = (u8)(v >> 16);
 91+	data[2] = (u8)(v >> 8);
 92+	data[3] = (u8)v;
 93+	return bufwrite(buffer, data, sizeof(data));
 94+}
 95+
 96+int bufwrite_i64(MCBuffer* buffer, i64 value)
 97+{
 98+	u64 v = (u64)value;
 99+	u8 data[8];
100+	int i;
101+	for (i = 0; i < 8; ++i)
102+		data[i] = (u8)(v >> (56 - i * 8));
103+	return bufwrite(buffer, data, sizeof(data));
104+}
105+
106+int bufwrite_f32(MCBuffer* buffer, float value)
107+{
108+	u32 bits;
109+	memcpy(&bits, &value, sizeof(bits));
110+	return bufwrite_i32(buffer, (i32)bits);
111+}
112+
113+int bufwrite_f64(MCBuffer* buffer, double value)
114+{
115+	u64 bits;
116+	memcpy(&bits, &value, sizeof(bits));
117+	return bufwrite_i64(buffer, (i64)bits);
118+}
119+
120+int bufwrite_varint(MCBuffer* buffer, i32 value)
121+{
122+	u32 v = (u32)value;
123+	/*
124+	 * minecraft VarInt is 7 data bits per byte (5 bytes max on i32) and high bit means more bytes follow
125+	 * https://wikivg.booky.dev/Protocol#VarInt_and_VarLong
126+	 */
127+	do {
128+		u8 byte = (u8)(v & 0x7f);
129+		v >>= 7;
130+		if (v)
131+			byte |= 0x80;
132+		if (!bufwrite_u8(buffer, byte))
133+			return 0;
134+	} while (v);
135+	return 1;
136+}
137+
138+int bufwrite_string(MCBuffer* buffer, const char* value)
139+{
140+	size_t size = strlen(value);
141+	if (size > MC_MAX_STRING_SIZE)
142+		return 0;
143+	return bufwrite_varint(buffer, (i32)size) &&
144+	       bufwrite(buffer, value, size);
145+}
146+
147+int bufread(MCBuffer* buffer, void* out, size_t size)
148+{
149+	if (size > buffer->size - buffer->cursor)
150+		return 0;
151+	if (size)
152+		memcpy(out, buffer->data + buffer->cursor, size);
153+	buffer->cursor += size;
154+	return 1;
155+}
156+
157+int bufread_u8(MCBuffer* buffer, u8* value)
158+{
159+	return bufread(buffer, value, 1);
160+}
161+
162+int bufread_u16(MCBuffer* buffer, u16* value)
163+{
164+	u8 data[2];
165+	if (!bufread(buffer, data, sizeof(data)))
166+		return 0;
167+	*value = (u16)(((u16)data[0] << 8) | data[1]);
168+	return 1;
169+}
170+
171+int bufread_i32(MCBuffer* buffer, i32* value)
172+{
173+	u8 data[4];
174+	u32 result;
175+	if (!bufread(buffer, data, sizeof(data)))
176+		return 0;
177+	result = ((u32)data[0] << 24) | ((u32)data[1] << 16) |
178+	         ((u32)data[2] << 8) | data[3];
179+	*value = (i32)result;
180+	return 1;
181+}
182+
183+int bufread_i64(MCBuffer* buffer, i64* value)
184+{
185+	u8 data[8];
186+	u64 result = 0;
187+	int i;
188+	if (!bufread(buffer, data, sizeof(data)))
189+		return 0;
190+	for (i = 0; i < 8; ++i)
191+		result = (result << 8) | data[i];
192+	*value = (i64)result;
193+	return 1;
194+}
195+
196+int bufread_f32(MCBuffer* buffer, float* value)
197+{
198+	i32 bits;
199+	if (!bufread_i32(buffer, &bits))
200+		return 0;
201+	memcpy(value, &bits, sizeof(*value));
202+	return 1;
203+}
204+
205+int bufread_f64(MCBuffer* buffer, double* value)
206+{
207+	i64 bits;
208+	if (!bufread_i64(buffer, &bits))
209+		return 0;
210+	memcpy(value, &bits, sizeof(*value));
211+	return 1;
212+}
213+
214+int bufread_varint(MCBuffer* buffer, i32* value)
215+{
216+	u32 result = 0;
217+	int shift;
218+
219+	/*
220+	 * VarInt is at best 5 bytes for a 32-bit value
221+	 * https://wikivg.booky.dev/Protocol#VarInt_and_VarLong
222+	 *
223+	 * 35 = 7*5 
224+	 */
225+	for (shift = 0; shift < 35; shift += 7) {
226+		u8 byte;
227+		if (!bufread_u8(buffer, &byte))
228+			return 0;
229+		result |= (u32)(byte & 0x7f) << shift;
230+		if (!(byte & 0x80)) {
231+			*value = (i32)result;
232+			return 1;
233+		}
234+	}
235+	return 0;
236+}
237+
238+int bufread_string(MCBuffer* buffer, char* value, size_t value_size)
239+{
240+	i32 length;
241+	if (!bufread_varint(buffer, &length) || length < 0 ||
242+	    (size_t)length >= value_size || (size_t)length > MC_MAX_STRING_SIZE)
243+		return 0;
244+	if (!bufread(buffer, value, (size_t)length))
245+		return 0;
246+	value[length] = '\0';
247+	return 1;
248+}
249+
250+int bufread_bytes(MCBuffer* buffer, const u8** value, size_t* size)
251+{
252+	i32 length;
253+	if (!bufread_varint(buffer, &length) || length < 0 ||
254+	    (size_t)length > buffer->size - buffer->cursor)
255+		return 0;
256+	*value = buffer->data + buffer->cursor;
257+	*size = (size_t)length;
258+	buffer->cursor += (size_t)length;
259+	return 1;
260+}
+466, -0
  1@@ -0,0 +1,466 @@
  2+#define _POSIX_C_SOURCE 200112L
  3+
  4+#include <errno.h>
  5+#include <netdb.h>
  6+#include <stdio.h>
  7+#include <stdlib.h>
  8+#include <string.h>
  9+#include <sys/socket.h>
 10+#include <unistd.h>
 11+
 12+#include <zlib.h>
 13+
 14+#include "internal.h"
 15+
 16+#ifndef MSG_NOSIGNAL
 17+#define MSG_NOSIGNAL 0
 18+#endif
 19+
 20+static int split_address(
 21+    const char* address,
 22+    char* host,
 23+    size_t host_size,
 24+    u16* port,
 25+    char* error,
 26+    size_t error_size)
 27+{
 28+	const char* colon = strrchr(address, ':');
 29+	const char* end = address + strlen(address);
 30+	char* port_end = NULL;
 31+	size_t length;
 32+	long parsed_port = 25565;
 33+
 34+	if (address[0] == '[') {
 35+		const char* close = strchr(address, ']');
 36+		if (!close) {
 37+			mc_set_error(error, error_size, "invalid bracketed IPv6 address");
 38+			return 0;
 39+		}
 40+		length = (size_t)(close - address - 1);
 41+		address += 1;
 42+		if (close[1] == ':')
 43+			parsed_port = strtol(close + 2, &port_end, 10);
 44+		else if (close[1] != '\0') {
 45+			mc_set_error(error, error_size, "invalid address suffix");
 46+			return 0;
 47+		}
 48+	} else if (colon && strchr(address, ':') == colon) {
 49+		length = (size_t)(colon - address);
 50+		parsed_port = strtol(colon + 1, &port_end, 10);
 51+	} else {
 52+		length = (size_t)(end - address);
 53+	}
 54+	if ((port_end && *port_end) || length == 0 || length >= host_size ||
 55+	    parsed_port < 1 || parsed_port > 65535) {
 56+		mc_set_error(error, error_size, "invalid server address");
 57+		return 0;
 58+	}
 59+	memcpy(host, address, length);
 60+	host[length] = '\0';
 61+	*port = (u16)parsed_port;
 62+	return 1;
 63+}
 64+
 65+static int tcp_connect(MCClient* client, char* error, size_t error_size)
 66+{
 67+	struct addrinfo hints;
 68+	struct addrinfo* addresses = NULL;
 69+	struct addrinfo* address;
 70+	char service[6];
 71+	int result;
 72+
 73+	memset(&hints, 0, sizeof(hints));
 74+	hints.ai_family = AF_UNSPEC;
 75+	hints.ai_socktype = SOCK_STREAM;
 76+	snprintf(service, sizeof(service), "%u", client->port);
 77+	result = getaddrinfo(client->host, service, &hints, &addresses);
 78+	if (result != 0) {
 79+		mc_set_error(error, error_size, "could not resolve %s: %s",
 80+		    client->host, gai_strerror(result));
 81+		return 0;
 82+	}
 83+	for (address = addresses; address; address = address->ai_next) {
 84+		client->socket_fd = socket(
 85+		    address->ai_family, address->ai_socktype, address->ai_protocol);
 86+		if (client->socket_fd < 0)
 87+			continue;
 88+		if (connect(client->socket_fd, address->ai_addr, address->ai_addrlen) == 0)
 89+			break;
 90+		close(client->socket_fd);
 91+		client->socket_fd = -1;
 92+	}
 93+	freeaddrinfo(addresses);
 94+	if (client->socket_fd < 0) {
 95+		mc_set_error(error, error_size, "could not connect to %s:%u: %s",
 96+		    client->host, client->port, strerror(errno));
 97+		return 0;
 98+	}
 99+	return 1;
100+}
101+
102+static int socket_write(MCClient* client, const u8* data, size_t size)
103+{
104+	size_t sent = 0;
105+	while (sent < size) {
106+		ssize_t count = send(client->socket_fd, data + sent,
107+		    size - sent, MSG_NOSIGNAL);
108+		if (count > 0)
109+			sent += (size_t)count;
110+		else if (count < 0 && errno == EINTR)
111+			continue;
112+		else
113+			return 0;
114+	}
115+	return 1;
116+}
117+
118+static int socket_read(MCClient* client, u8* data, size_t size)
119+{
120+	size_t offset = 0;
121+	while (offset < size) {
122+		ssize_t count = recv(client->socket_fd, data + offset, size - offset, 0);
123+		if (count > 0)
124+			offset += (size_t)count;
125+		else if (count < 0 && errno == EINTR)
126+			continue;
127+		else
128+			return 0;
129+	}
130+	return 1;
131+}
132+
133+static int socket_read_varint(MCClient* client, i32* value)
134+{
135+	u32 result = 0;
136+	int shift;
137+	for (shift = 0; shift < 35; shift += 7) {
138+		u8 byte;
139+		if (!socket_read(client, &byte, 1))
140+			return 0;
141+		result |= (u32)(byte & 0x7f) << shift;
142+		if (!(byte & 0x80)) {
143+			*value = (i32)result;
144+			return 1;
145+		}
146+	}
147+	return 0;
148+}
149+
150+int mc_client_write(MCClient* client, i32 packet_id, const void* data, size_t size)
151+{
152+	MCBuffer body;
153+	MCBuffer framed;
154+	MCBuffer length;
155+	u8* compressed = NULL;
156+	uLongf compressed_size;
157+	int ok = 0;
158+
159+	if (!client || client->socket_fd < 0 || size > MC_MAX_PACKET_SIZE)
160+		return 0;
161+	bufinit(&body);
162+	bufinit(&framed);
163+	bufinit(&length);
164+	if (!bufwrite_varint(&body, packet_id) ||
165+	    !bufwrite(&body, data, size))
166+		goto cleanup;
167+	/*
168+	 * packet layout:
169+	 *   length VarInt
170+	 *   [data length VarInt if compression is enabled]
171+	 *   packet id VarInt
172+	 *   payload
173+	 *
174+	 * https://wikivg.booky.dev/Protocol#Packet_format
175+	 */
176+	if (client->compression_threshold >= 0) {
177+		/*
178+		 * if in compressed mode, data length 0 means this packet body is
179+		 * not actually compressed. otherwise it is the size after zlib decompression
180+		 * https://wikivg.booky.dev/Protocol#With_compression
181+		 */
182+		if (body.size >= (size_t)client->compression_threshold) {
183+			compressed_size = compressBound((uLong)body.size);
184+			compressed = (u8*)malloc(compressed_size);
185+			if (!compressed ||
186+			    compress2(compressed, &compressed_size, body.data,
187+			        (uLong)body.size, Z_DEFAULT_COMPRESSION) != Z_OK)
188+				goto cleanup;
189+			if (!bufwrite_varint(&framed, (i32)body.size) ||
190+			    !bufwrite(&framed, compressed, compressed_size))
191+				goto cleanup;
192+		} else if (!bufwrite_varint(&framed, 0) ||
193+		           !bufwrite(&framed, body.data, body.size)) {
194+			goto cleanup;
195+		}
196+	} else if (!bufwrite(&framed, body.data, body.size)) {
197+		goto cleanup;
198+	}
199+	if (bufwrite_varint(&length, (i32)framed.size) &&
200+	    socket_write(client, length.data, length.size) &&
201+	    socket_write(client, framed.data, framed.size))
202+		ok = 1;
203+
204+cleanup:
205+	free(compressed);
206+	buffree(&length);
207+	buffree(&framed);
208+	buffree(&body);
209+	return ok;
210+}
211+
212+int mc_client_read(MCClient* client, MCPacket* packet, char* error, size_t error_size)
213+{
214+	i32 frame_length;
215+	MCBuffer frame;
216+	MCBuffer decoded;
217+	i32 data_length;
218+	i32 packet_id;
219+	int decoded_allocated = 0;
220+	int ok = 0;
221+
222+	memset(packet, 0, sizeof(*packet));
223+	bufinit(&frame);
224+	bufinit(&decoded);
225+	if (!socket_read_varint(client, &frame_length)) {
226+		mc_set_error(error, error_size, "connection closed while reading packet length");
227+		goto cleanup;
228+	}
229+	if (frame_length <= 0 || (u32)frame_length > MC_MAX_PACKET_SIZE ||
230+	    !bufreserve(&frame, (size_t)frame_length)) {
231+		mc_set_error(error, error_size, "invalid packet length: %d", frame_length);
232+		goto cleanup;
233+	}
234+	frame.size = (size_t)frame_length;
235+	if (!socket_read(client, frame.data, frame.size)) {
236+		mc_set_error(error, error_size, "connection closed while reading packet");
237+		goto cleanup;
238+	}
239+	if (client->compression_threshold >= 0) {
240+		/*
241+		 * same as before data length 0 means no compression
242+		 */
243+		if (!bufread_varint(&frame, &data_length) || data_length < 0 ||
244+		    (u32)data_length > MC_MAX_PACKET_SIZE) {
245+			mc_set_error(error, error_size, "invalid compressed packet header");
246+			goto cleanup;
247+		}
248+		if (data_length == 0) {
249+			decoded.data = frame.data + frame.cursor;
250+			decoded.size = frame.size - frame.cursor;
251+		} else {
252+			uLongf output_size = (uLongf)data_length;
253+			if (!bufreserve(&decoded, (size_t)data_length) ||
254+			    uncompress(decoded.data, &output_size,
255+			        frame.data + frame.cursor,
256+			        (uLong)(frame.size - frame.cursor)) != Z_OK ||
257+			    output_size != (uLongf)data_length) {
258+				mc_set_error(error, error_size, "invalid zlib packet");
259+				goto cleanup;
260+			}
261+			decoded_allocated = 1;
262+			decoded.size = (size_t)output_size;
263+		}
264+	} else {
265+		decoded.data = frame.data;
266+		decoded.size = frame.size;
267+	}
268+	if (!bufread_varint(&decoded, &packet_id)) {
269+		mc_set_error(error, error_size, "packet has no valid id");
270+		goto cleanup;
271+	}
272+	packet->size = decoded.size - decoded.cursor;
273+	packet->data = (u8*)malloc(packet->size ? packet->size : 1);
274+	if (!packet->data) {
275+		mc_set_error(error, error_size, "out of memory reading packet");
276+		goto cleanup;
277+	}
278+	memcpy(packet->data, decoded.data + decoded.cursor, packet->size);
279+	packet->id = packet_id;
280+	ok = 1;
281+
282+cleanup:
283+	if (decoded_allocated)
284+		buffree(&decoded);
285+	else
286+		decoded.data = NULL;
287+	buffree(&frame);
288+	if (!ok)
289+		mc_packet_free(packet);
290+	return ok;
291+}
292+
293+void mc_packet_free(MCPacket* packet)
294+{
295+	if (!packet)
296+		return;
297+	free(packet->data);
298+	memset(packet, 0, sizeof(*packet));
299+}
300+
301+static int send_handshake(MCClient* client)
302+{
303+	MCBuffer payload;
304+	int ok;
305+	bufinit(&payload);
306+	/*
307+	 * handshake to next state 2 switches the connection into login state
308+	 */
309+	ok = bufwrite_varint(&payload, MC_PROTOCOL_1_8_9) &&
310+	     bufwrite_string(&payload, client->host) &&
311+	     bufwrite_u16(&payload, client->port) &&
312+	     bufwrite_varint(&payload, 2) &&
313+	     mc_client_write(client, 0x00, payload.data, payload.size);
314+	buffree(&payload);
315+	return ok;
316+}
317+
318+static int send_login_start(MCClient* client)
319+{
320+	MCBuffer payload;
321+	int ok;
322+	bufinit(&payload);
323+	ok = bufwrite_string(&payload, client->username) &&
324+	     mc_client_write(client, 0x00, payload.data, payload.size);
325+	buffree(&payload);
326+	return ok;
327+}
328+
329+static int login_loop(MCClient* client, char* error, size_t error_size)
330+{
331+	/*
332+	 * login state packet ids for protocol 47:
333+	 *   0x00 disconnect 
334+	 *   0x01 encryption request
335+	 *   0x02 login success
336+	 *   0x03 set compression
337+	 *
338+	 * https://prismarinejs.github.io/minecraft-data/protocol/pc/1.8/#login.toClient.types
339+	 */
340+	for (;;) {
341+		MCPacket packet;
342+		MCBuffer input;
343+		if (!mc_client_read(client, &packet, error, error_size))
344+			return 0;
345+		input.data = packet.data;
346+		input.size = packet.size;
347+		input.capacity = packet.size;
348+		input.cursor = 0;
349+		if (packet.id == 0x00) {
350+			char reason[1024];
351+			if (!bufread_string(&input, reason, sizeof(reason)))
352+				strcpy(reason, "malformed disconnect reason");
353+			mc_set_error(error, error_size, "server disconnected during login: %s", reason);
354+			mc_packet_free(&packet);
355+			return 0;
356+		}
357+		if (packet.id == 0x01) {
358+			mc_set_error(error, error_size,
359+			    "server requested online-mode encryption, idfk know what to do");
360+			mc_packet_free(&packet);
361+			return 0;
362+		}
363+		if (packet.id == 0x02) {
364+			char username[64];
365+			if (!bufread_string(&input, client->uuid, sizeof(client->uuid)) ||
366+			    !bufread_string(&input, username, sizeof(username))) {
367+				mc_set_error(error, error_size, "malformed login success packet");
368+				mc_packet_free(&packet);
369+				return 0;
370+			}
371+			printf("login success: uuid=%s username=%s\n", client->uuid, username);
372+			mc_packet_free(&packet);
373+			return 1;
374+		}
375+		if (packet.id == 0x03) {
376+			i32 threshold;
377+			if (!bufread_varint(&input, &threshold) || threshold < 0) {
378+				mc_set_error(error, error_size, "invalid compression threshold");
379+				mc_packet_free(&packet);
380+				return 0;
381+			}
382+			client->compression_threshold = threshold;
383+			printf("compression enabled: threshold=%d\n", threshold);
384+			mc_packet_free(&packet);
385+			continue;
386+		}
387+		mc_set_error(error, error_size, "unexpected login packet 0x%02x", packet.id);
388+		mc_packet_free(&packet);
389+		return 0;
390+	}
391+}
392+
393+int mc_client_login(
394+    MCClient** out,
395+    const char* address,
396+    const MCAccount* account,
397+    char* error,
398+    size_t error_size)
399+{
400+	MCClient* client;
401+	size_t username_size;
402+
403+	*out = NULL;
404+	if (error && error_size)
405+		error[0] = '\0';
406+	if (!address || !account || !account->username) {
407+		mc_set_error(error, error_size, "missing server address or account");
408+		return 0;
409+	}
410+	username_size = strlen(account->username);
411+	if (username_size == 0 || username_size > 16) {
412+		mc_set_error(error, error_size, "Minecraft username must be 1 to 16 characters");
413+		return 0;
414+	}
415+	client = (MCClient*)calloc(1, sizeof(*client));
416+	if (!client) {
417+		mc_set_error(error, error_size, "out of memory creating client");
418+		return 0;
419+	}
420+	client->socket_fd = -1;
421+	client->compression_threshold = -1;
422+	memcpy(client->username, account->username, username_size + 1);
423+	printf("connecting to %s as %s\n", address, client->username);
424+	if (!split_address(address, client->host, sizeof(client->host),
425+	        &client->port, error, error_size) ||
426+	    !tcp_connect(client, error, error_size) ||
427+	    !send_handshake(client) ||
428+	    !send_login_start(client)) {
429+		if (!error || !error_size || !error[0])
430+			mc_set_error(error, error_size, "could not start Minecraft login");
431+		mc_client_destroy(client);
432+		return 0;
433+	}
434+	printf("sent handshake and login start\n");
435+	if (!login_loop(client, error, error_size)) {
436+		mc_client_destroy(client);
437+		return 0;
438+	}
439+	*out = client;
440+	return 1;
441+}
442+
443+void mc_client_interrupt(MCClient* client)
444+{
445+	if (client && client->socket_fd >= 0)
446+		shutdown(client->socket_fd, SHUT_RDWR);
447+}
448+
449+void mc_client_destroy(MCClient* client)
450+{
451+	if (!client)
452+		return;
453+	if (client->socket_fd >= 0)
454+		close(client->socket_fd);
455+	memset(client, 0, sizeof(*client));
456+	free(client);
457+}
458+
459+const char* mc_client_username(const MCClient* client)
460+{
461+	return client ? client->username : "";
462+}
463+
464+const char* mc_client_uuid(const MCClient* client)
465+{
466+	return client ? client->uuid : "";
467+}
+54, -0
 1@@ -0,0 +1,54 @@
 2+#ifndef MCPROTO_INTERNAL_H
 3+#define MCPROTO_INTERNAL_H
 4+
 5+#include <stddef.h>
 6+
 7+#include "mcproto/mcproto.h"
 8+
 9+#define MC_MAX_PACKET_SIZE (16u * 1024u * 1024u)
10+#define MC_MAX_STRING_SIZE 32767u
11+
12+typedef struct {
13+	u8* data;
14+	size_t size;
15+	size_t capacity;
16+	/* read offset used by bufread funcs, writes always append at size */
17+	size_t cursor;
18+} MCBuffer;
19+
20+struct MCClient {
21+	int socket_fd;
22+	char host[256];
23+	u16 port;
24+	char username[17];
25+	char uuid[37];
26+	i32 compression_threshold;
27+};
28+
29+void mc_set_error(char* error, size_t error_size, const char* fmt, ...);
30+
31+void bufinit(MCBuffer* buffer);
32+void buffree(MCBuffer* buffer);
33+int bufreserve(MCBuffer* buffer, size_t extra);
34+int bufwrite(MCBuffer* buffer, const void* data, size_t size);
35+int bufwrite_u8(MCBuffer* buffer, u8 value);
36+int bufwrite_u16(MCBuffer* buffer, u16 value);
37+int bufwrite_i32(MCBuffer* buffer, i32 value);
38+int bufwrite_i64(MCBuffer* buffer, i64 value);
39+int bufwrite_f32(MCBuffer* buffer, float value);
40+int bufwrite_f64(MCBuffer* buffer, double value);
41+int bufwrite_varint(MCBuffer* buffer, i32 value);
42+int bufwrite_string(MCBuffer* buffer, const char* value);
43+
44+int bufread(MCBuffer* buffer, void* out, size_t size);
45+int bufread_u8(MCBuffer* buffer, u8* value);
46+int bufread_u16(MCBuffer* buffer, u16* value);
47+int bufread_i32(MCBuffer* buffer, i32* value);
48+int bufread_i64(MCBuffer* buffer, i64* value);
49+int bufread_f32(MCBuffer* buffer, float* value);
50+int bufread_f64(MCBuffer* buffer, double* value);
51+int bufread_varint(MCBuffer* buffer, i32* value);
52+int bufread_string(MCBuffer* buffer, char* value, size_t value_size);
53+int bufread_bytes(MCBuffer* buffer, const u8** value, size_t* size);
54+
55+#endif /* MCPROTO_INTERNAL_H */