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, thread_joinAll;
9 import core.time;
10 import std.experimental.logger;
11 
12 import voxelman.math;
13 import dlib.math.matrix : Matrix4f;
14 import dlib.math.affine : translationMatrix;
15 import derelict.enet.enet;
16 import derelict.opengl3.gl3;
17 import derelict.imgui.imgui;
18 
19 import anchovy.fpshelper;
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 import voxelman.block.plugin;
31 import voxelman.world.clientworld;
32 import voxelman.dbg.plugin;
33 
34 import voxelman.core.config;
35 import voxelman.core.chunkmesh;
36 import voxelman.net.events;
37 import voxelman.core.events;
38 import voxelman.core.packets;
39 import voxelman.net.packets;
40 
41 import voxelman.config.configmanager;
42 import voxelman.input.keybindingmanager;
43 
44 import voxelman.world.storage.chunk;
45 import voxelman.world.storage.coordinates;
46 import voxelman.world.storage.utils;
47 import voxelman.world.storage.worldaccess;
48 import voxelman.utils.textformatter;
49 
50 import voxelman.client.appstatistics;
51 import voxelman.client.console;
52 
53 //version = manualGC;
54 version(manualGC) import core.memory;
55 
56 shared static this()
57 {
58 	auto c = new ClientPlugin;
59 	pluginRegistry.regClientPlugin(c);
60 	pluginRegistry.regClientMain(&c.run);
61 }
62 
63 auto formatDuration(Duration dur)
64 {
65 	import std.string : format;
66 	auto splitted = dur.split();
67 	return format("%s.%03s,%03s secs",
68 		splitted.seconds, splitted.msecs, splitted.usecs);
69 }
70 
71 final class ClientPlugin : IPlugin
72 {
73 private:
74 	PluginManager pluginman;
75 
76 	// Plugins
77 	EventDispatcherPlugin evDispatcher;
78 	GraphicsPlugin graphics;
79 	GuiPlugin guiPlugin;
80 	CommandPluginClient commandPlugin;
81 	ClientWorld clientWorld;
82 	NetClientPlugin connection;
83 	Debugger dbg;
84 
85 	// valid when !dedicated
86 	Thread serverThread;
87 
88 public:
89 	AppStatistics stats;
90 	Console console;
91 
92 	// Client data
93 	bool isRunning = false;
94 	bool mouseLocked;
95 
96 	double delta;
97 	Duration frameTime;
98 	ConfigOption maxFpsOpt;
99 	bool limitFps = true;
100 	FpsHelper fpsHelper;
101 
102 	// Graphics stuff
103 	bool isCullingEnabled = true;
104 	bool isConsoleShown = false;
105 	bool triangleMode = true;
106 
107 	// IPlugin stuff
108 	mixin IdAndSemverFrom!(voxelman.client.plugininfo);
109 
110 	override void registerResources(IResourceManagerRegistry resmanRegistry)
111 	{
112 		ConfigManager config = resmanRegistry.getResourceManager!ConfigManager;
113 		maxFpsOpt = config.registerOption!int("max_fps", true);
114 
115 		dbg = resmanRegistry.getResourceManager!Debugger;
116 
117 		KeyBindingManager keyBindingMan = resmanRegistry.getResourceManager!KeyBindingManager;
118 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_Q, "key.lockMouse", null, &onLockMouse));
119 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_C, "key.toggleCulling", null, &onToggleCulling));
120 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_Y, "key.toggleTriangle", null, &onToggleTriangle));
121 		keyBindingMan.registerKeyBinding(new KeyBinding(KeyCode.KEY_GRAVE_ACCENT, "key.toggle_console", null, &onConsoleToggleKey));
122 	}
123 
124 	override void preInit()
125 	{
126 		fpsHelper.maxFps = maxFpsOpt.get!uint;
127 		if (fpsHelper.maxFps == 0) fpsHelper.limitFps = false;
128 		console.init();
129 	}
130 
131 	override void init(IPluginManager pluginman)
132 	{
133 		clientWorld = pluginman.getPlugin!ClientWorld;
134 		evDispatcher = pluginman.getPlugin!EventDispatcherPlugin;
135 
136 		graphics = pluginman.getPlugin!GraphicsPlugin;
137 		guiPlugin = pluginman.getPlugin!GuiPlugin;
138 
139 		evDispatcher.subscribeToEvent(&onPreUpdateEvent);
140 		evDispatcher.subscribeToEvent(&onPostUpdateEvent);
141 		evDispatcher.subscribeToEvent(&drawSolid);
142 		evDispatcher.subscribeToEvent(&drawOverlay);
143 		evDispatcher.subscribeToEvent(&onClosePressedEvent);
144 
145 		commandPlugin = pluginman.getPlugin!CommandPluginClient;
146 		commandPlugin.registerCommand("cl_stop|stop", &onStopCommand);
147 
148 		console.messageWindow.messageHandler = &onConsoleCommand;
149 
150 		connection = pluginman.getPlugin!NetClientPlugin;
151 	}
152 
153 	override void postInit() {}
154 
155 	void onStopCommand(CommandParams) { isRunning = false; }
156 
157 	void printDebug()
158 	{
159 		igBegin("Debug");
160 		with(stats) {
161 			igTextf("FPS: %s", fps); igSameLine();
162 
163 			int fpsLimitVal = maxFpsOpt.get!int;
164 			igPushItemWidth(60);
165 			//igInputInt("limit", &fpsLimitVal, 5, 20, 0);
166 			igSliderInt(limitFps ? "limited##limit" : "unlimited##limit", &fpsLimitVal, 0, 240, null);
167 			igPopItemWidth();
168 			maxFpsOpt.set!int(fpsLimitVal);
169 			updateFrameTime();
170 
171 			ulong totalRendered = chunksRenderedSemitransparent + chunksRendered;
172 			igTextf("(S/ST)/total (%s/%s)/%s/%s %.0f%%",
173 				chunksRendered, chunksRenderedSemitransparent, totalRendered, chunksVisible,
174 				chunksVisible ? cast(float)totalRendered/chunksVisible*100.0 : 0);
175 			igTextf("Chunks per frame loaded: %s",
176 				totalLoadedChunks - lastFrameLoadedChunks);
177 			igTextf("Chunks total loaded: %s",
178 				totalLoadedChunks);
179 			igTextf("Chunk mem %s", DigitSeparator!(long, 3, ' ')(clientWorld.chunkManager.totalLayerDataBytes));
180 			igTextf("Vertices %s", vertsRendered);
181 			igTextf("Triangles %s", trisRendered);
182 			vec3 pos = graphics.camera.position;
183 			igTextf("Pos: X %.2f, Y %.2f, Z %.2f", pos.x, pos.y, pos.z);
184 		}
185 
186 		ChunkWorldPos chunkPos = clientWorld.observerPosition;
187 		igTextf("Chunk: %s %s %s", chunkPos.x, chunkPos.y, chunkPos.z);
188 		igTextf("Dimention: %s", chunkPos.w); igSameLine();
189 			if (igButton("-##decDimention")) clientWorld.decDimention(); igSameLine();
190 			if (igButton("+##incDimention")) clientWorld.incDimention();
191 
192 		vec3 target = graphics.camera.target;
193 		vec2 heading = graphics.camera.heading;
194 		igTextf("Heading: %.2f %.2f", heading.x, heading.y);
195 		igTextf("Target: X %.2f, Y %.2f, Z %.2f", target.x, target.y, target.z);
196 
197 		with(clientWorld.chunkMeshMan) {
198 			igTextf("Buffers: %s Mem: %s", ChunkMesh.numBuffersAllocated, DigitSeparator!(long, 3, ' ')(totalMeshDataBytes));
199 			igTextf("Chunks to mesh: %s", numMeshChunkTasks);
200 			igTextf("New meshes: %s", newChunkMeshes.length);
201 			size_t sum;
202 			foreach(ref w; meshWorkers.workers) sum += w.resultQueue.length;
203 			igTextf("Task Queues: %s", sum);
204 			sum = 0;
205 			foreach(ref w; meshWorkers.workers) sum += w.taskQueue.length;
206 			igTextf("Res Queues: %s", sum);
207 			float percent = totalMeshedChunks > 0 ? cast(float)totalMeshes / totalMeshedChunks * 100 : 0.0;
208 			igTextf("Meshed/Meshes %s/%s %.0f%%", totalMeshedChunks, totalMeshes, percent);
209 		}
210 
211 		with(clientWorld) {
212 			igTextf("View radius: %s", viewRadius); igSameLine();
213 			if (igButton("-##decVRadius")) decViewRadius(); igSameLine();
214 			if (igButton("+##incVRadius")) incViewRadius();
215 		}
216 
217 		igEnd();
218 	}
219 
220 	this()
221 	{
222 		pluginman = new PluginManager;
223 	}
224 
225 	void load(string[] args)
226 	{
227 		// register all plugins and managers
228 		import voxelman.pluginlib.plugininforeader : filterEnabledPlugins;
229 		foreach(p; pluginRegistry.clientPlugins.byValue.filterEnabledPlugins(args))
230 		{
231 			pluginman.registerPlugin(p);
232 		}
233 
234 		// Actual loading sequence
235 		pluginman.initPlugins();
236 	}
237 
238 	void startServer(string[] args)
239 	{
240 		void exec()
241 		{
242 			pluginRegistry.serverMain(args, false/*internal*/);
243 		}
244 		serverThread = new Thread(&exec);
245 		serverThread.start();
246 	}
247 
248 	void run(string[] args, bool dedicated)
249 	{
250 		import std.datetime : MonoTime, Duration, usecs, dur;
251 		import core.thread : Thread;
252 
253 		version(manualGC) GC.disable;
254 
255 		if (!dedicated)
256 		{
257 			startServer(args);
258 		}
259 
260 		load(args);
261 		evDispatcher.postEvent(GameStartEvent());
262 
263 		MonoTime prevTime = MonoTime.currTime;
264 		updateFrameTime();
265 
266 		isRunning = true;
267 		ulong frame;
268 		while(isRunning)
269 		{
270 			MonoTime newTime = MonoTime.currTime;
271 			delta = (newTime - prevTime).total!"usecs" / 1_000_000.0;
272 			prevTime = newTime;
273 
274 				evDispatcher.postEvent(PreUpdateEvent(delta, frame));
275 				evDispatcher.postEvent(UpdateEvent(delta, frame));
276 				evDispatcher.postEvent(PostUpdateEvent(delta, frame));
277 				evDispatcher.postEvent(DoGuiEvent(frame));
278 				evDispatcher.postEvent(RenderEvent());
279 
280 				version(manualGC) {
281 					GC.collect();
282 					GC.minimize();
283 				}
284 
285 				if (limitFps) {
286 					Duration updateTime = MonoTime.currTime - newTime;
287 					Duration sleepTime = frameTime - updateTime;
288 					if (sleepTime > Duration.zero)
289 						Thread.sleep(sleepTime);
290 				}
291 
292 				++frame;
293 		}
294 
295 		if (!dedicated)
296 		{
297 			// Stop the server
298 			import voxelman.client.servercontrol : stopServer;
299 			stopServer();
300 		}
301 
302 		infof("Stopping...");
303 		evDispatcher.postEvent(GameStopEvent());
304 		thread_joinAll();
305 		infof("[Stopped]");
306 	}
307 
308 	void updateFrameTime()
309 	{
310 		uint maxFps = maxFpsOpt.get!uint;
311 		if (maxFps == 0) {
312 			limitFps = false;
313 			frameTime = Duration.zero;
314 			return;
315 		}
316 
317 		limitFps = true;
318 		frameTime = (1_000_000 / maxFpsOpt.get!uint).usecs;
319 	}
320 
321 	void onPreUpdateEvent(ref PreUpdateEvent event)
322 	{
323 		fpsHelper.update(event.deltaTime);
324 	}
325 
326 	void onPostUpdateEvent(ref PostUpdateEvent event)
327 	{
328 		stats.fps = fpsHelper.fps;
329 		stats.totalLoadedChunks = clientWorld.totalLoadedChunks;
330 
331 		printDebug();
332 		stats.resetCounters();
333 		if (isConsoleShown)
334 			console.draw();
335 		dbg.logVar("delta, ms", delta*1000.0, 256);
336 	}
337 
338 	void onConsoleCommand(string command)
339 	{
340 		infof("Executing command '%s'", command);
341 		ExecResult res = commandPlugin.execute(command, ClientId(0));
342 
343 		if (res.status == ExecStatus.notRegistered)
344 		{
345 			if (connection.isConnected)
346 				connection.send(CommandPacket(command));
347 			else
348 				console.lineBuffer.putfln("Unknown client command '%s', not connected to server", command);
349 		}
350 		else if (res.status == ExecStatus.error)
351 			console.lineBuffer.putfln("Error executing command '%s': %s", command, res.error);
352 		else
353 			console.lineBuffer.putln(command);
354 	}
355 
356 	void onConsoleToggleKey(string)
357 	{
358 		isConsoleShown = !isConsoleShown;
359 	}
360 
361 	void onClosePressedEvent(ref ClosePressedEvent event)
362 	{
363 		isRunning = false;
364 	}
365 
366 	void onLockMouse(string)
367 	{
368 		mouseLocked = !mouseLocked;
369 		if (mouseLocked)
370 			guiPlugin.window.mousePosition = cast(ivec2)(guiPlugin.window.size) / 2;
371 	}
372 
373 	void onToggleCulling(string)
374 	{
375 		isCullingEnabled = !isCullingEnabled;
376 	}
377 	void onToggleTriangle(string)
378 	{
379 		triangleMode = !triangleMode;
380 	}
381 
382 	void drawSolid(ref RenderSolid3dEvent event)
383 	{
384 		glEnable(GL_CULL_FACE);
385 		glCullFace(GL_BACK);
386 
387 		graphics.chunkShader.bind;
388 		glUniformMatrix4fv(graphics.viewLoc, 1, GL_FALSE,
389 			graphics.camera.cameraMatrix);
390 		glUniformMatrix4fv(graphics.projectionLoc, 1, GL_FALSE,
391 			cast(const float*)graphics.camera.perspective.arrayof);
392 
393 		import dlib.geometry.aabb;
394 		import dlib.geometry.frustum;
395 		Matrix4f vp = graphics.camera.perspective * graphics.camera.cameraToClipMatrix;
396 		Frustum frustum;
397 		frustum.fromMVP(vp);
398 
399 		Matrix4f modelMatrix;
400 
401 		foreach(ref mesh; clientWorld.chunkMeshMan.chunkMeshes[0].byValue)
402 		{
403 			++stats.chunksVisible;
404 			if (isCullingEnabled) // Frustum culling
405 			{
406 				vec3 vecMin = mesh.position;
407 				vec3 vecMax = vecMin + CHUNK_SIZE;
408 				AABB aabb = boxFromMinMaxPoints(vecMin, vecMax);
409 				auto intersects = frustum.intersectsAABB(aabb);
410 				if (!intersects) continue;
411 			}
412 
413 			modelMatrix = translationMatrix!float(mesh.position);
414 			glUniformMatrix4fv(graphics.modelLoc, 1, GL_FALSE, cast(const float*)modelMatrix.arrayof);
415 
416 			mesh.bind;
417 			mesh.render(triangleMode);
418 
419 			++stats.chunksRendered;
420 			stats.vertsRendered += mesh.numVertexes;
421 			stats.trisRendered += mesh.numTris;
422 		}
423 
424 		graphics.chunkShader.unbind;
425 
426 		graphics.renderer.enableAlphaBlending();
427 		glDepthMask(GL_FALSE);
428 
429 		graphics.transChunkShader.bind;
430 		glUniformMatrix4fv(graphics.viewLoc, 1, GL_FALSE,
431 			graphics.camera.cameraMatrix);
432 		glUniformMatrix4fv(graphics.projectionLoc, 1, GL_FALSE,
433 			cast(const float*)graphics.camera.perspective.arrayof);
434 		foreach(ref mesh; clientWorld.chunkMeshMan.chunkMeshes[1].byValue)
435 		{
436 			++stats.chunksVisible;
437 			if (isCullingEnabled) // Frustum culling
438 			{
439 				vec3 vecMin = mesh.position;
440 				vec3 vecMax = vecMin + CHUNK_SIZE;
441 				AABB aabb = boxFromMinMaxPoints(vecMin, vecMax);
442 				auto intersects = frustum.intersectsAABB(aabb);
443 				if (!intersects) continue;
444 			}
445 
446 			modelMatrix = translationMatrix!float(mesh.position);
447 			glUniformMatrix4fv(graphics.modelLoc, 1, GL_FALSE, cast(const float*)modelMatrix.arrayof);
448 
449 			mesh.bind;
450 			mesh.render(triangleMode);
451 
452 			++stats.chunksRenderedSemitransparent;
453 			stats.vertsRendered += mesh.numVertexes;
454 			stats.trisRendered += mesh.numTris;
455 		}
456 		glUniformMatrix4fv(graphics.modelLoc, 1, GL_FALSE, cast(const float*)Matrix4f.identity.arrayof);
457 		graphics.transChunkShader.unbind;
458 		glDisable(GL_CULL_FACE);
459 		glDepthMask(GL_TRUE);
460 		graphics.renderer.disableAlphaBlending();
461 	}
462 
463 	void drawOverlay(ref Render2Event event)
464 	{
465 		//event.renderer.setColor(Color(0,0,0,1));
466 		//event.renderer.fillRect(Rect(guiPlugin.window.size.x/2-7, guiPlugin.window.size.y/2-1, 14, 2));
467 		//event.renderer.fillRect(Rect(guiPlugin.window.size.x/2-1, guiPlugin.window.size.y/2-7, 2, 14));
468 	}
469 }