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 }