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