1 /**
2 Copyright: Copyright (c) 2015-2016 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.client.plugin;
7 
8 import core.thread : thread_joinAll;
9 import core.time;
10 import std.datetime : StopWatch;
11 import std.experimental.logger;
12 
13 import dlib.math.vector;
14 import dlib.math.matrix : Matrix4f;
15 import dlib.math.affine : translationMatrix;
16 import derelict.enet.enet;
17 import derelict.opengl3.gl3;
18 import derelict.imgui.imgui;
19 import tharsis.prof;
20 
21 import netlib;
22 import pluginlib;
23 import pluginlib.pluginmanager;
24 
25 import voxelman.eventdispatcher.plugin;
26 import voxelman.graphics.plugin;
27 import voxelman.gui.plugin;
28 import voxelman.net.plugin;
29 import voxelman.command.plugin;
30 
31 import voxelman.net.events;
32 import voxelman.core.packets;
33 import voxelman.net.packets;
34 
35 import voxelman.config.configmanager;
36 import voxelman.input.keybindingmanager;
37 
38 import voxelman.core.config;
39 import voxelman.core.events;
40 import voxelman.storage.chunk;
41 import voxelman.storage.coordinates;
42 import voxelman.storage.utils;
43 import voxelman.storage.worldaccess;
44 import voxelman.utils.math;
45 import voxelman.utils.trace : traceRay;
46 import voxelman.utils.textformatter;
47 
48 import voxelman.client.appstatistics;
49 import voxelman.client.chunkman;
50 import voxelman.client.console;
51 
52 //version = manualGC;
53 version(manualGC) import core.memory;
54 
55 version = profiling;
56 
57 shared static this()
58 {
59 	auto c = new ClientPlugin;
60 	pluginRegistry.regClientPlugin(c);
61 	pluginRegistry.regClientMain(&c.run);
62 }
63 
64 struct ThisClientLoggedInEvent {
65 	ClientId thisClientId;
66 	Profiler profiler;
67 	bool continuePropagation = true;
68 }
69 
70 auto formatDuration(Duration dur)
71 {
72 	import std.string : format;
73 	auto splitted = dur.split();
74 	return format("%s.%03s,%03s secs",
75 		splitted.seconds, splitted.msecs, splitted.usecs);
76 }
77 
78 final class ClientPlugin : IPlugin
79 {
80 private:
81 	PluginManager pluginman;
82 
83 	// Plugins
84 	EventDispatcherPlugin evDispatcher;
85 	GraphicsPlugin graphics;
86 	GuiPlugin guiPlugin;
87 	CommandPluginClient commandPlugin;
88 
89 	// Resource managers
90 	KeyBindingManager keyBindingMan;
91 	ConfigManager config;
92 
93 public:
94 	AppStatistics stats;
95 	Console console;
96 
97 	// Game stuff
98 	ChunkMan chunkMan;
99 	WorldAccess worldAccess;
100 
101 	// Debug
102 	Profiler profiler;
103 	DespikerSender profilerSender;
104 
105 	// Client data
106 	bool isRunning = false;
107 	bool isDisconnecting = false;
108 	bool isSpawned = false;
109 	bool mouseLocked;
110 
111 	ConfigOption serverIpOpt;
112 	ConfigOption serverPortOpt;
113 	ConfigOption runDespikerOpt;
114 	ConfigOption numWorkersOpt;
115 	ConfigOption nicknameOpt;
116 
117 	// Graphics stuff
118 	bool isCullingEnabled = true;
119 	bool doUpdateObserverPosition = true;
120 	vec3 updatedCameraPos;
121 	bool isConsoleShown = false;
122 
123 	// Client id stuff
124 	ClientId thisClientId;
125 	string[ClientId] clientNames;
126 
127 	// Send position interval
128 	double sendPositionTimer = 0;
129 	enum sendPositionInterval = 0.1;
130 	ChunkWorldPos prevChunkPos;
131 
132 	NetClientPlugin connection;
133 
134 	// IPlugin stuff
135 	mixin IdAndSemverFrom!(voxelman.client.plugininfo);
136 
137 	override void registerResources(IResourceManagerRegistry resmanRegistry)
138 	{
139 		config = resmanRegistry.getResourceManager!ConfigManager;
140 		keyBindingMan = resmanRegistry.getResourceManager!KeyBindingManager;
141 
142 		serverIpOpt = config.registerOption!string("ip", "127.0.0.1");
143 		serverPortOpt = config.registerOption!ushort("port", 1234);
144 		runDespikerOpt = config.registerOption!bool("run_despiker", false);
145 		numWorkersOpt = config.registerOption!uint("num_workers", 4);
146 		nicknameOpt = config.registerOption!string("name", "Player");
147 
148 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_Q, "key.lockMouse", null, &onLockMouse));
149 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_RIGHT_BRACKET, "key.incViewRadius", null, &onIncViewRadius));
150 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_LEFT_BRACKET, "key.decViewRadius", null, &onDecViewRadius));
151 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_C, "key.toggleCulling", null, &onToggleCulling));
152 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_U, "key.togglePosUpdate", null, &onTogglePositionUpdate));
153 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_GRAVE_ACCENT, "key.toggle_console", null, &onConsoleToggleKey));
154 	}
155 
156 	override void preInit()
157 	{
158 		chunkMan.init(numWorkersOpt.get!uint);
159 		worldAccess.onChunkModifiedHandlers ~= &chunkMan.onChunkChanged;
160 		console.init();
161 	}
162 
163 	override void init(IPluginManager pluginman)
164 	{
165 		evDispatcher = pluginman.getPlugin!EventDispatcherPlugin;
166 		evDispatcher.profiler = profiler;
167 
168 		graphics = pluginman.getPlugin!GraphicsPlugin;
169 		guiPlugin = pluginman.getPlugin!GuiPlugin;
170 
171 		evDispatcher.subscribeToEvent(&onPreUpdateEvent);
172 		evDispatcher.subscribeToEvent(&onPostUpdateEvent);
173 		evDispatcher.subscribeToEvent(&drawScene);
174 		evDispatcher.subscribeToEvent(&drawOverlay);
175 		evDispatcher.subscribeToEvent(&onClosePressedEvent);
176 		evDispatcher.subscribeToEvent(&onGameStopEvent);
177 		evDispatcher.subscribeToEvent(&handleThisClientConnected);
178 		evDispatcher.subscribeToEvent(&handleThisClientDisconnected);
179 
180 		commandPlugin = pluginman.getPlugin!CommandPluginClient;
181 		commandPlugin.registerCommand("connect", &connectCommand);
182 		console.messageWindow.messageHandler = &onConsoleCommand;
183 
184 		connection = pluginman.getPlugin!NetClientPlugin;
185 
186 		connection.printPacketMap();
187 
188 		connection.registerPacketHandler!PacketMapPacket(&handlePacketMapPacket);
189 		connection.registerPacketHandler!SessionInfoPacket(&handleSessionInfoPacket);
190 		connection.registerPacketHandler!ClientLoggedInPacket(&handleUserLoggedInPacket);
191 		connection.registerPacketHandler!ClientLoggedOutPacket(&handleUserLoggedOutPacket);
192 		connection.registerPacketHandler!ClientPositionPacket(&handleClientPositionPacket);
193 		connection.registerPacketHandler!ChunkDataPacket(&handleChunkDataPacket);
194 		connection.registerPacketHandler!MultiblockChangePacket(&handleMultiblockChangePacket);
195 		connection.registerPacketHandler!SpawnPacket(&handleSpawnPacket);
196 	}
197 
198 	override void postInit()
199 	{
200 		updatedCameraPos = graphics.camera.position;
201 		chunkMan.updateObserverPosition(graphics.camera.position);
202 		ConnectionSettings settings = {null, 1, 2, 0, 0};
203 
204 		connection.start(settings);
205 		static if (ENABLE_RLE_PACKET_COMPRESSION)
206 			enet_host_compress_with_range_coder(connection.host);
207 		connect(serverIpOpt.get!string, serverPortOpt.get!ushort);
208 
209 		if (runDespikerOpt.get!bool)
210 			toggleProfiler();
211 	}
212 
213 	void printDebug()
214 	{
215 		igSetNextWindowSize(ImVec2(400, 300), ImGuiSetCond_FirstUseEver);
216 		igSetNextWindowPos(ImVec2(0, 0), ImGuiSetCond_FirstUseEver);
217 		igBegin("Debug");
218 		with(stats) {
219 			igTextf("FPS: %s", fps);
220 			igTextf("Chunks visible/rendered %s/%s %.0f%%",
221 				chunksVisible, chunksRendered,
222 				chunksVisible ? cast(float)chunksRendered/chunksVisible*100 : 0);
223 			igTextf("Chunks per frame loaded: %s",
224 				totalLoadedChunks - lastFrameLoadedChunks);
225 			igTextf("Chunks total loaded: %s",
226 				totalLoadedChunks);
227 			igTextf("Vertexes %s", vertsRendered);
228 			igTextf("Triangles %s", trisRendered);
229 			vec3 pos = graphics.camera.position;
230 			igTextf("Pos: X %.2f, Y %.2f, Z %.2f", pos.x, pos.y, pos.z);
231 		}
232 
233 		ChunkWorldPos chunkPos = chunkMan.observerPosition;
234 		auto regionPos = RegionWorldPos(chunkPos);
235 		auto localChunkPosition = ChunkRegionPos(chunkPos);
236 		igTextf("C: %s R: %s L: %s", chunkPos, regionPos, localChunkPosition);
237 
238 		vec3 target = graphics.camera.target;
239 		vec2 heading = graphics.camera.heading;
240 		igTextf("Heading: %.2f %.2f Target: X %.2f, Y %.2f, Z %.2f",
241 			heading.x, heading.y, target.x, target.y, target.z);
242 		igTextf("Chunks to remove: %s", chunkMan.removeQueue.length);
243 		igTextf("Chunks to mesh: %s", chunkMan.chunkMeshMan.numMeshChunkTasks);
244 
245 		igSeparator();
246 		if (igButton("Profiler"))
247 			toggleProfiler();
248 		igSameLine();
249 		if (igButton("Stop server"))
250 			connection.send(CommandPacket("sv_stop"));
251 		igSameLine();
252 		if (igButton("Connect"))
253 			connect(serverIpOpt.get!string, serverPortOpt.get!ushort);
254 		igEnd();
255 	}
256 
257 	this()
258 	{
259 		pluginman = new PluginManager;
260 
261 		version(profiling)
262 		{
263 			ubyte[] storage  = new ubyte[Profiler.maxEventBytes + 20 * 1024 * 1024];
264 			profiler = new Profiler(storage);
265 		}
266 		profilerSender = new DespikerSender([profiler]);
267 		worldAccess = WorldAccess(&chunkMan.chunkStorage.getChunk, () => 0);
268 	}
269 
270 	void load(string[] args)
271 	{
272 		// register all plugins and managers
273 		import voxelman.pluginlib.plugininforeader : filterEnabledPlugins;
274 		foreach(p; pluginRegistry.clientPlugins.byValue.filterEnabledPlugins(args))
275 		{
276 			pluginman.registerPlugin(p);
277 		}
278 
279 		// Actual loading sequence
280 		pluginman.initPlugins();
281 	}
282 
283 	void run(string[] args)
284 	{
285 		import std.datetime : TickDuration, Clock, usecs;
286 		import core.thread : Thread;
287 
288 		version(manualGC) GC.disable;
289 
290 		load(args);
291 
292 		TickDuration lastTime = Clock.currAppTick;
293 		TickDuration newTime = TickDuration.from!"seconds"(0);
294 
295 		isRunning = true;
296 		while(isRunning)
297 		{
298 			Zone frameZone = Zone(profiler, "frame");
299 
300 			newTime = Clock.currAppTick;
301 			double delta = (newTime - lastTime).usecs / 1_000_000.0;
302 			lastTime = newTime;
303 
304 			{
305 				Zone subZone = Zone(profiler, "preUpdate");
306 				evDispatcher.postEvent(PreUpdateEvent(delta));
307 			}
308 			{
309 				Zone subZone = Zone(profiler, "update");
310 				evDispatcher.postEvent(UpdateEvent(delta));
311 			}
312 			{
313 				Zone subZone = Zone(profiler, "postUpdate");
314 				evDispatcher.postEvent(PostUpdateEvent(delta));
315 			}
316 			{
317 				Zone subZone = Zone(profiler, "render");
318 				evDispatcher.postEvent(RenderEvent());
319 			}
320 			{
321 				version(manualGC) {
322 					Zone subZone = Zone(profiler, "GC.collect()");
323 					GC.collect();
324 				}
325 			}
326 			{
327 				Zone subZone = Zone(profiler, "sleepAfterFrame");
328 				// time used in frame
329 				delta = (lastTime - Clock.currAppTick).usecs / 1_000_000.0;
330 				guiPlugin.fpsHelper.sleepAfterFrame(delta);
331 			}
332 
333 			version(profiling) {
334 				frameZone.__dtor;
335 				profilerSender.update();
336 			}
337 		}
338 		profilerSender.reset();
339 
340 		isDisconnecting = connection.isConnected;
341 		connection.disconnect();
342 
343 		infof("disconnecting");
344 		while (isDisconnecting)
345 		{
346 			connection.update();
347 		}
348 		infof("stop");
349 
350 		evDispatcher.postEvent(GameStopEvent());
351 	}
352 
353 	void connect(string ip, ushort port)
354 	{
355 		console.lineBuffer.putfln("Connecting to %s:%s", ip, port);
356 		if (connection.isConnecting)
357 			connection.disconnect();
358 		connection.connect(ip, port);
359 	}
360 
361 	void connectCommand(CommandParams params)
362 	{
363 		short port = serverPortOpt.get!ushort;
364 		string serverIp = serverIpOpt.get!string;
365 		getopt(params.args,
366 			"ip", &serverIp,
367 			"port", &port);
368 		connect(serverIp, port);
369 	}
370 
371 	void toggleProfiler()
372 	{
373 		if (profilerSender.sending)
374 			profilerSender.reset();
375 		else
376 		{
377 			import std.file : exists;
378 			if (exists(DESPIKER_PATH))
379 				profilerSender.startDespiker(DESPIKER_PATH);
380 			else
381 				warningf(`No despiker executable found at "%s"`, DESPIKER_PATH);
382 		}
383 	}
384 
385 	void onGameStopEvent(ref GameStopEvent gameStopEvent)
386 	{
387 		chunkMan.stop();
388 		thread_joinAll();
389 	}
390 
391 	void onPreUpdateEvent(ref PreUpdateEvent event)
392 	{
393 		if (doUpdateObserverPosition)
394 		{
395 			updatedCameraPos = graphics.camera.position;
396 		}
397 		chunkMan.updateObserverPosition(updatedCameraPos);
398 		connection.update();
399 		chunkMan.update();
400 	}
401 
402 	void onPostUpdateEvent(ref PostUpdateEvent event)
403 	{
404 		updateStats();
405 		printDebug();
406 		stats.resetCounters();
407 		if (isConsoleShown)
408 			console.draw();
409 		if (doUpdateObserverPosition)
410 			sendPosition(event.deltaTime);
411 		connection.flush();
412 	}
413 
414 	void sendPosition(double dt)
415 	{
416 		ChunkWorldPos chunkPos = BlockWorldPos(graphics.camera.position);
417 
418 		if (isSpawned)
419 		{
420 			sendPositionTimer += dt;
421 			if (sendPositionTimer > sendPositionInterval ||
422 				chunkPos != prevChunkPos)
423 			{
424 				connection.send(ClientPositionPacket(
425 					graphics.camera.position,
426 					graphics.camera.heading));
427 
428 				if (sendPositionTimer < sendPositionInterval)
429 					sendPositionTimer = 0;
430 				else
431 					sendPositionTimer -= sendPositionInterval;
432 			}
433 		}
434 
435 		prevChunkPos = chunkPos;
436 	}
437 
438 	void updateStats()
439 	{
440 		stats.fps = guiPlugin.fpsHelper.fps;
441 		stats.totalLoadedChunks = chunkMan.totalLoadedChunks;
442 	}
443 
444 	void onConsoleCommand(string command)
445 	{
446 		infof("Executing command '%s'", command);
447 		ExecResult res = commandPlugin.execute(command, ClientId(0));
448 
449 		if (res.status == ExecStatus.notRegistered)
450 		{
451 			if (connection.isConnected)
452 				connection.send(CommandPacket(command));
453 			else
454 				console.lineBuffer.putfln("Unknown client command '%s', not connected to server", command);
455 		}
456 		else if (res.status == ExecStatus.error)
457 			console.lineBuffer.putfln("Error executing command '%s': %s", command, res.error);
458 		else
459 			console.lineBuffer.putln(command);
460 	}
461 
462 	void onConsoleToggleKey(string)
463 	{
464 		isConsoleShown = !isConsoleShown;
465 	}
466 
467 	void incViewRadius()
468 	{
469 		setViewRadius(getViewRadius() + 1);
470 	}
471 
472 	void decViewRadius()
473 	{
474 		setViewRadius(getViewRadius() - 1);
475 	}
476 
477 	int getViewRadius()
478 	{
479 		return chunkMan.viewRadius;
480 	}
481 
482 	void setViewRadius(int newViewRadius)
483 	{
484 		auto oldViewRadius = chunkMan.viewRadius;
485 		chunkMan.viewRadius = clamp(newViewRadius,
486 			MIN_VIEW_RADIUS, MAX_VIEW_RADIUS);
487 
488 		if (oldViewRadius != chunkMan.viewRadius)
489 		{
490 			connection.send(ViewRadiusPacket(chunkMan.viewRadius));
491 		}
492 	}
493 
494 	void sendMessage(string msg)
495 	{
496 		connection.send(MessagePacket(0, msg));
497 	}
498 
499 	void onClosePressedEvent(ref ClosePressedEvent event)
500 	{
501 		isRunning = false;
502 	}
503 
504 	void onLockMouse(string)
505 	{
506 		mouseLocked = !mouseLocked;
507 		if (mouseLocked)
508 			guiPlugin.window.mousePosition = cast(ivec2)(guiPlugin.window.size) / 2;
509 	}
510 
511 	void onIncViewRadius(string)
512 	{
513 		incViewRadius();
514 	}
515 
516 	void onDecViewRadius(string)
517 	{
518 		decViewRadius();
519 	}
520 
521 	void onToggleCulling(string)
522 	{
523 		isCullingEnabled = !isCullingEnabled;
524 	}
525 
526 	void onTogglePositionUpdate(string)
527 	{
528 		doUpdateObserverPosition = !doUpdateObserverPosition;
529 	}
530 
531 	void drawScene(ref Render1Event event)
532 	{
533 		Zone drawSceneZone = Zone(profiler, "drawScene");
534 
535 		graphics.chunkShader.bind;
536 		glUniformMatrix4fv(graphics.viewLoc, 1, GL_FALSE,
537 			graphics.camera.cameraMatrix);
538 		glUniformMatrix4fv(graphics.projectionLoc, 1, GL_FALSE,
539 			cast(const float*)graphics.camera.perspective.arrayof);
540 
541 		import dlib.geometry.aabb;
542 		import dlib.geometry.frustum;
543 		Matrix4f vp = graphics.camera.perspective * graphics.camera.cameraToClipMatrix;
544 		Frustum frustum;
545 		frustum.fromMVP(vp);
546 
547 		Matrix4f modelMatrix;
548 		foreach(ChunkWorldPos cwp; chunkMan.chunkMeshMan.visibleChunks.items)
549 		{
550 			Chunk* c = chunkMan.getChunk(cwp);
551 			assert(c);
552 			++stats.chunksVisible;
553 
554 			if (isCullingEnabled)
555 			{
556 				// Frustum culling
557 				ivec3 ivecMin = c.position.vector * CHUNK_SIZE;
558 				vec3 vecMin = vec3(ivecMin.x, ivecMin.y, ivecMin.z);
559 				vec3 vecMax = vecMin + CHUNK_SIZE;
560 				AABB aabb = boxFromMinMaxPoints(vecMin, vecMax);
561 				auto intersects = frustum.intersectsAABB(aabb);
562 				if (!intersects) continue;
563 			}
564 
565 			modelMatrix = translationMatrix!float(c.mesh.position);
566 			glUniformMatrix4fv(graphics.modelLoc, 1, GL_FALSE, cast(const float*)modelMatrix.arrayof);
567 
568 			c.mesh.bind;
569 			c.mesh.render;
570 
571 			++stats.chunksRendered;
572 			stats.vertsRendered += c.mesh.numVertexes;
573 			stats.trisRendered += c.mesh.numTris;
574 		}
575 
576 		glUniformMatrix4fv(graphics.modelLoc, 1, GL_FALSE, cast(const float*)Matrix4f.identity.arrayof);
577 		graphics.chunkShader.unbind;
578 	}
579 
580 	void drawOverlay(ref Render2Event event)
581 	{
582 		//event.renderer.setColor(Color(0,0,0,1));
583 		//event.renderer.fillRect(Rect(guiPlugin.window.size.x/2-7, guiPlugin.window.size.y/2-1, 14, 2));
584 		//event.renderer.fillRect(Rect(guiPlugin.window.size.x/2-1, guiPlugin.window.size.y/2-7, 2, 14));
585 	}
586 
587 	void handleThisClientConnected(ref ThisClientConnectedEvent event)
588 	{
589 		infof("Connection to %s:%s established", serverIpOpt.get!string, serverPortOpt.get!ushort);
590 	}
591 
592 	void handleThisClientDisconnected(ref ThisClientDisconnectedEvent event)
593 	{
594 		infof("disconnected with data %s", event.data);
595 
596 		isDisconnecting = false;
597 		isSpawned = false;
598 	}
599 
600 	void handlePacketMapPacket(ubyte[] packetData, ClientId clientId)
601 	{
602 		auto packetMap = unpackPacket!PacketMapPacket(packetData);
603 
604 		connection.setPacketMap(packetMap.packetNames);
605 		connection.printPacketMap();
606 
607 		connection.send(ViewRadiusPacket(chunkMan.viewRadius));
608 		connection.send(LoginPacket(nicknameOpt.get!string));
609 	}
610 
611 	void handleSessionInfoPacket(ubyte[] packetData, ClientId clientId)
612 	{
613 		auto loginInfo = unpackPacket!SessionInfoPacket(packetData);
614 
615 		clientNames = loginInfo.clientNames;
616 		thisClientId = loginInfo.yourId;
617 		evDispatcher.postEvent(ThisClientLoggedInEvent(thisClientId));
618 	}
619 
620 	void handleUserLoggedInPacket(ubyte[] packetData, ClientId clientId)
621 	{
622 		auto newUser = unpackPacket!ClientLoggedInPacket(packetData);
623 		clientNames[newUser.clientId] = newUser.clientName;
624 		infof("%s has connected", newUser.clientName);
625 		evDispatcher.postEvent(ClientLoggedInEvent(clientId));
626 	}
627 
628 	void handleUserLoggedOutPacket(ubyte[] packetData, ClientId clientId)
629 	{
630 		auto packet = unpackPacket!ClientLoggedOutPacket(packetData);
631 		infof("%s has disconnected", clientName(packet.clientId));
632 		evDispatcher.postEvent(ClientLoggedOutEvent(clientId));
633 		clientNames.remove(packet.clientId);
634 	}
635 
636 	void handleClientPositionPacket(ubyte[] packetData, ClientId peer)
637 	{
638 		import voxelman.utils.math : nansToZero;
639 
640 		auto packet = unpackPacket!ClientPositionPacket(packetData);
641 		tracef("Received ClientPositionPacket(%s, %s)",
642 			packet.pos, packet.heading);
643 
644 		nansToZero(packet.pos);
645 		graphics.camera.position = packet.pos;
646 
647 		nansToZero(packet.heading);
648 		graphics.camera.setHeading(packet.heading);
649 	}
650 
651 	void handleSpawnPacket(ubyte[] packetData, ClientId peer)
652 	{
653 		auto packet = unpackPacket!SpawnPacket(packetData);
654 		isSpawned = true;
655 	}
656 
657 	void handleChunkDataPacket(ubyte[] packetData, ClientId peer)
658 	{
659 		auto packet = unpackPacket!ChunkDataPacket(packetData);
660 		//tracef("Received %s ChunkDataPacket(%s,%s)", packetData.length,
661 		//	packet.chunkPos, packet.blockData.blocks.length);
662 		chunkMan.onChunkLoaded(ChunkWorldPos(packet.chunkPos), packet.blockData);
663 	}
664 
665 	void handleMultiblockChangePacket(ubyte[] packetData, ClientId peer)
666 	{
667 		auto packet = unpackPacket!MultiblockChangePacket(packetData);
668 		Chunk* chunk = chunkMan.chunkStorage.getChunk(ChunkWorldPos(packet.chunkPos));
669 		// We can receive data for chunk that is already deleted.
670 		if (chunk is null || chunk.isMarkedForDeletion)
671 			return;
672 		chunkMan.onChunkChanged(chunk, packet.blockChanges);
673 	}
674 
675 	string clientName(ClientId clientId)
676 	{
677 		return clientId in clientNames ? clientNames[clientId] : format("? %s", clientId);
678 	}
679 }