1 /* 2 * Copyright (c) 2017-2019 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: Copyright (c) 2017-2019 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/selery/source/selery/world/group.d, selery/world/group.d) 28 */ 29 module selery.world.group; 30 31 import core.atomic : atomicOp; 32 import core.thread : Thread; 33 34 import std.concurrency : Tid, send, receiveTimeout; 35 import std.conv : to; 36 import std.datetime : dur; 37 import std.datetime.stopwatch : StopWatch; 38 import std.random : Random, unpredictableSeed, uniform, dice; 39 import std..string : toUpper; 40 import std.traits : isAbstractClass, Parameters; 41 import std.typetuple : TypeTuple; 42 43 import selery.about : SupportedBedrockProtocols, SupportedJavaProtocols; 44 import selery.config : Difficulty, Gamemode, Config; 45 import selery.hncom.about : __BEDROCK__, __JAVA__; 46 import selery.hncom.status : HncomAddWorld = AddWorld, HncomRemoveWorld = RemoveWorld; 47 import selery.log : Message; 48 import selery.math.vector : EntityPosition; 49 import selery.node.handler : Handler; 50 import selery.node.server : NodeServer; 51 import selery.player.bedrock : BedrockPlayerImpl; 52 import selery.player.java : JavaPlayerImpl; 53 import selery.player.player : PlayerInfo, Player, PermissionLevel; 54 import selery.util.util : call; 55 import selery.world.plugin : loadWorld; 56 import selery.world.world : WorldInfo, World; 57 58 private shared uint _id; 59 60 /** 61 * Generic informations about a group of worlds. 62 */ 63 final class GroupInfo { 64 65 /** 66 * Group's id. It is unique on the node. 67 */ 68 public immutable uint id; 69 70 /** 71 * World's name, which is given by the user who creates the world. 72 * It is unique in the node and every world in the same group has 73 * the same name. 74 */ 75 public immutable string name; 76 77 /** 78 * Thread where the group exists. 79 */ 80 public Tid tid; 81 82 /** 83 * Worlds in the group. 84 */ 85 shared(WorldInfo)[uint] worlds; 86 87 /** 88 * Group's default world. 89 */ 90 shared WorldInfo defaultWorld; 91 92 public shared this(string name) { 93 this.id = atomicOp!"+="(_id, 1); 94 this.name = name; 95 } 96 97 } 98 99 void spawnWorldGroup(shared NodeServer server, shared GroupInfo info, bool main) { 100 101 debug Thread.getThis().name = "world_group#" ~ to!string(info.id); 102 103 try { 104 105 WorldGroup group = new WorldGroup(server, info, main); 106 group.start(); 107 108 } catch(Throwable t) { 109 110 import selery.crash; 111 logCrash("world", server.lang, t); 112 113 } 114 115 //TODO catch exceptions per-world 116 117 } 118 119 final class WorldGroup { 120 121 private shared NodeServer _server; 122 private shared GroupInfo info; 123 private bool main; 124 private Random random; 125 126 private bool _stopped = false; 127 128 private World[uint] _worlds; 129 private Player[uint] _players; 130 131 Gamemode gamemode; 132 Difficulty difficulty; 133 134 // rules that the client does not need to know about 135 bool pvp; 136 bool naturalRegeneration; 137 bool depleteHunger; 138 uint randomTickSpeed; 139 uint viewDistance; 140 141 Time time; 142 Weather weather; 143 144 private this(shared NodeServer server, shared GroupInfo info, bool main) { 145 this._server = server; 146 this.info = info; 147 this.main = main; 148 this.random = Random(unpredictableSeed); 149 with(server.config.node) { 150 this.gamemode = gamemode; 151 this.difficulty = difficulty; 152 this.depleteHunger = depleteHunger; 153 this.naturalRegeneration = naturalRegeneration; 154 this.pvp = pvp; 155 this.randomTickSpeed = randomTickSpeed; 156 this.viewDistance = viewDistance; 157 //TODO more rules 158 this.time = new Time(doDaylightCycle); 159 this.weather = new Weather(doWeatherCycle); 160 this.weather.clear(); 161 } 162 } 163 164 public pure nothrow @property @safe @nogc shared(NodeServer) server() { 165 return this._server; 166 } 167 168 public pure nothrow @property World[] worlds() { 169 return this._worlds.values; 170 } 171 172 public pure nothrow @property Player[] players() { 173 return this._players.values; 174 } 175 176 //TODO constructor from world's save file 177 178 // updates 179 180 void addPlayer(Player player) { 181 this._players.call!"sendAddList"([player]); 182 this._players[player.hubId] = player; 183 player.sendAddList(this.players); 184 } 185 186 void removePlayer(Player player, bool closed) { 187 if(this._players.remove(player.hubId)) { 188 this._players.call!"sendRemoveList"([player]); 189 if(!closed) player.sendRemoveList(this.players); 190 } 191 } 192 193 void updateDifficulty(Difficulty difficulty) { 194 this.difficulty = difficulty; 195 this._players.call!"sendDifficulty"(difficulty); 196 } 197 198 void updateGamemode(Gamemode gamemode) { 199 this.gamemode = gamemode; 200 this.players.call!"sendWorldGamemode"(gamemode); 201 } 202 203 private class Time { 204 205 bool _cycle; 206 uint day; 207 uint _time; 208 209 this(bool cycle) { 210 this._cycle = cycle; 211 } 212 213 @property bool cycle() { 214 return _cycle; 215 } 216 217 @property bool cycle(bool cycle) { 218 players.call!"sendDoDaylightCycle"(cycle); 219 return _cycle = cycle; 220 } 221 222 @property uint time() { 223 return _time; 224 } 225 226 @property uint time(uint time) { 227 time %= 24000; 228 players.call!"sendTime"(time); 229 return _time = time; 230 } 231 232 alias time this; 233 234 } 235 236 private class Weather { 237 238 public bool cycle; 239 private bool _raining, _thunderous; 240 private uint _time; 241 private uint _intensity; 242 243 this(bool cycle) { 244 this.cycle = cycle; 245 } 246 247 @property bool raining() { 248 return _raining; 249 } 250 251 @property bool thunderous() { 252 return _thunderous; 253 } 254 255 @property uint intensity() { 256 return _intensity; 257 } 258 259 void clear(uint duration) { 260 _raining = _thunderous = false; 261 _time = duration; 262 _intensity = 0; 263 update(); 264 } 265 266 void clear() { 267 clear(uniform!"[]"(12000u, 180000u, random)); 268 } 269 270 void start(uint time, uint intensity, bool thunderous) { 271 assert(intensity > 0); 272 _raining = true; 273 _thunderous = thunderous; 274 _time = time; 275 _intensity = intensity; 276 update(); 277 } 278 279 void start(uint time, bool thunderous) { 280 start(time, uniform!"[]"(1, 4, random), thunderous); 281 } 282 283 void start() { 284 start(uniform!"[]"(12000u, 24000u, random), !dice(random, .5, .5)); 285 } 286 287 private void update() { 288 players.call!"sendWeather"(_raining, _thunderous, _time, _intensity); 289 } 290 291 } 292 293 /** 294 * Starts the group. 295 */ 296 public void start() { 297 298 StopWatch timer; 299 ulong duration; 300 301 while(!this._stopped) { 302 303 timer.start(); 304 305 // handle server's message (new worlds, new players, new packets, ...) 306 this.handleServerPackets(); 307 308 // tick the group 309 this.tick(); 310 311 // tick the worlds 312 foreach(world ; this._worlds) world.tick(); 313 314 // flush player's packets 315 foreach(player ; this._players) player.flush(); 316 317 // sleep until next tick 318 timer.stop(); 319 timer.peek.split!"usecs"(duration); 320 if(duration < 50_000) { 321 Thread.sleep(dur!"usecs"(50_000 - duration)); 322 } else { 323 //TODO server is less than 20 tps! 324 } 325 timer.reset(); 326 327 } 328 329 //TODO make sure no players are online 330 331 foreach(world ; this._worlds) { 332 world.stop(); 333 } 334 335 //TODO send RemoveWorld to the hub (children will be removed automatically) 336 337 send(cast()this.server.tid, CloseResult(this.info.id, CloseResult.REMOVED)); 338 339 } 340 341 private void handleServerPackets() { 342 while(receiveTimeout(dur!"msecs"(0), 343 &this.handleAddWorld, 344 &this.handleRemoveWorld, 345 &this.handleAddPlayer, 346 &this.handleRemovePlayer, 347 &this.handleGamePacket, 348 &this.handleBroadcast, 349 &this.handleUpdateDifficulty, 350 &this.handleUpdatePlayerGamemode, 351 &this.handleUpdatePlayerPermissionLevel, 352 &this.handleClose, 353 )) {} 354 } 355 356 private void handleAddWorld(AddWorld packet) { 357 (cast()packet.create).create(this); 358 } 359 360 private void handleRemoveWorld(RemoveWorld packet) { 361 this.removeWorld(packet.worldId); 362 } 363 364 private void handleAddPlayer(AddPlayer packet) { 365 //TODO allow spawning in a child 366 this.spawnPlayer(packet.player, packet.transferred); 367 } 368 369 private void handleRemovePlayer(RemovePlayer packet) { 370 auto player = packet.playerId in this._players; 371 if(player) { 372 this.removePlayer(*player, false); //TODO whether the player has left the server 373 (*player).world.despawnPlayer(*player); 374 (*player).close(); 375 } 376 } 377 378 private void handleGamePacket(GamePacket packet) { 379 auto player = packet.playerId in this._players; 380 if(player) { 381 (*player).handle(packet.payload[0], packet.payload[1..$].dup); 382 } 383 } 384 385 private void handleBroadcast(Broadcast packet) { 386 this.broadcast(packet.message); 387 } 388 389 private void handleUpdateDifficulty(UpdateDifficulty packet) { 390 this.difficulty = packet.difficulty; 391 } 392 393 private void handleUpdatePlayerGamemode(UpdatePlayerGamemode packet) { 394 auto player = packet.playerId in this._players; 395 if(player) { 396 (*player).gamemode = packet.gamemode; 397 } 398 } 399 400 private void handleUpdatePlayerPermissionLevel(UpdatePlayerPermissionLevel packet) { 401 auto player = packet.playerId in this._players; 402 if(player) { 403 (*player).permissionLevel = packet.permissionLevel; 404 } 405 } 406 407 private void handleClose(Close packet) { 408 if(this.players.length) { 409 // cannot close if there are players online in the world or in the children 410 // the world is not stopped 411 send(cast()this.server.tid, CloseResult(this.info.id, CloseResult.PLAYERS_ONLINE)); 412 } else { 413 // the world will be stopped at the end of the next tick 414 this._stopped = true; 415 } 416 } 417 418 private void tick() { 419 420 if(this.time.cycle) { 421 if(++this.time._time == 24000) { 422 this.time._time = 0; 423 this.time.day++; 424 } 425 } 426 427 if(this.weather.cycle) { 428 if(--this.weather._time == 0) { 429 if(this.weather._raining) this.weather.clear(); 430 else this.weather.start(); 431 } 432 } 433 434 } 435 436 /** 437 * Broadcasts a message to every world in the group. 438 */ 439 public final void broadcast(E...)(E args) { 440 static if(E.length == 1 && is(E[0] == Message[])) this.broadcastImpl(args[0]); 441 else this.broadcastImpl(Message.convert(args)); 442 } 443 444 protected void broadcastImpl(Message[] message) { 445 foreach(world ; this._worlds) world.broadcast(message); 446 } 447 448 /** 449 * Adds a world. 450 */ 451 public void addWorld(T:World=World, E...)(shared WorldInfo info, E args) { 452 T world = new T(args); 453 this.addWorldImpl(info, world); 454 // register events and similar stuff 455 world._update_state = delegate(int oldState, uint newState){ loadWorld(world, oldState, newState); }; 456 world.updateState(0); 457 world.start(this); 458 } 459 460 private void addWorldImpl(shared WorldInfo info, World world) { 461 if(info is null) info = new shared WorldInfo(); 462 world.info = info; 463 world.info.group = this.info; 464 this.info.worlds[world.id] = world.info; 465 if(this.info.defaultWorld is null) this.info.defaultWorld = world.info; 466 this._worlds[world.id] = world; 467 // send packet to the hub 468 Handler.sharedInstance.send(new HncomAddWorld(info.id, this.info.id, this.info.name, world.dimension, false).autoEncode()); //TODO default? 469 // set events listener 470 world.setListener(cast()server.globalListener); 471 } 472 473 /** 474 * Removes a world. 475 */ 476 public void removeWorld(uint worldId) { 477 World* world = worldId in this._worlds; 478 if(world) { 479 //TODO check whether it can be stopped 480 // send packet to the hub 481 Handler.sharedInstance.send(new HncomRemoveWorld(worldId).autoEncode()); 482 //TODO save and delete 483 world.stop(); 484 } 485 } 486 487 /* 488 * Creates and spawn a player when it comes from another world group, 489 * node or from a new connection. 490 */ 491 private Player spawnPlayer(shared PlayerInfo info, bool transferred) { 492 493 World world = this._worlds[this.info.defaultWorld.id]; //TODO allow spawning in custom world 494 495 //TODO load saved info from file 496 497 Player player = (){ 498 final switch(info.type) { 499 foreach(type ; TypeTuple!("Bedrock", "Java")) { 500 case mixin("__" ~ toUpper(type) ~ "__"): { 501 final switch(info.protocol) { 502 foreach(protocol ; mixin("Supported" ~ type ~ "Protocols")) { 503 case protocol: { 504 mixin("alias ReturnPlayer = " ~ type ~ "PlayerImpl;"); 505 return cast(Player)new ReturnPlayer!protocol(info, world, cast(EntityPosition)world.spawnPoint); 506 } 507 } 508 } 509 } 510 } 511 } 512 }(); 513 514 //TODO if the player is transferred from another world or from the hub, send the change dimension packet or unload every chunk 515 516 // send generic informations that will not change when changing dimension 517 player.sendJoinPacket(); 518 519 // add and send to the list 520 this.addPlayer(player); 521 522 // register server's commands 523 foreach(name, command; this.server.commands) { 524 player.registerCommand(cast()command); 525 } 526 527 // prepare for spawning (send chunks and rules) 528 world.preSpawnPlayer(player); 529 530 // announce and spawn to entities 531 world.afterSpawnPlayer(player, true); 532 533 return player; 534 535 } 536 537 } 538 539 struct AddWorld { 540 541 shared CreateImpl create; 542 543 static class CreateImpl { 544 545 abstract void create(WorldGroup group); 546 547 } 548 549 static class Create(T:World, E...) : CreateImpl { 550 551 shared WorldInfo info; 552 E args; 553 554 this(shared WorldInfo info, E args) { 555 this.info = info; 556 this.args = args; 557 } 558 559 override void create(WorldGroup group) { 560 group.addWorld!T(info, args); 561 } 562 563 } 564 565 } 566 567 struct RemoveWorld { 568 569 uint worldId; 570 571 } 572 573 // server to world 574 struct AddPlayer { 575 576 shared PlayerInfo player; 577 uint worldId; 578 bool transferred; 579 580 } 581 582 // server to world 583 struct RemovePlayer { 584 585 uint playerId; 586 587 } 588 589 // server to world 590 struct GamePacket { 591 592 uint playerId; 593 immutable(ubyte)[] payload; 594 595 } 596 597 // server to world 598 struct Broadcast { 599 600 string message; 601 602 } 603 604 // server to world 605 struct UpdateDifficulty { 606 607 Difficulty difficulty; 608 609 } 610 611 // server to world 612 struct UpdatePlayerGamemode { 613 614 uint playerId; 615 Gamemode gamemode; 616 617 } 618 619 // server to world 620 struct UpdatePlayerPermissionLevel { 621 622 uint playerId; 623 PermissionLevel permissionLevel; 624 625 } 626 627 // server to world 628 struct Close {} 629 630 // world to server 631 struct CloseResult { 632 633 enum ubyte REMOVED = 0; 634 enum ubyte PLAYERS_ONLINE = 1; 635 636 uint groupId; 637 ubyte status; 638 639 }