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