1 /**
2 Copyright: Copyright (c) 2015-2018 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.world.clientworld;
7 
8 import core.time : MonoTime, Duration, usecs, dur;
9 import voxelman.log;
10 import netlib;
11 import pluginlib;
12 import voxelman.graphics.irenderer;
13 import voxelman.math;
14 import voxelman.geometry;
15 import voxelman.text.textformatter;
16 
17 import voxelman.core.config;
18 import voxelman.core.events;
19 import voxelman.net.events;
20 import voxelman.platform.compression;
21 import voxelman.container.hash.set;
22 
23 import voxelman.block.plugin;
24 import voxelman.blockentity.plugin;
25 import voxelman.config.configmanager : ConfigOption, ConfigManager;
26 import voxelman.eventdispatcher.plugin : EventDispatcherPlugin;
27 import voxelman.gui.plugin : GuiPlugin;
28 import voxelman.graphics.plugin;
29 import voxelman.input.keybindingmanager;
30 import voxelman.session.client;
31 import voxelman.net.plugin : NetServerPlugin, NetClientPlugin;
32 import voxelman.dbg.plugin;
33 
34 import voxelman.net.packets;
35 import voxelman.core.packets;
36 
37 import voxelman.world.block;
38 import voxelman.world.storage;
39 import voxelman.world.storage.dimensionobservermanager;
40 import voxelman.world.blockentity.blockentityaccess;
41 import voxelman.world.blockentity.blockentitydata;
42 import voxelman.world.blockentity.blockentitymap;
43 
44 import voxelman.world.mesh.chunkmeshman;
45 import voxelman.world.mesh.vertex;
46 
47 struct IdMapManagerClient
48 {
49 	void delegate(string[])[string] onMapReceivedHandlers;
50 
51 	void regIdMapHandler(string mapName, void delegate(string[]) onMapReceived)
52 	{
53 		onMapReceivedHandlers[mapName] = onMapReceived;
54 	}
55 }
56 
57 string fmtDur(Duration dur)
58 {
59 	import std..string : format;
60 	int seconds, msecs, usecs;
61 	dur.split!("seconds", "msecs", "usecs")(seconds, msecs, usecs);
62 	return format("%s.%03s,%03ss", seconds, msecs, usecs);
63 }
64 
65 //version = DBG_COMPR;
66 final class ClientWorld : IPlugin
67 {
68 private:
69 	EventDispatcherPlugin evDispatcher;
70 	NetClientPlugin connection;
71 	GraphicsPlugin graphics;
72 	IRenderer renderer;
73 	ClientSession session;
74 	BlockPluginClient blockPlugin;
75 	BlockEntityClient blockEntityPlugin;
76 
77 	ConfigOption numWorkersOpt;
78 	Debugger dbg;
79 
80 	size_t wastedClientLoads;
81 
82 public:
83 	ChunkManager chunkManager;
84 	ChunkEditor chunkEditor;
85 	ChunkObserverManager chunkObserverManager;
86 	WorldAccess worldAccess;
87 	BlockEntityAccess entityAccess;
88 
89 	ChunkMeshMan chunkMeshMan;
90 	HashSet!ChunkWorldPos chunksToRemesh;
91 
92 	DimensionManager dimMan;
93 	IdMapManagerClient idMapManager;
94 	TimestampType currentTimestamp;
95 
96 
97 	// toggles/debug
98 	bool doUpdateObserverPosition = true;
99 	size_t dbg_totalLoadedChunks;
100 	size_t dbg_lastFrameLoadedChunks;
101 	size_t dbg_meshesRenderedSolid;
102 	size_t dbg_meshesRenderedSemitransparent;
103 	size_t dbg_vertsRendered;
104 	size_t dbg_trisRendered;
105 
106 	// Observer data
107 	vec3 updatedCameraPos;
108 	ChunkWorldPos observerPosition;
109 	ubyte positionKey;
110 	SessionId observerSessionId;
111 
112 	ConfigOption viewRadiusOpt;
113 	int viewRadius;
114 
115 	// Graphics stuff
116 	bool isCullingEnabled = true;
117 	bool wireframeMode = false;
118 	ChunkShader chunkShader;
119 	Texture blockAtlasTexture;
120 
121 	// Send position interval
122 	double sendPositionTimer = 0;
123 	enum sendPositionInterval = 0.1;
124 	ChunkWorldPos prevChunkPos;
125 
126 	StringMap serverStrings;
127 
128 	mixin IdAndSemverFrom!"voxelman.world.plugininfo";
129 
130 	override void registerResourceManagers(void delegate(IResourceManager) registerHandler) {}
131 
132 	override void registerResources(IResourceManagerRegistry resmanRegistry)
133 	{
134 		ConfigManager config = resmanRegistry.getResourceManager!ConfigManager;
135 		numWorkersOpt = config.registerOption!int("num_workers", 4);
136 		viewRadiusOpt = config.registerOption!int("view_distance", DEFAULT_VIEW_RADIUS);
137 
138 		KeyBindingManager keyBindingMan = resmanRegistry.getResourceManager!KeyBindingManager;
139 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_RIGHT_BRACKET, "key.incViewRadius", null, &onIncViewRadius));
140 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_LEFT_BRACKET, "key.decViewRadius", null, &onDecViewRadius));
141 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_U, "key.togglePosUpdate", null, &onTogglePositionUpdate));
142 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_F2, "key.toggleChunkGrid", null, &onToggleChunkGrid));
143 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_F5, "key.remesh", null, &onRemeshViewBox));
144 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_F1, "key.chunkmeta", null, &onPrintChunkMeta));
145 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_C, "key.toggleCulling", null, &onToggleCulling));
146 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_Y, "key.toggleWireframe", null, &onToggleWireframe));
147 
148 		dbg = resmanRegistry.getResourceManager!Debugger;
149 	}
150 
151 	// stubs
152 	private void nullChunkHandler(ChunkWorldPos) {}
153 	private void handleChunkObserverAdded(ChunkWorldPos, SessionId) {}
154 
155 	override void preInit()
156 	{
157 		chunkManager = new ChunkManager(NUM_CHUNK_LAYERS);
158 		chunkEditor = new ChunkEditor(NUM_CHUNK_LAYERS, chunkManager);
159 		worldAccess = new WorldAccess(chunkManager, chunkEditor);
160 		entityAccess = new BlockEntityAccess(chunkManager, chunkEditor);
161 		chunkObserverManager = new ChunkObserverManager();
162 
163 		chunkManager.setLayerInfo(METADATA_LAYER, ChunkLayerInfo(BLOCK_METADATA_UNIFORM_FILL_BITS));
164 		chunkManager.onChunkRemovedHandler = &chunkMeshMan.onChunkRemoved;
165 
166 		chunkEditor.addServerObserverHandler = &chunkObserverManager.addServerObserver;
167 		chunkEditor.removeServerObserverHandler = &chunkObserverManager.removeServerObserver;
168 
169 		chunkMeshMan.getDimensionBorders = &dimMan.dimensionBorders;
170 
171 		chunkObserverManager.loadChunkHandler = &chunkManager.loadChunk;
172 		chunkObserverManager.unloadChunkHandler = &chunkManager.unloadChunk;
173 		chunkObserverManager.chunkObserverAddedHandler = &handleChunkObserverAdded;
174 
175 		idMapManager.regIdMapHandler("string_map", &onServerStringMapReceived);
176 	}
177 
178 	override void init(IPluginManager pluginman)
179 	{
180 		import std.algorithm.comparison : clamp;
181 		numWorkersOpt.set(clamp(numWorkersOpt.get!uint, 1, 16));
182 
183 		viewRadius = viewRadiusOpt.get!uint;
184 		// duplicated code
185 		viewRadius = clamp(viewRadius, MIN_VIEW_RADIUS, MAX_VIEW_RADIUS);
186 
187 		session = pluginman.getPlugin!ClientSession;
188 
189 		auto gui = pluginman.getPlugin!GuiPlugin;
190 		auto debugClient = pluginman.getPlugin!DebugClient;
191 		debugClient.registerDebugGuiHandler(&showDebugGuiPosition, INFO_ORDER, "Position");
192 		debugClient.registerDebugGuiHandler(&showDebugGuiSettings, SETTINGS_ORDER, "Settings");
193 		debugClient.registerDebugGuiHandler(&showDebugGuiGraphics, DEBUG_ORDER, "Graphics");
194 		debugClient.registerDebugGuiHandler(&showDebugGuiChunks, DEBUG_ORDER, "Chunks");
195 
196 		blockPlugin = pluginman.getPlugin!BlockPluginClient;
197 		blockEntityPlugin = pluginman.getPlugin!BlockEntityClient;
198 		chunkMeshMan.init(chunkManager, blockPlugin.getBlocks(),
199 			blockEntityPlugin.blockEntityInfos(), numWorkersOpt.get!uint);
200 
201 		evDispatcher = pluginman.getPlugin!EventDispatcherPlugin;
202 		evDispatcher.subscribeToEvent(&handlePreUpdateEvent);
203 		evDispatcher.subscribeToEvent(&handlePostUpdateEvent);
204 		evDispatcher.subscribeToEvent(&handleGameStopEvent);
205 		evDispatcher.subscribeToEvent(&handleSendClientSettingsEvent);
206 		evDispatcher.subscribeToEvent(&drawSolid);
207 
208 		connection = pluginman.getPlugin!NetClientPlugin;
209 		connection.registerPacketHandler!ChunkDataPacket(&handleChunkDataPacket);
210 		connection.registerPacketHandler!FillBlockBoxPacket(&handleFillBlockBoxPacket);
211 		connection.registerPacketHandler!MultiblockChangePacket(&handleMultiblockChangePacket);
212 		connection.registerPacketHandler!PlaceBlockEntityPacket(&handlePlaceBlockEntityPacket);
213 		connection.registerPacketHandler!RemoveBlockEntityPacket(&handleRemoveBlockEntityPacket);
214 		connection.registerPacketHandler!IdMapPacket(&handleIdMapPacket);
215 
216 		graphics = pluginman.getPlugin!GraphicsPlugin;
217 
218 		worldAccess.blockInfos = blockPlugin.getBlocks();
219 	}
220 
221 	override void postInit()
222 	{
223 		renderer = graphics.renderer;
224 		chunkShader.compile(renderer);
225 		blockAtlasTexture = renderer.createTexture(blockPlugin.texAtlas.bitmap);
226 	}
227 
228 	WorldBox calcClampedBox(ChunkWorldPos cwp, int boxRadius)
229 	{
230 		int size = boxRadius*2 + 1;
231 		return WorldBox(cast(ivec3)(cwp.ivector3 - boxRadius),
232 			ivec3(size, size, size), cwp.w).intersection(dimMan.dimensionBorders(cwp.w));
233 	}
234 
235 	private void handleIdMapPacket(ubyte[] packetData)
236 	{
237 		auto packet = unpackPacket!IdMapPacket(packetData);
238 		if (auto h = idMapManager.onMapReceivedHandlers.get(packet.mapName, null))
239 		{
240 			tracef("Load id map %s", packet.names);
241 			h(packet.names);
242 		}
243 	}
244 
245 	private void onServerStringMapReceived(string[] strings)
246 	{
247 		serverStrings.load(strings);
248 	}
249 
250 	private void onTogglePositionUpdate(string)
251 	{
252 		doUpdateObserverPosition = !doUpdateObserverPosition;
253 	}
254 
255 	private void onToggleChunkGrid(string) {
256 		chunkDebug_showGrid = !chunkDebug_showGrid;
257 	}
258 
259 	private void onRemeshViewBox(string) {
260 		WorldBox box = chunkObserverManager.viewBoxes[observerSessionId];
261 		remeshBox(box, true);
262 	}
263 
264 	private void onPrintChunkMeta(string) {
265 		import voxelman.world.block : printChunkMetadata;
266 		auto cwp = observerPosition;
267 		auto snap = chunkManager.getChunkSnapshot(cwp, BLOCK_LAYER);
268 
269 		if (snap.isNull) {
270 			infof("No snapshot for %s", cwp);
271 			return;
272 		}
273 		printChunkMetadata(snap.metadata);
274 	}
275 
276 	private void handlePreUpdateEvent(ref PreUpdateEvent event)
277 	{
278 		++currentTimestamp;
279 
280 		if (doUpdateObserverPosition)
281 		{
282 			observerPosition = ChunkWorldPos(
283 				BlockWorldPos(graphics.camera.position, observerPosition.w));
284 		}
285 
286 		updateObserverPosition();
287 		chunkObserverManager.update();
288 		chunkMeshMan.update();
289 	}
290 
291 	private void showDebugGuiPosition()
292 	{
293 		// heading
294 		vec3 target = graphics.camera.target;
295 		vec2 heading = graphics.camera.heading;
296 		graphics.debugText.putfln("Heading: %.1f %.1f", heading.x, heading.y);
297 		graphics.debugText.putfln("Target: X %.1f Y %.1f Z %.1f", target.x, target.y, target.z);
298 
299 		// position
300 		vec3 pos = graphics.camera.position;
301 		graphics.debugText.putfln("Pos: X %.1f Y %.1f Z %.1f", pos.x, pos.y, pos.z);
302 		ChunkWorldPos chunkPos = observerPosition;
303 		graphics.debugText.putfln("Chunk: %s %s %s", chunkPos.x, chunkPos.y, chunkPos.z);
304 	}
305 
306 	private void showDebugGuiSettings()
307 	{
308 		graphics.debugText.putfln("Dimension: %s", observerPosition.w);// igSameLine();
309 			//if (igButton("-##decDimension")) decDimension(); igSameLine();
310 			//if (igButton("+##incDimension")) incDimension();
311 
312 		graphics.debugText.putfln("View radius: %s", viewRadius);// igSameLine();
313 			//if (igButton("-##decVRadius")) decViewRadius(); igSameLine();
314 			//if (igButton("+##incVRadius")) incViewRadius();
315 
316 		//igCheckbox("[U]pdate observer pos", &doUpdateObserverPosition);
317 	}
318 
319 	private void showDebugGuiGraphics()
320 	{
321 		//if (igCollapsingHeader("Graphics"))
322 		//{
323 			size_t dbg_meshesVisible = chunkMeshMan.chunkMeshes[0].length + chunkMeshMan.chunkMeshes[1].length;
324 			size_t dbg_totalRendered = dbg_meshesRenderedSemitransparent + dbg_meshesRenderedSolid;
325 			graphics.debugText.putfln("(S/ST)/total (%s/%s)/%s/%s %.0f%%",
326 				dbg_meshesRenderedSolid, dbg_meshesRenderedSemitransparent, dbg_totalRendered, dbg_meshesVisible,
327 				dbg_meshesVisible ? cast(float)dbg_totalRendered/dbg_meshesVisible*100.0 : 0);
328 			graphics.debugText.putfln("Vertices %,?s", ' ', dbg_vertsRendered);
329 			graphics.debugText.putfln("Triangles %,?s", ' ', dbg_trisRendered);
330 			import voxelman.graphics.vbo;
331 			graphics.debugText.putfln("Buffers: %s Mem: %,?s",
332 				Vbo.numAllocated,
333 				' ', chunkMeshMan.totalMeshDataBytes);
334 		//}
335 		dbg_meshesRenderedSolid = 0;
336 		dbg_meshesRenderedSemitransparent = 0;
337 		dbg_vertsRendered = 0;
338 		dbg_trisRendered = 0;
339 	}
340 
341 	private void showDebugGuiChunks()
342 	{
343 		//if (igCollapsingHeader("Chunks"))
344 		//{
345 			drawDebugChunkInfoGui();
346 
347 			graphics.debugText.putfln("Chunks per frame loaded: %s", dbg_totalLoadedChunks - dbg_lastFrameLoadedChunks);
348 			dbg_lastFrameLoadedChunks = dbg_totalLoadedChunks;
349 			graphics.debugText.putfln("Chunks total loaded: %s", dbg_totalLoadedChunks);
350 			graphics.debugText.putfln("Chunks tracked/loaded: %s/%s", chunkManager.numTrackedChunks, chunkManager.numLoadedChunks);
351 			graphics.debugText.putfln("Chunk mem %,?s", ' ', chunkManager.totalLayerDataBytes);
352 
353 			with(chunkMeshMan) {
354 				graphics.debugText.putfln("Chunks to mesh: %s", numMeshChunkTasks);
355 				graphics.debugText.putfln("New meshes: %s", newChunkMeshes.length);
356 				size_t sum;
357 				foreach(ref w; meshWorkers.workers) sum += w.taskQueue.length;
358 				graphics.debugText.putfln("Task Queues: %s", sum);
359 				sum = 0;
360 				foreach(ref w; meshWorkers.workers) sum += w.resultQueue.length;
361 				graphics.debugText.putfln("Res Queues: %s", sum);
362 				float percent = totalMeshedChunks > 0 ? cast(float)totalMeshes / totalMeshedChunks * 100 : 0.0;
363 				graphics.debugText.putfln("Meshed/Meshes %s/%s %.0f%%", totalMeshedChunks, totalMeshes, percent);
364 			}
365 		//}
366 	}
367 
368 	private bool chunkDebug_showGrid;
369 	private int chunkDebug_viewRadius = 2;
370 	private bool chunkDebug_showUniform;
371 	private bool chunkDebug_showSideMetadata;
372 	private bool chunkDebug_showBlockEntities;
373 	private bool chunkDebug_showWastedMeshes;
374 	private bool chunkDebug_showChunkLayers;
375 
376 	private void drawDebugChunkInfoGui()
377 	{
378 		// debug view radius
379 		graphics.debugText.putfln("Debug radius: %s", chunkDebug_viewRadius);
380 		//igSameLine();
381 		//	if (igButton("-##decDebugRadius"))
382 		//		--chunkDebug_viewRadius;
383 		//	igSameLine();
384 		//	if (igButton("+##incDebugRadius"))
385 		//		++chunkDebug_viewRadius;
386 
387 		//igCheckbox("show grid", &chunkDebug_showGrid);
388 		//igCheckbox("show uniform", &chunkDebug_showUniform);
389 		//igCheckbox("show side meta", &chunkDebug_showSideMetadata);
390 		//igCheckbox("show block entities", &chunkDebug_showBlockEntities);
391 		//igCheckbox("show wasted meshes", &chunkDebug_showWastedMeshes);
392 		//igCheckbox("show chunk layers", &chunkDebug_showChunkLayers);
393 	}
394 
395 	private void drawDebugChunkInfo()
396 	{
397 		ChunkWorldPos chunkPos = BlockWorldPos(graphics.camera.position, currentDimension);
398 		chunkDebug_viewRadius = clamp(chunkDebug_viewRadius, 0, 10);
399 		WorldBox nearBox = calcBox(chunkPos, chunkDebug_viewRadius);
400 
401 		if (chunkDebug_showGrid) drawDebugChunkGrid(nearBox);
402 		if (chunkDebug_showSideMetadata) drawDebugChunkSideMetadata(nearBox);
403 		if (chunkDebug_showUniform) drawDebugChunkUniform(nearBox);
404 		if (chunkDebug_showBlockEntities) drawDebugBlockentity(nearBox);
405 		if (chunkDebug_showWastedMeshes) chunkMeshMan.drawDebug(graphics.debugBatch);
406 		if (chunkDebug_showChunkLayers) drawDebugChunkLayers(nearBox);
407 	}
408 
409 	private void drawDebugChunkSideMetadata(WorldBox box)
410 	{
411 		foreach(pos; box.positions)
412 		{
413 			vec3 blockPos = pos * CHUNK_SIZE;
414 			auto snap = chunkManager.getChunkSnapshot(ChunkWorldPos(pos, box.dimension), BLOCK_LAYER);
415 			if (snap.isNull) continue;
416 
417 			foreach(ubyte side; 0..6) {
418 				Solidity solidity = chunkSideSolidity(snap.metadata, cast(CubeSide)side);
419 				static Color4ub[3] colors = [Colors.white, Colors.gray, Colors.black];
420 				Color4ub color = colors[solidity];
421 				graphics.debugBatch.putCubeFace(blockPos + CHUNK_SIZE/2, vec3(2,2,2), cast(CubeSide)side, color, true);
422 			}
423 		}
424 	}
425 
426 	private void drawDebugChunkUniform(WorldBox box)
427 	{
428 		foreach(pos; box.positions) {
429 			vec3 blockPos = pos * CHUNK_SIZE;
430 			auto snap = chunkManager.getChunkSnapshot(ChunkWorldPos(pos, box.dimension), BLOCK_LAYER);
431 			if (!snap.isNull && snap.isUniform) {
432 				graphics.debugBatch.putCube(blockPos + CHUNK_SIZE/2-2, vec3(6,6,6), Colors.green, false);
433 			}
434 		}
435 	}
436 
437 	private void drawDebugBlockentity(WorldBox box)
438 	{
439 		foreach(pos; box.positions)
440 		{
441 			ivec3 chunkPos = pos * CHUNK_SIZE;
442 
443 			auto snap = chunkManager.getChunkSnapshot(
444 				ChunkWorldPos(pos, box.dimension), ENTITY_LAYER, Yes.Uncompress);
445 
446 			if (snap.isNull) continue;
447 			auto map = getHashMapFromLayer(snap);
448 			foreach(id, entity; map)
449 			{
450 				if (BlockEntityData(entity).type == BlockEntityType.localBlockEntity)
451 				{
452 					auto pos = BlockChunkPos(id);
453 					auto entityPos = chunkPos + pos.vector;
454 					graphics.debugBatch.putCube(vec3(entityPos)+0.25, vec3(0.5,0.5,0.5), Colors.red, true);
455 				}
456 				else if (BlockEntityData(entity).type == BlockEntityType.foreignBlockEntity)
457 				{
458 					auto pos = BlockChunkPos(id);
459 					auto entityPos = chunkPos + pos.vector;
460 					graphics.debugBatch.putCube(vec3(entityPos)+0.25, vec3(0.5,0.5,0.5), Colors.orange, true);
461 				}
462 			}
463 		}
464 	}
465 
466 	private void drawDebugChunkGrid(WorldBox box)
467 	{
468 		vec3 gridPos = vec3(box.position*CHUNK_SIZE);
469 		ivec3 gridCount = box.size+1;
470 		vec3 gridOffset = vec3(CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE);
471 		graphics.debugBatch.put3dGrid(gridPos, gridCount, gridOffset, Colors.blue);
472 	}
473 
474 	private void drawDebugChunkLayers(WorldBox box)
475 	{
476 		foreach(pos; box.positions)
477 		{
478 			auto cwp = ChunkWorldPos(pos, box.dimension);
479 			ivec3 chunkPos = pos * CHUNK_SIZE + CHUNK_SIZE/2;
480 			graphics.debugBatch.putCube(vec3(chunkPos)+vec3(0.75, 0, 0), vec3(0.25,1,1), Colors.red, true);
481 			foreach(ubyte layer; 0..chunkManager.numLayers)
482 			{
483 				ivec3 layerBlockPos = chunkPos;
484 				layerBlockPos.x += layer + 1;
485 				if (chunkManager.hasSnapshot(cwp, layer))
486 					graphics.debugBatch.putCube(vec3(layerBlockPos)+0.25, vec3(0.5,0.5,0.5), Colors.white, true);
487 				else
488 					graphics.debugBatch.putCube(vec3(layerBlockPos)+0.25, vec3(0.5,0.5,0.5), Colors.black, false);
489 			}
490 		}
491 	}
492 
493 	private void handlePostUpdateEvent(ref PostUpdateEvent event)
494 	{
495 		chunkEditor.commitSnapshots(currentTimestamp);
496 		//chunkMeshMan.remeshChangedChunks(chunkManager.getModifiedChunks());
497 		chunkManager.clearModifiedChunks();
498 		chunkMeshMan.remeshChangedChunks(chunksToRemesh);
499 		chunksToRemesh.clear();
500 
501 		if (doUpdateObserverPosition)
502 			sendPosition(event.deltaTime);
503 
504 		dbg.setVar("wasted client loads", wastedClientLoads);
505 		drawDebugChunkInfo();
506 	}
507 
508 	void drawSolid(ref RenderSolid3dEvent event)
509 	{
510 		renderer.wireFrameMode(wireframeMode);
511 
512 		renderer.faceCulling(true);
513 		renderer.faceCullMode(FaceCullMode.back);
514 
515 		chunkShader.bind;
516 		chunkShader.setTransparency(1.0f);
517 
518 		blockAtlasTexture.bind;
519 
520 		drawMeshes(chunkMeshMan.chunkMeshes[0].byValue,
521 			chunkShader,
522 			dbg_meshesRenderedSolid);
523 
524 		renderer.alphaBlending(true);
525 		renderer.depthWrite(false);
526 
527 		chunkShader.setTransparency(0.5f);
528 
529 		drawMeshes(chunkMeshMan.chunkMeshes[1].byValue,
530 			chunkShader,
531 			dbg_meshesRenderedSemitransparent);
532 
533 		blockAtlasTexture.unbind;
534 
535 		chunkShader.unbind;
536 
537 		renderer.faceCulling(false);
538 		renderer.depthWrite(true);
539 		renderer.alphaBlending(false);
540 
541 		if (wireframeMode)
542 			renderer.wireFrameMode(false);
543 	}
544 
545 	private void drawMeshes(R, S)(R meshes, ref S shader, ref size_t meshCounter)
546 	{
547 		import dlib.geometry.frustum;
548 		Matrix4f vp = graphics.camera.perspective * graphics.camera.cameraToClipMatrix;
549 
550 		Frustum frustum;
551 		frustum.fromMVP(vp);
552 
553 		foreach(const ref mesh; meshes)
554 		{
555 			if (isCullingEnabled) // Frustum culling
556 			{
557 				import dlib.geometry.aabb;
558 				vec3 vecMin = mesh.position;
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 			Matrix4f mvp = vp * translationMatrix!float(mesh.position);
566 			shader.setMvp(mvp);
567 
568 			mesh.render();
569 
570 			++meshCounter;
571 			dbg_vertsRendered += mesh.numVertexes;
572 			dbg_trisRendered += mesh.numTris;
573 		}
574 	}
575 
576 	private void handleGameStopEvent(ref GameStopEvent gameStopEvent)
577 	{
578 		import core.thread;
579 		while(chunkMeshMan.numMeshChunkTasks > 0)
580 		{
581 			chunkMeshMan.update();
582 		}
583 		chunkMeshMan.stop();
584 	}
585 
586 	private void handleSendClientSettingsEvent(ref SendClientSettingsEvent event)
587 	{
588 		connection.send(ViewRadiusPacket(viewRadius));
589 	}
590 
591 	private void handleChunkDataPacket(ubyte[] packetData)
592 	{
593 		auto packet = unpackPacketNoDup!ChunkDataPacket(packetData);
594 		//tracef("Received %s ChunkDataPacket(%s,%s)", packetData.length,
595 		//	packet.chunkPos, packet.blockData.blocks.length);
596 
597 		ChunkLayerItem[MAX_CHUNK_LAYERS] layers;
598 		auto cwp = ChunkWorldPos(packet.chunkPos);
599 
600 		ubyte numChunkLayers;
601 		foreach(layer; packet.layers)
602 		{
603 			layers[numChunkLayers] = fromBlockData(layer);
604 			++numChunkLayers;
605 		}
606 
607 		onChunkLoaded(cwp, layers[0..numChunkLayers]);
608 	}
609 
610 	void onChunkLoaded(ChunkWorldPos cwp, ChunkLayerItem[] layers)
611 	{
612 		//tracef("onChunkLoaded %s added %s", cwp, chunkManager.isChunkAdded(cwp));
613 		++dbg_totalLoadedChunks;
614 
615 		if (chunkManager.isChunkLoaded(cwp))
616 		{
617 			// TODO possible bug, copyLayer could be called when write buffer is not empty
618 			foreach(layer; layers)
619 			{
620 				WriteBuffer* writeBuffer = chunkEditor.getOrCreateWriteBuffer(cwp, layer.layerId);
621 				copyLayer(layer, writeBuffer.layer);
622 			}
623 		}
624 		else if (chunkManager.isChunkAdded(cwp))
625 		{
626 			foreach(ref layer; layers)
627 			{
628 				if (!layer.isUniform)
629 				{
630 					ubyte[] data = allocLayerArray(layer.getArray!ubyte);
631 					layer.dataLength = cast(LayerDataLenType)data.length;
632 					layer.dataPtr = data.ptr;
633 				}
634 			}
635 			chunkManager.onSnapshotLoaded(cwp, layers, true);
636 		}
637 		else
638 		{
639 			// we received chunk data for unloaded chunk. Ignore it.
640 			++wastedClientLoads;
641 			return;
642 		}
643 
644 		foreach(ChunkWorldPos pos; calcClampedBox(cwp, 1))
645 			chunksToRemesh.put(pos);
646 	}
647 
648 	private void handleMultiblockChangePacket(ubyte[] packetData)
649 	{
650 		auto packet = unpackPacket!MultiblockChangePacket(packetData);
651 		auto cwp = ChunkWorldPos(packet.chunkPos);
652 
653 		worldAccess.applyBlockChanges(cwp, packet.blockChanges);
654 
655 		foreach(ChunkWorldPos pos; calcClampedBox(cwp, 1))
656 			chunksToRemesh.put(pos);
657 	}
658 
659 	private void handleFillBlockBoxPacket(ubyte[] packetData)
660 	{
661 		auto packet = unpackPacketNoDup!FillBlockBoxPacket(packetData);
662 
663 		worldAccess.fillBox(packet.box, packet.blockId, packet.blockMeta);
664 		onBlockBoxChanged(packet.box);
665 	}
666 
667 	void onBlockBoxChanged(WorldBox blockBox)
668 	{
669 		WorldBox observedBox = chunkObserverManager.viewBoxes[observerSessionId];
670 		WorldBox modifiedBox = calcModifiedMeshesBox(blockBox);
671 		WorldBox box = worldBoxIntersection(observedBox, modifiedBox);
672 
673 		foreach(ChunkWorldPos pos; box)
674 			chunksToRemesh.put(pos);
675 	}
676 
677 	private void handlePlaceBlockEntityPacket(ubyte[] packetData)
678 	{
679 		auto packet = unpackPacket!PlaceBlockEntityPacket(packetData);
680 		placeEntity(packet.box, packet.data,
681 			worldAccess, entityAccess);
682 		onBlockBoxChanged(packet.box);
683 	}
684 
685 	private void handleRemoveBlockEntityPacket(ubyte[] packetData)
686 	{
687 		auto packet = unpackPacket!RemoveBlockEntityPacket(packetData);
688 		WorldBox changedBox = removeEntity(BlockWorldPos(packet.blockPos),
689 			blockEntityPlugin.blockEntityInfos, worldAccess, entityAccess, /*AIR*/1);
690 		onBlockBoxChanged(changedBox);
691 	}
692 
693 	private void remeshBox(WorldBox box, bool printTime = false)
694 	{
695 		MonoTime startTime = MonoTime.currTime;
696 
697 		void onRemeshDone(size_t chunksRemeshed, Duration totalDuration) {
698 			auto duration = MonoTime.currTime - startTime;
699 			auto avg = totalDuration / chunksRemeshed;
700 			infof("Remeshed %s chunks in\ttotal %s\tavg %s\tsingle thread %s", chunksRemeshed, fmtDur(totalDuration), fmtDur(avg), fmtDur(duration));
701 		}
702 
703 		HashSet!ChunkWorldPos remeshedChunks;
704 		foreach(pos; box.positions) {
705 			remeshedChunks.put(ChunkWorldPos(pos, box.dimension));
706 		}
707 
708 		if (printTime)
709 		{
710 			size_t numMeshed = chunkMeshMan.remeshChangedChunks(remeshedChunks, &onRemeshDone);
711 			auto submitDuration = MonoTime.currTime - startTime;
712 			auto avg = submitDuration / numMeshed;
713 			infof("%s tasks submitted in %s, avg per task %s", numMeshed, submitDuration.fmtDur, avg.fmtDur);
714 		}
715 		else
716 			chunkMeshMan.remeshChangedChunks(remeshedChunks);
717 	}
718 
719 	void sendPosition(double dt)
720 	{
721 		if (session.isSpawned)
722 		{
723 			sendPositionTimer += dt;
724 			if (sendPositionTimer > sendPositionInterval ||
725 				observerPosition != prevChunkPos)
726 			{
727 				connection.send(ClientPositionPacket(
728 					ClientDimPos(
729 						graphics.camera.position,
730 						graphics.camera.heading),
731 					observerPosition.w, positionKey));
732 
733 				if (sendPositionTimer < sendPositionInterval)
734 					sendPositionTimer = 0;
735 				else
736 					sendPositionTimer -= sendPositionInterval;
737 			}
738 		}
739 
740 		prevChunkPos = observerPosition;
741 	}
742 
743 	void setDimensionBorders(DimensionId dim, Box borders)
744 	{
745 		DimensionInfo* dimInfo = dimMan.getOrCreate(dim);
746 		dimInfo.borders = borders;
747 		updateObserverPosition();
748 	}
749 
750 	void setCurrentDimension(DimensionId dimension, ubyte positionKey) {
751 		observerPosition.w = dimension;
752 		this.positionKey = positionKey;
753 		updateObserverPosition();
754 	}
755 
756 	bool isBlockSolid(ivec3 blockWorldPos) {
757 		auto block = worldAccess.getBlock(
758 			BlockWorldPos(blockWorldPos, observerPosition.w));
759 		return block != 0 && blockPlugin.getBlocks()[block].isVisible;
760 	}
761 
762 	DimensionId currentDimension() @property {
763 		return observerPosition.w;
764 	}
765 
766 	Box currentDimensionBorders() @property {
767 		return dimMan.dimensionBorders(observerPosition.dimension);
768 	}
769 
770 	void incDimension() {
771 		string com = cast(string)makeFormattedText("dim %s", currentDimension() + 1);
772 		connection.send(CommandPacket(com));
773 	}
774 	void decDimension() {
775 		string com = cast(string)makeFormattedText("dim %s", currentDimension() - 1);
776 		connection.send(CommandPacket(com));
777 	}
778 
779 	void updateObserverPosition() {
780 		if (session.isSpawned) {
781 			if (observerSessionId != session.thisSessionId) {
782 				chunkObserverManager.removeObserver(observerSessionId);
783 				observerSessionId = cast(SessionId)session.thisSessionId;
784 			}
785 
786 			auto borders = dimMan.dimensionBorders(observerPosition.dimension);
787 			chunkObserverManager.changeObserverBox(
788 				observerSessionId, observerPosition, viewRadius, borders);
789 		}
790 	}
791 
792 	void onIncViewRadius(string) {
793 		incViewRadius();
794 	}
795 
796 	void onDecViewRadius(string) {
797 		decViewRadius();
798 	}
799 
800 	void incViewRadius() {
801 		setViewRadius(getViewRadius() + 1);
802 	}
803 
804 	void decViewRadius() {
805 		setViewRadius(getViewRadius() - 1);
806 	}
807 
808 	int getViewRadius() {
809 		return viewRadius;
810 	}
811 
812 	void setViewRadius(int newViewRadius) {
813 		auto oldViewRadius = viewRadius;
814 		// duplicated code
815 		viewRadius = clamp(newViewRadius, MIN_VIEW_RADIUS, MAX_VIEW_RADIUS);
816 
817 		if (oldViewRadius != viewRadius)
818 		{
819 			connection.send(ViewRadiusPacket(viewRadius));
820 		}
821 	}
822 
823 	void onToggleCulling(string) {
824 		isCullingEnabled = !isCullingEnabled;
825 	}
826 
827 	void onToggleWireframe(string) {
828 		wireframeMode = !wireframeMode;
829 	}
830 }