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 }