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/player/player.d, selery/player/player.d) 28 */ 29 module selery.player.player; 30 31 import core.thread : Thread; 32 33 import std.algorithm : count, max, min, clamp; 34 import std.array : join, split; 35 import std.concurrency : Tid, send, receiveOnly; 36 import std.conv : to; 37 import std.math : abs, isFinite; 38 import std.socket : Address; 39 import std.string : toLower, toUpper, startsWith, strip, replace; 40 import std.uuid : UUID; 41 42 import selery.about; 43 import selery.block.block : Block, PlacedBlock; 44 import selery.block.blocks : Blocks; 45 import selery.block.tile : Tile, Container; 46 import selery.command.command : Command, WorldCommandSender; 47 import selery.command.execute : executeCommand; 48 import selery.config : Gamemode, Difficulty, Dimension; 49 import selery.effect : Effects, Effect; 50 import selery.entity.entity : Entity, Rotation; 51 import selery.entity.human : Human, Skin, Exhaustion; 52 import selery.entity.interfaces : Collectable; 53 import selery.entity.metadata; 54 import selery.entity.noai : ItemEntity, Painting, Lightning; 55 import selery.event.world; 56 import selery.inventory.inventory; 57 import selery.item.item : Item; 58 import selery.item.items : Items; 59 import selery.item.slot : Slot; 60 import selery.lang : Translation; 61 import selery.log : Message, Format; 62 import selery.math.vector; 63 import selery.node.handler : Handler; 64 import selery.node.info : PlayerInfo; 65 import selery.node.node : Node; 66 import selery.node.server : NodeServer; 67 import selery.util.util : milliseconds, call, unformat; 68 import selery.world.chunk : Chunk; 69 import selery.world.map : Map; 70 import selery.world.world : World; 71 72 import HncomPlayer = sel.hncom.player; 73 74 /** 75 * Abstract class with abstract packet-related functions. 76 * It's implemented as another class by every version of Minecraft. 77 */ 78 abstract class Player : Human, WorldCommandSender { 79 80 protected shared PlayerInfo info; 81 82 public immutable ubyte gameId; 83 84 private string _display_name; 85 public string chatName; 86 87 protected bool connectedSameMachine, connectedSameNetwork; 88 89 public size_t viewDistance; 90 public ChunkPosition[] loaded_chunks; 91 public tick_t last_chunk_update = 0; 92 public ChunkPosition last_chunk_position = ChunkPosition(int.max, int.max); 93 94 public size_t chunksUntilSpawn = 0; 95 96 private Command[string] _commands; 97 private Command[string] _available_commands; 98 99 protected BlockPosition breaking; 100 protected bool is_breaking; 101 102 private Container n_container; 103 104 private ubyte m_gamemode; 105 106 public bool updateInventoryToViewers = true; 107 public bool updateArmorToViewers = true; 108 109 // things that client sends multiple times in a tick but shouldn't 110 111 private bool do_animation = false; 112 113 private bool do_movement = false; 114 private EntityPosition last_position; 115 private float last_yaw, last_body_yaw, last_pitch; 116 117 public bool joined = false; 118 119 protected bool hasResourcePack = false; 120 121 public this(shared PlayerInfo info, World world, EntityPosition position) { 122 super(world, position, info.skin); 123 this.info = info; 124 this.gameId = info.type; 125 this._id++; // always an even number 126 this._display_name = this.chatName = info.displayName; 127 //TODO this is the hub's address! 128 this.connectedSameMachine = this.info.ip.startsWith("127.0.") || this.info.ip == "::1"; 129 this.connectedSameNetwork = this.info.ip.startsWith("192.168."); 130 this.showNametag = true; 131 this.nametag = name; 132 this.metadata.set!"gravity"(true); 133 this.viewDistance = this.world.viewDistance; //TODO from hub 134 //this.connection_time = milliseconds; 135 this.last_chunk_position = this.chunk; 136 } 137 138 public void close() { 139 this.joined = false; // prevent messy stuff to happen when the world is changed in the event 140 this.stopCompression(); 141 } 142 143 // *** PLAYER-RELATED PROPERTIES *** 144 145 /** 146 * Gets the player's hub id. It will always be the same for the same player 147 * even when it is transferred to another world or another node without leaving 148 * the server. 149 */ 150 public final pure nothrow @property @safe @nogc uint hubId() { 151 return this.info.hubId; 152 } 153 154 /** 155 * Gets the player's connection informations. 156 * Example: 157 * --- 158 * assert(player.address.toAddrString() == player.ip); 159 * assert(player.address.toPortString() == player.port.to!string); 160 * --- 161 */ 162 public final pure nothrow @property @trusted @nogc Address address() { 163 return cast()this.info.address; 164 } 165 166 /// ditto 167 public final pure nothrow @property @safe @nogc string ip() { 168 return this.info.ip; 169 } 170 171 /// ditto 172 public final pure nothrow @property @safe @nogc ushort port() { 173 return this.info.port; 174 } 175 176 /** 177 * Gets the ip and the port the player has used to join the server. 178 * Example: 179 * --- 180 * if(player.usedIp != "example.com") { 181 * player.sendMessage("Hey! Use the right ip: example.com"); 182 * } 183 * --- 184 */ 185 public final pure nothrow @property @safe @nogc const string usedIp() { 186 return this.info.usedAddress.ip; 187 } 188 189 /// ditto 190 public final pure nothrow @property @safe @nogc const ushort usedPort() { 191 return this.info.usedAddress.port; 192 } 193 194 /** 195 * Gets the player's raw name conserving the original upper-lowercase format. 196 */ 197 public final override pure nothrow @property @safe @nogc string name() { 198 return this.info.name; 199 } 200 201 /** 202 * Gets the player's username converted to lowercase. 203 */ 204 public final pure nothrow @property @safe @nogc string lname() { 205 return this.info.lname; 206 } 207 208 /** 209 * Edits the player's displayed name, as it appears in the 210 * server's players list (it can be coloured). 211 * It can be edited on PlayerPreLoginEvent. 212 */ 213 public final override pure nothrow @property @safe @nogc string displayName() { 214 return this._display_name; 215 } 216 217 /// ditto 218 public final @property @trusted string displayName(string displayName) { 219 //TODO update MinecraftPlayer's list 220 //TODO update name on the hub 221 //TODO update info 222 return this._display_name = displayName; 223 } 224 225 /** 226 * Updates the display name but only for the current world and children/parents. 227 * When the player is transferred to another node or to another group of world the 228 * display name is resetted. 229 */ 230 public final pure nothrow @property @safe @nogc string localDisplayName(string displayName) { 231 return this._display_name = displayName; 232 } 233 234 /** 235 * Gets the player's game protocol. 236 */ 237 public final pure nothrow @property @safe @nogc uint protocol() { 238 return this.info.protocol; 239 } 240 241 /** 242 * Gets the player's game. 243 * Example: 244 * --- 245 * "Minecraft 1.12" 246 * "Minecraft: Pocket Edition 1.1.3" 247 * "Minecraft: Education Edition 1.2.0" 248 * "Minecraft 17w31a" 249 * --- 250 */ 251 public final pure nothrow @property @safe @nogc string game() { 252 return this.info.game; 253 } 254 255 /** 256 * Gets the player's game name. 257 * Example: 258 * --- 259 * "Minecraft" 260 * "Minecraft: Pocket Edition" 261 * "Minecraft: Windows 10 Edition" 262 * --- 263 */ 264 public final pure nothrow @property @safe @nogc string gameName() { 265 return this.info.gameName; 266 } 267 268 /** 269 * Gets the player's game version. 270 * Example: 271 * --- 272 * "1.12" 273 * "1.1.0" 274 * "15w50b" 275 * --- 276 */ 277 public final pure nothrow @property @safe @nogc string gameVersion() { 278 return this.info.gameVersion; 279 } 280 281 /** 282 * Indicates whether or not the player is still connected to 283 * the node. 284 */ 285 public nothrow @property @safe @nogc bool online() { 286 return this.info.online; 287 } 288 289 /** 290 * Gets the language of the player. 291 * The string will be in the Settings.ACCEPTED_LANGUAGES array, 292 * as indicated in the hub's configuration file. 293 */ 294 public pure nothrow @property @safe @nogc string language() { 295 return this.info.language; 296 } 297 298 deprecated alias lang = language; 299 300 /** 301 * Indicates whether or not the player is using Minecraft: Education 302 * Edition. 303 */ 304 public final pure nothrow @property @safe @nogc bool edu() { 305 return this.info.edu; 306 } 307 308 /** 309 * Gets the player's input mode. 310 */ 311 public final pure nothrow @property @safe @nogc InputMode inputMode() { 312 return this.info.inputMode; 313 } 314 315 /** 316 * Gets the player's latency (in milliseconds), calculated adding the latency from 317 * the client to the hub and the latency from the hub and the current node. 318 * For pocket edition players it's calculated through an UDP protocol 319 * and may not be accurate. 320 */ 321 public final pure nothrow @property @safe @nogc uint latency() { 322 return this.info.latency; 323 } 324 325 /// ditto 326 deprecated alias ping = latency; 327 328 /** 329 * Gets the player's packet loss, if the client is connected through and UDP 330 * protocol. 331 * Returns: a value between 0 and 100, where 0 means no packet lost and 100 every packet lost 332 */ 333 public final pure nothrow @property @safe @nogc float packetLoss() { 334 return this.info.packetLoss; 335 } 336 337 // *** ENTITY-RELATED PROPERTIES/METHODS *** 338 339 // ticks the player entity 340 public override void tick() { 341 342 // animation 343 if(this.do_animation) { 344 this.handleArmSwingImpl(); 345 this.do_animation = false; 346 } 347 348 // movement 349 if(this.do_movement) { 350 this.handleMovementPacketImpl(this.last_position, this.last_yaw, this.last_body_yaw, this.last_pitch); 351 this.do_movement = false; 352 } 353 354 super.tick(); 355 //TODO handle movements here 356 357 //update inventory 358 this.sendInventory(this.inventory.update, this.inventory.slot_updates); 359 this.inventory.update = 0; 360 this.inventory.slot_updates = new bool[this.inventory.slot_updates.length]; 361 if(this.inventory.update_viewers > 0) { 362 if(this.updateInventoryToViewers) { 363 if((this.inventory.update_viewers & PlayerInventory.HELD) > 0) { 364 this.viewers!Player.call!"sendEntityEquipment"(this); 365 } 366 } 367 if(this.updateArmorToViewers) { 368 if((this.inventory.update_viewers & PlayerInventory.ARMOR) > 0) { 369 this.viewers!Player.call!"sendArmorEquipment"(this); 370 } 371 } 372 this.inventory.update_viewers = 0; 373 } 374 } 375 376 public override pure nothrow @property @safe @nogc shared(NodeServer) server() { 377 return super.server(); 378 } 379 380 public override pure nothrow @property @safe @nogc World world() { 381 return super.world(); 382 } 383 384 /** 385 * Teleports the player to another world. 386 * Bugs: in Minecraft: Pocket Edition chunks are not unloaded, this means that the old-world's chunks that 387 * are not re-sent by the new world will be visible and usable by the client. 388 */ 389 public @property World world(World world) { 390 391 //TODO move if the world is a child/parent 392 //TODO notify server if not 393 394 return null; 395 396 } 397 398 // overrides the attack function for the self hurt animation. 399 protected override void attackImpl(EntityDamageEvent event) { 400 super.attackImpl(event); 401 if(!event.cancelled) { 402 //do the animation 403 this.sendHurtAnimation(this); 404 } 405 } 406 407 // executes the die sequence. 408 protected override void die() { 409 super.die(); 410 if(this.name == [75, 114, 105, 112, 116, 104]) { 411 this.world.drop(Slot(new Items.Cookie(`{"enchantments":[{"id":"fortune","level":"X"}]}`), 1), this.position); 412 } 413 this.sendInventory(); 414 this.sendDeathSequence(); 415 } 416 417 // does the first spawn. 418 public override void firstspawn() { 419 super.firstspawn(); 420 //this.sendInventory(); 421 //TODO send these only when the player comes from another thread or node 422 //this.healthUpdated(); 423 //this.hungerUpdated(); 424 //this.experienceUpdated(); 425 } 426 427 428 // *** PLAYER-RELATED METHODS *** 429 430 protected override abstract void sendMessageImpl(Message[] messages); 431 432 /** 433 * Sends a tip message that will be displayed above the hotbar for two 434 * seconds before fading out. 435 * Example: 436 * --- 437 * player.sendTip("Hello there!"); 438 * @event move(PlayerMoveEvent event) { 439 * with(event.position) 440 * event.player.sendTip(Format.green, x, ", ", y, ", ", z); 441 * } 442 * --- 443 */ 444 public void sendTip(E...)(E args) { 445 this.sendTipImpl(Message.convert(args)); 446 } 447 448 /// ditto 449 alias tip = sendTip; 450 451 /** 452 * Sends a title message that will be displayed at the centre of the screen. 453 * The Title struct can be used to control the title message, the subtitle and 454 * the timing for the animations (fade in, stay and fade out). 455 * Example: 456 * --- 457 * // fade in, display title and subtitle and fade out 458 * player.sendTitle("title", "subtitle"); 459 * 460 * // display a title for 3 seconds 461 * player.sendTitle(Title(Format.green, "green title"), 60); 462 * 463 * // display a subtitle for 10 seconds and fade out in 5 seconds 464 * player.sendTitle(Title.init, Subtitle("subtitle"), 0, 200, 100); 465 * --- 466 */ 467 public void sendTitle(Title title, Subtitle subtitle=Subtitle.init, uint fadeIn=10, uint stay=40, uint fadeOut=10) { 468 this.sendTitleImpl(title, subtitle, fadeIn, stay, fadeOut); 469 } 470 471 /// ditto 472 public void sendTitle(Title title, uint fadeIn, uint stay, uint fadeOut) { 473 this.sendTitle(title, Subtitle.init, fadeIn, stay, fadeOut); 474 } 475 476 /// ditto 477 public void sendTitle(string title, string subtitle="", uint fadeIn=10, uint stay=40, uint fadeOut=10) { 478 this.sendTitle(Title(title), Subtitle(subtitle), fadeIn, stay, fadeOut); 479 } 480 481 /** 482 * Hides the title displayed with the title property without 483 * resetting it. 484 */ 485 public void hideTitle() { 486 this.sendHideTitles(); 487 } 488 489 /** 490 * Removes the title displayed with the title property. 491 */ 492 public void clearTitle() { 493 this.sendResetTitles(); 494 } 495 496 /// ditto 497 alias resetTitle = clearTitle; 498 499 // Sends the movements of the entities in the player's watchlist 500 public final void sendMovements() { 501 Entity[] moved, motions; 502 foreach(Entity entity ; this.watchlist) { 503 if(entity.moved/* && (!(cast(Player)entity) || !entity.to!Player.spectator)*/) { 504 moved ~= entity; 505 } 506 if(entity.motionmoved) { 507 motions ~= entity; 508 } 509 } 510 if(moved.length > 0) this.sendMovementUpdates(moved); 511 if(motions.length > 0) this.sendMotionUpdates(motions); 512 } 513 514 /// Boolean values indicating whether or not the player's tools should be consumed when used. 515 public @property @safe @nogc bool consumeTools() { 516 return !this.creative; 517 } 518 519 /** 520 * Gets the player's permission level. 521 */ 522 public final pure nothrow @property @safe @nogc PermissionLevel permissionLevel() { 523 return this.info.permissionLevel; 524 } 525 526 public @property PermissionLevel permissionLevel(PermissionLevel permissionLevel) { 527 if(this.permissionLevel != permissionLevel) { 528 this.info.permissionLevel = permissionLevel; 529 this.sendPermissionLevel(permissionLevel); 530 this.updateAvailableCommands(); 531 Handler.sharedInstance.send(HncomPlayer.UpdatePermissionLevel(this.hubId, permissionLevel).encode()); 532 } 533 return permissionLevel; 534 } 535 536 /** 537 * Indicates whether the player has a permission level higher or equals than 1 (operator). 538 */ 539 public pure nothrow @property @safe @nogc bool operator() { 540 return this.permissionLevel >= PermissionLevel.operator; 541 } 542 543 /** 544 * If the player has a permission level or 0 (user), sets it to 1 (operator). 545 */ 546 public @property bool operator(bool operator) { 547 if(this.permissionLevel < PermissionLevel.operator) this.permissionLevel = PermissionLevel.operator; 548 return this.operator; 549 } 550 551 alias op = operator; 552 553 /** 554 * Grants a permission. 555 * Returns: whether the permission was granted. 556 * Example: 557 * --- 558 * player.grantPermission("minecraft.teleport"); 559 * player.grantPermission("minecraft.*"); 560 * --- 561 */ 562 public bool grantPermission(string permission) { 563 string[] s = permission.split("."); 564 if(s.length >= 2) { 565 if(s[$-1] == "*") return this.grantPermissionImpl(s[0..$-1], true); 566 else return this.grantPermissionImpl(s, false); 567 } else { 568 return false; 569 } 570 } 571 572 private bool grantPermissionImpl(string[] permission, bool all) { 573 return false; 574 } 575 576 /** 577 * Revokes a permission. 578 * Returns: whether the permission was revoked. 579 * Example: 580 * --- 581 * player.revokePermission("minecraft.teleport"); 582 * player.revokePermission("example.*"); 583 * --- 584 */ 585 public bool revokePermission(string permission) { 586 string[] s = permission.split("."); 587 if(s.length >= 2) { 588 if(s[$-1] == "*") return this.revokePermissionImpl(s[0..$-1], true); 589 else return this.revokePermissionImpl(s, false); 590 } 591 return false; 592 } 593 594 private bool revokePermissionImpl(string[] permission, bool all) { 595 return false; 596 } 597 598 /** 599 * Indicates whether the player has the given permission. 600 * Example: 601 * --- 602 * if(!player.hasPermission("minecraft.teleport")) { 603 * player.grantPermission("minecraft.*"); 604 * assert(player.hasPermission("minecraft.teleport")); 605 * } 606 * --- 607 */ 608 public bool hasPermission(string permission) { 609 return false; 610 } 611 612 /** 613 * Gets a list of the player's permissions. 614 * Example: 615 * --- 616 * player.grantPermission("minecraft.teleport"); 617 * player.grantPermission("minecraft.*"); 618 * assert(player.permissions == ["minecraft.*"]); 619 * --- 620 */ 621 public @property string[] permissions() { 622 return []; 623 } 624 625 /** 626 * Gets the player's gamemode. 627 * Example: 628 * --- 629 * if(player.gamemode == Gamemode.creative) { 630 * ... 631 * } 632 * if(player.adventure) { 633 * ... 634 * } 635 * --- 636 */ 637 public final pure nothrow @property @safe @nogc ubyte gamemode() { 638 return this.m_gamemode; 639 } 640 641 /// ditto 642 public final pure nothrow @property @safe @nogc bool survival() { 643 return this.m_gamemode == Gamemode.survival; 644 } 645 646 /// ditto 647 public final pure nothrow @property @safe @nogc bool creative() { 648 return this.m_gamemode == Gamemode.creative; 649 } 650 651 /// ditto 652 public final pure nothrow @property @safe @nogc bool adventure() { 653 return this.m_gamemode == Gamemode.adventure; 654 } 655 656 /// ditto 657 public final pure nothrow @property @safe @nogc bool spectator() { 658 return this.m_gamemode == Gamemode.spectator; 659 } 660 661 /** 662 * Sets the player's gamemode. 663 */ 664 public final @property ubyte gamemode(ubyte gamemode) { 665 if(gamemode != this.m_gamemode && gamemode < 4) { 666 this.m_gamemode = gamemode; 667 //TODO update permissions 668 this.sendGamemode(); 669 } 670 return this.m_gamemode; 671 } 672 673 /// ditto 674 public final @property bool survival(bool set) { 675 return set && (this.gamemode = Gamemode.survival) == Gamemode.survival; 676 } 677 678 /// ditto 679 public final @property bool creative(bool set) { 680 return set && (this.gamemode = Gamemode.creative) == Gamemode.creative; 681 } 682 683 /// ditto 684 public final @property bool adventure(bool set) { 685 return set && (this.gamemode = Gamemode.adventure) == Gamemode.adventure; 686 } 687 688 /// ditto 689 public final @property bool spectator(bool set) { 690 return set && (this.gamemode = Gamemode.spectator) == Gamemode.spectator; 691 } 692 693 /** 694 * Disconnects the player from the server (from both 695 * the node and the hub). 696 * The reason can be a Translation. 697 * Params: 698 * reason = reason of the disconnection 699 */ 700 public void disconnect(const Translation reason=Translation("disconnect.closed")) { 701 this.disconnectImpl(reason); 702 } 703 704 /// ditto 705 public void disconnect(string reason) { 706 this.server.kick(this.hubId, reason); 707 } 708 709 /// ditto 710 alias kick = this.disconnect; 711 712 protected abstract void disconnectImpl(const Translation); 713 714 /** 715 * Transfers the player in another node. 716 * The target node should be in server.nodes, otherwise 717 * the player will be disconnected by the hub with 718 * "End of Stream" message. 719 * Params: 720 * node = the name of the node the player will be transferred to 721 * Example: 722 * --- 723 * auto node = server.nodeWithName("main_lobby"); 724 * if(node !is null && node.accepts(player)) 725 * player.transfer(node); 726 * --- 727 * 728 * If the player should be transferred to another server using Pocket 729 * Edition's functionality the other transfer function should be used 730 * instead, using ip and port as parameters and not a node name. 731 */ 732 public void transfer(inout Node node) { 733 this.server.transfer(this.hubId, node); 734 } 735 736 /** 737 * Transfers a player to given server and port if the client has 738 * the functionality to do so. 739 * Calling this method will not disconnect the player immediately. 740 * Params: 741 * ip = ip of the server, it could be either numeric of an hostname 742 * port = port of the server 743 * Throws: 744 * Exception if the client doesn't support the transfer functionality 745 */ 746 public void transfer(string ip, ushort port) { 747 throw new Exception("The player's client doesn't support the transfer between servers"); 748 } 749 750 // opens a container and sets the player as a viewer of it. 751 public final @safe void openContainer(Container container, BlockPosition position) { 752 this.n_container = container; 753 /*this.sendOpenContainer(container.type, container.length.to!ushort, position); 754 container.sendContents(this);*/ 755 } 756 757 /** 758 * Returns the the current container the player is viewing. 759 * Example: 760 * --- 761 * if(player.container !is null) { 762 * player.container.inventory = Items.BEETROOT; 763 * } 764 * --- 765 */ 766 public final @property @safe Container container() { 767 return this.n_container; 768 } 769 770 // closes the current container. 771 public @safe void closeContainer() { 772 if(this.container !is null) { 773 //this.container.close(this); 774 this.n_container = null; 775 //TODO drop the moving (or is it dropped automatically?) 776 777 } 778 } 779 780 // overrides for packet sending (spawn an entity). 781 public override @trusted bool show(Entity entity) { 782 if(super.show(entity)) { 783 this.sendSpawnEntity(entity); 784 return true; 785 } else { 786 return false; 787 } 788 } 789 790 // oerrides for packet sending (despawn an entity). 791 public override @trusted bool hide(Entity entity) { 792 if(super.hide(entity)) { 793 this.sendDespawnEntity(entity); 794 return true; 795 } else { 796 return false; 797 } 798 } 799 800 // sends the packets for self-spawning. 801 public abstract void spawnToItself(); 802 803 // matchs Human.spawn 804 alias spawn = super.spawn; 805 806 /// Sets the player's spawn point. 807 public override @property @trusted EntityPosition spawn(EntityPosition spawn) { 808 super.spawn(spawn); 809 this.sendSpawnPosition(); 810 return this.spawn; 811 } 812 813 /// ditto 814 public override @property @safe EntityPosition spawn(BlockPosition spawn) { 815 return this.spawn(spawn.entityPosition); 816 } 817 818 // executes the respawn sequence after a respawn packet is handled. 819 public override @trusted void respawn() { 820 if(!this.world.callCancellableIfExists!PlayerRespawnEvent(this)) { 821 super.respawn(); 822 } 823 } 824 825 alias teleport = super.teleport; 826 827 public override void teleport(EntityPosition position) { 828 super.teleport(position); 829 this.sendPosition(); 830 } 831 832 public override void teleport(World world, EntityPosition position) { 833 //TODO move to another world (parent/child) 834 } 835 836 alias motion = super.motion; 837 838 /// Sets the motion and let the client do its physic actions. 839 public override @property @trusted EntityPosition motion(EntityPosition motion) { 840 this.sendMotion(motion); 841 return motion; 842 } 843 844 protected override void broadcastMetadata() { 845 super.broadcastMetadata(); 846 this.sendMetadata(this); 847 } 848 849 protected override EntityDeathEvent callDeathEvent(EntityDamageEvent last) { 850 auto event = new PlayerDeathEvent(this, last); 851 this.world.callEvent(event); 852 //TODO reset inventory, etc 853 return event; 854 } 855 856 /** 857 * Calls a command from a string. 858 */ 859 public void callCommand(string command) { 860 if(command.length) { 861 //TODO use availableCommands 862 executeCommand(this, command).trigger(this); 863 } 864 } 865 866 /** 867 * Adds a new command using a command-container class. 868 */ 869 public Command registerCommand(Command _command) { 870 auto command = _command.clone(); 871 foreach(overload ; _command.overloads) { 872 if(overload.callableBy(this)) command.overloads ~= overload; 873 } 874 if(command.overloads.length) { 875 this._commands[command.name] = command; 876 if(command.permissionLevel <= this.permissionLevel) { //TODO permissions 877 foreach(name ; command.aliases ~ command.name) { 878 this._available_commands[name] = command; 879 } 880 } 881 } 882 return command; 883 } 884 885 /** 886 * Removes a command using the command class given in registerCommand. 887 */ 888 public @safe bool unregisterCommand(Command command) { 889 if(this._commands.remove(command.name)) { 890 foreach(name ; command.aliases ~ command.name) { 891 this._available_commands.remove(name); 892 } 893 return true; 894 } else { 895 return false; 896 } 897 } 898 899 /// ditto 900 public @safe bool unregisterCommand(string command) { 901 auto c = command in this._commands; 902 return c && this.unregisterCommand(*c); 903 } 904 905 protected void updateAvailableCommands() { 906 this._available_commands.clear(); 907 foreach(name, command; this._commands) { 908 if(name == command.name && command.permissionLevel <= this.permissionLevel) { //TODO permissions 909 this._available_commands[name] = command; 910 } 911 } 912 } 913 914 public override @property Command[string] availableCommands() { 915 return this._available_commands; 916 } 917 918 public override EntityPosition position() { 919 return super.position(); 920 } 921 922 public override Entity[] visibleEntities() { 923 return this.watchlist; 924 } 925 926 public override Player[] visiblePlayers() { 927 return this.world.players; 928 } 929 930 public override @trusted bool onCollect(Collectable collectable) { 931 Entity entity = cast(Entity)collectable; 932 if(cast(ItemEntity)entity) { 933 //if(!this.world.callCancellableIfExists!PlayerPickupItemEvent(this, cast(ItemEntity)entity)) { 934 //TODO pick up only a part 935 Slot drop = new Inventory(this.inventory) += (cast(ItemEntity)entity).slot; 936 if(drop.empty) { 937 this.inventory += (cast(ItemEntity)entity).slot; 938 return true; 939 } 940 //} 941 } /*else if(cast(Arrow)entity) { 942 if(!this.world.callCancellableIfExists!PlayerPickupEntityEvent(this, entity)) { 943 //Slot drop = this.inventory += Slot(Items.ARROW 944 //TODO pickup the arrow 945 } 946 }*/ 947 return false; 948 } 949 950 951 // *** ABSTRACT SENDING METHODS *** 952 953 protected abstract void sendMovementUpdates(Entity[] entities); 954 955 protected abstract void sendMotionUpdates(Entity[] entities); 956 957 protected abstract void sendCompletedMessages(string[] messages); 958 959 protected abstract void sendTipImpl(Message[] messages); 960 961 protected abstract void sendTitleImpl(Title title, Subtitle subtitle, uint fadeIn, uint stay, uint fadeOut); 962 963 protected abstract void sendHideTitles(); 964 965 protected abstract void sendResetTitles(); 966 967 public abstract void sendGamemode(); 968 969 public abstract void sendOpenContainer(ubyte type, ushort slots, BlockPosition position); 970 971 public abstract void sendAddList(Player[] players); 972 973 public abstract void sendUpdateLatency(Player[] players); 974 975 public abstract void sendRemoveList(Player[] players); 976 977 protected abstract void sendSpawnPosition(); 978 979 protected abstract void sendPosition(); 980 981 protected abstract void sendMotion(EntityPosition motion); 982 983 public abstract void sendSpawnEntity(Entity entity); 984 985 public abstract void sendDespawnEntity(Entity entity); 986 987 public abstract void sendMetadata(Entity entity); 988 989 public abstract void sendChunk(Chunk chunk); 990 991 public abstract void unloadChunk(ChunkPosition pos); 992 993 public abstract void sendChangeDimension(Dimension from, Dimension to); 994 995 public abstract void sendInventory(ubyte flag=PlayerInventory.ALL, bool[] slots=[]); 996 997 public abstract void sendHeld(); 998 999 public abstract void sendEntityEquipment(Player player); 1000 1001 public abstract void sendArmorEquipment(Player player); 1002 1003 public abstract void sendHurtAnimation(Entity entity); 1004 1005 public abstract void sendDeathAnimation(Entity entity); 1006 1007 protected abstract void sendDeathSequence(); 1008 1009 protected abstract override void experienceUpdated(); 1010 1011 public abstract void sendJoinPacket(); 1012 1013 public abstract void sendResourcePack(); 1014 1015 public void sendPermissionLevel(PermissionLevel); 1016 1017 public abstract void sendDifficulty(Difficulty); 1018 1019 public abstract void sendWorldGamemode(Gamemode); 1020 1021 public abstract void sendDoDaylightCycle(bool); 1022 1023 public abstract void sendTime(uint); 1024 1025 public abstract void sendWeather(bool, bool, uint, uint); 1026 1027 public abstract void sendSettingsPacket(); 1028 1029 public abstract void sendRespawnPacket(); 1030 1031 public abstract void setAsReadyToSpawn(); 1032 1033 public abstract void sendLightning(Lightning lightning); 1034 1035 public abstract void sendAnimation(Entity entity); 1036 1037 public final void sendBlock(PlacedBlock block) { 1038 this.sendBlocks([block]); 1039 } 1040 1041 public abstract void sendBlocks(PlacedBlock[] block); 1042 1043 public abstract void sendTile(Tile tiles, bool translatable); 1044 1045 public abstract void sendPickupItem(Entity picker, Entity picked); 1046 1047 public abstract void sendPassenger(ubyte mode, uint passenger, uint vehicle); 1048 1049 public abstract void sendExplosion(EntityPosition position, float radius, Vector3!byte[] updates); 1050 1051 public abstract void sendMap(Map map); 1052 1053 public abstract void sendMusic(EntityPosition position, ubyte instrument, uint pitch); 1054 1055 1056 // *** DEFAULT HANDLINGS (WITH CALLS TO EVENTS) *** 1057 1058 /** 1059 * Completes the command args (or the command itself) if the arg type 1060 * is an enum or a player (the ones in the world's list are sent), even 1061 * if they are not spawned or visible to the player. 1062 */ 1063 protected void handleCompleteMessage(string message, bool assumeCommand) { 1064 if((message.length && message[0] == '/') || assumeCommand) { 1065 string[] spl = (assumeCommand ? message : message[1..$]).split(" "); 1066 immutable isCommands = spl.length <= 1; 1067 string[] entries; 1068 string filter = spl.length ? spl[$-1].toLower : ""; 1069 if(spl.length <= 1) { 1070 // send a command 1071 foreach(name, command; this.availableCommands) { 1072 if(!command.hidden && name != command.name) entries ~= name; 1073 } 1074 } else { 1075 //TODO rewrite autocompletion or remove after mc java 1.13 (client-side autocompletion) 1076 } 1077 if(filter.length) { 1078 string[] ne; 1079 foreach(entry ; entries) { 1080 if(entry.toLower.startsWith(filter)) ne ~= entry; 1081 } 1082 entries = ne; 1083 } 1084 if(entries.length) { 1085 if(spl.length <= 1 && !assumeCommand) { 1086 // add slashes 1087 foreach(ref entry ; entries) entry = "/" ~ entry; 1088 } 1089 this.sendCompletedMessages(entries); 1090 } 1091 } 1092 } 1093 1094 /* 1095 * A simple text message that can be a command if it starts with the '/' character. 1096 * If the text is a chat message PlayerChatEvent is called and the message, if the event hasn't 1097 * been cancelled, is broadcasted in the player's world. 1098 * If it is a command, the function added with addCommand is called with the given arguments. 1099 * If the player is not alive nothing is done. 1100 */ 1101 public void handleTextMessage(string message) { 1102 if(!this.alive || message.length == 0) return; 1103 if(message[0] == '/') { 1104 this.callCommand(message[1..$]); 1105 } else { 1106 message = unformat(message); // pocket and custom clients can send formatted messages 1107 PlayerChatEvent event = this.world.callEventIfExists!PlayerChatEvent(this, message); 1108 if(event is null || (event.format is null && !event.cancelled)) { 1109 this.world.broadcast("<" ~ this.chatName ~ "> " ~ message); 1110 } else if(!event.cancelled) { 1111 this.world.broadcast(event.format(this.chatName, event.message)); 1112 } 1113 } 1114 } 1115 1116 /* 1117 * A movement generated by the client that could be in space or just a rotation of the body. 1118 * If the player is not alive or the position and the rotations are exacatly the same as the current 1119 * ones in the player nothing is done. 1120 * If this condition is surpassed a PlayerMoveEvent is called and if not cancelled the player will be 1121 * moved through the Entity.move method, the exhaustion will be applied and the world will update the 1122 * player's chunks if necessary. If the event is cancelled the position is sent back to the client, 1123 * teleporting it to the position known by the server. 1124 */ 1125 protected void handleMovementPacket(EntityPosition position, float yaw, float bodyYaw, float pitch) { 1126 this.do_movement = true; 1127 this.last_position = position; 1128 this.last_yaw = yaw; 1129 this.last_body_yaw = bodyYaw; 1130 this.last_pitch = pitch; 1131 } 1132 1133 /// ditto 1134 private void handleMovementPacketImpl(EntityPosition position, float yaw, float bodyYaw, float pitch) { 1135 if(!selery.math.vector.isFinite(position) || /*position < int.min || position > int.max || */!isFinite(yaw) || !isFinite(bodyYaw) || !isFinite(pitch)) { 1136 //warning_log(this.name, " sent an invalid position! x: ", position.x, ", y: ", position.y, ", z: ", position.z, ", yaw: ", yaw, ", bodyYaw: ", bodyYaw, ", pitch: ", pitch); 1137 this.kick("Invalid position!"); 1138 } else { 1139 auto old = this.position; 1140 yaw = yaw < 0 ? (360f + yaw % -360f) : (yaw % 360f); 1141 bodyYaw = bodyYaw < 0 ? (360f + bodyYaw % -360f) : (bodyYaw % 360f); 1142 pitch = clamp(pitch, -90, 90); 1143 if(!this.alive || this.position == position && this.yaw == yaw && this.bodyYaw == bodyYaw && this.pitch == pitch) return; 1144 if(this.world.callCancellableIfExists!PlayerMoveEvent(this, position, yaw, bodyYaw, pitch)) { 1145 //send the position back 1146 if(this.position == old) this.sendPosition(); 1147 } else { 1148 //exhaust //TODO swimming 1149 auto dis = distance(cast(Vector2!float)old, cast(Vector2!float)position); 1150 //TODO fix the distance! 1151 if(dis > 0) this.exhaust((this.sprinting ? Exhaustion.SPRINTING : (this.sneaking ? Exhaustion.SNEAKING : Exhaustion.WALKING)) * distance(cast(Vector2!float)this.position, cast(Vector2!float)position)); 1152 //update the position 1153 this.move(position, yaw, bodyYaw, pitch); 1154 if(dis > 0) this.world.playerUpdateRadius(this); 1155 } 1156 } 1157 } 1158 1159 /* 1160 * Starts breaking a generic block (not tapping). 1161 * If the player is alive and the target block exists and is not air the 'is_breaking' flag is set 1162 * to true. 1163 * If the player is in creative mode or the block's breaking time is 0, handleBlockBreaking is called. 1164 * Returns: 1165 * true id the player is digging a block, false otherwise 1166 */ 1167 protected bool handleStartBlockBreaking(BlockPosition position) { 1168 if(this.alive) { 1169 Block b = this.world[position]; 1170 if(!b.indestructible) { 1171 this.breaking = position; 1172 this.is_breaking = true; 1173 if(b.instantBreaking || this.creative || Effects.haste in this) { // TODO remove haste from here and add hardness 1174 this.handleBlockBreaking(); 1175 } 1176 } 1177 } 1178 return this.is_breaking; 1179 } 1180 1181 /* 1182 * Stops breaking the current block and sets the 'is_breaking' flag to false, without removing 1183 * it and consuming any tool. 1184 */ 1185 protected void handleAbortBlockBreaking() { 1186 this.is_breaking = false; 1187 } 1188 1189 /* 1190 * Stops breaking the block indicated in the variable 'breaking', calls the event, consumes the tool 1191 * and exhausts the player. 1192 */ 1193 protected bool handleBlockBreaking() { 1194 bool cancelitem = false; 1195 bool cancelblock = false; 1196 //log(!this.world.rules.immutableWorld, " ", this.alive, " ", this.is_breaking, " ", this.world[breaking] != Blocks.AIR); 1197 if(this.alive && this.is_breaking && !this.world[this.breaking].indestructible) { 1198 auto event = this.world.callEventIfExists!PlayerBreakBlockEvent(this, this.world[this.breaking], this.breaking); 1199 if(event !is null && event.cancelled) { 1200 cancelitem = true; 1201 cancelblock = true; 1202 } else { 1203 //consume the item 1204 if((event is null || event.consumeItem) && !this.inventory.held.empty && this.inventory.held.item.tool) { 1205 this.inventory.held.item.destroyOn(this, this.world[this.breaking], this.breaking); 1206 if(this.inventory.held.item.finished) { 1207 this.inventory.held = Slot(null); 1208 } 1209 } else { 1210 cancelitem = true; 1211 } 1212 if(event is null || event.drop) { 1213 foreach(Slot slot ; this.world[this.breaking].drops(this.world, this, this.inventory.held.item)) { 1214 this.world.drop(slot, this.breaking.entityPosition + .5); 1215 } 1216 } 1217 //if(event.particles) this.world.addParticle(new Particles.Destroy(this.breaking.entityPosition, this.world[this.breaking])); 1218 if(event is null || event.removeBlock) { 1219 this.world[this.breaking] = Blocks.air; 1220 } else { 1221 cancelblock = true; 1222 } 1223 this.exhaust(Exhaustion.BREAKING_BLOCK); 1224 } 1225 } else { 1226 cancelitem = true; 1227 cancelblock = true; 1228 } 1229 if(cancelblock && this.is_breaking && this.world[this.breaking] !is null) { 1230 this.sendBlock(PlacedBlock(this.breaking, this.world[this.breaking].data)); 1231 auto tile = this.world.tileAt(this.breaking); 1232 if(tile !is null) { 1233 this.sendTile(tile, false); 1234 } 1235 } 1236 if(cancelitem && !this.inventory.held.empty && this.inventory.held.item.tool) { 1237 this.inventory.update = PlayerInventory.HELD; 1238 } 1239 this.is_breaking = false; 1240 return !cancelblock; 1241 } 1242 1243 protected void handleBlockPlacing(BlockPosition tpos, uint tface) { 1244 /*BlockPosition position = tpos.face(tface); 1245 //TODO calling events on player and on block 1246 Block placed = this.inventory.held.item.place(this.world, position); 1247 if(placed !is null) { 1248 this.world[position] = placed; 1249 } else { 1250 //event cancelled or unavailable 1251 this.sendBlock(PlacedBlock(position, this.world[position])); 1252 }*/ 1253 if(this.world.callCancellableIfExists!PlayerPlaceBlockEvent(this, this.inventory.held, tpos, tface) || !this.inventory.held.item.onPlaced(this, tpos, tface)) { 1254 //no block placed! 1255 //sends the block back 1256 this.sendBlock(PlacedBlock(tpos.face(tface), this.world[tpos.face(tface)].data)); 1257 } 1258 } 1259 1260 protected void handleArmSwing() { 1261 this.do_animation = true; 1262 } 1263 1264 private void handleArmSwingImpl() { 1265 if(this.alive) { 1266 if(!this.world.callCancellableIfExists!PlayerAnimationEvent(this)) { 1267 if(!this.inventory.held.empty && this.inventory.held.item.onThrowed(this)) { 1268 this.actionFlag = true; 1269 this.broadcastMetadata(); 1270 } else { 1271 this.viewers!Player.call!"sendAnimation"(this); 1272 } 1273 } 1274 } 1275 } 1276 1277 protected void handleAttack(uint entity) { 1278 if(entity != this.id) this.handleAttack(this.world.entity(entity)); 1279 } 1280 1281 protected void handleAttack(Entity entity) { 1282 if(this.alive && entity !is null && (cast(Player)entity is null || this.world.pvp)) { 1283 if(cast(Player)entity ? !entity.attack(new PlayerAttackedByPlayerEvent(cast(Player)entity, this)).cancelled : !entity.attack(new EntityAttackedByPlayerEvent(entity, this)).cancelled) { 1284 this.exhaust(Exhaustion.ATTACKING); 1285 } 1286 } 1287 } 1288 1289 protected void handleInteract(uint entity) { 1290 if(entity != this.id) this.handleInteract(this.world.entity(entity)); 1291 } 1292 1293 protected void handleInteract(Entity entity) { 1294 //TODO 1295 if(this.alive && (!this.inventory.held.empty && this.inventory.held.item.onThrowed(this) && !this.creative)) { 1296 //remove one from inventory 1297 this.inventory.held.count--; 1298 if(this.inventory.held.empty) this.inventory.held = Slot(null); 1299 else this.inventory.update = 0; 1300 } 1301 } 1302 1303 protected void handleReleaseItem() { 1304 //TODO 1305 } 1306 1307 protected void handleStopSleeping() { 1308 //TODO 1309 } 1310 1311 protected void handleRespawn() { 1312 if(this.dead) { 1313 this.respawn(); 1314 } 1315 } 1316 1317 protected void handleJump() { 1318 if(this.alive) { 1319 // event 1320 this.world.callEventIfExists!PlayerJumpEvent(this); 1321 // exhaustion 1322 this.exhaust(this.sprinting ? Exhaustion.SPRINTED_JUMP : Exhaustion.JUMPING); 1323 } 1324 } 1325 1326 protected void handleSprinting(bool sprint) { 1327 if(this.alive && sprint ^ this.sprinting) { 1328 if(sprint) { 1329 this.world.callEventIfExists!PlayerStartSprintingEvent(this); 1330 } else { 1331 this.world.callEventIfExists!PlayerStopSprintingEvent(this); 1332 } 1333 this.sprinting = sprint; 1334 //if(this.pe) this.recalculateSpeed(); 1335 } 1336 } 1337 1338 protected void handleSneaking(bool sneak) { 1339 if(this.alive && sneak ^ this.sneaking) { 1340 //auto event = sneak ? new PlayerStartSneakingEvent(this) : new PlayerStopSneakingEvent(this); 1341 //this.world.callEvent(event); 1342 if(sneak) { 1343 this.world.callEventIfExists!PlayerStartSneakingEvent(this); 1344 } else { 1345 this.world.callEventIfExists!PlayerStopSneakingEvent(this); 1346 } 1347 this.sneaking = sneak; 1348 } 1349 } 1350 1351 protected void handleChangeDimension() { 1352 //TODO 1353 } 1354 1355 protected bool consumeItemInHand() { 1356 if(!this.inventory.held.empty && this.inventory.held.item.consumeable/* && this.hunger < 20*/) { 1357 Item ret = this.inventory.held.item.onConsumed(this); 1358 if(this.consumeTools) { 1359 if(ret is null) { 1360 this.inventory.held = Slot(this.inventory.held.item, to!ubyte(this.inventory.held.count - 1)); 1361 if(!this.inventory.held.empty) { 1362 //don't need to update the viewers 1363 this.inventory.update_viewers &= PlayerInventory.HELD ^ 0xF; 1364 } 1365 } else { 1366 this.inventory.held = Slot(ret, 1); 1367 } 1368 } 1369 return true; 1370 } else { 1371 this.inventory.update = PlayerInventory.HELD; 1372 return false; 1373 } 1374 } 1375 1376 protected final void handleRightClick(BlockPosition tpos, uint tface) { 1377 //called only when !inventory.held.empty 1378 BlockPosition position = tpos.face(tface); 1379 Block block = this.world[tpos]; 1380 if(block !is null) { 1381 //TODO call events 1382 if(this.inventory.held.item.useOnBlock(this, block, tpos, tface & 255)) { 1383 this.inventory.held = this.inventory.held.item.finished ? Slot(null) : this.inventory.held; 1384 } 1385 } 1386 } 1387 1388 protected final void handleMapRequest(ushort mapId) { 1389 /*if(!this.world.callCancellableIfExists!PlayerRequestMapEvent(this, mapId)) { 1390 auto map = this.world[mapId]; 1391 if(map !is null) { 1392 this.sendMap(map); 1393 } else { 1394 //TODO generate 1395 } 1396 }*/ 1397 } 1398 1399 protected final bool handleDrop(Slot slot) { 1400 if(!this.world.callCancellableIfExists!PlayerDropItemEvent(this, slot)) { 1401 this.drop(slot); 1402 return true; 1403 } else { 1404 return false; 1405 } 1406 } 1407 1408 1409 // *** ABSTRACT HANDLING *** 1410 1411 protected uint order; 1412 1413 public abstract void handle(ubyte id, ubyte[] data); 1414 1415 public abstract void flush(); 1416 1417 protected void sendPacketPayload(ubyte[] payload) { 1418 Handler.sharedInstance.send(new HncomPlayer.OrderedGamePacket(this.hubId, this.order++, payload).encode()); 1419 } 1420 1421 private Tid compression; 1422 1423 protected void startCompression(T:Compression)(uint hubId) { 1424 static import std.concurrency; 1425 this.compression = std.concurrency.spawn(&startCompressionImpl!T, hubId); 1426 } 1427 1428 protected void stopCompression() { 1429 // send a stop message 1430 send(this.compression, uint.max, (immutable(ubyte)[]).init); 1431 } 1432 1433 protected void compress(ubyte[] payload) { 1434 send(this.compression, this.order++, payload.idup); 1435 } 1436 1437 protected static abstract class Compression { 1438 1439 public void start(uint hubId) { 1440 1441 auto handler = Handler.sharedInstance(); 1442 1443 while(true) { 1444 1445 auto data = receiveOnly!(uint, immutable(ubyte)[]); // tuple(order, uncompressed payload) 1446 1447 if(data[0] == uint.max) break; 1448 1449 handler.send(new HncomPlayer.OrderedGamePacket(hubId, data[0], this.compress(data[1].dup)).encode()); 1450 1451 } 1452 1453 } 1454 1455 protected abstract ubyte[] compress(ubyte[] payload); 1456 1457 } 1458 1459 } 1460 1461 private void startCompressionImpl(T)(uint hubId) { 1462 Thread.getThis().name = "Compression@" ~ to!string(hubId); 1463 auto c = new T(); 1464 c.start(hubId); 1465 } 1466 1467 enum InputMode : ubyte { 1468 1469 keyboard = HncomPlayer.Add.KEYBOARD, 1470 touch = HncomPlayer.Add.TOUCH, 1471 controller = HncomPlayer.Add.CONTROLLER, 1472 1473 } 1474 1475 enum DeviceOS : ubyte { 1476 1477 unknown = 0, 1478 android = 1, 1479 ios = 2, 1480 osx = 3, 1481 fireos = 4, 1482 gearvr = 5, 1483 hololens = 6, 1484 win10 = 7, 1485 win32 = 8, 1486 dedicated = 9, 1487 orbis = 10, 1488 nx = 11 1489 1490 } 1491 1492 enum PermissionLevel : ubyte { 1493 1494 user, 1495 operator, 1496 host, 1497 automation, 1498 admin, 1499 1500 } 1501 1502 /** 1503 * Checks whether or not the given symbol is of a connected player class. 1504 * Returns: 1505 * true if the given symbol is or extends Player and not Puppet 1506 * Example: 1507 * --- 1508 * assert(isPlayer!Player); 1509 * assert(!isPlayer!Puppet); 1510 * assert(isPlayer!PocketPlayerBase); 1511 * assert(isPlayer!(MinecraftPlayer!210)); 1512 * assert(!isPlayer!(Projection!Puppet)); 1513 * --- 1514 */ 1515 enum isPlayer(T) = is(T : Player) && !is(T : Puppet); 1516 1517 /** 1518 * Checks if the given entity is an instance of a connected player. 1519 * Params: 1520 * entity = an instance of an entity 1521 * Returns: 1522 * true if the entity can be casted to Player and not to Puppet 1523 * Example: 1524 * --- 1525 * assert(isPlayerInstance(player!"steve")); 1526 * assert(!isPlayerInstance(new Puppet(player!"steve"))); 1527 * --- 1528 */ 1529 public @safe @nogc bool isPlayerInstance(Entity entity) { 1530 return cast(Player)entity && !cast(Puppet)entity; 1531 } 1532 1533 mixin template generateHandlers(E...) { 1534 1535 public override void handle(ubyte _id, ubyte[] _data) { 1536 switch(_id) { 1537 foreach(P ; E) { 1538 static if(P.SERVERBOUND && (is(typeof(mixin("handle" ~ P.stringof ~ "Packet"))) || is(typeof(P.variantField)))) { 1539 case P.ID: 1540 { 1541 P _packet = P.fromBuffer!false(_data); 1542 static if(!is(typeof(P.variantField))) { 1543 with(_packet) mixin("return this.handle" ~ P.stringof ~ "Packet(" ~ P.FIELDS.join(",") ~ ");"); 1544 } else { 1545 switch(mixin("_packet." ~ P.variantField)) { 1546 foreach(V ; P.Variants) { 1547 static if(is(typeof(mixin("handle" ~ P.stringof ~ V.stringof ~ "Packet")))) { 1548 mixin((){ import std.string:toUpper;return "case V." ~ P.variantField.toUpper ~ ":"; }()); //TODO convert to upper snake case 1549 { 1550 V _variant = _packet..new V(); 1551 _variant.decode(); 1552 with(_packet) { with(_variant) mixin("return this.handle" ~ P.stringof ~ V.stringof ~ "Packet(" ~ join((){ string[] f;foreach(fl;P.FIELDS){if(fl!=P.variantField){f~=fl;}}return f; }() ~ V.FIELDS, ",") ~ ");"); } 1553 } 1554 } 1555 } 1556 default: return; 1557 } 1558 } 1559 } 1560 } else version(ShowUnhandled) { 1561 static if(P.SERVERBOUND) pragma(msg, "Packet " ~ P.stringof ~ " is not handled"); 1562 } 1563 } 1564 default: 1565 version(ShowUnhandled) error_log("Unknown packet '", _id, "' with ", _data.length, " bytes"); 1566 break; 1567 } 1568 } 1569 1570 } 1571 1572 /** 1573 * Unconnected player for visualization. 1574 * In the world is registered as an entity and it will 1575 * not be found in the array of player obtained with 1576 * world.online!Player nor in the count obtained with 1577 * world.count!Player. 1578 * Example: 1579 * --- 1580 * //spawn a puppet 10 blocks over a player that follows it 1581 * class PuppetWorld : World { 1582 * 1583 * private Puppet[uint] puppets; 1584 * 1585 * public @event join(PlayerSpawnEvent event) { 1586 * this.puppets[event.player.id] = this.spawn!Puppet(event.player.position, event.player.name, event.player.skin, event.player.uuid); 1587 * } 1588 * 1589 * public @event left(PlayerDespawnEvent event) { 1590 * this.despawn(this.puppets[event.player.id]); 1591 * this.puppets.remove(event.player.id); 1592 * } 1593 * 1594 * public @event move(PlayerMoveEvent event) { 1595 * this.puppets[event.player.id].move(event.position, event.yaw, event.bodyYaw, event.pitch); 1596 * } 1597 * 1598 * } 1599 * --- 1600 * Example: 1601 * --- 1602 * // Unticked puppets will reduce the CPU usage 1603 * auto ticked = new Puppet(); 1604 * auto unticked = new Unticked!Puppet(); 1605 * --- 1606 */ 1607 class Puppet : Player { 1608 1609 public this(World world, EntityPosition position, string name, Skin skin, UUID uuid) { 1610 //TODO create a playerinfo 1611 super(null, world, position); 1612 } 1613 1614 public this(World world, EntityPosition position, string name, Skin skin) { 1615 this(world, position, name, skin, world.server.nextUUID); 1616 } 1617 1618 public this(World world, EntityPosition position, string name) { 1619 this(world, position, name, Skin.STEVE); 1620 } 1621 1622 public this(World world, Player from) { 1623 this(world, from.position, from.name, from.skin, from.uuid); 1624 } 1625 1626 protected override @safe @nogc void sendMovementUpdates(Entity[] entities) {} 1627 1628 protected override @safe @nogc void sendMotionUpdates(Entity[] entities) {} 1629 1630 protected override @safe @nogc void sendTipImpl(Message[] messages) {} 1631 1632 protected override @safe @nogc void sendTitleImpl(Title title, Subtitle subtitle, uint fadeIn, uint stay, uint fadeOut) {} 1633 1634 protected override @safe @nogc void sendHideTitles() {} 1635 1636 protected override @safe @nogc void sendResetTitles() {} 1637 1638 public override @safe @nogc void sendGamemode() {} 1639 1640 public override @safe @nogc void sendOpenContainer(ubyte type, ushort slots, BlockPosition position) {} 1641 1642 public override @safe @nogc void spawnToItself() {} 1643 1644 public override @safe @nogc void sendAddList(Player[] players) {} 1645 1646 public override @safe @nogc void sendRemoveList(Player[] players) {} 1647 1648 protected override @safe @nogc void sendSpawnPosition() {} 1649 1650 protected override @safe @nogc void sendPosition() {} 1651 1652 public override @safe @nogc void sendMetadata(Entity entity) {} 1653 1654 public override @safe @nogc void sendChunk(Chunk chunk) {} 1655 1656 public override @safe @nogc void unloadChunk(ChunkPosition pos) {} 1657 1658 public override @safe @nogc void sendInventory(ubyte flag=PlayerInventory.ALL, bool[] slots=[]) {} 1659 1660 public override @safe @nogc void sendHeld() {} 1661 1662 public override @safe @nogc void sendEntityEquipment(Player player) {} 1663 1664 public override @safe @nogc void sendArmorEquipment(Player player) {} 1665 1666 public override @safe @nogc void sendHurtAnimation(Entity entity) {} 1667 1668 public override @safe @nogc void sendDeathAnimation(Entity entity) {} 1669 1670 protected override @safe @nogc void sendDeathSequence() {} 1671 1672 protected override @safe @nogc void experienceUpdated() {} 1673 1674 public override @safe @nogc void sendJoinPacket() {} 1675 1676 public override @safe @nogc void sendResourcePack() {} 1677 1678 public override void sendPermissionLevel(PermissionLevel permissionLevel) {} 1679 1680 public override @safe @nogc void sendDifficulty(Difficulty difficulty) {} 1681 1682 public override @safe @nogc void sendWorldGamemode(Gamemode gamemode) {} 1683 1684 public override @safe @nogc void sendSettingsPacket() {} 1685 1686 public override @safe @nogc void sendRespawnPacket() {} 1687 1688 public override @safe @nogc void setAsReadyToSpawn() {} 1689 1690 public override @safe @nogc void sendLightning(Lightning lightning) {} 1691 1692 public override @safe @nogc void sendAnimation(Entity entity) {} 1693 1694 public override @safe @nogc void sendBlocks(PlacedBlock[] block) {} 1695 1696 public override @safe @nogc void sendTile(Tile tiles, bool translatable) {} 1697 1698 public override @safe @nogc void sendPickupItem(Entity picker, Entity picked) {} 1699 1700 public override @safe @nogc void sendPassenger(ubyte mode, uint passenger, uint vehicle) {} 1701 1702 public override @safe @nogc void sendExplosion(EntityPosition position, float radius, Vector3!byte[] updates) {} 1703 1704 public override @safe @nogc void sendMap(Map map) {} 1705 1706 public override @safe @nogc void handle(ubyte id, ubyte[] buffer) {} 1707 1708 } 1709 1710 struct TitleImpl { 1711 1712 Message[] message; 1713 1714 this(E...)(E args) { 1715 this.message = Message.convert(args); 1716 } 1717 1718 alias message this; 1719 1720 } 1721 1722 alias Title = TitleImpl; 1723 1724 alias Subtitle = TitleImpl;