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