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 }