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/java.d, selery/player/java.d) 28 */ 29 module selery.player.java; 30 31 import std.algorithm : sort, min, canFind, clamp; 32 import std.conv : to; 33 import std.digest.digest : toHexString; 34 import std.digest.sha : sha1Of; 35 import std.json : JSONValue; 36 import std.math : abs, log2, ceil; 37 import std.socket : Address; 38 import std.string : split, join, toLower; 39 import std.system : Endian; 40 import std.uuid : UUID; 41 import std.zlib : Compress, HeaderFormat; 42 43 import sel.nbt.stream; 44 import sel.nbt.tags; 45 46 import selery.about; 47 import selery.block.block : Block, PlacedBlock; 48 import selery.block.tile : Tile; 49 import selery.config : Gamemode, Difficulty, Dimension; 50 import selery.effect : Effect; 51 import selery.entity.entity : Entity; 52 import selery.entity.human : Skin; 53 import selery.entity.living : Living; 54 import selery.entity.metadata : SelMetadata = Metadata; 55 import selery.entity.noai : ItemEntity, Lightning; 56 import selery.event.world.player : PlayerMoveEvent; 57 import selery.inventory.inventory; 58 import selery.item.slot : Slot; 59 import selery.lang : Translation; 60 import selery.log : Format, Message; 61 import selery.math.vector; 62 import selery.node.info : PlayerInfo; 63 import selery.player.player; 64 import selery.util.util : array_index; 65 import selery.world.chunk : Chunk; 66 import selery.world.map : Map; 67 import selery.world.world : World; 68 69 import sul.utils.var : varuint; 70 71 abstract class JavaPlayer : Player { 72 73 protected static string resourcePack, resourcePackPort, resourcePack2Hash, resourcePack3Hash; 74 75 public static ulong ulongPosition(BlockPosition position) { 76 return (to!long(position.x & 0x3FFFFFF) << 38) | (to!long(position.y & 0xFFF) << 26) | (position.z & 0x3FFFFFF); 77 } 78 79 public static BlockPosition blockPosition(ulong position) { 80 int nval(uint num) { 81 if((num & 0x3000000) == 0) return num; 82 else return -(num ^ 0x3FFFFFF) - 1; 83 } 84 return BlockPosition(nval((position >> 38) & 0x3FFFFFF), (position >> 26) & 0xFFF, nval(position & 0x3FFFFFF)); 85 } 86 87 protected static byte convertDimension(Dimension dimension) { 88 with(Dimension) final switch(dimension) { 89 case overworld: return 0; 90 case nether: return -1; 91 case end: return 1; 92 } 93 } 94 95 public static void updateResourcePacks(void[] rp2, void[] rp3, string url, ushort port) { 96 resourcePack = url; 97 resourcePackPort = ":" ~ to!string(port); 98 resourcePack2Hash = toLower(toHexString(sha1Of(rp2))); 99 resourcePack3Hash = toLower(toHexString(sha1Of(rp3))); 100 } 101 102 private bool consuming; 103 private uint consuming_time; 104 105 private bool first_spawned; 106 107 private ushort[] loaded_maps; 108 109 public this(shared PlayerInfo info, World world, EntityPosition position) { 110 super(info, world, position); 111 if(resourcePack.length == 0) { 112 // no resource pack 113 this.hasResourcePack = true; 114 } 115 } 116 117 public override void tick() { 118 super.tick(); 119 if(this.consuming) { 120 if(++this.consuming_time == 30) { 121 this.consuming_time = 0; 122 if(!this.consumeItemInHand()) { 123 this.consuming = false; 124 } 125 } 126 } 127 } 128 129 alias world = super.world; 130 131 public override @property @trusted World world(World world) { 132 this.loaded_maps.length = 0; 133 return super.world(world); 134 } 135 136 public final override void disconnectImpl(const Translation translation) { 137 if(translation.translatable.java.length) { 138 this.server.kick(this.hubId, translation.translatable.java, translation.parameters); 139 } else { 140 this.disconnect(this.server.lang.translate(translation, this.language)); 141 } 142 } 143 144 /** 145 * Encodes a message into a JSONValue that can be parsed and displayed 146 * by the client. 147 * More info on the format: wiki.vg/Chat 148 */ 149 public JSONValue encodeMessage(Message[] messages) { 150 JSONValue[] array; 151 JSONValue[string] current_format; 152 void parseText(string text) { 153 auto e = current_format.dup; 154 e["text"] = text; 155 array ~= JSONValue(e); 156 } 157 foreach(message ; messages) { 158 final switch(message.type) { 159 case Message.FORMAT: 160 switch(message.format) with(Format) { 161 case darkBlue: current_format["color"] = "dark_blue"; break; 162 case darkGreen: current_format["color"] = "dark_green"; break; 163 case darkAqua: current_format["color"] = "dark_aqua"; break; 164 case darkRed: current_format["color"] = "dark_red"; break; 165 case darkPurple: current_format["color"] = "dark_purple"; break; 166 case darkGray: current_format["color"] = "dark_gray"; break; 167 case lightPurple: current_format["color"] = "light_purple"; break; 168 case obfuscated: 169 case bold: 170 case strikethrough: 171 case underlined: 172 case italic: 173 current_format[message.format.to!string] = true; 174 break; 175 case reset: current_format.clear(); break; 176 default: 177 current_format["color"] = message.format.to!string; 178 break; 179 } 180 break; 181 case Message.TEXT: 182 parseText(message.text); 183 break; 184 case Message.TRANSLATION: 185 if(message.translation.translatable.java.length) { 186 auto e = current_format.dup; 187 e["translate"] = message.translation.translatable.java; 188 e["with"] = message.translation.parameters; 189 array ~= JSONValue(e); 190 } else { 191 parseText(this.server.lang.translate(message.translation.translatable.default_, message.translation.parameters, this.language)); 192 } 193 break; 194 } 195 } 196 if(array.length == 1) return array[0]; 197 else if(array.length) return JSONValue(["text": JSONValue(""), "extra": JSONValue(array)]); 198 else return JSONValue(["text": ""]); 199 } 200 201 protected void handleClientStatus() { 202 this.respawn(); 203 this.sendRespawnPacket(); 204 this.sendPosition(); 205 } 206 207 public void handleResourcePackStatusPacket(uint status) { 208 this.hasResourcePack = (status == 0); 209 //log(status); 210 } 211 212 } 213 214 class JavaPlayerImpl(uint __protocol) : JavaPlayer if(supportedJavaProtocols.canFind(__protocol)) { 215 216 mixin("import Types = sul.protocol.java" ~ __protocol.to!string ~ ".types;"); 217 mixin("import Clientbound = sul.protocol.java" ~ __protocol.to!string ~ ".clientbound;"); 218 mixin("import Serverbound = sul.protocol.java" ~ __protocol.to!string ~ ".serverbound;"); 219 220 mixin("import sul.attributes.java" ~ __protocol.to!string ~ " : Attributes;"); 221 mixin("import sul.metadata.java" ~ __protocol.to!string ~ " : Metadata;"); 222 223 // also used by ItemEntity 224 public static Types.Slot toSlot(Slot slot) { 225 if(slot.empty) { 226 return Types.Slot(-1); 227 } else { 228 auto ret = Types.Slot(slot.item.javaId, slot.count, slot.item.javaMeta, [NBT_TYPE.END]); 229 if(slot.item.javaCompound !is null) { 230 auto stream = new ClassicStream!(Endian.bigEndian)(); 231 stream.writeTag(cast(Tag)slot.item.javaCompound); 232 ret.nbt = stream.buffer; 233 } 234 return ret; 235 } 236 } 237 238 protected Slot fromSlot(Types.Slot slot) { 239 if(slot.id <= 0) { 240 return Slot(null); 241 } else { 242 auto item = this.world.items.fromJava(slot.id, slot.damage); 243 if(slot.nbt.length) { 244 auto tag = new ClassicStream!(Endian.bigEndian)(slot.nbt).readTag(); 245 if(cast(Compound)tag) item.parseJavaCompound(cast(Compound)tag); 246 } 247 return Slot(item, slot.count); 248 } 249 } 250 251 protected Types.Slot[] toSlots(Slot[] slots) { 252 Types.Slot[] ret = new Types.Slot[slots.length]; 253 foreach(i, slot; slots) { 254 ret[i] = toSlot(slot); 255 } 256 return ret; 257 } 258 259 protected Slot[] fromSlots(Types.Slot[] slots) { 260 Slot[] ret = new Slot[slots.length]; 261 foreach(i, slot; slots) { 262 ret[i] = this.fromSlot(slot); 263 } 264 return ret; 265 } 266 267 public Metadata metadataOf(SelMetadata metadata) { 268 mixin("return metadata.java" ~ __protocol.to!string ~ ";"); 269 } 270 271 private Slot picked_up_item; 272 273 private bool dragging; 274 private size_t[] dragged_slots; 275 276 public this(shared PlayerInfo info, World world, EntityPosition position) { 277 super(info, world, position); 278 this.startCompression!Compression(hubId); 279 } 280 281 protected void sendPacket(T)(T packet) if(is(typeof(T.encode))) { 282 ubyte[] payload = packet.encode(); 283 if(payload.length > 1024) { 284 this.compress(payload); 285 } else { 286 this.sendPacketPayload(0 ~ payload); 287 } 288 } 289 290 public override void flush() {} 291 292 293 protected override void sendCompletedMessages(string[] messages) { 294 static if(__protocol < 307) { 295 sort!"a < b"(messages); 296 } 297 this.sendPacket(new Clientbound.TabComplete(messages)); 298 } 299 300 protected override void sendMessageImpl(Message[] messages) { 301 this.sendPacket(new Clientbound.ChatMessage(this.encodeMessage(messages).toString(), Clientbound.ChatMessage.CHAT)); 302 } 303 304 protected override void sendTipImpl(Message[] messages) { 305 static if(__protocol >= 305) { 306 this.sendPacket(new Clientbound.Title().new SetActionBar(this.encodeMessage(messages).toString())); 307 } else { 308 this.sendPacket(new Clientbound.ChatMessage(this.encodeMessage(messages).toString(), Clientbound.ChatMessage.ABOVE_HOTBAR)); 309 } 310 } 311 312 protected override void sendTitleImpl(Title title, Subtitle subtitle, uint fadeIn, uint stay, uint fadeOut) { 313 this.sendPacket(new Clientbound.Title().new SetTitle(this.encodeMessage(title).toString())); 314 if(subtitle.length) this.sendPacket(new Clientbound.Title().new SetSubtitle(this.encodeMessage(subtitle).toString())); 315 this.sendPacket(new Clientbound.Title().new SetTimings(fadeIn, stay, fadeOut)); 316 } 317 318 protected override void sendHideTitles() { 319 this.sendPacket(new Clientbound.Title().new Hide()); 320 } 321 322 protected override void sendResetTitles() { 323 this.sendPacket(new Clientbound.Title().new Reset()); 324 } 325 326 public override void sendMovementUpdates(Entity[] entities) { 327 foreach(Entity entity ; entities) { 328 //TODO check for old rotation 329 if(entity.oldposition != entity.position) { 330 if(abs(entity.position.x - entity.oldposition.x) <= 8 && abs(entity.position.y - entity.oldposition.y) <= 8 && abs(entity.position.z - entity.oldposition.z) <= 8) { 331 this.sendPacket(new Clientbound.EntityLookAndRelativeMove(entity.id, (cast(Vector3!short)round((entity.position * 32 - entity.oldposition * 32) * 128)).tuple, entity.angleYaw, entity.anglePitch, entity.onGround)); 332 } else { 333 this.sendPacket(new Clientbound.EntityTeleport(entity.id, entity.position.tuple, entity.angleYaw, entity.anglePitch, entity.onGround)); 334 } 335 } else { 336 this.sendPacket(new Clientbound.EntityLook(entity.id, entity.angleYaw, entity.anglePitch, entity.onGround)); 337 } 338 this.sendPacket(new Clientbound.EntityHeadLook(entity.id, cast(Living)entity ? (cast(Living)entity).angleBodyYaw : entity.angleYaw)); 339 } 340 } 341 342 public override void sendMotionUpdates(Entity[] entities) { 343 foreach(Entity entity ; entities) { 344 this.sendPacket(new Clientbound.EntityVelocity(entity.id, entity.velocity.tuple)); 345 } 346 } 347 348 public override void sendGamemode() { 349 this.sendPacket(new Clientbound.ChangeGameState(Clientbound.ChangeGameState.CHANGE_GAMEMODE, this.gamemode)); 350 } 351 352 public override void sendSpawnPosition() { 353 //this.sendPacket(Packet.SpawnPosition(toLongPosition(this.spawn.blockPosition))); 354 } 355 356 public override void spawnToItself() { 357 this.sendPacket(new Clientbound.PlayerListItem().new AddPlayer([this.encodePlayer(this)])); 358 } 359 360 public override void sendAddList(Player[] players) { 361 Types.ListAddPlayer[] list; 362 foreach(Player player ; players) { 363 list ~= this.encodePlayer(player); 364 } 365 this.sendPacket(new Clientbound.PlayerListItem().new AddPlayer(list)); 366 } 367 368 private Types.ListAddPlayer encodePlayer(Player player) { 369 return Types.ListAddPlayer(player.uuid, player.name, new Types.Property[0], player.gamemode, player.latency, player.name != player.displayName, JSONValue(["text": player.displayName]).toString()); 370 } 371 372 public override void sendUpdateLatency(Player[] players) { 373 Types.ListUpdateLatency[] list; 374 foreach(player ; players) { 375 list ~= Types.ListUpdateLatency(player.uuid, player.latency); 376 } 377 this.sendPacket(new Clientbound.PlayerListItem().new UpdateLatency(list)); 378 } 379 380 public override void sendRemoveList(Player[] players) { 381 UUID[] list; 382 foreach(Player player ; players) { 383 list ~= player.uuid; 384 } 385 this.sendPacket(new Clientbound.PlayerListItem().new RemovePlayer(list)); 386 } 387 388 alias sendMetadata = super.sendMetadata; 389 390 public override void sendMetadata(Entity entity) { 391 this.sendPacket(new Clientbound.EntityMetadata(entity.id, metadataOf(entity.metadata))); 392 } 393 394 public override void sendChunk(Chunk chunk) { 395 396 immutable overworld = chunk.world.dimension == Dimension.overworld; 397 398 uint sections = 0; 399 ubyte[] buffer; 400 foreach(ubyte i ; 0..16) { 401 auto s = i in chunk; 402 if(s) { 403 sections |= 1 << i; 404 405 auto section = *s; 406 407 uint[] palette = section.full ? [] : [0]; 408 uint[] pointers; 409 foreach(ubyte y ; 0..16) { 410 foreach(ubyte z ; 0..16) { 411 foreach(ubyte x ; cast(ubyte[])[7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8]) { 412 auto block = section[x, y, z]; 413 if(block && (*block).javaId != 0) { 414 uint b = (*block).javaId << 4 | (*block).javaMeta; 415 auto p = array_index(b, palette); 416 if(p >= 0) { 417 pointers ~= p & 255; 418 } else { 419 palette ~= b; 420 pointers ~= (palette.length - 1) & 255; 421 } 422 } else { 423 pointers ~= 0; 424 } 425 } 426 } 427 } 428 429 // using 8 = ubyte.sizeof 430 // something lower can be used (?) 431 uint size = to!uint(ceil(log2(palette.length))); 432 //if(size < 4) size = 4; 433 size = 8; //TODO this limits to 256 different blocks! 434 buffer ~= size & 255; 435 buffer ~= varuint.encode(palette.length.to!uint); 436 foreach(uint p ; palette) { 437 buffer ~= varuint.encode(p); 438 } 439 440 buffer ~= varuint.encode(4096 >> 3); // 4096 / 8 as ulong[].length 441 foreach(j ; pointers) { 442 buffer ~= j & 255; 443 } 444 445 buffer ~= section.skyLight; 446 if(overworld) buffer ~= section.blocksLight; 447 } 448 } 449 450 ubyte[16 * 16] biomes; 451 foreach(i, biome; chunk.biomes) { 452 biomes[i] = biome.id; 453 } 454 455 buffer ~= biomes; 456 457 auto packet = new Clientbound.ChunkData(chunk.position.tuple, true, sections, buffer); 458 459 auto stream = new ClassicStream!(Endian.bigEndian)(); 460 foreach(tile ; chunk.tiles) { 461 if(tile.javaCompound !is null) { 462 packet.tilesCount++; 463 auto compound = tile.javaCompound.dup; 464 compound["x"] = new Int(tile.position.x); 465 compound["y"] = new Int(tile.position.y); 466 compound["z"] = new Int(tile.position.z); 467 stream.writeTag(compound); 468 } 469 } 470 packet.tiles = stream.buffer; 471 472 this.sendPacket(packet); 473 474 } 475 476 public override void unloadChunk(ChunkPosition pos) { 477 this.sendPacket(new Clientbound.UnloadChunk(pos.tuple)); 478 } 479 480 public override void sendChangeDimension(Dimension _from, Dimension _to) { 481 auto from = convertDimension(_from); 482 auto to = convertDimension(_to); 483 if(from != to) this.sendPacket(new Clientbound.Respawn(to==-1?1:to-1)); 484 this.sendPacket(new Clientbound.Respawn(to, this.world.difficulty, this.world.gamemode, this.world.type)); 485 } 486 487 public override void sendInventory(ubyte flag=PlayerInventory.ALL, bool[] slots=[]) { 488 foreach(uint index, bool slot; slots) { 489 if(slot) { 490 auto s = this.inventory[index]; 491 this.sendPacket(new Clientbound.SetSlot(cast(ubyte)0, to!ushort(index < 9 ? index + 36 : index), toSlot(s))); 492 /*if(!s.empty && s.item == Items.MAP) { 493 ushort id = s.metas.pc; 494 if(!in_array(id, this.loaded_maps)) { 495 this.loaded_maps ~= id; 496 this.handleMapRequest(id); 497 } 498 }*/ 499 } 500 } 501 if((flag & PlayerInventory.HELD) != 0) this.sendHeld(); 502 } 503 504 public override void sendHeld() { 505 this.sendPacket(new Clientbound.SetSlot(cast(ubyte)0, to!ushort(27 + this.inventory.selected), toSlot(this.inventory.held))); 506 } 507 508 public override void sendEntityEquipment(Player player) { 509 this.sendPacket(new Clientbound.EntityEquipment(player.id, 0, toSlot(player.inventory.held))); 510 } 511 512 public override void sendArmorEquipment(Player player) { 513 foreach(uint i, Slot slot; player.inventory.armor) { 514 this.sendPacket(new Clientbound.EntityEquipment(player.id, 5 - i, toSlot(slot))); 515 } 516 } 517 518 public override void sendOpenContainer(ubyte type, ushort slots, BlockPosition position) { 519 //TODO 520 } 521 522 public override void sendHurtAnimation(Entity entity) { 523 this.sendPacket(new Clientbound.EntityStatus(entity.id, Clientbound.EntityStatus.PLAY_HURT_ANIMATION_AND_SOUND)); 524 } 525 526 public override void sendDeathAnimation(Entity entity) { 527 this.sendPacket(new Clientbound.EntityStatus(entity.id, Clientbound.EntityStatus.PLAY_DEATH_ANIMATION_AND_SOUND)); 528 } 529 530 protected override void sendDeathSequence() {} 531 532 protected override @trusted void experienceUpdated() { 533 this.sendPacket(new Clientbound.SetExperience(this.experience, this.level, 0)); //TODO total 534 } 535 536 protected override void sendPosition() { 537 this.sendPacket(new Clientbound.PlayerPositionAndLook(this.position.tuple, this.yaw, this.pitch, ubyte.init, 0)); 538 } 539 540 protected override void sendMotion(EntityPosition motion) { 541 auto ret = motion * 8000; 542 auto m = Vector3!short(clamp(ret.x, short.min, short.max), clamp(ret.y, short.min, short.max), clamp(ret.z, short.min, short.max)); 543 this.sendPacket(new Clientbound.EntityVelocity(this.id, m.tuple)); 544 } 545 546 public override void sendSpawnEntity(Entity entity) { 547 if(cast(Player)entity) this.sendAddPlayer(cast(Player)entity); 548 else this.sendAddEntity(entity); 549 } 550 551 public override void sendDespawnEntity(Entity entity) { 552 this.sendPacket(new Clientbound.DestroyEntities([entity.id])); 553 } 554 555 protected void sendAddPlayer(Player player) { 556 this.sendPacket(new Clientbound.SpawnPlayer(player.id, player.uuid, player.position.tuple, player.angleYaw, player.anglePitch, metadataOf(player.metadata))); 557 } 558 559 protected void sendAddEntity(Entity entity) { 560 //TODO xp orb 561 //TODO painting 562 if(entity.java) { 563 if(entity.object) this.sendPacket(new Clientbound.SpawnObject(entity.id, entity.uuid, entity.javaId, entity.position.tuple, entity.anglePitch, entity.angleYaw, entity.objectData, entity.velocity.tuple)); 564 else this.sendPacket(new Clientbound.SpawnMob(entity.id, entity.uuid, entity.javaId, entity.position.tuple, entity.angleYaw, entity.anglePitch, cast(Living)entity ? (cast(Living)entity).angleBodyYaw : entity.angleYaw, entity.velocity.tuple, metadataOf(entity.metadata))); 565 if(cast(ItemEntity)entity) this.sendMetadata(entity); 566 } 567 } 568 569 public override @trusted void healthUpdated() { 570 super.healthUpdated(); 571 this.sendPacket(new Clientbound.UpdateHealth(this.healthNoAbs, this.hunger, this.saturation)); 572 this.sendPacket(new Clientbound.EntityProperties(this.id, [Types.Attribute(Attributes.maxHealth.name, this.maxHealthNoAbs)])); 573 } 574 575 public override @trusted void hungerUpdated() { 576 super.hungerUpdated(); 577 this.sendPacket(new Clientbound.UpdateHealth(this.healthNoAbs, this.hunger, this.saturation)); 578 } 579 580 protected override void onEffectAdded(Effect effect, bool modified) { 581 if(effect.java) this.sendPacket(new Clientbound.EntityEffect(this.id, effect.java.id, effect.level, cast(uint)effect.duration, Clientbound.EntityEffect.SHOW_PARTICLES)); 582 } 583 584 protected override void onEffectRemoved(Effect effect) { 585 if(effect.java) this.sendPacket(new Clientbound.RemoveEntityEffect(this.id, effect.java.id)); 586 } 587 588 public override void recalculateSpeed() { 589 super.recalculateSpeed(); 590 this.sendPacket(new Clientbound.EntityProperties(this.id, [Types.Attribute(Attributes.movementSpeed.name, this.speed)])); 591 } 592 593 public override void sendJoinPacket() { 594 if(!this.first_spawned) { 595 this.sendPacket(new Clientbound.JoinGame(this.id, this.gamemode, convertDimension(this.world.dimension), this.world.difficulty, ubyte.max, this.world.type, false)); 596 this.first_spawned = true; 597 } 598 this.sendPacket(new Clientbound.PluginMessage("MC|Brand", cast(ubyte[])Software.name)); 599 } 600 601 public override void sendResourcePack() { 602 if(!this.hasResourcePack) { 603 // the game will show a confirmation popup for the first time the texture is downloaded 604 static if(__protocol < 301) { 605 enum v = "2"; 606 } else { 607 enum v = "3"; 608 } 609 string url = resourcePack; 610 if(this.connectedSameMachine) url = "127.0.0.1"; 611 else if(this.connectedSameNetwork) url = this.ip; // not tested 612 this.sendPacket(new Clientbound.ResourcePackSend("http://" ~ url ~ resourcePackPort ~ "/" ~ v, mixin("resourcePack" ~ v ~ "Hash"))); 613 } 614 } 615 616 public override void sendPermissionLevel(PermissionLevel permissionLevel) { 617 this.sendPacket(new Clientbound.EntityStatus(this.id, cast(ubyte)(Clientbound.EntityStatus.SET_OP_PERMISSION_LEVEL_0 + permissionLevel))); 618 } 619 620 public override void sendDifficulty(Difficulty difficulty) { 621 this.sendPacket(new Clientbound.ServerDifficulty(difficulty)); 622 } 623 624 public override void sendWorldGamemode(Gamemode gamemode) { 625 // not supported 626 } 627 628 public override void sendDoDaylightCycle(bool cycle) { 629 this.sendPacket(new Clientbound.TimeUpdate(this.world.ticks, cycle ? this.world.time : -this.world.time)); 630 } 631 632 public override void sendTime(uint time) { 633 this.sendPacket(new Clientbound.TimeUpdate(this.world.ticks, this.world.time.cycle ? time : -time)); 634 } 635 636 public override void sendWeather(bool raining, bool thunderous, uint time, uint intensity) { 637 this.sendPacket(new Clientbound.ChangeGameState(raining ? Clientbound.ChangeGameState.BEGIN_RAINING : Clientbound.ChangeGameState.END_RAINING, intensity - 1)); 638 } 639 640 public override void sendSettingsPacket() { 641 //TODO 642 //this.sendPacket(new MinecraftPlayerAbilites()); 643 } 644 645 public override void sendRespawnPacket() { 646 this.sendPacket(new Clientbound.Respawn(convertDimension(this.world.dimension), this.world.difficulty, to!ubyte(this.gamemode), this.world.type)); 647 } 648 649 public override void setAsReadyToSpawn() { 650 //if(!this.first_spawned) { 651 //this.sendPacket(packet!"PlayerPositionAndLook"(this)); 652 this.sendPosition(); 653 } 654 655 public override void sendLightning(Lightning lightning) { 656 this.sendPacket(new Clientbound.SpawnGlobalEntity(lightning.id, Clientbound.SpawnGlobalEntity.THUNDERBOLT, lightning.position.tuple)); 657 } 658 659 public override void sendAnimation(Entity entity) { 660 static if(__protocol >= 109) { 661 this.sendPacket(new Clientbound.Animation(entity.id, Clientbound.Animation.SWING_MAIN_ARM)); 662 } else { 663 this.sendPacket(new Clientbound.Animation(entity.id, Clientbound.Animation.SWING_ARM)); 664 } 665 } 666 667 public override void sendBlocks(PlacedBlock[] blocks) { 668 Types.BlockChange[][int][int] pc; 669 foreach(PlacedBlock block ; blocks) { 670 auto position = block.position; 671 pc[position.x >> 4][position.z >> 4] ~= Types.BlockChange((position.x & 15) << 4 | (position.z & 15), position.y & 255, block.java.id << 4 | block.java.meta); 672 } 673 foreach(x, pcz; pc) { 674 foreach(z, pb; pcz) { 675 this.sendPacket(new Clientbound.MultiBlockChange(ChunkPosition(x, z).tuple, pb)); 676 } 677 } 678 } 679 680 public override void sendTile(Tile tile, bool translatable) { 681 auto stream = new ClassicStream!(Endian.bigEndian)(); 682 auto packet = new Clientbound.UpdateBlockEntity(ulongPosition(tile.position), tile.action); 683 if(tile.javaCompound !is null) { 684 auto compound = tile.javaCompound.dup; 685 // signs become invisible without the coordinates 686 compound["x"] = new Int(tile.position.x); 687 compound["y"] = new Int(tile.position.y); 688 compound["z"] = new Int(tile.position.z); 689 stream.writeTag(compound); 690 packet.nbt = stream.buffer; 691 } else { 692 packet.nbt ~= 0; 693 } 694 this.sendPacket(packet); 695 /*if(translatable) { 696 tile.to!ITranslatable.translateStrings(this.lang); 697 } 698 //this.sendPacket(new MinecraftUpdateBlockEntity(tile)); 699 if(translatable) { 700 tile.to!ITranslatable.untranslateStrings(); 701 }*/ 702 } 703 704 public override @trusted void sendPickupItem(Entity picker, Entity picked) { 705 static if(__protocol >= 301) { 706 this.sendPacket(new Clientbound.CollectItem(picked.id, picker.id, cast(ItemEntity)picked ? (cast(ItemEntity)picked).item.count : 1)); 707 } else { 708 this.sendPacket(new Clientbound.CollectItem(picked.id, picker.id)); 709 } 710 } 711 712 public override void sendPassenger(ubyte mode, uint passenger, uint vehicle) { 713 //TODO 714 //this.sendPacket(packet!"SetPassengers"(mode == 0 ? [] : [passenger == this.id ? 0 : passenger], vehicle == this.id ? 0 : vehicle)); 715 } 716 717 public override void sendExplosion(EntityPosition position, float radius, Vector3!byte[] updates) { 718 Vector3!byte.Tuple[] records; 719 foreach(update ; updates) { 720 records ~= update.tuple; 721 } 722 this.sendPacket(new Clientbound.Explosion((cast(Vector3!float)position).tuple, radius, records, typeof(Clientbound.Explosion.motion)(0, 0, 0))); 723 } 724 725 public override void sendMap(Map map) { 726 //TODO 727 //this.sendPacket(map.minecraftpacket); 728 } 729 730 public override void sendMusic(EntityPosition position, ubyte instrument, uint pitch) { 731 /*@property string sound() { 732 final switch(instrument) { 733 case Instruments.HARP: return "harp"; 734 case Instruments.DOUBLE_BASS: return "bass"; 735 case Instruments.SNARE_DRUM: return "snare"; 736 case Instruments.CLICKS: return "pling"; 737 case Instruments.BASS_DRUM: return "basedrum"; 738 } 739 } 740 enum float[] pitches = [.5, .533333, .566666, .6, .633333, .666666, .7, .75, .8, .85, .9, .95, 1, 1.05, 1.1, 1.2, 1.25, 1.333333, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2]; 741 this.sendPacket(new Clientbound.NamedSoundEffect("block.note." ~ sound, 2, (cast(Vector3!int)position).tuple, 16, pitches[pitch]));*/ 742 } 743 744 745 746 mixin generateHandlers!(Serverbound.Packets); 747 748 protected void handleTeleportConfirmPacket(uint id) { 749 //TODO implement confirmations 750 } 751 752 protected void handleTabCompletePacket(string text, bool command, bool hasPosition, ulong position) { 753 this.handleCompleteMessage(text, command); 754 } 755 756 protected void handleChatMessagePacket(string message) { 757 this.handleTextMessage(message); 758 } 759 760 protected void handleClientStatusPacket(uint aid) { 761 this.handleClientStatus(aid); 762 } 763 764 protected void handleConfirmTransactionPacket(ubyte window, ushort action, bool accepted) {} 765 766 protected void handleEnchantItemPacket(ubyte window, ubyte enchantment) {} 767 768 protected void handleClickWindowPacket(ubyte window, ushort slot, ubyte button, ushort actionId, uint mode, Types.Slot item) { 769 int real_slot = slot >= 9 && slot <= 44 ? (slot >= 36 ? slot - 36 : slot) : -1; //TODO container's 770 // the "picked up slot/item" is the one attached to the player's mouse's pointer 771 bool accepted = true; 772 if(window == 0) { // inventory 773 switch(mode) { 774 case 0: 775 switch(button) { 776 case 0: 777 // left mouse click 778 // pick up the whole stack if the picked up slot is empty 779 // merge or switch them if the picked up item is not empty 780 // drop the picked up slot if the slot is -999 781 if(this.picked_up_item.empty) { 782 if(real_slot >= 0) { 783 // some valid stuff 784 Slot current = this.inventory[real_slot]; 785 if(!current.empty) { 786 // pick up that item 787 this.picked_up_item = current; 788 this.inventory[real_slot] = Slot(null); 789 } 790 } 791 } else { 792 if(real_slot >= 0) { 793 Slot current = this.inventory[real_slot]; 794 if(!current.empty && this.picked_up_item.item == current.item) { 795 // merge them 796 if(!current.full) { 797 uint count = current.count + this.picked_up_item.count; 798 if(count > current.item.max) { 799 count = current.item.max; 800 this.picked_up_item.count = (count - current.count) & ubyte.max; 801 } else { 802 this.picked_up_item = Slot(null); 803 } 804 this.inventory[real_slot] = Slot(current.item, count & ubyte.max); 805 } 806 } else { 807 // switch them (place if current is empty) 808 this.inventory[real_slot] = this.picked_up_item; 809 this.picked_up_item = current; 810 } 811 } else if(slot == -999) { 812 this.handleDropFromPickedUp(this.picked_up_item); 813 } 814 } 815 break; 816 case 1: 817 // right mouse click 818 // if the picked up slot is empty pick up half the stack (with the half picked up bigger if the slot's count is an odd number) 819 // if the picked up item is the same as the slot, place one (if the slot is already full, do nothing) 820 // if the picked up item is different from the slot, switch them 821 // drop one if the slot is -999 822 if(this.picked_up_item.empty) { 823 if(real_slot >= 0) { 824 Slot current = this.inventory[real_slot]; 825 if(!current.empty) { 826 ubyte picked_count = current.count / 2; 827 if(current.count % 2 == 1) { 828 picked_count++; 829 } 830 this.picked_up_item = Slot(current.item, picked_count); 831 this.inventory[real_slot] = Slot(current.count == 1 ? null : current.item, current.count / 2); 832 } 833 } 834 } else { 835 if(real_slot >= 0) { 836 Slot current = this.inventory[real_slot]; 837 if(current.empty || current.item == this.picked_up_item.item && !current.full) { 838 this.inventory[real_slot] = Slot(this.picked_up_item.item, current.empty ? 1 : (current.count + 1) & ubyte.max); 839 this.picked_up_item.count--; 840 } else if(this.picked_up_item != current && (current.empty || !current.full)) { 841 this.inventory[real_slot] = this.picked_up_item; 842 this.picked_up_item = current; 843 } 844 } else if(slot == -999) { 845 if(!this.creative) { 846 Slot drop = Slot(this.picked_up_item.item, 1); 847 this.handleDropFromPickedUp(drop); 848 this.picked_up_item.count--; 849 } else { 850 this.handleDropFromPickedUp(this.picked_up_item); 851 } 852 } 853 } 854 break; 855 default: 856 break; 857 } 858 break; 859 case 1: 860 // moves items in the inventory using the shift buttons 861 if(real_slot >= 0) { 862 InventoryRange location, target; 863 if(real_slot < 9) { 864 location = this.inventory[0..9]; 865 target = this.inventory[9..$]; 866 } else { 867 location = this.inventory[9..$]; 868 target = this.inventory[0..9]; 869 real_slot -= 9; 870 } 871 if(!location[real_slot].empty) location[real_slot] = target += location[real_slot]; 872 } 873 break; 874 case 2: 875 // switch items from somewhere in the inventory to the hotbar 876 if(button < 9 && real_slot >= 0 && real_slot != button) { 877 Slot target = this.inventory[real_slot]; 878 if(!target.empty || !this.inventory[button].empty) { 879 this.inventory[real_slot] = this.inventory[button]; 880 this.inventory[button] = target; 881 } 882 } 883 break; 884 case 3: 885 // middle click, used in creative mode 886 if(this.creative && real_slot >= 0 && !this.inventory[real_slot].empty) { 887 this.picked_up_item = Slot(this.inventory[real_slot].item); 888 } 889 break; 890 case 4: 891 // dropping items with the inventory opened 892 if(real_slot >= 0 && !this.inventory[real_slot].empty) { 893 if(button == 0) { 894 if(this.handleDrop(Slot(this.inventory[real_slot].item, 1))) { 895 if(--this.inventory[real_slot].count == 0) { 896 this.inventory[real_slot] = Slot(null); 897 } 898 } 899 } else { 900 if(this.handleDrop(this.inventory[real_slot])) { 901 this.inventory[real_slot] = Slot(null); 902 } 903 } 904 } 905 break; 906 case 5: 907 // drag items 908 switch(button) { 909 case 0: 910 case 4: 911 this.dragging = true; 912 break; 913 case 1: 914 case 5: 915 if(this.dragging && real_slot >= 0 && !this.dragged_slots.canFind(real_slot)) { 916 this.dragged_slots ~= real_slot; 917 } 918 break; 919 case 2: 920 if(!this.picked_up_item.empty) { 921 ubyte amount = (this.picked_up_item.count / this.dragged_slots.length) & ubyte.max; 922 if(amount == 0) amount = 1; 923 foreach(size_t index ; this.dragged_slots) { 924 Slot target = this.inventory[index]; 925 if(target.empty || (target.item == this.picked_up_item.item && !target.full)) { 926 927 } 928 } 929 } 930 this.dragging = false; 931 this.dragged_slots.length = 0; 932 break; 933 case 6: 934 if(!this.picked_up_item.empty) { 935 foreach(size_t index ; this.dragged_slots) { 936 Slot target = this.inventory[index]; 937 if(target.empty || (target.item == this.picked_up_item.item && !target.full)) { 938 this.inventory[index] = Slot(this.picked_up_item.item, target.empty ? 1 : (target.count + 1) & ubyte.max); 939 if(--this.picked_up_item.count == 0) break; 940 } 941 } 942 } 943 this.dragging = false; 944 this.dragged_slots.length = 0; 945 break; 946 default: 947 break; 948 } 949 break; 950 case 6: 951 // double click on an item (can only be done in the hotbar) 952 if(real_slot >= 0 && !this.picked_up_item.empty) { 953 // searches for the items not in the hotbar first 954 this.inventory[real_slot] = this.picked_up_item; 955 auto inv = new InventoryGroup(this.inventory[9..$], this.inventory[0..9]); 956 inv.group(real_slot < 9 ? (this.inventory.length - 9 + real_slot) : (real_slot - 9)); 957 this.picked_up_item = this.inventory[real_slot]; 958 this.inventory[real_slot] = Slot(null); 959 } 960 break; 961 default: 962 break; 963 } 964 } 965 this.sendPacket(new Clientbound.ConfirmTransaction(window, actionId, accepted)); 966 } 967 968 protected void handleCloseWindowPacket(ubyte window) { 969 //TODO match with open window (inventory / chest) 970 if(this.alive && !this.picked_up_item.empty) { 971 this.handleDropFromPickedUp(this.picked_up_item); 972 } 973 } 974 975 protected void handlePluginMessagePacket(string channel, ubyte[] bytes) {} 976 977 protected void handleUseEntityPacket(uint eid, uint type, typeof(Serverbound.UseEntity.targetPosition) targetPosition, uint hand) { 978 switch(type) { 979 case Serverbound.UseEntity.INTERACT: 980 this.handleInteract(eid); 981 break; 982 case Serverbound.UseEntity.ATTACK: 983 this.handleAttack(eid); 984 break; 985 case Serverbound.UseEntity.INTERACT_AT: 986 987 break; 988 default: 989 break; 990 } 991 } 992 993 protected void handlePlayerPositionPacket(typeof(Serverbound.PlayerPosition.position) position, bool onGround) { 994 this.handleMovementPacket(cast(EntityPosition)position, this.yaw, this.bodyYaw, this.pitch); 995 } 996 997 protected void handlePlayerPositionAndLookPacket(typeof(Serverbound.PlayerPositionAndLook.position) position, float yaw, float pitch, bool onGround) { 998 this.handleMovementPacket(cast(EntityPosition)position, yaw, yaw, pitch); 999 } 1000 1001 protected void handlePlayerLookPacket(float yaw, float pitch, bool onGround) { 1002 this.handleMovementPacket(this.position, yaw, yaw, pitch); 1003 } 1004 1005 protected void handleVehicleMovePacket(typeof(Serverbound.VehicleMove.position) position, float yaw, float pitch) {} 1006 1007 protected void handleSteerBoatPacket(bool right, bool left) {} 1008 1009 protected void handlePlayerAbilitiesPacket(ubyte flags, float flyingSpeed, float walkingSpeed) {} 1010 1011 protected void handlePlayerDiggingPacket(uint status, ulong position, ubyte face) { 1012 switch(status) { 1013 case Serverbound.PlayerDigging.START_DIGGING: 1014 this.handleStartBlockBreaking(blockPosition(position)); 1015 break; 1016 case Serverbound.PlayerDigging.CANCEL_DIGGING: 1017 this.handleAbortBlockBreaking(); 1018 break; 1019 case Serverbound.PlayerDigging.FINISH_DIGGING: 1020 this.handleBlockBreaking(); 1021 break; 1022 case Serverbound.PlayerDigging.DROP_ITEM_STACK: 1023 if(!this.inventory.held.empty && this.handleDrop(this.inventory.held)) { 1024 this.inventory.held = Slot(null); 1025 } 1026 break; 1027 case Serverbound.PlayerDigging.DROP_ITEM: 1028 Slot held = this.inventory.held; 1029 if(!held.empty && this.handleDrop(Slot(held.item, 1))) { 1030 held.count--; 1031 this.inventory.held = held; 1032 } 1033 break; 1034 case Serverbound.PlayerDigging.FINISH_EATING: 1035 this.actionFlag = false; 1036 this.consuming = false; 1037 break; 1038 case Serverbound.PlayerDigging.SWAP_ITEM_IN_HAND: 1039 1040 break; 1041 default: 1042 break; 1043 } 1044 } 1045 1046 protected void handleEntityActionPacket(uint eid, uint action, uint jumpBoost) { 1047 switch(action) { 1048 case Serverbound.EntityAction.START_SNEAKING: 1049 this.handleSneaking(true); 1050 break; 1051 case Serverbound.EntityAction.STOP_SNEAKING: 1052 this.handleSneaking(false); 1053 break; 1054 case Serverbound.EntityAction.LEAVE_BED: 1055 1056 break; 1057 case Serverbound.EntityAction.START_SPRINTING: 1058 this.handleSprinting(true); 1059 break; 1060 case Serverbound.EntityAction.STOP_SPRINTING: 1061 this.handleSprinting(false); 1062 break; 1063 case Serverbound.EntityAction.START_HORSE_JUMP: 1064 1065 break; 1066 case Serverbound.EntityAction.STOP_HORSE_JUMP: 1067 1068 break; 1069 case Serverbound.EntityAction.OPEN_HORSE_INVENTORY: 1070 1071 break; 1072 case Serverbound.EntityAction.START_ELYTRA_FLYING: 1073 1074 break; 1075 default: 1076 break; 1077 } 1078 } 1079 1080 protected void handleSteerVehiclePacket(float sideways, float forward, ubyte flags) {} 1081 1082 protected void handleHeldItemChangePacket(ushort slot) { 1083 if(slot < 9) { 1084 this.inventory.selected = slot; //TODO call event 1085 this.consuming_time = 0; 1086 } 1087 } 1088 1089 protected void handleCreativeInventoryActionPacket(ushort slot, Types.Slot item) {} 1090 1091 protected void handleUpdateSignPacket(ulong position, string[4] texts) {} 1092 1093 protected void handleAnimationPacket(uint hand) { 1094 this.handleArmSwing(); 1095 } 1096 1097 protected void handleSpectatePacket(UUID uuid) {} 1098 1099 protected void handlePlayerBlockPlacementPacket(ulong position, uint face, uint hand, typeof(Serverbound.PlayerBlockPlacement.cursorPosition) cursorPosition) { 1100 if(!this.inventory.held.empty) { 1101 if(this.inventory.held.item.placeable) { 1102 this.handleBlockPlacing(blockPosition(position), face); 1103 } else { 1104 this.handleRightClick(blockPosition(position), face); 1105 } 1106 } 1107 } 1108 1109 protected void handleUseItemPacket(uint hand) { 1110 if(!this.inventory.held.empty && this.inventory.held.item.consumeable) { 1111 this.actionFlag = true; 1112 this.consuming = true; 1113 this.consuming_time = 0; 1114 } 1115 } 1116 1117 1118 protected void handleClientStatus(uint aid) { 1119 if(aid == Serverbound.ClientStatus.RESPAWN) { 1120 super.handleClientStatus(); 1121 } 1122 } 1123 1124 private void handleDropFromPickedUp(ref Slot slot) { 1125 if(this.handleDrop(slot)) { 1126 slot = Slot(null); 1127 } 1128 } 1129 1130 enum string stringof = "MinecraftPlayer!" ~ to!string(__protocol); 1131 1132 private static class Compression : Player.Compression { 1133 1134 public override ubyte[] compress(ubyte[] payload) { 1135 ubyte[] data = varuint.encode(payload.length.to!uint); 1136 Compress compress = new Compress(6, HeaderFormat.deflate); 1137 data ~= cast(ubyte[])compress.compress(payload); 1138 data ~= cast(ubyte[])compress.flush(); 1139 return data; 1140 } 1141 1142 } 1143 1144 }