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 }