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 }