1 /** 2 Copyright: Copyright (c) 2016-2017 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.session.server; 7 8 import voxelman.log; 9 import std..string : format; 10 import netlib; 11 import pluginlib; 12 import datadriven; 13 import voxelman.math; 14 15 import voxelman.core.config; 16 import voxelman.core.events; 17 import voxelman.net.events; 18 import voxelman.core.packets; 19 import voxelman.net.packets; 20 import voxelman.world.storage; 21 22 import voxelman.command.plugin; 23 import voxelman.eventdispatcher.plugin; 24 import voxelman.entity.plugin : EntityComponentRegistry; 25 import voxelman.net.plugin; 26 import voxelman.world.serverworld; 27 28 import voxelman.session.components; 29 import voxelman.session.clientdb; 30 import voxelman.session.sessionman; 31 32 33 struct ClientPositionManager 34 { 35 ClientManager cm; 36 37 void tpToPos(Session* session, ClientDimPos dimPos, DimensionId dim) 38 { 39 auto position = cm.db.get!ClientPosition(session.dbKey); 40 41 position.dimPos = dimPos; 42 position.dimension = dim; 43 ++position.positionKey; 44 45 sendPositionToClient(*position, session.sessionId); 46 updateObserverBox(session); 47 } 48 49 void tpToPlayer(Session* session, Session* destination) 50 { 51 cm.connection.sendTo(session.sessionId, MessagePacket( 52 format("Teleporting to %s", destination.name))); 53 auto position = cm.db.get!ClientPosition(session.dbKey); 54 auto destposition = cm.db.get!ClientPosition(destination.dbKey); 55 56 position.dimPos = destposition.dimPos; 57 position.dimension = destposition.dimension; 58 ++position.positionKey; 59 60 sendPositionToClient(*position, session.sessionId); 61 updateObserverBox(session); 62 } 63 64 void updateClientViewRadius( 65 Session* session, 66 int viewRadius) 67 { 68 auto settings = cm.db.get!ClientSettings(session.dbKey); 69 settings.viewRadius = clamp(viewRadius, 70 MIN_VIEW_RADIUS, MAX_VIEW_RADIUS); 71 updateObserverBox(session); 72 } 73 74 // Set client postion on server side, without sending position update to client 75 void updateClientPosition( 76 Session* session, 77 ClientDimPos dimPos, 78 ushort dimension, 79 ubyte positionKey, 80 bool updatePositionKey, 81 bool sendPosUpdate = false) 82 { 83 auto position = cm.db.get!ClientPosition(session.dbKey); 84 85 // reject stale position. Dimension already has changed. 86 if (position.positionKey != positionKey) 87 return; 88 89 position.dimPos = dimPos; 90 position.dimension = dimension; 91 position.positionKey += cast(ubyte)updatePositionKey; 92 93 if(sendPosUpdate) 94 sendPositionToClient(*position, session.sessionId); 95 96 updateObserverBox(session); 97 } 98 99 void tpToDimension(Session* session, DimensionId dimension) 100 { 101 auto dimInfo = cm.serverWorld.dimMan.getOrCreate(dimension); 102 auto position = cm.db.get!ClientPosition(session.dbKey); 103 position.dimPos = dimInfo.spawnPos; 104 position.dimension = dimension; 105 ++position.positionKey; 106 107 sendPositionToClient(*position, session.sessionId); 108 updateObserverBox(session); 109 } 110 111 private void sendPositionToClient(ClientPosition position, SessionId sessionId) 112 { 113 cm.serverWorld.dimObserverMan.updateObserver(sessionId, position.dimension); 114 cm.connection.sendTo(sessionId, 115 ClientPositionPacket( 116 position.dimPos, 117 position.dimension, 118 position.positionKey)); 119 } 120 121 // updates WorldBox of observer in ChunkObserverManager 122 // must be called after new position was sent to client. 123 // ChunkObserverManager can initiate chunk sending when observed 124 // box changes. 125 private void updateObserverBox(Session* session) 126 { 127 if (cm.isSpawned(session)) { 128 auto position = cm.db.get!ClientPosition(session.dbKey); 129 auto settings = cm.db.get!ClientSettings(session.dbKey); 130 auto borders = cm.serverWorld.dimMan.dimensionBorders(position.dimension); 131 cm.serverWorld.chunkObserverManager.changeObserverBox( 132 session.sessionId, position.chunk, settings.viewRadius, borders); 133 } 134 } 135 } 136 137 immutable vec3[string] dirToVec; 138 static this() 139 { 140 dirToVec = [ 141 "u" : vec3( 0, 1, 0), 142 "d" : vec3( 0,-1, 0), 143 "l" : vec3(-1, 0, 0), 144 "r" : vec3( 1, 0, 0), 145 "f" : vec3( 0, 0,-1), 146 "b" : vec3( 0, 0, 1), 147 ]; 148 } 149 150 final class ClientManager : IPlugin 151 { 152 private: 153 EventDispatcherPlugin evDispatcher; 154 NetServerPlugin connection; 155 ServerWorld serverWorld; 156 157 public: 158 ClientDb db; 159 SessionManager sessions; 160 ClientPositionManager clientPosMan; 161 162 // IPlugin stuff 163 mixin IdAndSemverFrom!"voxelman.session.plugininfo"; 164 165 this() { 166 clientPosMan.cm = this; 167 } 168 169 override void registerResources(IResourceManagerRegistry resmanRegistry) 170 { 171 auto ioman = resmanRegistry.getResourceManager!IoManager; 172 ioman.registerWorldLoadSaveHandlers(&db.load, &db.save); 173 auto components = resmanRegistry.getResourceManager!EntityComponentRegistry; 174 db.eman = components.eman; 175 registerSessionComponents(db.eman); 176 } 177 178 override void init(IPluginManager pluginman) 179 { 180 evDispatcher = pluginman.getPlugin!EventDispatcherPlugin; 181 connection = pluginman.getPlugin!NetServerPlugin; 182 serverWorld = pluginman.getPlugin!ServerWorld; 183 184 evDispatcher.subscribeToEvent(&handleClientConnected); 185 evDispatcher.subscribeToEvent(&handleClientDisconnected); 186 187 connection.registerPacketHandler!LoginPacket(&handleLoginPacket); 188 connection.registerPacketHandler!ViewRadiusPacket(&handleViewRadius); 189 connection.registerPacketHandler!ClientPositionPacket(&handleClientPosition); 190 connection.registerPacketHandler!GameStartPacket(&handleGameStartPacket); 191 192 auto commandPlugin = pluginman.getPlugin!CommandPluginServer; 193 commandPlugin.registerCommand("spawn", &onSpawnCommand); 194 commandPlugin.registerCommand("dim_spawn", &onDimensionSpawnCommand); 195 commandPlugin.registerCommand("tp", &onTeleport); 196 commandPlugin.registerCommand("dim", &changeDimensionCommand); 197 commandPlugin.registerCommand("add_active", &onAddActive); 198 commandPlugin.registerCommand("remove_active", &onRemoveActive); 199 commandPlugin.registerCommand("preload_box", &onPreloadBox); 200 } 201 202 void onAddActive(CommandParams params) { 203 Session* session = sessions[params.source]; 204 auto position = db.get!ClientPosition(session.dbKey); 205 auto cwp = position.chunk; 206 serverWorld.activeChunks.add(cwp); 207 infof("add active %s", cwp); 208 } 209 210 void onRemoveActive(CommandParams params) { 211 Session* session = sessions[params.source]; 212 auto position = db.get!ClientPosition(session.dbKey); 213 auto cwp = position.chunk; 214 serverWorld.activeChunks.remove(cwp); 215 infof("remove active %s", cwp); 216 } 217 218 void onPreloadBox(CommandParams params) { 219 import std.regex : matchFirst, regex; 220 import std.conv : to; 221 Session* session = sessions[params.source]; 222 auto position = db.get!ClientPosition(session.dbKey); 223 auto cwp = position.chunk; 224 225 // preload <radius> // preload box at 226 auto regexRadius = regex(`(\d+)`, "m"); 227 auto captures = matchFirst(params.rawArgs, regexRadius); 228 229 if (!captures.empty) 230 { 231 int radius = to!int(captures[1]); 232 WorldBox box = calcBox(cwp, radius); 233 auto borders = serverWorld.dimMan.dimensionBorders(cwp.dimension); 234 serverWorld.chunkObserverManager.addServerObserverBox(box, borders); 235 infof("Preloading %s", box); 236 } 237 } 238 239 void onSpawnCommand(CommandParams params) 240 { 241 Session* session = sessions[params.source]; 242 if(session is null) return; 243 244 auto position = db.get!ClientPosition(session.dbKey); 245 if (params.args.length > 1 && params.args[1] == "set") 246 { 247 setWorldSpawn(position.dimension, session); 248 return; 249 } 250 251 clientPosMan.tpToPos( 252 session, serverWorld.worldInfo.spawnPos, 253 serverWorld.worldInfo.spawnDimension); 254 } 255 256 void setWorldSpawn(DimensionId dimension, Session* session) 257 { 258 auto position = db.get!ClientPosition(session.dbKey); 259 serverWorld.worldInfo.spawnPos = position.dimPos; 260 serverWorld.worldInfo.spawnDimension = dimension; 261 connection.sendTo(session.sessionId, MessagePacket( 262 format(`world spawn is now dim %s pos %s`, dimension, position.dimPos.pos))); 263 } 264 265 void onDimensionSpawnCommand(CommandParams params) 266 { 267 Session* session = sessions[params.source]; 268 if(session is null) return; 269 270 auto position = db.get!ClientPosition(session.dbKey); 271 if (params.args.length > 1 && params.args[1] == "set") 272 { 273 setDimensionSpawn(position.dimension, session); 274 return; 275 } 276 277 auto dimInfo = serverWorld.dimMan.getOrCreate(position.dimension); 278 clientPosMan.tpToPos(session, dimInfo.spawnPos, position.dimension); 279 } 280 281 void setDimensionSpawn(DimensionId dimension, Session* session) 282 { 283 auto dimInfo = serverWorld.dimMan.getOrCreate(dimension); 284 auto position = db.get!ClientPosition(session.dbKey); 285 dimInfo.spawnPos = position.dimPos; 286 connection.sendTo(session.sessionId, MessagePacket( 287 format(`spawn of dimension %s is now %s`, dimension, position.dimPos.pos))); 288 } 289 290 void onTeleport(CommandParams params) 291 { 292 import std.regex : matchFirst, regex; 293 import std.conv : to; 294 Session* session = sessions[params.source]; 295 if(session is null) return; 296 auto position = db.get!ClientPosition(session.dbKey); 297 298 vec3 pos; 299 300 // tp <x> <y> <z> 301 auto regex3 = regex(`(-?\d+)\W+(-?\d+)\W+(-?\d+)`, "m"); 302 auto captures3 = matchFirst(params.rawArgs, regex3); 303 304 if (!captures3.empty) 305 { 306 pos.x = to!int(captures3[1]); 307 pos.y = to!int(captures3[2]); 308 pos.z = to!int(captures3[3]); 309 connection.sendTo(session.sessionId, MessagePacket( 310 format("Teleporting to %s %s %s", pos.x, pos.y, pos.z))); 311 clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension); 312 return; 313 } 314 315 // tp u|d|l|r|f|b \d+ 316 auto regexDir = regex(`([udlrfb])\W+(-?\d+)`, "m"); 317 auto capturesDir = matchFirst(params.rawArgs, regexDir); 318 319 if (!capturesDir.empty) 320 { 321 string dir = capturesDir[1]; 322 if (auto dirVector = dir in dirToVec) 323 { 324 int delta = to!int(capturesDir[2]); 325 pos = position.dimPos.pos + *dirVector * delta; 326 connection.sendTo(session.sessionId, MessagePacket( 327 format("Teleporting to %s %s %s", pos.x, pos.y, pos.z))); 328 clientPosMan.tpToPos(session, ClientDimPos(pos, position.dimPos.heading), position.dimension); 329 return; 330 } 331 } 332 333 // tp <x> <z> 334 auto regex2 = regex(`(-?\d+)\W+(-?\d+)`, "m"); 335 auto captures2 = matchFirst(params.rawArgs, regex2); 336 if (!captures2.empty) 337 { 338 pos.x = to!int(captures2[1]); 339 pos.y = position.dimPos.pos.y; 340 pos.z = to!int(captures2[2]); 341 clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension); 342 return; 343 } 344 345 // tp <player> 346 auto regex1 = regex(`[a-Z]+[_a-Z0-9]+`); 347 auto captures1 = matchFirst(params.rawArgs, regex1); 348 if (!captures1.empty) 349 { 350 string destName = to!string(captures1[1]); 351 Session* destination = sessions[destName]; 352 if(destination is null) 353 { 354 connection.sendTo(params.source, MessagePacket( 355 format(`Player "%s" is not online`, destName))); 356 return; 357 } 358 clientPosMan.tpToPlayer(session, destination); 359 return; 360 } 361 362 connection.sendTo(params.source, MessagePacket( 363 `Wrong syntax: "tp <x> [<y>] <z>" | "tp <player>" | "tp u|d|l|r|f|b <num_blocks>"`)); 364 } 365 366 bool isLoggedIn(SessionId sessionId) 367 { 368 Session* session = sessions[sessionId]; 369 return session.isLoggedIn; 370 } 371 372 bool isSpawned(SessionId sessionId) 373 { 374 Session* session = sessions[sessionId]; 375 return isSpawned(session); 376 } 377 378 bool isSpawned(Session* session) 379 { 380 return session.isLoggedIn && db.has!SpawnedFlag(session.dbKey); 381 } 382 383 string[EntityId] clientNames() 384 { 385 string[EntityId] names; 386 foreach(session; sessions.byValue) { 387 if (session.isLoggedIn) 388 names[session.dbKey] = session.name; 389 } 390 391 return names; 392 } 393 394 string clientName(SessionId sessionId) 395 { 396 auto cl = sessions[sessionId]; 397 return cl ? cl.name : format("%s", sessionId); 398 } 399 400 auto loggedInClients() 401 { 402 import std.algorithm : filter, map; 403 return sessions.byValue.filter!(a=>a.isLoggedIn).map!(a=>a.sessionId); 404 } 405 406 private void handleClientConnected(ref ClientConnectedEvent event) 407 { 408 sessions.put(event.sessionId, SessionType.unknownClient); 409 } 410 411 private void handleClientDisconnected(ref ClientDisconnectedEvent event) 412 { 413 Session* session = sessions[event.sessionId]; 414 infof("%s %s disconnected", event.sessionId, session.name); 415 416 db.remove!LoggedInFlag(session.dbKey); 417 db.remove!ClientSessionInfo(session.dbKey); 418 db.remove!SpawnedFlag(session.dbKey); 419 420 connection.sendToAll(ClientLoggedOutPacket(session.dbKey)); 421 sessions.remove(event.sessionId); 422 } 423 424 private void changeDimensionCommand(CommandParams params) 425 { 426 import std.conv : to, ConvException; 427 428 Session* session = sessions[params.source]; 429 if (isSpawned(session)) 430 { 431 if (params.args.length > 1) 432 { 433 auto position = db.get!ClientPosition(session.dbKey); 434 auto dim = to!DimensionId(params.args[1]); 435 if (dim == position.dimension) 436 return; 437 438 clientPosMan.tpToDimension(session, dim); 439 } 440 } 441 } 442 443 private void handleLoginPacket(ubyte[] packetData, SessionId sessionId) 444 { 445 auto packet = unpackPacket!LoginPacket(packetData); 446 string name = packet.clientName; 447 Session* session = sessions[sessionId]; 448 449 bool createdNew; 450 EntityId clientId = db.getOrCreate(name, createdNew); 451 452 if (createdNew) 453 { 454 infof("new client registered %s %s", clientId, name); 455 db.set(clientId, ClientPosition(), ClientSettings()); 456 } 457 else 458 { 459 if (db.has!ClientSessionInfo(clientId)) 460 { 461 bool hasConflict(string name) { 462 EntityId clientId = db.getIdForName(name); 463 if (clientId == 0) return false; 464 return db.has!ClientSessionInfo(clientId); 465 } 466 467 // already logged in 468 infof("client with name %s is already logged in %s", name, clientId); 469 string requestedName = name; 470 name = db.resolveNameConflict(requestedName, &hasConflict); 471 infof("Using '%s' instead of '%s'", name, requestedName); 472 clientId = db.getOrCreate(name, createdNew); 473 474 if (createdNew) 475 { 476 infof("new client registered %s %s", clientId, name); 477 db.set(clientId, ClientPosition(), ClientSettings()); 478 } 479 } 480 infof("client logged in %s %s", clientId, name); 481 } 482 483 db.set(clientId, ClientSessionInfo(name, session.sessionId)); 484 db.set(clientId, LoggedInFlag()); 485 486 sessions.identifySession(session.sessionId, name, clientId); 487 488 connection.sendTo(sessionId, SessionInfoPacket(session.dbKey, clientNames)); 489 connection.sendToAllExcept(sessionId, ClientLoggedInPacket(session.dbKey, name)); 490 491 evDispatcher.postEvent(ClientLoggedInEvent(clientId, createdNew)); 492 493 infof("%s %s logged in", sessionId, sessions[sessionId].name); 494 } 495 496 private void handleGameStartPacket(ubyte[] packetData, SessionId sessionId) 497 { 498 Session* session = sessions[sessionId]; 499 if (session.isLoggedIn) 500 { 501 auto position = db.get!ClientPosition(session.dbKey); 502 clientPosMan.sendPositionToClient(*position, session.sessionId); 503 db.set(session.dbKey, SpawnedFlag()); 504 connection.sendTo(sessionId, SpawnPacket()); 505 } 506 } 507 508 private void handleViewRadius(ubyte[] packetData, SessionId sessionId) 509 { 510 import std.algorithm : clamp; 511 auto packet = unpackPacket!ViewRadiusPacket(packetData); 512 Session* session = sessions[sessionId]; 513 if (session.isLoggedIn) 514 { 515 clientPosMan.updateClientViewRadius(session, packet.viewRadius); 516 } 517 } 518 519 private void handleClientPosition(ubyte[] packetData, SessionId sessionId) 520 { 521 Session* session = sessions[sessionId]; 522 if (isSpawned(session)) 523 { 524 auto packet = unpackPacket!ClientPositionPacket(packetData); 525 526 clientPosMan.updateClientPosition( 527 session, packet.dimPos, packet.dimension, 528 packet.positionKey, false); 529 } 530 } 531 }