1 /** 2 Copyright: Copyright (c) 2016-2018 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 cm.evDispatcher.postEvent(ClientMovedEvent(session.dbKey, dimPos, dimension)); 99 } 100 101 void tpToDimension(Session* session, DimensionId dimension) 102 { 103 auto dimInfo = cm.serverWorld.dimMan.getOrCreate(dimension); 104 auto position = cm.db.get!ClientPosition(session.dbKey); 105 position.dimPos = dimInfo.spawnPos; 106 position.dimension = dimension; 107 ++position.positionKey; 108 109 sendPositionToClient(*position, session.sessionId); 110 updateObserverBox(session); 111 } 112 113 private void sendPositionToClient(ClientPosition position, SessionId sessionId) 114 { 115 cm.serverWorld.dimObserverMan.updateObserver(sessionId, position.dimension); 116 cm.connection.sendTo(sessionId, 117 ClientPositionPacket( 118 position.dimPos, 119 position.dimension, 120 position.positionKey)); 121 } 122 123 // updates WorldBox of observer in ChunkObserverManager 124 // must be called after new position was sent to client. 125 // ChunkObserverManager can initiate chunk sending when observed 126 // box changes. 127 private void updateObserverBox(Session* session) 128 { 129 if (cm.isSpawned(session)) { 130 auto position = cm.db.get!ClientPosition(session.dbKey); 131 auto settings = cm.db.get!ClientSettings(session.dbKey); 132 auto borders = cm.serverWorld.dimMan.dimensionBorders(position.dimension); 133 cm.serverWorld.chunkObserverManager.changeObserverBox( 134 session.sessionId, position.chunk, settings.viewRadius, borders); 135 } 136 } 137 } 138 139 immutable vec3[string] dirToVec; 140 static this() 141 { 142 dirToVec = [ 143 "u" : vec3( 0, 1, 0), 144 "d" : vec3( 0,-1, 0), 145 "l" : vec3(-1, 0, 0), 146 "r" : vec3( 1, 0, 0), 147 "f" : vec3( 0, 0,-1), 148 "b" : vec3( 0, 0, 1), 149 ]; 150 } 151 152 final class ClientManager : IPlugin 153 { 154 private: 155 EventDispatcherPlugin evDispatcher; 156 NetServerPlugin connection; 157 ServerWorld serverWorld; 158 159 public: 160 ClientDb db; 161 SessionManager sessions; 162 ClientPositionManager clientPosMan; 163 164 // IPlugin stuff 165 mixin IdAndSemverFrom!"voxelman.session.plugininfo"; 166 167 this() { 168 clientPosMan.cm = this; 169 } 170 171 override void registerResources(IResourceManagerRegistry resmanRegistry) 172 { 173 auto ioman = resmanRegistry.getResourceManager!IoManager; 174 ioman.registerWorldLoadSaveHandlers(&db.load, &db.save); 175 auto components = resmanRegistry.getResourceManager!EntityComponentRegistry; 176 db.eman = components.eman; 177 registerSessionComponents(db.eman); 178 } 179 180 override void init(IPluginManager pluginman) 181 { 182 evDispatcher = pluginman.getPlugin!EventDispatcherPlugin; 183 connection = pluginman.getPlugin!NetServerPlugin; 184 serverWorld = pluginman.getPlugin!ServerWorld; 185 186 evDispatcher.subscribeToEvent(&handleClientConnected); 187 evDispatcher.subscribeToEvent(&handleClientDisconnected); 188 189 connection.registerPacketHandler!LoginPacket(&handleLoginPacket); 190 connection.registerPacketHandler!ViewRadiusPacket(&handleViewRadius); 191 connection.registerPacketHandler!ClientPositionPacket(&handleClientPosition); 192 connection.registerPacketHandler!GameStartPacket(&handleGameStartPacket); 193 194 auto commandPlugin = pluginman.getPlugin!CommandPluginServer; 195 commandPlugin.registerCommand(CommandInfo("spawn", &onSpawnCommand, ["[set]"], "Teleports user to or set a world spawn point")); 196 commandPlugin.registerCommand(CommandInfo("dim_spawn", &onDimensionSpawnCommand, ["[set]"], "Teleports user to or set a dimension spawn point")); 197 commandPlugin.registerCommand(CommandInfo("tp", &onTeleport, ["<x> [<y>] <z>", "<player_name>", "u|d|l|r|f|b <num_blocks>"], "Teleports user to coordinates, to another player, or in choosen direction")); 198 commandPlugin.registerCommand(CommandInfo("dim", &changeDimensionCommand, ["<dimension_number>"], "Teleports user to the spawn point of specified dimension")); 199 commandPlugin.registerCommand(CommandInfo("add_active", &onAddActive, null, "Marks user's current chunk as active. (Chunk is always loaded)")); 200 commandPlugin.registerCommand(CommandInfo("remove_active", &onRemoveActive, null, "Unmarks user's current chunk as active. (Chunk is no longer always loaded)")); 201 commandPlugin.registerCommand(CommandInfo("preload_box", &onPreloadBox, ["<radius>"], "Adds server observer to all chunks in specified radius. Chunks will stay loaded, until server restarts")); 202 } 203 204 void onAddActive(CommandParams params) { 205 Session* session = sessions[params.sourceSession]; 206 auto position = db.get!ClientPosition(session.dbKey); 207 auto cwp = position.chunk; 208 serverWorld.activeChunks.add(cwp); 209 infof("add active %s", cwp); 210 } 211 212 void onRemoveActive(CommandParams params) { 213 Session* session = sessions[params.sourceSession]; 214 auto position = db.get!ClientPosition(session.dbKey); 215 auto cwp = position.chunk; 216 serverWorld.activeChunks.remove(cwp); 217 infof("remove active %s", cwp); 218 } 219 220 void onPreloadBox(CommandParams params) { 221 import std.regex : matchFirst, regex; 222 import std.conv : to; 223 Session* session = sessions[params.sourceSession]; 224 auto position = db.get!ClientPosition(session.dbKey); 225 auto cwp = position.chunk; 226 227 // preload <radius> // preload box at 228 auto regexRadius = regex(`(\d+)`, "m"); 229 auto captures = matchFirst(params.rawArgs, regexRadius); 230 231 if (!captures.empty) 232 { 233 int radius = to!int(captures[1]); 234 WorldBox box = calcBox(cwp, radius); 235 auto borders = serverWorld.dimMan.dimensionBorders(cwp.dimension); 236 serverWorld.chunkObserverManager.addServerObserverBox(box, borders); 237 infof("Preloading %s", box); 238 } 239 } 240 241 void onSpawnCommand(CommandParams params) 242 { 243 Session* session = sessions[params.sourceSession]; 244 if(session is null) return; 245 246 auto position = db.get!ClientPosition(session.dbKey); 247 if (params.args.length > 1 && params.args[1] == "set") 248 { 249 setWorldSpawn(position.dimension, session); 250 return; 251 } 252 253 clientPosMan.tpToPos( 254 session, serverWorld.worldInfo.spawnPos, 255 serverWorld.worldInfo.spawnDimension); 256 } 257 258 void setWorldSpawn(DimensionId dimension, Session* session) 259 { 260 auto position = db.get!ClientPosition(session.dbKey); 261 serverWorld.worldInfo.spawnPos = position.dimPos; 262 serverWorld.worldInfo.spawnDimension = dimension; 263 connection.sendTo(session.sessionId, MessagePacket( 264 format(`world spawn is now dim %s pos %s`, dimension, position.dimPos.pos))); 265 } 266 267 void onDimensionSpawnCommand(CommandParams params) 268 { 269 Session* session = sessions[params.sourceSession]; 270 if(session is null) return; 271 272 auto position = db.get!ClientPosition(session.dbKey); 273 if (params.args.length > 1 && params.args[1] == "set") 274 { 275 setDimensionSpawn(position.dimension, session); 276 return; 277 } 278 279 auto dimInfo = serverWorld.dimMan.getOrCreate(position.dimension); 280 clientPosMan.tpToPos(session, dimInfo.spawnPos, position.dimension); 281 } 282 283 void setDimensionSpawn(DimensionId dimension, Session* session) 284 { 285 auto dimInfo = serverWorld.dimMan.getOrCreate(dimension); 286 auto position = db.get!ClientPosition(session.dbKey); 287 dimInfo.spawnPos = position.dimPos; 288 connection.sendTo(session.sessionId, MessagePacket( 289 format(`spawn of dimension %s is now %s`, dimension, position.dimPos.pos))); 290 } 291 292 void onTeleport(CommandParams params) 293 { 294 import std.regex : matchFirst, regex; 295 import std.conv : to; 296 Session* session = sessions[params.sourceSession]; 297 if(session is null) return; 298 auto position = db.get!ClientPosition(session.dbKey); 299 300 vec3 pos; 301 302 // tp <x> <y> <z> 303 auto regex3 = regex(`(-?\d+)\W+(-?\d+)\W+(-?\d+)`, "m"); 304 auto captures3 = matchFirst(params.rawArgs, regex3); 305 306 if (!captures3.empty) 307 { 308 pos.x = to!int(captures3[1]); 309 pos.y = to!int(captures3[2]); 310 pos.z = to!int(captures3[3]); 311 connection.sendTo(session.sessionId, MessagePacket( 312 format("Teleporting to %s %s %s", pos.x, pos.y, pos.z))); 313 clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension); 314 return; 315 } 316 317 // tp u|d|l|r|f|b \d+ 318 auto regexDir = regex(`([udlrfb])\W+(-?\d+)`, "m"); 319 auto capturesDir = matchFirst(params.rawArgs, regexDir); 320 321 if (!capturesDir.empty) 322 { 323 string dir = capturesDir[1]; 324 if (auto dirVector = dir in dirToVec) 325 { 326 int delta = to!int(capturesDir[2]); 327 pos = position.dimPos.pos + *dirVector * delta; 328 connection.sendTo(session.sessionId, MessagePacket( 329 format("Teleporting to %s %s %s", pos.x, pos.y, pos.z))); 330 clientPosMan.tpToPos(session, ClientDimPos(pos, position.dimPos.heading), position.dimension); 331 return; 332 } 333 } 334 335 // tp <x> <z> 336 auto regex2 = regex(`(-?\d+)\W+(-?\d+)`, "m"); 337 auto captures2 = matchFirst(params.rawArgs, regex2); 338 if (!captures2.empty) 339 { 340 pos.x = to!int(captures2[1]); 341 pos.y = position.dimPos.pos.y; 342 pos.z = to!int(captures2[2]); 343 clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension); 344 return; 345 } 346 347 // tp <player> 348 auto regex1 = regex(`[a-Z]+[_a-Z0-9]+`); 349 auto captures1 = matchFirst(params.rawArgs, regex1); 350 if (!captures1.empty) 351 { 352 string destName = to!string(captures1[1]); 353 Session* destination = sessions[destName]; 354 if(destination is null) 355 { 356 connection.sendTo(params.sourceSession, MessagePacket( 357 format(`Player "%s" is not online`, destName))); 358 return; 359 } 360 clientPosMan.tpToPlayer(session, destination); 361 return; 362 } 363 364 connection.sendTo(params.sourceSession, MessagePacket( 365 `Wrong syntax: "tp <x> [<y>] <z>" | "tp <player>" | "tp u|d|l|r|f|b <num_blocks>"`)); 366 } 367 368 bool isLoggedIn(SessionId sessionId) 369 { 370 Session* session = sessions[sessionId]; 371 return session.isLoggedIn; 372 } 373 374 /// Returns ClientId for specified SessionId 375 EntityId sessionClientId(SessionId sessionId) 376 { 377 Session* session = sessions[sessionId]; 378 if (session) 379 { 380 return session.dbKey; 381 } 382 return EntityId(0); 383 } 384 385 bool isSpawned(SessionId sessionId) 386 { 387 Session* session = sessions[sessionId]; 388 return isSpawned(session); 389 } 390 391 bool isSpawned(Session* session) 392 { 393 return session.isLoggedIn && db.has!SpawnedFlag(session.dbKey); 394 } 395 396 string[EntityId] clientNames() 397 { 398 string[EntityId] names; 399 foreach(session; sessions.byValue) { 400 if (session.isLoggedIn) 401 names[session.dbKey] = session.name; 402 } 403 404 return names; 405 } 406 407 string clientName(SessionId sessionId) 408 { 409 auto cl = sessions[sessionId]; 410 return cl ? cl.name : format("%s", sessionId); 411 } 412 413 auto loggedInClients() 414 { 415 import std.algorithm : filter, map; 416 return sessions.byValue.filter!(a=>a.isLoggedIn).map!(a=>a.sessionId); 417 } 418 419 private void handleClientConnected(ref ClientConnectedEvent event) 420 { 421 sessions.put(event.sessionId, SessionType.unknownClient); 422 } 423 424 private void handleClientDisconnected(ref ClientDisconnectedEvent event) 425 { 426 Session* session = sessions[event.sessionId]; 427 infof("%s %s disconnected", event.sessionId, session.name); 428 429 db.remove!LoggedInFlag(session.dbKey); 430 db.remove!ClientSessionInfo(session.dbKey); 431 db.remove!SpawnedFlag(session.dbKey); 432 433 connection.sendToAll(ClientLoggedOutPacket(session.dbKey)); 434 sessions.remove(event.sessionId); 435 } 436 437 // dim <dimension_number> 438 private void changeDimensionCommand(CommandParams params) 439 { 440 import std.conv : to, ConvException; 441 442 Session* session = sessions[params.sourceSession]; 443 if (isSpawned(session)) 444 { 445 if (params.args.length > 1) 446 { 447 auto position = db.get!ClientPosition(session.dbKey); 448 auto dim = to!DimensionId(params.args[1]); 449 if (dim == position.dimension) 450 return; 451 452 clientPosMan.tpToDimension(session, dim); 453 } 454 } 455 } 456 457 private void handleLoginPacket(ubyte[] packetData, SessionId sessionId) 458 { 459 auto packet = unpackPacket!LoginPacket(packetData); 460 string name = packet.clientName; 461 Session* session = sessions[sessionId]; 462 463 bool createdNew; 464 EntityId clientId = db.getOrCreate(name, createdNew); 465 466 if (createdNew) 467 { 468 onCreatedNewClientRecord(clientId, name); 469 } 470 else 471 { 472 if (db.has!ClientSessionInfo(clientId)) 473 { 474 bool hasConflict(string name) { 475 EntityId clientId = db.getIdForName(name); 476 if (clientId == EntityId(0)) return false; 477 return db.has!ClientSessionInfo(clientId); 478 } 479 480 // already logged in 481 infof("client with name %s is already logged in %s", name, clientId); 482 string requestedName = name; 483 name = db.resolveNameConflict(requestedName, &hasConflict); 484 infof("Using '%s' instead of '%s'", name, requestedName); 485 486 clientId = db.getOrCreate(name, createdNew); 487 488 if (createdNew) onCreatedNewClientRecord(clientId, name); 489 } 490 infof("client logged in %s %s", clientId, name); 491 } 492 493 db.set(clientId, ClientSessionInfo(name, session.sessionId)); 494 db.set(clientId, LoggedInFlag()); 495 496 sessions.identifySession(session.sessionId, name, clientId); 497 498 connection.sendTo(sessionId, SessionInfoPacket(session.dbKey, clientNames)); 499 connection.sendToAllExcept(sessionId, ClientLoggedInPacket(session.dbKey, name)); 500 501 evDispatcher.postEvent(ClientLoggedInEvent(clientId, createdNew)); 502 503 infof("%s %s logged in", sessionId, sessions[sessionId].name); 504 } 505 506 private void onCreatedNewClientRecord(EntityId clientId, string name) { 507 infof("new client registered %s %s", clientId, name); 508 db.set(clientId, ClientPosition(), ClientSettings()); 509 } 510 511 private void handleGameStartPacket(ubyte[] packetData, SessionId sessionId) 512 { 513 Session* session = sessions[sessionId]; 514 if (session.isLoggedIn) 515 { 516 auto position = db.get!ClientPosition(session.dbKey); 517 clientPosMan.sendPositionToClient(*position, session.sessionId); 518 db.set(session.dbKey, SpawnedFlag()); 519 connection.sendTo(sessionId, SpawnPacket()); 520 } 521 } 522 523 private void handleViewRadius(ubyte[] packetData, SessionId sessionId) 524 { 525 import std.algorithm : clamp; 526 auto packet = unpackPacket!ViewRadiusPacket(packetData); 527 Session* session = sessions[sessionId]; 528 if (session.isLoggedIn) 529 { 530 clientPosMan.updateClientViewRadius(session, packet.viewRadius); 531 } 532 } 533 534 private void handleClientPosition(ubyte[] packetData, SessionId sessionId) 535 { 536 Session* session = sessions[sessionId]; 537 if (isSpawned(session)) 538 { 539 auto packet = unpackPacket!ClientPositionPacket(packetData); 540 541 clientPosMan.updateClientPosition( 542 session, packet.dimPos, packet.dimension, 543 packet.positionKey, false); 544 } 545 } 546 }