commit 17cb258

uint  ·  2026-08-02 20:23:25 +0000 UTC
parent bf83f21
add multiplayer

very dirty bc SOMEONE asked me to js push smh
6 files changed,  +528, -50
+11, -0
 1@@ -28,8 +28,19 @@ void mc_client_destroy(MCClient* client);
 2 
 3 int  mc_client_read(MCClient* client, MCPacket* packet, char* error, size_t error_size);
 4 int  mc_client_write(MCClient* client, i32 packet_id, const void* data, size_t size);
 5+
 6+/* poll for packets.
 7+   returns 1: packet recieved,
 8+   0: no packet awaiting,
 9+   -1: connection error */
10+int mc_client_poll(MCClient* client, MCPacket* packet, char* error, size_t error_size);
11+
12 void mc_packet_free(MCPacket* packet);
13 
14+/* send server player look data */
15+int mc_client_send_position_look(MCClient* client, double x, double feet_y,
16+                                 double z, float yaw, float pitch, i8 grounded);
17+
18 const char* mc_client_username(const MCClient* client);
19 const char* mc_client_uuid(const MCClient* client);
20 
+382, -28
  1@@ -3,47 +3,352 @@
  2 
  3 #include "mgmath.h"
  4 #include "util.h"
  5+#include "mcproto/mcproto.h"
  6+#include "mcproto/packet.h"
  7 #include "client/camera.h"
  8+#include "client/player.h"
  9 #include "renderer/renderer.h"
 10 #include "world/block.h"
 11 #include "world/chunk.h"
 12 
 13+/* big TODO. most of this stuff belongs elsewhere !!! */
 14+
 15+typedef struct {
 16+	i32 x;
 17+	i32 z;
 18+
 19+	Chunk sections[16]; /* a 16x256x16 chunk */
 20+	u16   section_mask;
 21+
 22+	i8 used;
 23+} ServerChunk;
 24+
 25+typedef struct {
 26+	i32 x;
 27+	i32 z;
 28+	u16 mask;
 29+} BulkChunkMeta;
 30+
 31 static MGTexture block_textures[BLOCK_LAST];
 32-static Chunk     chunks[WORLD_WIDTH][WORLD_DEPTH];
 33 static MGCamera  cam;
 34+static Chunk     chunks[WORLD_WIDTH][WORLD_DEPTH];
 35+
 36+#define NCHUNKS 128
 37+static ServerChunk server_chunks[NCHUNKS];
 38+static i8          server_mode;
 39+
 40+ServerChunk* server_chunk_find(i32 x, i32 z)
 41+{
 42+	i32 i;
 43+
 44+	for (i = 0; i < NCHUNKS; i++) {
 45+		if (!server_chunks[i].used)
 46+			continue;
 47+		
 48+		if (server_chunks[i].x == x && server_chunks[i].z == z)
 49+			return &server_chunks[i];
 50+	}
 51+
 52+	return NULL;
 53+}
 54+
 55+ServerChunk* server_chunk_get(i32 x, i32 z)
 56+{
 57+	ServerChunk* col;
 58+	i32          i;
 59+
 60+	col = server_chunk_find(x, z);
 61+	if (col)
 62+		return col;
 63+
 64+	for (i = 0; i < NCHUNKS; i++) {
 65+		if (server_chunks[i].used)
 66+			continue;
 67+
 68+		col = &server_chunks[i];
 69+
 70+		memset(col, 0, sizeof(*col));
 71+
 72+		col->used = 1;
 73+		col->x = x;
 74+		col->z = z;
 75+
 76+		return col;
 77+	}
 78+
 79+	mg_log(LOG_ERR, "Server chunk storage is full");
 80+	return NULL;
 81+}
 82+
 83+void server_chunk_clear(ServerChunk* column)
 84+{
 85+	i32 section;
 86+
 87+	if (!column)
 88+		return;
 89+
 90+	for (section = 0; section < 16; section++) {
 91+		chunk_mesh_destroy(&column->sections[section]);
 92+		memset(&column->sections[section], 0, sizeof(column->sections[section]));
 93+	}
 94+
 95+	column->section_mask = 0;
 96+}
 97 
 98-void test_scene_create(void)
 99+void server_chunk_remove(ServerChunk* column)
100 {
101+	if (!column)
102+		return;
103+
104+	server_chunk_clear(column);
105+	memset(column, 0, sizeof(*column));
106+}
107+
108+i32 section_count(u16 mask)
109+{
110+	i32 section;
111+	i32 count;
112+
113+	count = 0;
114+
115+	for (section = 0; section < 16; section++) {
116+		if (mask & (1U << section))
117+			count++;
118+	}
119+
120+	return count;
121+}
122+
123+i8 load_server_section(Chunk* section, const u8* data, size_t size)
124+{
125+	i32 index;
126 	i32 x;
127+	i32 y;
128 	i32 z;
129+	u16 state;
130+
131+	if (size < 8192)
132+		return 0;
133+
134+	chunk_mesh_destroy(section);
135+	memset(section, 0, sizeof(*section));
136+
137+	for (index = 0; index < 4096; index++) {
138+		state = (u16)data[index * 2] |
139+		       ((u16)data[index * 2 + 1] << 8);
140+
141+		x = index & 15;
142+		z = (index >> 4) & 15;
143+		y = (index >> 8) & 15;
144+
145+		if (state != 0)
146+			chunk_set(section, x, y, z, BLOCK_BRICK);
147+	}
148+
149+	if (!chunk_mesh_create(section)) {
150+		mg_log(LOG_ERR, "Failed to create server section mesh");
151+		return 0;
152+	}
153+
154+	return 1;
155+}
156+
157+void test_scene_chunk(const MCPacket* packet)
158+{
159+	PacketReader reader = packet_reader_init(packet->data, packet->size);
160+	ServerChunk* column;
161+	const u8*    data;
162+	size_t       required_size;
163+
164+	i32 chunk_x;
165+	i32 chunk_z;
166+	i32 data_size;
167+	i32 included;
168+	i32 section;
169+	i32 data_section;
170+	u8  ground_up;
171+	u16 mask;
172+
173+	if (!packet_read_i32(&reader, &chunk_x) ||
174+	    !packet_read_i32(&reader, &chunk_z) ||
175+	    !packet_read_u8(&reader, &ground_up) ||
176+	    !packet_read_u16(&reader, &mask) ||
177+	    !packet_read_varint(&reader, &data_size)) {
178+		mg_log(LOG_ERR, "Malformed Chunk Data packet");
179+		return;
180+	}
181+
182+	if (data_size < 0)
183+		return;
184+
185+	if ((size_t)data_size > reader.size - reader.cursor)
186+		return;
187+
188+	if (ground_up && mask == 0) {
189+		column = server_chunk_find(chunk_x, chunk_z);
190+		if (column)
191+			server_chunk_remove(column);
192+		return;
193+	}
194+
195+	included = section_count(mask);
196+	required_size = included * 8192;
197 
198-	block_textures[BLOCK_BRICK] =
199-	    renderer_texture_load("assets/resourcepacks/default/blocks/brick.png");
200+	if (required_size > (size_t)data_size) {
201+		mg_log(LOG_ERR, "Chunk packet block data is imcomplete");
202+		return;
203+	}
204+
205+	column = server_chunk_get(chunk_x, chunk_z);
206+
207+	if (!column)
208+		return;
209+
210+	if (ground_up)
211+		server_chunk_clear(column);
212+
213+	data = reader.data + reader.cursor;
214+	data_section = 0;
215+
216+	for (section = 0; section < 16; section++) {
217+		if (!(mask & (1U << section)))
218+			continue;
219+
220+		if (!load_server_section(&column->sections[section], data + data_section * 8192, 8192))
221+			return;
222+
223+		column->section_mask |= (u16)(1U << section);
224+		data_section++;
225+	}
226+}
227+
228+void test_scene_bulk_chunks(const MCPacket* packet)
229+{
230+	PacketReader reader = packet_reader_init(packet->data, packet->size);
231+	BulkChunkMeta* metadata;
232+	ServerChunk* column;
233+	const u8* data;
234+	size_t offset;
235+	size_t block_size;
236+	size_t column_size;
237+	i32 count;
238+	i32 index;
239+	i32 sections;
240+	i32 section;
241+	i32 data_section;
242+	u8 skylight;
243+
244+	reader.data = packet->data;
245+	reader.size = packet->size;
246+	reader.cursor = 0;
247+
248+	if (!packet_read_u8(&reader, &skylight) || !packet_read_varint(&reader, &count)) {
249+		mg_log(LOG_ERR, "Malformed Map Chunk Bulk packet");
250+		return;
251+	}
252+
253+	if (count <= 0 || count > 4096)
254+		return;
255+
256+	metadata = malloc(count * sizeof(*metadata));
257+
258+	if (!metadata)
259+		return;
260+
261+	for (index = 0; index < count; index++) {
262+		if (!packet_read_i32(&reader, &metadata[index].x) ||
263+		    !packet_read_i32( &reader, &metadata[index].z) ||
264+		    !packet_read_u16( &reader, &metadata[index].mask)) {
265+			free(metadata);
266+			return;
267+		}
268+	}
269+
270+	data = reader.data + reader.cursor;
271+	offset = 0;
272+
273+	for (index = 0; index < count; index++) {
274+		sections = section_count(metadata[index].mask);
275+		block_size = sections * 8192;
276+		column_size = block_size + sections * 2048 + (skylight ? sections * 2048 : 0) + 256;
277+
278+		if (offset > reader.size - reader.cursor)
279+			break;
280+
281+		if (column_size > reader.size - reader.cursor - offset) {
282+			mg_log(LOG_ERR, "Bulk chunk data is incomplete");
283+			break;
284+		}
285+
286+		column = server_chunk_get(metadata[index].x, metadata[index].z);
287+		if (!column) {
288+			offset += column_size;
289+			continue;
290+		}
291+
292+		server_chunk_clear(column);
293+		data_section = 0;
294+
295+		for (section = 0; section < 16; section++) {
296+			if (!(metadata[index].mask & (1U << section)))
297+				continue;
298+
299+			if (!load_server_section(&column->sections[section], data + offset + data_section * 8192, 8192))
300+				break;
301+
302+			column->section_mask |= (u16)(1U << section);
303+			data_section++;
304+		}
305+
306+		offset += column_size;
307+	}
308+
309+	free(metadata);
310+}
311+
312+void test_scene_create(Player* player, i8 server)
313+{
314+	i32 x;
315+	i32 z;
316+
317+	block_textures[BLOCK_BRICK] = renderer_texture_load("assets/resourcepacks/default/blocks/bedrock.png");
318 	if (block_textures[BLOCK_BRICK] == MGTEXTURE_INVALID)
319 		mg_die(MG_ERR_TEXTURE_LOAD, "Failed to load brick texture");
320 
321-	block_textures[BLOCK_DIRT] =
322-	    renderer_texture_load("assets/resourcepacks/default/blocks/dirt.png");
323+	block_textures[BLOCK_DIRT] = renderer_texture_load("assets/resourcepacks/default/blocks/dirt.png");
324 	if (block_textures[BLOCK_DIRT] == MGTEXTURE_INVALID)
325 		mg_die(MG_ERR_TEXTURE_LOAD, "Failed to load dirt texture");
326 
327-	block_textures[BLOCK_STONE] =
328-	    renderer_texture_load("assets/resourcepacks/default/blocks/stone.png");
329+	block_textures[BLOCK_STONE] = renderer_texture_load("assets/resourcepacks/default/blocks/stone.png");
330 	if (block_textures[BLOCK_STONE] == MGTEXTURE_INVALID)
331 		mg_die(MG_ERR_TEXTURE_LOAD, "Failed to load stone texture");
332 
333-	for (z = 0; z < WORLD_DEPTH; z++) {
334-		for (x = 0; x < WORLD_WIDTH; x++) {
335-			chunk_create(&chunks[x][z]);
336-
337-			if (!chunk_mesh_create(&chunks[x][z]))
338-				mg_die(MG_ERR_MESH_CREATE, "Failed to create chunk mesh");
339-		}
340-	}
341-
342 	camera_init(&cam, MG_V3(8.0f, 38.0f, 24.0f));
343 	cam.yaw = MG_AngleDeg(-180.0f);
344 	cam.pitch = MG_AngleDeg(-25.0f);
345+
346+	server_mode = server;
347+	memset(server_chunks, 0, sizeof(server_chunks));
348+	player->x = 0.0;
349+	player->y = 0.0;
350+	player->z = 0.0;
351+	player->yaw = 0.0f;
352+	player->pitch = 0.0f;
353+	player->positioned = 0;
354+
355+	if (server_mode) {
356+		cam.far = 512.0f;
357+	}
358+	else { /* test world */
359+		for (z = 0; z < WORLD_DEPTH; z++) {
360+			for (x = 0; x < WORLD_WIDTH; x++) {
361+				chunk_create(&chunks[x][z]);
362+
363+				if (!chunk_mesh_create(&chunks[x][z]))
364+					mg_die(MG_ERR_MESH_CREATE, "Failed to create chunk mesh");
365+			}
366+		}
367+	}
368 }
369 
370 void test_scene_draw(MGContext* ctx)
371@@ -59,6 +364,11 @@ void test_scene_draw(MGContext* ctx)
372 	float     aspect;
373 	BlockType block;
374 
375+	ServerChunk* column;
376+	Chunk*       section_chunk;
377+	i32          section;
378+	i32          i;
379+
380 	platform_get_drawable_size(ctx, &width, &height);
381 
382 	if (width <= 0 || height <= 0)
383@@ -69,16 +379,46 @@ void test_scene_draw(MGContext* ctx)
384 	proj = camera_get_proj(&cam, aspect);
385 	view = camera_get_view(&cam);
386 
387-	for (z = 0; z < WORLD_DEPTH; z++) {
388-		for (x = 0; x < WORLD_WIDTH; x++) {
389-			model = MG_Translate(MG_V3(x * CHUNK_WIDTH, 0.0f, z * CHUNK_DEPTH));
390-			mvp = MG_MulM4(proj, MG_MulM4(view, model));
391+	if (server_mode) {
392+		for (i = 0; i < NCHUNKS; i++) {
393+			column = &server_chunks[i];
394 
395-			for (block = BLOCK_BRICK; block < BLOCK_LAST; block++) {
396-				if (chunks[x][z].meshes[block] == MGMESH_INVALID)
397+			if (!column->used)
398+				continue;
399+
400+			for (section = 0; section < 16; section++) {
401+				if (!(column->section_mask & (1U << section)))
402+					continue;
403+
404+				section_chunk = &column->sections[section];
405+				if (section_chunk->meshes[BLOCK_BRICK] == MGMESH_INVALID)
406 					continue;
407-				renderer_mesh_draw(chunks[x][z].meshes[block],
408-				                   block_textures[block], &mvp);
409+
410+				model = MG_Translate(MG_V3(
411+					column->x * CHUNK_WIDTH,
412+					section * CHUNK_HEIGHT,
413+					column->z * CHUNK_DEPTH
414+				));
415+				mvp = MG_MulM4(proj, MG_MulM4(view, model));
416+				renderer_mesh_draw(
417+					section_chunk ->meshes[BLOCK_BRICK],
418+					block_textures[BLOCK_BRICK], &mvp
419+				);
420+			}
421+		}
422+	}
423+	else { /* test world */
424+		for (z = 0; z < WORLD_DEPTH; z++) {
425+			for (x = 0; x < WORLD_WIDTH; x++) {
426+				model = MG_Translate(MG_V3(x * CHUNK_WIDTH, 0.0f, z * CHUNK_DEPTH));
427+				mvp = MG_MulM4(proj, MG_MulM4(view, model));
428+
429+				for (block = BLOCK_BRICK; block < BLOCK_LAST; block++) {
430+					if (chunks[x][z].meshes[block] == MGMESH_INVALID)
431+						continue;
432+					renderer_mesh_draw(chunks[x][z].meshes[block],
433+							   block_textures[block], &mvp);
434+				}
435 			}
436 		}
437 	}
438@@ -89,9 +429,23 @@ void test_scene_destroy(void)
439 	i32 x;
440 	i32 z;
441 
442-	for (z = 0; z < WORLD_DEPTH; z++) {
443-		for (x = 0; x < WORLD_WIDTH; x++)
444-			chunk_mesh_destroy(&chunks[x][z]);
445+	i32 section;
446+	i32 i;
447+
448+	if (server_mode) {
449+		for (i = 0; i < NCHUNKS; i++) {
450+			if (!server_chunks[i].used)
451+				continue;
452+
453+			for (section = 0; section < 16; section++)
454+				chunk_mesh_destroy(&server_chunks[i] .sections[section]);
455+		}
456+	}
457+	else {
458+		for (z = 0; z < WORLD_DEPTH; z++) {
459+			for (x = 0; x < WORLD_WIDTH; x++)
460+				chunk_mesh_destroy(&chunks[x][z]);
461+		}
462 	}
463 
464 	renderer_texture_destroy(block_textures[BLOCK_BRICK]);
+19, -15
 1@@ -4,29 +4,33 @@
 2 #define RGBA(r, g, b, a) (((u32)(r)) | ((u32)(g) << 8) | ((u32)(b) << 16) | ((u32)(a) << 24))
 3 
 4 typedef enum { /* TODO: rename to MG_EC_... */
 5-	       MG_ERR_EXIT_SUCCESS,
 6-	       MG_ERR_EXIT_FAILURE,
 7+	MG_ERR_EXIT_SUCCESS,
 8+	MG_ERR_EXIT_FAILURE,
 9 
10-	       MG_ERR_SDL_INIT,
11-	       MG_ERR_SDL_CREATE_WIN,
12+	MG_ERR_INVALID_ARGS,
13 
14-	       MG_ERR_GL_CTX_CREATE,
15-	       MG_ERR_GL_CTX_ACTIVATE,
16+	MG_ERR_MCCLIENT_LOGIN,
17 
18-	       MG_ERR_SHADER_CREATE,
19+	MG_ERR_SDL_INIT,
20+	MG_ERR_SDL_CREATE_WIN,
21 
22-	       MG_ERR_PIPELINE_CREATE,
23+	MG_ERR_GL_CTX_CREATE,
24+	MG_ERR_GL_CTX_ACTIVATE,
25 
26-	       MG_ERR_MESH_CREATE,
27+	MG_ERR_SHADER_CREATE,
28 
29-	       MG_ERR_TEXTURE_LOAD,
30-	       MG_ERR_TEXTURE_CREATE,
31-	       MG_ERR_SAMPLER_CREATE,
32-	       MG_ERR_VIEW_CREATE,
33+	MG_ERR_PIPELINE_CREATE,
34 
35-	       MG_ERR_REN_CREATE,
36+	MG_ERR_MESH_CREATE,
37 
38-	       MG_ERR_LAST
39+	MG_ERR_TEXTURE_LOAD,
40+	MG_ERR_TEXTURE_CREATE,
41+	MG_ERR_SAMPLER_CREATE,
42+	MG_ERR_VIEW_CREATE,
43+
44+	MG_ERR_REN_CREATE,
45+
46+	MG_ERR_LAST
47 } MGErrorCodes;
48 
49 typedef enum {
+1, -1
1@@ -24,5 +24,5 @@ void config_default(void)
2 	mg_config.debug_wireframe = 0;
3 
4 	/* temp */
5-	mg_config.temp_movement_speed = 4.0f;
6+	mg_config.temp_movement_speed = 16.0f;
7 }
+51, -6
 1@@ -3,6 +3,7 @@
 2 
 3 #include "config.h"
 4 #include "util.h"
 5+#include "client/player.h"
 6 #include "mcproto/mcproto.h"
 7 #include "platform/platform.h"
 8 #include "renderer/renderer.h"
 9@@ -97,12 +98,24 @@ static int protocol_poc(const char* address, const char* username)
10 
11 int main(int argc, char** argv)
12 {
13-	if (argc != 1 && argc != 3) {
14-		fprintf(stderr, "usage: %s [server[:port] username]\n", argv[0]);
15-		return MG_ERR_EXIT_FAILURE;
16+	MCClient* client = NULL;
17+	MCAccount account;
18+	MCPacket  packet;
19+	char      error[1024];
20+	int       poll_result;
21+
22+	Player player;
23+
24+	if (argc != 1 && argc != 3)
25+		mg_die(MG_ERR_INVALID_ARGS, "usage: %s [server[:port] username]\n", argv[0]);
26+	if (argc == 3) {
27+		account.username = argv[2];
28+		account.profile_uuid = getenv("MANGOLIA_PROFILE_UUID");
29+		account.access_token = getenv("MANGOLIA_ACCESS_TOKEN");
30+
31+		if (!mc_client_login(&client, argv[1], &account, error, sizeof(error)))
32+			mg_die(MG_ERR_MCCLIENT_LOGIN, "%s\n", error);
33 	}
34-	if (argc == 3)
35-		return protocol_poc(argv[1], argv[2]);
36 
37 	config_default();
38 
39@@ -112,11 +125,41 @@ int main(int argc, char** argv)
40 	platform_init(&ctx);
41 	platform_window_create(&ctx);
42 	renderer_create(&ctx);
43-	test_scene_create();
44+	test_scene_create(&player, client != NULL);
45 	SDL_SetRelativeMouseMode(SDL_TRUE);
46 
47 	while (platform_poll(&ctx)) {
48+		if (client) {
49+			for (;;) {
50+				poll_result = mc_client_poll(client, &packet, error, sizeof(error));
51+				if (poll_result <= 0)
52+					break;
53+
54+				printf("packet 0x%02x size %lu\n", packet.id, packet.size);
55+				switch (packet.id) {
56+				case 0x00:
57+					if (!mc_client_write(client, 0x00, packet.data, packet.size))
58+						mg_log(LOG_ERR, "Failed to echo keepalive");
59+					break;
60+				case 0x08: player_server_handle_position(&player, &cam, &packet); break;
61+				case 0x21: test_scene_chunk(&packet); break;
62+				case 0x26: test_scene_bulk_chunks(&packet); break;
63+				}
64+
65+				mc_packet_free(&packet);
66+			}
67+
68+			if (poll_result < 0) {
69+				mg_log(LOG_ERR, "%s", error);
70+				break;
71+			}
72+		}
73+
74 		input();
75+
76+		if (client)
77+			player_server_send_position(&player, &cam, client);
78+
79 		renderer_begin();
80 
81 		/* ... */
82@@ -127,6 +170,8 @@ int main(int argc, char** argv)
83 		SDL_Delay(1); /* TODO: replace with target fps */
84 	}
85 
86+	if (client)
87+		mc_client_destroy(client);
88 	test_scene_destroy();
89 	renderer_destroy();
90 	platform_shutdown(&ctx);
+64, -0
 1@@ -2,6 +2,7 @@
 2 
 3 #include <errno.h>
 4 #include <netdb.h>
 5+#include <poll.h>
 6 #include <stdio.h>
 7 #include <stdlib.h>
 8 #include <string.h>
 9@@ -525,6 +526,69 @@ void mc_client_destroy(MCClient* client)
10 	free(client);
11 }
12 
13+int mc_client_poll(MCClient* client, MCPacket* packet, char* error, size_t error_size)
14+{
15+	struct pollfd fd;
16+	int result;
17+
18+	if (!client || client->socket_fd < 0) {
19+		mc_set_error(error, error_size, "invalid Minecraft client");
20+		return -1;
21+	}
22+
23+	fd.fd = client->socket_fd;
24+	fd.events = POLLIN;
25+	fd.revents = 0;
26+
27+	do {
28+		result = poll(&fd, 1, 0);
29+	} while (result < 0 && errno == EINTR);
30+
31+	if (result < 0) {
32+		mc_set_error(error, error_size, "socket poll failed: %s", strerror(errno));
33+		return -1;
34+	}
35+
36+	if (result == 0)
37+		return 0;
38+
39+	if (fd.revents & POLLIN) {
40+		if (!mc_client_read(client, packet, error, error_size))
41+			return -1;
42+
43+		return 1;
44+	}
45+
46+	if (fd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
47+		mc_set_error(error, error_size, "Minecraft connection closed");
48+		return -1;
49+	}
50+
51+	return 0;
52+}
53+
54+int mc_client_send_position_look(MCClient* client, double x, double feet_y,
55+                                 double z, float yaw, float pitch, i8 grounded)
56+{
57+	MCBuffer payload;
58+	int ok;
59+
60+	bufinit(&payload);
61+
62+	ok =
63+	    bufwrite_f64(&payload, x) &&
64+	    bufwrite_f64(&payload, feet_y) &&
65+	    bufwrite_f64(&payload, z) &&
66+	    bufwrite_f32(&payload, yaw) &&
67+	    bufwrite_f32(&payload, pitch) &&
68+	    bufwrite_u8(&payload, grounded ? 1 : 0) &&
69+	    /* player (look) position */
70+	    mc_client_write(client, 0x06, payload.data, payload.size);
71+
72+	buffree(&payload);
73+	return ok;
74+}
75+
76 const char* mc_client_username(const MCClient* client) { return client ? client->username : ""; }
77 
78 const char* mc_client_uuid(const MCClient* client) { return client ? client->uuid : ""; }