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/node/server.d, selery/node/server.d) 28 */ 29 module selery.node.server; 30 31 import core.atomic : atomicOp; 32 import core.thread : getpid, Thread; 33 34 import std.algorithm : canFind, min; 35 import std.bitmanip : nativeToBigEndian; 36 static import std.concurrency; 37 import std.conv : to; 38 import std.datetime : dur, Duration; 39 import std.datetime.stopwatch : StopWatch; 40 static import std.file; 41 import std.json : JSON_TYPE, JSONValue, parseJSON, JSONException; 42 import std.process : executeShell; 43 import std.socket : SocketException, Address; 44 import std..string; //TODO selective imports 45 import std.traits : Parameters; 46 import std.uuid : UUID; 47 48 import imageformats.png : read_png_from_mem; 49 50 import resusage.memory; 51 import resusage.cpu; 52 53 import sel.format : Format; 54 import sel.server.bedrock : bedrockSupportedProtocols; 55 56 import selery.world.world : World; // do not move this import down 57 58 import selery.about; 59 import selery.command.command : Command; 60 import selery.command.execute : executeCommand; 61 import selery.command.util : CommandSender; 62 import selery.config : Config, Difficulty, Gamemode; 63 import selery.entity.human : Skin; 64 import selery.event.event : Event, EventListener; 65 import selery.event.node; 66 import selery.event.world.world : WorldEvent; 67 import selery.hncom.about; 68 import selery.hncom.handler : HncomHandler; 69 import selery.lang : LanguageManager, Translation; 70 import selery.log : Message, Logger; 71 import selery.node.commands : Commands; 72 import selery.node.handler; //TODO selective imports 73 import selery.node.node : Node; 74 import selery.node.plugin.plugin : NodePluginInfo; 75 import selery.player.bedrock : BedrockPlayer, BedrockPlayerImpl; 76 import selery.player.java : JavaPlayer; 77 import selery.player.player : PlayerInfo, PermissionLevel; 78 import selery.plugin : Plugin, Description; 79 import selery.server : Server; 80 import selery.util.resourcepack : createResourcePacks, serveResourcePacks; 81 import selery.util.tuple : Tuple; 82 import selery.util.util : milliseconds, microseconds; 83 import selery.world.group; 84 import selery.world.world : WorldInfo; 85 86 import terminal : Terminal; 87 88 import HncomLogin = selery.hncom.login; 89 import HncomStatus = selery.hncom.status; 90 import HncomPlayer = selery.hncom.player; 91 92 // the signal could be handled on another thread! 93 private shared bool running = true; 94 private shared bool stoppedWithSignal = false; 95 96 private shared std.concurrency.Tid server_tid; 97 98 public nothrow @property @safe @nogc bool isServerRunning() { 99 return running; 100 } 101 102 private struct Stop {} 103 104 /** 105 * Singleton for the server instance. 106 */ 107 final class NodeServer : EventListener!NodeServerEvent, Server, HncomHandler!clientbound { 108 109 public immutable bool lite; 110 111 private shared ulong start_time; 112 113 private Handler handler; 114 private Address n_hub_address; 115 116 private const string[] n_args; 117 118 private shared ulong n_id; 119 private shared ulong uuid_count; 120 121 private shared uint n_hub_latency; 122 123 public shared std.concurrency.Tid tid; //TODO make private 124 125 private shared Config _config; 126 private shared Logger _logger; 127 private shared ServerLogger serverlogger; 128 129 private shared size_t n_online; 130 private shared size_t n_max; 131 132 private shared Node[uint] nodes_hubid; 133 private shared Node[string] nodes_names; 134 135 private shared Tuple!(string, "website", string, "facebook", string, "twitter", string, "youtube", string, "instagram", string, "googlePlus") n_social; 136 137 private shared(uint) _main_group_id = 0; // 0 = no default group 138 private shared(GroupInfo)[uint] _groups; 139 private shared(GroupInfo)[string] _groups_names; 140 141 private shared(PlayerInfo)[uint] _players; 142 143 private shared Plugin[] n_plugins; 144 145 public shared EventListener!WorldEvent globalListener; 146 147 private shared Command[string] _commands; 148 149 public shared this(Address hub, Config config, Plugin[] plugins=[], string[] args=[]) { 150 151 assert(config.node !is null); 152 153 debug Thread.getThis().name = "node"; 154 155 this.lite = cast(TidAddress)hub !is null; 156 157 this.n_plugins = cast(shared)plugins; 158 159 this.n_args = cast(shared)args; 160 161 this.tid = server_tid = cast(shared)std.concurrency.thisTid; 162 163 this.n_hub_address = cast(shared)hub; 164 165 if(config.hub is null) config.hub = config..new Config.Hub(); 166 167 this._config = cast(shared)config; 168 169 Terminal terminal = new Terminal(); 170 this._logger = cast(shared)new Logger(terminal, config.lang); // only writes in the console 171 172 if(lite) { 173 174 this.handler = new shared MessagePassingHandler(cast(shared TidAddress)hub); 175 this.handleInfoImpl(cast()std.concurrency.receiveOnly!(shared HncomLogin.HubInfo)()); 176 177 } else { 178 179 this.logger.log(Translation("startup.connecting", [to!string(hub), config.node.name])); 180 181 try { 182 this.handler = new shared SocketHandler(hub); 183 this.handler.send(new HncomLogin.ConnectionRequest(__PROTOCOL__, config.node.name, config.node.password, config.node.main).encode()); 184 } catch(SocketException e) { 185 this.logger.logError(Translation("warning.connectionError", [to!string(hub), e.msg])); 186 return; 187 } 188 189 // remove variables in config that plugins should not read 190 config.node.password = ""; 191 config.node.ip = ""; 192 config.node.port = ushort(0); 193 194 // wait for ConnectionResponse 195 ubyte[] buffer = this.handler.receive(); 196 if(buffer.length && buffer[0] == HncomLogin.ConnectionResponse.ID) { 197 auto response = HncomLogin.ConnectionResponse.fromBuffer(buffer); 198 if(response.status == HncomLogin.ConnectionResponse.OK) { 199 this.handleInfo(); 200 } else { 201 immutable reason = (){ 202 switch(response.status) with(HncomLogin.ConnectionResponse) { 203 case OUTDATED_HUB: return "outdatedHub"; 204 case OUTDATED_NODE: return "outdatedNode"; 205 case PASSWORD_REQUIRED: return "passwordRequired"; 206 case WRONG_PASSWORD: return "wrongPassword"; 207 case INVALID_NAME_LENGTH: return "invalidNameLength"; 208 case INVALID_NAME_CHARACTERS: return "invalidNameCharacters"; 209 case NAME_ALREADY_USED: return "nameAlreadyUsed"; 210 case NAME_RESERVED: return "nameReserved"; 211 default: return "unknown"; 212 } 213 }(); 214 this.logger.logError(Translation("status." ~ reason)); 215 if(response.status == HncomLogin.ConnectionResponse.OUTDATED_HUB || response.status == HncomLogin.ConnectionResponse.OUTDATED_NODE) { 216 this.logger.logError(Translation("warning.protocolRequired", [to!string(__PROTOCOL__), to!string(response.protocol)])); 217 } 218 } 219 } else { 220 this.logger.logError(Translation("warning.refused")); 221 } 222 223 this.handler.close(); 224 225 } 226 227 } 228 229 private shared void handleInfo() { 230 231 ubyte[] buffer = this.handler.receive(); 232 if(buffer.length && buffer[0] == HncomLogin.HubInfo.ID) { 233 this.handleInfoImpl(HncomLogin.HubInfo.fromBuffer(buffer)); 234 } else { 235 this.logger.logError(Translation("warning.closed")); 236 } 237 238 } 239 240 private shared void handleInfoImpl(HncomLogin.HubInfo info) { 241 242 Config config = cast()this._config; 243 244 this.n_id = info.serverId; 245 this.uuid_count = info.reservedUUIDs; 246 247 JSONValue additionalJSON; 248 try additionalJSON = parseJSON(info.additionalJSON); 249 catch(JSONException) {} 250 if(additionalJSON.type != JSON_TYPE.OBJECT) additionalJSON = parseJSON("{}"); 251 252 auto minecraft = "minecraft" in additionalJSON; 253 if(minecraft && minecraft.type == JSON_TYPE.OBJECT) { 254 auto edu = "edu" in *minecraft; 255 config.hub.edu = edu && edu.type == JSON_TYPE.TRUE; 256 } 257 258 config.hub.displayName = info.displayName; 259 260 this.n_online = info.online; 261 this.n_max = info.max; 262 263 auto social = "social" in additionalJSON; 264 if(social && social.type == JSON_TYPE.OBJECT) { 265 if("website" in *social) this.n_social.website = (*social)["website"].str; 266 if("facebook" in *social) this.n_social.facebook = (*social)["facebook"].str; 267 if("twitter" in *social) this.n_social.twitter = (*social)["twitter"].str; 268 if("youtube" in *social) this.n_social.youtube = (*social)["youtube"].str; 269 if("instagram" in *social) this.n_social.instagram = (*social)["instagram"].str; 270 if("google-plus" in *social) this.n_social.googlePlus = (*social)["google-plus"].str; 271 } 272 273 if(!this.lite) this.logger.terminal.title = info.displayName ~ " | node | " ~ Software.simpleDisplay; 274 275 void handleGameInfo(ubyte type, HncomLogin.HubInfo.GameInfo info) { 276 void set(ref Config.Hub.Game game) { 277 game.enabled = true; 278 game.protocols = info.protocols; 279 game.motd = info.motd; 280 game.onlineMode = info.onlineMode; 281 } 282 if(type == __JAVA__) { 283 set(config.hub.java); 284 } else if(type == __BEDROCK__) { 285 set(config.hub.bedrock); 286 } else { 287 this.logger.logError(Translation("warning.invalidGame", [to!string(type), Software.name])); 288 } 289 } 290 291 foreach(game, info ; info.gamesInfo) { 292 handleGameInfo(game, info); 293 } 294 295 // check protocols and print warnings if necessary 296 void check(string name, uint[] requested, uint[] supported) { 297 foreach(req ; requested) { 298 if(!supported.canFind(req)) { 299 this.logger.logWarning(Translation("warning.invalidProtocol", [to!string(req), name])); 300 } 301 } 302 } 303 304 check("Minecraft: Java Edition", config.hub.java.protocols, supportedJavaProtocols); 305 check("Minecraft (Bedrock Engine)", config.hub.bedrock.protocols, supportedBedrockProtocols); 306 307 this._config = cast(shared)config; 308 309 this.finishConstruction(); 310 311 } 312 313 private shared void finishConstruction() { 314 315 if(!this.lite) this.logger.log(Translation("startup.starting", [Format.green ~ Software.name ~ Format.white ~ " " ~ Software.fullVersion ~ Format.reset ~ " " ~ Software.fullCodename])); 316 317 static if(!__supported) { 318 this.logger.logWarning(Translation("startup.unsupported", [Software.name])); 319 } 320 321 this.globalListener = new EventListener!WorldEvent(); 322 323 // default skins for players that connect with invalid skins 324 Skin.STEVE = Skin("Standard_Steve", read_png_from_mem(cast(ubyte[])this.config.files.readAsset("skin/steve.png")).pixels); 325 Skin.ALEX = Skin("Standard_Alex", read_png_from_mem(cast(ubyte[])this.config.files.readAsset("skin/alex.png")).pixels); 326 327 // load creative inventories 328 foreach(protocol ; SupportedBedrockProtocols) { 329 string[] failed; 330 if(this.config.hub.bedrock.protocols.canFind(protocol)) { 331 if(!mixin("BedrockPlayerImpl!" ~ protocol.to!string).loadCreativeInventory(this.config.files)) { 332 failed ~= bedrockSupportedProtocols[protocol]; 333 } 334 } 335 if(failed.length) { 336 this.logger.logWarning(Translation("warning.creativeFailed", [failed.join(", ")])); 337 } 338 } 339 340 // create resource pack files 341 /+string[] textures = []; // ordered from least prioritised to most prioritised 342 if(textures.length) { 343 344 this.logger.log(Translation("startup.resourcePacks")); 345 346 auto rp_uuid = this.nextUUID; 347 auto rp = createResourcePacks(this, rp_uuid, textures); 348 std.concurrency.spawn(&serveResourcePacks, std.concurrency.thisTid, cast(string)rp.java2.idup, cast(string)rp.java3.idup); 349 ushort port = std.concurrency.receiveOnly!ushort(); 350 351 import myip : publicAddress4; 352 353 auto ip = publicAddress4; 354 //TODO also try to use private addresses before using 127.0.0.1 355 356 JavaPlayer.updateResourcePacks(rp.java2, rp.java3, ip.length ? ip : "127.0.0.1", port); 357 BedrockPlayer.updateResourcePacks(rp_uuid, rp.pocket1); 358 359 }+/ 360 361 foreach(_plugin ; this.n_plugins) { 362 auto plugin = cast(NodePluginInfo)_plugin; 363 plugin.load(this); 364 if(plugin.main) { 365 auto args = [ 366 Format.green ~ plugin.name ~ Format.reset, 367 Format.white ~ (plugin.authors.length ? plugin.authors.join(Format.reset ~ ", " ~ Format.white) : "?") ~ Format.reset, 368 Format.white ~ plugin.version_[1..$] 369 ]; 370 this.logger.log(Translation("startup.plugin.enabled" ~ (plugin.version_.startsWith("v") ? ".version" : (plugin.authors.length ? ".author" : "")), args)); 371 } 372 } 373 374 // register commands if enabled in the settings 375 Commands.register(this); 376 377 // send node's informations to the hub and switch to a non-blocking connection 378 HncomLogin.NodeInfo nodeInfo = new HncomLogin.NodeInfo(); 379 uint[][ubyte] games; 380 if(this.config.node.bedrock) nodeInfo.acceptedGames[__BEDROCK__] = cast(uint[])this.config.node.bedrock.protocols; 381 if(this.config.node.java) nodeInfo.acceptedGames[__JAVA__] = cast(uint[])this.config.node.java.protocols; 382 nodeInfo.max = this.config.node.maxPlayers; // 0 for unlimited, like in the config file 383 foreach(_plugin ; this.n_plugins) { 384 auto plugin = cast()_plugin; 385 nodeInfo.plugins ~= HncomLogin.NodeInfo.Plugin(plugin.id, plugin.name, plugin.version_); 386 } 387 if(this.lite) { 388 std.concurrency.send(cast()(cast(shared MessagePassingHandler)this.handler).hub, cast(shared)nodeInfo); 389 } else { 390 this.handler.send(nodeInfo.encode()); 391 } 392 393 // load plugin's language files 394 foreach(_plugin ; this.n_plugins) { 395 auto plugin = cast()_plugin; 396 foreach(language, messages; this.config.lang.loadPlugin(plugin)) { 397 this.updateLanguageFiles(language, messages); 398 } 399 } 400 401 if(!this.lite) std.concurrency.spawn(&this.handler.receiveLoop, cast()this.tid); 402 403 this.start_time = milliseconds; 404 405 // call @start functions 406 foreach(plugin ; this.n_plugins) { 407 foreach(del ; plugin.onstart) { 408 del(); 409 } 410 } 411 412 if(this._main_group_id == 0) { 413 //TODO load world in worlds/world 414 this.addWorld("world"); 415 } 416 417 //TODO wait unitl world's spawn area is ready 418 419 this.logger.log(Translation("startup.started")); 420 421 Terminal terminal = (cast()this._logger).terminal; 422 if(this.lite) { 423 this._logger = this.serverlogger = cast(shared)new LiteServerLogger(terminal, this.lang); 424 } else { 425 this._logger = this.serverlogger = cast(shared)new NodeServerLogger(terminal, this.lang); 426 } 427 428 // start calculation of used resources 429 std.concurrency.spawn(&startResourceUsageThread, getpid); 430 431 // start command reader 432 std.concurrency.spawn(&startCommandReaderThread, cast()this.tid); 433 434 this.start(); 435 436 } 437 438 private shared void start() { 439 440 //TODO request first latency calculation 441 442 while(running) { 443 444 // receive messages 445 std.concurrency.receive( 446 &handlePromptCommand, 447 &handleCloseResult, 448 (immutable(ubyte)[] payload){ 449 // from the hub 450 if(payload.length) { 451 (cast()this).handleHncom(payload.dup); 452 } else { 453 // close 454 running = false; 455 } 456 }, 457 (Stop stop){ 458 running = false; 459 }, 460 ); 461 462 } 463 464 this.handler.close(); 465 466 // call @stop plugins 467 foreach(plugin ; this.n_plugins) { 468 foreach(void delegate() del ; (cast()plugin).onstop) { 469 del(); 470 } 471 } 472 473 this.logger.log(Translation("startup.stopped")); 474 475 /*version(Windows) { 476 // perform suicide 477 executeShell("taskkill /PID " ~ to!string(getpid) ~ " /F"); 478 } else {*/ 479 import core.stdc.stdlib : exit; 480 exit(0); 481 //} 482 483 } 484 485 /** 486 * Stops the server setting the running variable to false and kicks every 487 * player from the server. 488 */ 489 public shared void shutdown() { 490 std.concurrency.send(cast()server_tid, Stop()); 491 } 492 493 /** 494 * Gets the server's id, which is equal in the hub and all 495 * connected nodes. 496 * It is generated by SEL's snooping system or randomly if 497 * the service cannot be reached. 498 */ 499 public shared pure nothrow @property @safe @nogc immutable(long) id() { 500 return this.n_id; 501 } 502 503 public shared pure nothrow @property @nogc UUID nextUUID() { 504 ubyte[16] data; 505 data[0..8] = nativeToBigEndian(this.id); 506 data[8..16] = nativeToBigEndian(this.uuid_count); 507 atomicOp!"+="(this.uuid_count, 1); 508 return UUID(data); 509 } 510 511 /** 512 * Gets the arguments the server has been launched with, excluding the ones 513 * used to edit the configuration files. 514 * Example: 515 * --- 516 * // from command-line 517 * ./selery --display-name=test -a -b 518 * assert(server.args == ["-a", "-b"]); 519 * --- 520 * 521 * Custom arguments can be used by plugins to load optional settings. 522 * Example: 523 * --- 524 * @start load() { 525 * if(!server.args.canFind("--disable-example")) { 526 * this.loadImpl(); 527 * } 528 * } 529 * --- 530 */ 531 public shared pure nothrow @property @safe @nogc const args() { 532 return this.n_args; 533 } 534 535 public override shared pure nothrow @property @trusted @nogc const(Config) config() { 536 return cast()this._config; 537 } 538 539 public override shared @property Logger logger() { 540 return cast()this._logger; 541 } 542 543 public shared void logCommand(Message[] messages, int commandId) { 544 (cast()this.serverlogger).logWith(messages, commandId); 545 } 546 547 public shared void logWorld(Message[] messages, int worldId) { 548 (cast()this.serverlogger).logWith(messages, HncomStatus.Log.NO_COMMAND, worldId); 549 } 550 551 public override shared pure nothrow @property @trusted @nogc const(Plugin)[] plugins() { 552 return cast(const(Plugin)[])this.n_plugins; 553 } 554 555 /** 556 * Gets the server's name, as indicated in the hub's 557 * settings.txt file. 558 * The name should just the name of the server without 559 * any formatting code nor description. 560 * Example: 561 * --- 562 * "Potato Empire" // do 563 * "potato empire" // don't 564 * "Potato Empire: NEW MINIGAMES!" // don't 565 * "§aPotato §5Empire" // don't 566 * --- 567 */ 568 public shared pure nothrow @property @safe @nogc string name() { 569 return this._config.hub.displayName; 570 } 571 572 /** 573 * Gets the number of online players in the current 574 * node (not in the whole server). 575 */ 576 public shared pure nothrow @property @safe @nogc size_t online() { 577 return this._players.length; 578 } 579 580 /** 581 * Gets the maximum number of players that can connect on the 582 * current node (not in the whole server). 583 * If the value is 0 there's no limit. 584 */ 585 public shared pure nothrow @property @safe @nogc size_t max() { 586 return this._config.node.maxPlayers; 587 } 588 589 /** 590 * Sets the maximum number of players that can connect to the 591 * current node. 592 * 0 can be used for unlimited players. 593 */ 594 public shared @property size_t max(uint max) { 595 this._config.node.maxPlayers = max; 596 this.handler.send(new HncomStatus.UpdateMaxPlayers(max).encode()); 597 return max; 598 } 599 600 /** 601 * Gets the number of online players in the whole server 602 * (not just in the current node). 603 */ 604 public shared pure nothrow @property @safe @nogc size_t hubOnline() { 605 return this.n_online; 606 } 607 608 /** 609 * Gets the number of maximum players that can connect to 610 * the hub. 611 * If the value is 0 it means that no limit has been set and 612 * players will never be kicked because the server is full. 613 */ 614 public shared pure nothrow @property @safe @nogc size_t hubMax() { 615 return this.n_max; 616 } 617 618 /** 619 * Gets the current's node name. 620 */ 621 public shared pure nothrow @property @safe @nogc string nodeName() { 622 return this.config.node.name; 623 } 624 625 /** 626 * Gets whether or not this is a main node (players are added 627 * when connected to hub) or not (player are added only when 628 * transferred by other nodes). 629 */ 630 public shared pure nothrow @property @safe @nogc bool isMainNode() { 631 return this.config.node.main; 632 } 633 634 /** 635 * Gets the server's social informations like website and social 636 * networks' names. 637 * Example: 638 * --- 639 * if(server.social.facebook.length) { 640 * world.broadcast("Follow us on facebook! facebook.com/" ~ server.social.facebook); 641 * } 642 * --- 643 */ 644 public shared pure nothrow @property @safe @nogc const social() { 645 return this.n_social; 646 } 647 648 /** 649 * Gets the server's website. 650 * Example: 651 * --- 652 * assert(server.website == server.social.website); 653 * --- 654 */ 655 public shared pure nothrow @property @safe @nogc string website() { 656 return this.social.website; 657 } 658 659 /** 660 * Gets the address of the hub this node is connected to. 661 * Returns: the address of the hub connected to, or null if not connected yet 662 * Example: 663 * --- 664 * if(server.hubAddress.toAddrString() == "127.0.0.1") { 665 * writeln("the hub is ipv4 localhost!"); 666 * } else if(server.hubAddress.toAddrString() == "::1") { 667 * writeln("The hub is ipv6 localhost!"); 668 * } 669 * --- 670 */ 671 public shared pure nothrow @property @trusted @nogc Address hubAddress() { 672 return cast()this.n_hub_address; 673 } 674 675 /** 676 * Gets the latency between the node and the hub. 677 */ 678 public shared pure nothrow @property @safe @nogc uint hubLatency() { 679 return this.n_hub_latency; 680 } 681 682 /** 683 * Gets the server's uptime as a Duration instance with milliseconds precision. 684 * The count starts when the node is duccessfully connected with the hub. 685 * Example: 686 * --- 687 * writeln("The server is online from ", server.uptime.minutes, " minutes"); 688 * 689 * ulong m, s; 690 * server.uptime.split!("minutes", "seconds")(m, s); 691 * writeln("The server is online from ", m, " minutes and ", s, " seconds"); 692 * --- 693 */ 694 public shared @property @safe Duration uptime() { 695 return dur!"msecs"(milliseconds - this.start_time); 696 } 697 698 /** 699 * Gets a list with the nodes connected to the hub, this excluded. 700 * Example: 701 * --- 702 * foreach(node ; server.nodes) { 703 * assert(node.name != server.nodeName); 704 * } 705 * --- 706 */ 707 public shared pure nothrow @property @trusted const(Node)[] nodes() { 708 return cast(const(Node)[])this.nodes_hubid.values; 709 } 710 711 /** 712 * Gets a node by its name. It can be used to transfer players 713 * to it. 714 * Example: 715 * --- 716 * auto lobby = server.nodeWithName("lobby"); 717 * if(lobby !is null) lobby.transfer(player); 718 * --- 719 */ 720 public shared inout pure nothrow @trusted const(Node) nodeWithName(string name) { 721 auto ret = name in this.nodes_names; 722 return ret ? cast(const)*ret : null; 723 } 724 725 /** 726 * Gets a node by its hub id, which is given by the hub and 727 * unique for every session. 728 */ 729 public shared inout pure nothrow @trusted const(Node) nodeWithHubId(uint hubId) { 730 auto ret = hubId in this.nodes_hubid; 731 return ret ? cast(const)*ret : null; 732 } 733 734 /** 735 * Sends a message to a node. 736 */ 737 public shared void sendMessage(Node[] nodes, ubyte[] payload) { 738 uint[] addressees; 739 foreach(node ; nodes) { 740 if(node !is null) addressees ~= node.hubId; 741 } 742 this.handler.send(new HncomStatus.SendMessage(addressees, payload).encode()); 743 } 744 745 /// ditto 746 public shared void sendMessage(Node node, ubyte[] payload) { 747 this.sendMessage([node], payload); 748 } 749 750 /** 751 * Broadcasts a message to every node connected to the hub. 752 */ 753 public shared void broadcast(ubyte[] payload) { 754 this.sendMessage([], payload); 755 } 756 757 /** 758 * Updates langauge files (in config) and send the UpdateLanguageFiles packet to 759 * the hub if needed. 760 */ 761 protected shared void updateLanguageFiles(string language, string[string] messages) { 762 if(!this.lite) this.config.lang.add(language, messages); 763 this.handler.send(new HncomStatus.UpdateLanguageFiles(language, messages).encode()); 764 } 765 766 public shared pure nothrow @property shared(GroupInfo) mainWorldGroup() { 767 return this._groups[this._main_group_id]; 768 } 769 770 /** 771 * Gets the default world of the server's main group of worlds. 772 */ 773 public shared pure nothrow @property shared(WorldInfo) defaultWorld() { 774 return this.mainWorldGroup.defaultWorld; 775 } 776 777 /** 778 * Gets a list with every world registered in the server. 779 * The list is a copy of the one kept by the server and its 780 * modification has no effect on the server. 781 */ 782 public shared pure nothrow @property shared(GroupInfo)[] worldGroups() { 783 return this._groups.values; 784 } 785 786 /** 787 * Gets a world by its name. 788 * Returns: the WorldInfo of the world with the given name or null if a world with the given name doesn't exists. 789 */ 790 public shared @property shared(GroupInfo) getGroupByName(string name) { 791 auto group = name in this._groups_names; 792 return group ? *group : null; 793 } 794 795 /** 796 * Creates a new group of worlds with the given name and starts it in a new thread. 797 * This method only creates a group, which is a container for worlds, but no actual world. 798 * To create a world the addWorld method must be used. 799 * Example: 800 * --- 801 * auto group = server.addWorldGroup("MyGroup"); 802 * auto world = server.addWorld(group, 0); // where 0 is the seed 803 * --- 804 */ 805 public shared synchronized shared(GroupInfo) addWorldGroup(string name) { 806 if(name !in this._groups_names) { 807 shared GroupInfo group = new shared GroupInfo(name); 808 this._groups[group.id] = group; 809 this._groups_names[name] = group; 810 bool main = false; 811 if(this._main_group_id == 0) { 812 this._main_group_id = group.id; 813 main = true; 814 } 815 group.tid = cast(shared)std.concurrency.spawn(&spawnWorldGroup, this, group, main); 816 return group; 817 } else { 818 return null; 819 } 820 } 821 822 /** 823 * Creates and registers a world in the given group, initialising its terrain, 824 * registering events, commands and tasks. 825 * Returns: the WorldInfo of the created world. 826 * Example: 827 * --- 828 * server.addWorld(server.mainWorldGroup); 829 * server.addWorld(server.addWorldGroup("test")); 830 * --- 831 */ 832 public shared synchronized shared(WorldInfo) addWorld(T:World=World, E...)(shared GroupInfo group, E args) { 833 shared WorldInfo world = new shared WorldInfo(); 834 // this is also done after the world is created, but the thread can 835 // be busy and the world may not be asigned in time, resulting in an 836 // error when the list of worlds or the `defaultWorld` variable are requested 837 group.worlds[world.id] = world; 838 if(group.defaultWorld is null) group.defaultWorld = world; 839 // request the new world 840 std.concurrency.send(cast()group.tid, AddWorld(cast(shared)new AddWorld.Create!T(world, args))); // the world is then added to the group in the group's thread 841 return world; 842 } 843 844 /** 845 * Creates a group with the given name and adds the given world. 846 * Example: 847 * --- 848 * server.addWorld("test"); 849 * --- 850 */ 851 public shared synchronized shared(WorldInfo) addWorld(T:World=World, E...)(string name, E args) { 852 return this.addWorld!T(this.addWorldGroup(name), args); 853 } 854 855 public shared synchronized bool removeWorldGroup(uint groupId) { 856 auto group = groupId in this._groups; 857 if(group) { 858 if(groupId == this._main_group_id) { 859 this.logger.logWarning(Translation("warning.removingMainGroup", group.name)); 860 } else { 861 std.concurrency.send(cast()group.tid, Close()); // wait for CloseResult before removing the world 862 return true; 863 } 864 } 865 return false; 866 } 867 868 /// ditto 869 public shared synchronized bool removeWorldGroup(shared GroupInfo group) { 870 return this.removeWorldGroup(group.id); 871 } 872 873 public shared synchronized void removeWorld(shared WorldInfo world) { 874 std.concurrency.send(cast()world.group.tid, RemoveWorld(world.id)); 875 } 876 877 protected shared void handleCloseResult(CloseResult result) { 878 auto group = result.groupId in this._groups; 879 if(group) { 880 if(result.status == CloseResult.PLAYERS_ONLINE) { 881 this.logger.logWarning(Translation("warning.removingWithPlayers", group.name)); 882 } else { 883 this._groups.remove(group.id); 884 this._groups_names.remove(group.name); 885 } 886 } 887 } 888 889 /** 890 * Gets a list with all the players in the server. 891 */ 892 public shared pure nothrow @property shared(PlayerInfo)[] players() { 893 return this._players.values; 894 } 895 896 /** 897 * Broadcasts a message in every registered group and their worlds` 898 * calling the world's broadcast method. 899 */ 900 public shared void broadcast(string message) { 901 foreach(group ; this._groups) { 902 std.concurrency.send(cast()group.tid, Broadcast(message)); 903 } 904 } 905 906 public shared void updateGroupDifficulty(shared GroupInfo group, Difficulty difficulty) { 907 std.concurrency.send(cast()group.tid, UpdateDifficulty(difficulty)); 908 } 909 910 public shared void updatePlayerGamemode(shared PlayerInfo player, Gamemode gamemode) { 911 std.concurrency.send(cast()player.world.group.tid, UpdatePlayerGamemode(player.hubId, gamemode)); 912 } 913 914 public shared void updatePlayerPermissionLevel(shared PlayerInfo player, PermissionLevel permissionLevel) { 915 std.concurrency.send(cast()player.world.group.tid, UpdatePlayerPermissionLevel(player.hubId, permissionLevel)); 916 } 917 918 /** 919 * Registers a command. 920 */ 921 public void registerCommand(alias func)(void delegate(Parameters!func) del, string command, Description description, string[] aliases, ubyte permissionLevel, string[] permissions, bool hidden, bool implemented=true) { 922 if(command !in this._commands) this._commands[command] = cast(shared)new Command(command, description, aliases, permissionLevel, permissions, hidden); 923 auto ptr = command in this._commands; 924 (cast()*ptr).add!func(del, implemented); 925 foreach(alias_ ; aliases) this._commands[alias_] = *ptr; 926 } 927 928 public shared @property auto commands() { 929 return this._commands; 930 } 931 932 // hub-node communication and related methods 933 934 /* 935 * Kicks a player from the server using Player.kick. 936 */ 937 public shared void kick(uint hubId, string reason) { 938 if(this.removePlayer(hubId, PlayerLeftEvent.Reason.kicked)) { 939 this.handler.send(new HncomPlayer.Kick(hubId, reason, false).encode()); 940 } 941 } 942 943 /// ditto 944 public shared void kick(uint hubId, string reason, inout(string)[] args) { 945 if(this.removePlayer(hubId, PlayerLeftEvent.Reason.kicked)) { 946 this.handler.send(new HncomPlayer.Kick(hubId, reason, true, cast(string[])args).encode()); 947 } 948 } 949 950 /* 951 * Transfers a player to another node using Player.transfer 952 */ 953 public shared void transfer(uint hubId, inout Node node) { 954 if(this.removePlayer(hubId, PlayerLeftEvent.Reason.transferred)) { 955 this.handler.send(new HncomPlayer.Transfer(hubId, node.hubId).encode()); 956 } 957 } 958 959 // removes with a reason a player spawned in the server 960 private shared synchronized bool removePlayer(uint hubId, ubyte reason) { 961 auto player = hubId in this._players; 962 if(player) { 963 if((*player).world !is null) { 964 std.concurrency.send(cast()(*player).world.group.tid, RemovePlayer(hubId)); 965 } 966 this._players.remove(hubId); 967 (cast()this).callEventIfExists!PlayerLeftEvent(this, cast(const)*player, reason); 968 return true; 969 } else { 970 return false; 971 } 972 } 973 974 public shared void updatePlayerDisplayName(uint hubId) { 975 auto player = hubId in this._players; 976 if(player) this.handler.send(new HncomPlayer.UpdateDisplayName(hubId, (*player).displayName).encode()); 977 } 978 979 // hncom handlers 980 981 protected override void handleStatusLatency(HncomStatus.Latency packet) { 982 //TODO send packet back 983 } 984 985 protected override void handleStatusRemoteCommand(HncomStatus.RemoteCommand packet) { 986 with(packet) (cast(shared)this).handleCommand(cast(ubyte)(origin + 1), sender, command, commandId); 987 } 988 989 protected override void handleStatusAddNode(HncomStatus.AddNode packet) { 990 auto node = new Node(cast(shared)this, packet.hubId, packet.name, packet.main, packet.acceptedGames); 991 this.nodes_hubid[node.hubId] = cast(shared)node; 992 this.nodes_names[node.name] = cast(shared)node; 993 this.callEventIfExists!NodeAddedEvent(node); 994 } 995 996 protected override void handleStatusRemoveNode(HncomStatus.RemoveNode packet) { 997 auto node = packet.hubId in this.nodes_hubid; 998 if(node) { 999 this.nodes_hubid.remove((*node).hubId); 1000 this.nodes_names.remove((*node).name); 1001 this.callEventIfExists!NodeRemovedEvent(cast()*node); 1002 } 1003 } 1004 1005 protected override void handleStatusReceiveMessage(HncomStatus.ReceiveMessage packet) { 1006 auto node = (cast(shared)this).nodeWithHubId(packet.sender); 1007 // only accept message from nodes that didn't disconnect 1008 if(node !is null) { 1009 this.callEventIfExists!NodeMessageEvent(cast()node, cast(ubyte[])packet.payload); 1010 } 1011 } 1012 1013 protected override void handleStatusUpdatePlayers(HncomStatus.UpdatePlayers packet) { 1014 this.n_online = packet.online; 1015 this.n_max = packet.max; 1016 } 1017 1018 protected override void handleStatusUpdateDisplayName(HncomStatus.UpdateDisplayName packet) { 1019 this._config.hub.displayName = packet.displayName; 1020 } 1021 1022 protected override void handleStatusUpdateMOTD(HncomStatus.UpdateMOTD packet) { 1023 // assuming that is already parsed 1024 if(packet.type == __BEDROCK__) this._config.hub.bedrock.motd = packet.motd; 1025 else if(packet.type == __JAVA__) this._config.hub.java.motd = packet.motd; 1026 } 1027 1028 protected override void handleStatusUpdateSupportedProtocols(HncomStatus.UpdateSupportedProtocols packet) { 1029 //TODO 1030 } 1031 1032 protected override void handlePlayerAdd(HncomPlayer.Add packet) { 1033 1034 Skin skin = Skin(packet.skin.name, packet.skin.data, packet.skin.cape, packet.skin.geometryName, packet.skin.geometryData); 1035 1036 if(!skin.valid) { 1037 // http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/UUID.java#l394 1038 string data = packet.uuid.toString().replace("-", ""); 1039 skin = (data[7] ^ data[15] ^ data[23] ^ data[31]) ? Skin.ALEX : Skin.STEVE; 1040 } 1041 1042 shared PlayerInfo player = cast(shared)new PlayerInfo(packet); 1043 player.skin = skin; 1044 1045 // add to the lists 1046 this._players[player.hubId] = player; 1047 1048 auto event = (cast()this).callEventIfExists!PlayerJoinEvent(cast(shared)this, cast(const)player, packet.reason); 1049 1050 //TODO allow kicking from event 1051 1052 // do not spawn if it has been disconnected during the event 1053 if(player.hubId in this._players) { 1054 1055 shared(WorldInfo) world; 1056 if(event is null || event.world is null || event.world.group.id !in this._groups) { 1057 world = (cast(shared)this).defaultWorld; 1058 } 1059 1060 // set the world before the group's thread does to avoid null references 1061 player.world = world; 1062 1063 std.concurrency.send(cast()world.group.tid, AddPlayer(player, world.id, packet.reason != HncomPlayer.Add.FIRST_JOIN)); 1064 1065 } 1066 1067 } 1068 1069 protected override void handlePlayerRemove(HncomPlayer.Remove packet) { 1070 (cast(shared)this).removePlayer(packet.hubId, packet.reason); 1071 } 1072 1073 protected override void handlePlayerUpdateDisplayName(HncomPlayer.UpdateDisplayName packet) { 1074 //TODO 1075 } 1076 1077 protected override void handlePlayerUpdatePermissionLevel(HncomPlayer.UpdatePermissionLevel packet) { 1078 auto player = packet.hubId in this._players; 1079 if(player) { 1080 (cast(shared)this).updatePlayerPermissionLevel(*player, cast(PermissionLevel)packet.permissionLevel); 1081 } 1082 } 1083 1084 protected override void handlePlayerUpdateViewDistance(HncomPlayer.UpdateViewDistance packet) { 1085 //TODO 1086 } 1087 1088 protected override void handlePlayerUpdateLanguage(HncomPlayer.UpdateLanguage packet) { 1089 //TODO 1090 } 1091 1092 protected override void handlePlayerUpdateLatency(HncomPlayer.UpdateLatency packet) { 1093 //TODO 1094 } 1095 1096 protected override void handlePlayerUpdatePacketLoss(HncomPlayer.UpdatePacketLoss packet) { 1097 //TODO 1098 } 1099 1100 protected override void handlePlayerGamePacket(HncomPlayer.GamePacket packet) { 1101 auto player = packet.hubId in this._players; 1102 if(player && packet.payload.length) { 1103 std.concurrency.send(cast()(*player).world.group.tid, GamePacket(packet.hubId, packet.payload.idup)); 1104 } 1105 } 1106 1107 private shared void handlePromptCommand(string command) { 1108 this.handleCommand(0, null, command); 1109 } 1110 1111 // handles a command from various sources. 1112 private shared void handleCommand(ubyte origin, Address address, string command, int id=-1) { 1113 auto sender = new ServerCommandSender(this, this._commands, origin, address, id); 1114 executeCommand(sender, command).trigger(sender); 1115 } 1116 1117 } 1118 1119 final class ServerCommandSender : CommandSender { 1120 1121 enum Origin : ubyte { 1122 1123 prompt = 0, 1124 hub = HncomStatus.RemoteCommand.HUB + 1, 1125 webAdmin = HncomStatus.RemoteCommand.WEB_ADMIN + 1, 1126 rcon = HncomStatus.RemoteCommand.RCON + 1, 1127 1128 } 1129 1130 private shared NodeServer _server; 1131 private shared Command[string] _commands; 1132 public immutable ubyte origin; 1133 public const Address address; 1134 private int id; 1135 1136 public this(shared NodeServer server, shared Command[string] commands, ubyte origin, Address address, int id) { 1137 this._server = server; 1138 this._commands = commands; 1139 this.origin = origin; 1140 this.address = address; 1141 this.id = id; 1142 } 1143 1144 public override pure nothrow @property @safe @nogc shared(NodeServer) server() { 1145 return this._server; 1146 } 1147 1148 public override @property Command[string] availableCommands() { 1149 return cast(Command[string])this._commands; 1150 } 1151 1152 protected override void sendMessageImpl(Message[] messages) { 1153 this._server.logCommand(messages, this.id); 1154 } 1155 1156 alias server this; 1157 1158 } 1159 1160 private abstract class ServerLogger : Logger { 1161 1162 public this(Terminal terminal, inout LanguageManager lang) { 1163 super(terminal, lang); 1164 } 1165 1166 public override void logMessage(Message[] messages) { 1167 this.logWith(messages); 1168 } 1169 1170 /** 1171 * Creates a log with commandId and worldId. 1172 */ 1173 public void logWith(Message[] messages, int commandId=HncomStatus.Log.NO_COMMAND, int worldId=HncomStatus.Log.NO_WORLD) { 1174 this.logWithImpl(messages, commandId, worldId); 1175 } 1176 1177 protected abstract void logWithImpl(Message[], int, int); 1178 1179 } 1180 1181 private class NodeServerLogger : ServerLogger { 1182 1183 public this(Terminal terminal, inout LanguageManager lang) { 1184 super(terminal, lang); 1185 } 1186 1187 // prints to the console and send to the hub 1188 protected override void logWithImpl(Message[] messages, int commandId, int worldId) { 1189 this.logImpl(messages); 1190 Handler.sharedInstance.send(new HncomStatus.Log(encodeHncomMessage(messages), milliseconds, commandId, worldId).encode()); 1191 } 1192 1193 } 1194 1195 private class LiteServerLogger : ServerLogger { 1196 1197 public this(Terminal terminal, inout LanguageManager lang) { 1198 super(terminal, lang); 1199 } 1200 1201 // only send to the hub 1202 protected override void logWithImpl(Message[] messages, int commandId, int worldId) { 1203 Handler.sharedInstance.send(new HncomStatus.Log(encodeHncomMessage(messages), milliseconds, commandId, worldId).encode()); 1204 } 1205 1206 } 1207 1208 private HncomStatus.Log.Message[] encodeHncomMessage(Message[] messages) { 1209 HncomStatus.Log.Message[] ret; 1210 string next; 1211 void addText() { 1212 ret ~= HncomStatus.Log.Message(false, next, []); 1213 next.length = 0; 1214 } 1215 foreach(message ; messages) { 1216 final switch(message.type) { 1217 case Message.FORMAT: 1218 next ~= message.format; 1219 break; 1220 case Message.TEXT: 1221 next ~= message.text; 1222 break; 1223 case Message.TRANSLATION: 1224 if(next.length) addText(); 1225 ret ~= HncomStatus.Log.Message(true, message.translation.translatable.default_, message.translation.parameters); 1226 break; 1227 } 1228 } 1229 if(next.length) addText(); 1230 return ret; 1231 } 1232 1233 private void startResourceUsageThread(int pid) { 1234 1235 debug Thread.getThis().name = "ResourcesUsage"; 1236 1237 ProcessMemInfo ram = processMemInfo(pid); 1238 ProcessCPUWatcher cpu = new ProcessCPUWatcher(pid); 1239 1240 while(true) { 1241 ram.update(); 1242 //TODO send packet directly to the socket 1243 Handler.sharedInstance.send(new HncomStatus.UpdateUsage(cast(uint)(ram.usedRAM / 1024u), cpu.current()).encode()); 1244 Thread.sleep(dur!"seconds"(5)); 1245 } 1246 1247 } 1248 1249 //TODO use Terminal 1250 private void startCommandReaderThread(std.concurrency.Tid tid) { 1251 1252 debug Thread.getThis().name = "CommandReader"; 1253 1254 import std.stdio : readln; 1255 while(true) { 1256 std.concurrency.send(tid, readln()); 1257 } 1258 1259 }