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/bedrock.d, selery/player/bedrock.d)
28  */
29 module selery.player.bedrock;
30 
31 import std.algorithm : min, sort, canFind;
32 import std.array : Appender;
33 import std.base64 : Base64;
34 import std.conv : to;
35 import std.digest.digest : toHexString;
36 import std.digest.sha : sha256Of;
37 import std.json;
38 import std.string : split, join, startsWith, replace, strip, toLower;
39 import std.system : Endian;
40 import std.typecons : Tuple;
41 import std.uuid : UUID;
42 import std.zlib : Compress, HeaderFormat;
43 
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.command.command : Command, Position, Target;
51 import selery.command.util : PocketType;
52 import selery.config : Gamemode, Difficulty, Dimension, Files;
53 import selery.effect : Effect, Effects;
54 import selery.entity.entity : Entity;
55 import selery.entity.human : Skin;
56 import selery.entity.living : Living;
57 import selery.entity.metadata : SelMetadata = Metadata;
58 import selery.entity.noai : Lightning, ItemEntity;
59 import selery.inventory.inventory;
60 import selery.item.slot : Slot;
61 import selery.lang : Translation;
62 import selery.log : Format, Message;
63 import selery.math.vector;
64 import selery.node.info : PlayerInfo;
65 import selery.player.player;
66 import selery.plugin : Description;
67 import selery.world.chunk : Chunk;
68 import selery.world.map : Map;
69 import selery.world.world : World;
70 
71 import sul.utils.var : varuint;
72 
73 abstract class BedrockPlayer : Player {
74 
75 	protected static ubyte[][] resourcePackChunks;
76 	protected static size_t resourcePackSize;
77 	protected static string resourcePackId;
78 	protected static string resourcePackHash;
79 
80 	public static void updateResourcePacks(UUID uuid, void[] rp) {
81 		for(size_t i=0; i<rp.length; i+=4096) {
82 			resourcePackChunks ~= cast(ubyte[])rp[i..min($, i+4096)];
83 		}
84 		resourcePackSize = rp.length;
85 		resourcePackId = uuid.toString();
86 		resourcePackHash = toLower(toHexString(sha256Of(rp)));
87 	}
88 
89 	private bool n_edu;
90 	private long n_xuid;
91 	private ubyte n_os;
92 	private string n_device_model;
93 	
94 	private BlockPosition[] broken_by_this;
95 
96 	protected bool send_commands;
97 	
98 	public this(shared PlayerInfo info, World world, EntityPosition position) {
99 		super(info, world, position);
100 		if(resourcePackId.length == 0) {
101 			// no resource pack
102 			this.hasResourcePack = true;
103 		}
104 	}
105 
106 	/**
107 	 * Gets the player's XBOX user id.
108 	 * It's always the same value for the same user, if authenticated.
109 	 * It's 0 if the server is not in online mode.
110 	 * This value can be used to retrieve more informations about the
111 	 * player using the XBOX live services.
112 	 */
113 	public final pure nothrow @property @trusted @nogc long xuid() {
114 		return this.info.xuid;
115 	}
116 
117 	/**
118 	 * Gets the player's operative system, as indicated by the client
119 	 * in the login packet.
120 	 * Example:
121 	 * ---
122 	 * if(player.os != PlayerOS.android) {
123 	 *    player.kick("Only android players are allowed");
124 	 * }
125 	 * ---
126 	 */
127 	public final pure nothrow @property @trusted @nogc DeviceOS deviceOs() {
128 		return this.info.deviceOs;
129 	}
130 
131 	/**
132 	 * Gets the player's device model (name and identifier) as indicated
133 	 * by the client in the login packet.
134 	 * Example:
135 	 * ---
136 	 * if(!player.deviceModel.toLower.startsWith("oneplus")) {
137 	 *    player.kick("This server is reserved for oneplus users");
138 	 * }
139 	 */
140 	public final pure nothrow @property @trusted @nogc string deviceModel() {
141 		return this.info.deviceModel;
142 	}
143 
144 	public final override void disconnectImpl(const Translation translation) {
145 		if(translation.translatable.bedrock.length) {
146 			this.server.kick(this.hubId, translation.translatable.bedrock, translation.parameters);
147 		} else {
148 			this.disconnect(this.server.lang.translate(translation, this.language));
149 		}
150 	}
151 
152 	alias operator = super.operator;
153 
154 	public override @property bool operator(bool op) {
155 		if(super.operator(op) == op && this.send_commands) {
156 			this.sendCommands();
157 		}
158 		return op;
159 	}
160 	
161 	/**
162 	 * Encodes a Message[] into a string that can be parsed by the client.
163 	 * Every instance of Translation is translated server-side.
164 	 */
165 	public string encodeServerMessage(Message[] messages) {
166 		Appender!string appender;
167 		foreach(message ; messages) {
168 			final switch(message.type) {
169 				case Message.FORMAT:
170 					appender.put(cast(string)message.format);
171 					break;
172 				case Message.TEXT:
173 					appender.put(message.text);
174 					break;
175 				case Message.TRANSLATION:
176 					appender.put(this.server.lang.translate(message.translation.translatable.default_, message.translation.parameters, this.language));
177 					break;
178 			}
179 		}
180 		return appender.data;
181 	}
182 
183 	/**
184 	 * Creates a string message that can be displayed by the client.
185 	 */
186 	protected override void sendMessageImpl(Message[] messages) {
187 		Appender!string appender;
188 		string[] params;
189 		bool translation = false;
190 		foreach(message ; messages) {
191 			final switch(message.type) {
192 				case Message.FORMAT:
193 					appender.put(cast(string)message.format);
194 					break;
195 				case Message.TEXT:
196 					appender.put(message.text);
197 					break;
198 				case Message.TRANSLATION:
199 					if(message.translation.translatable.bedrock.length) {
200 						//TODO check whether it's possible to use multiple translations
201 						appender.put("%");
202 						appender.put(message.translation.translatable.bedrock);
203 						translation = true;
204 					} else {
205 						appender.put(this.server.lang.translate(message.translation.translatable.default_, message.translation.parameters, this.language));
206 					}
207 					break;
208 			}
209 		}
210 		if(!translation) this.sendRawMessageImpl(appender.data);
211 		else this.sendTranslationMessageImpl(appender.data, params);
212 	}
213 
214 	protected abstract void sendRawMessageImpl(string message);
215 
216 	protected abstract void sendTranslationMessageImpl(string message, string[] params);
217 
218 	public override @trusted Command registerCommand(Command command) {
219 		super.registerCommand(command);
220 		if(this.send_commands) {
221 			this.sendCommands();
222 		}
223 		return command;
224 	}
225 
226 	public override @trusted bool unregisterCommand(Command command) {
227 		immutable ret = super.unregisterCommand(command);
228 		if(ret && this.send_commands) {
229 			this.sendCommands();
230 		}
231 		return ret;
232 	}
233 
234 	protected void sendCommands();
235 
236 
237 	// generic PE handlings
238 	
239 	protected override bool handleBlockBreaking() {
240 		BlockPosition position = this.breaking;
241 		if(super.handleBlockBreaking()) {
242 			this.broken_by_this ~= position;
243 			return true;
244 		} else {
245 			return false;
246 		}
247 	}
248 	
249 }
250 
251 // send function are overwritten with static ifs
252 // handle functions are created for every version using static ifs
253 class BedrockPlayerImpl(uint __protocol) : BedrockPlayer if(supportedBedrockProtocols.canFind(__protocol)) {
254 
255 	mixin("import Types = sul.protocol.bedrock" ~ __protocol.to!string ~ ".types;");
256 	mixin("import Play = sul.protocol.bedrock" ~ __protocol.to!string ~ ".play;");
257 
258 	mixin("import sul.attributes.bedrock" ~ __protocol.to!string ~ " : Attributes;");
259 	mixin("import sul.metadata.bedrock" ~ __protocol.to!string ~ " : Metadata;");
260 
261 	private static __gshared ubyte[] creative_inventory;
262 
263 	public static bool loadCreativeInventory(const Files files) {
264 		immutable cached = "creative_" ~ __protocol.to!string;
265 		if(!files.hasTemp(cached)) {
266 			immutable asset = "creative/" ~ __protocol.to!string ~ ".json";
267 			if(!files.hasAsset(asset)) return false;
268 			static if(__protocol < 120) auto packet = new Play.ContainerSetContent(121, 0);
269 			else auto packet = new Play.InventoryContent(121);
270 			foreach(item ; parseJSON(cast(string)files.readAsset(asset))["items"].array) {
271 				auto obj = item.object;
272 				auto meta = "meta" in obj;
273 				auto nbt = "nbt" in obj;
274 				auto ench = "enchantments" in obj;
275 				packet.slots ~= Types.Slot(obj["id"].integer.to!int, (meta ? (*meta).integer.to!int << 8 : 0) | 1, nbt && nbt.str.length ? Base64.decode(nbt.str) : []);
276 			}
277 			ubyte[] encoded = packet.encode();
278 			Compress c = new Compress(9);
279 			creative_inventory = cast(ubyte[])c.compress(varuint.encode(encoded.length.to!uint) ~ encoded);
280 			creative_inventory ~= cast(ubyte[])c.flush();
281 			files.writeTemp(cached, creative_inventory);
282 		} else {
283 			creative_inventory = cast(ubyte[])files.readTemp(cached);
284 		}
285 		return true;
286 	}
287 
288 	protected Types.BlockPosition toBlockPosition(BlockPosition vector) {
289 		return Types.BlockPosition(typeof(Types.BlockPosition.x)(vector.x), typeof(Types.BlockPosition.y)(vector.y), typeof(Types.BlockPosition.z)(vector.z));
290 	}
291 
292 	protected BlockPosition fromBlockPosition(Types.BlockPosition blockPosition) {
293 		return BlockPosition(blockPosition.x, blockPosition.y, blockPosition.z);
294 	}
295 
296 	protected Types.Slot toSlot(Slot slot) {
297 		if(slot.empty) {
298 			return Types.Slot(0);
299 		} else {
300 			auto stream = new ClassicStream!(Endian.littleEndian)();
301 			if(!slot.empty && slot.item.pocketCompound !is null) {
302 				stream.writeTag(slot.item.pocketCompound);
303 			}
304 			return Types.Slot(slot.item.bedrockId, slot.item.bedrockMeta << 8 | slot.count, stream.buffer);
305 		}
306 	}
307 
308 	protected Slot fromSlot(Types.Slot slot) {
309 		if(slot.id <= 0) {
310 			return Slot(null);
311 		} else {
312 			auto item = this.world.items.fromBedrock(slot.id & ushort.max, (slot.metaAndCount >> 8) & ushort.max);
313 			if(slot.nbt.length) {
314 				auto stream = new ClassicStream!(Endian.littleEndian)(slot.nbt);
315 				//TODO verify that this is right
316 				auto tag = stream.readTag();
317 				if(cast(Compound)tag) item.parseBedrockCompound(cast(Compound)tag);
318 			}
319 			return Slot(item, slot.metaAndCount & 255);
320 		}
321 	}
322 
323 	protected Types.Slot[] toSlots(Slot[] slots) {
324 		Types.Slot[] ret = new Types.Slot[slots.length];
325 		foreach(i, slot; slots) {
326 			ret[i] = toSlot(slot);
327 		}
328 		return ret;
329 	}
330 
331 	protected Slot[] fromSlots(Types.Slot[] slots) {
332 		Slot[] ret = new Slot[slots.length];
333 		foreach(i, slot; slots) {
334 			ret[i] = this.fromSlot(slot);
335 		}
336 		return ret;
337 	}
338 
339 	public static Types.McpeUuid toUUID(UUID uuid) {
340 		ubyte[8] msb, lsb;
341 		foreach(i ; 0..8) {
342 			msb[i] = uuid.data[i];
343 			lsb[i] = uuid.data[i+8];
344 		}
345 		import std.bitmanip : bigEndianToNative;
346 		return Types.McpeUuid(bigEndianToNative!long(msb), bigEndianToNative!long(lsb));
347 	} 
348 
349 	public static uint convertGamemode(uint gamemode) {
350 		if(gamemode == 3) return 1;
351 		else return gamemode;
352 	}
353 
354 	public static Metadata metadataOf(SelMetadata metadata) {
355 		mixin("return metadata.bedrock" ~ __protocol.to!string ~ ";");
356 	}
357 
358 	private bool has_creative_inventory = false;
359 	
360 	private Tuple!(string, PocketType)[][][string] sent_commands; // [command][overload] = [(name, type), (name, type), ...]
361 
362 	private ubyte[][] queue;
363 	private size_t total_queue_length = 0;
364 	
365 	public this(shared PlayerInfo info, World world, EntityPosition position) {
366 		super(info, world, position);
367 		this.startCompression!Compression(hubId);
368 	}
369 
370 	protected void sendPacket(T)(T packet) if(is(T == ubyte[]) || is(typeof(T.encode))) {
371 		static if(is(T == ubyte[])) {
372 			alias buffer = packet;
373 		} else {
374 			ubyte[] buffer = packet.encode();
375 		}
376 		this.queue ~= buffer;
377 		this.total_queue_length += buffer.length;
378 	}
379 
380 	public override void flush() {
381 		enum padding = [ubyte(0), ubyte(0)];
382 		// since protocol 110 everything is compressed
383 		if(this.queue.length) {
384 			ubyte[] payload;
385 			size_t total;
386 			size_t total_bytes = 0;
387 			foreach(ubyte[] packet ; this.queue) {
388 				static if(__protocol >= 120) packet = packet[0] ~ padding ~ packet[1..$];
389 				total++;
390 				total_bytes += packet.length;
391 				payload ~= varuint.encode(packet.length.to!uint);
392 				payload ~= packet;
393 				if(payload.length > 1048576) {
394 					// do not compress more than 1 MiB
395 					break;
396 				}
397 			}
398 			this.queue = this.queue[total..$];
399 			this.compress(payload);
400 			this.total_queue_length -= total_bytes;
401 			if(this.queue.length) this.flush();
402 		}
403 	}
404 
405 	alias world = super.world;
406 
407 	public override @property World world(World world) {
408 		this.send_commands = false; // world-related commands are removed but no packet is needed as they are updated at respawn
409 		return super.world(world);
410 	}
411 
412 	public override void transfer(string ip, ushort port) {
413 		this.sendPacket(new Play.Transfer(ip, port));
414 	}
415 
416 	public override void firstspawn() {
417 		super.firstspawn();
418 		this.recalculateSpeed();
419 	}
420 
421 	protected override void sendRawMessageImpl(string message) {
422 		this.sendPacket(new Play.Text().new Raw(message));
423 	}
424 
425 	protected override void sendTranslationMessageImpl(string message,string[] params) {
426 		this.sendPacket(new Play.Text().new Translation(message, params));
427 	}
428 
429 	protected override void sendCompletedMessages(string[] messages) {
430 		// unsupported
431 	}
432 	
433 	protected override void sendTipImpl(Message[] messages) {
434 		this.sendPacket(new Play.SetTitle(Play.SetTitle.SET_ACTION_BAR, this.encodeServerMessage(messages)));
435 	}
436 
437 	protected override void sendTitleImpl(Title title, Subtitle subtitle, uint fadeIn, uint stay, uint fadeOut) {
438 		this.sendPacket(new Play.SetTitle(Play.SetTitle.SET_TITLE, this.encodeServerMessage(title)));
439 		if(subtitle.length) this.sendPacket(new Play.SetTitle(Play.SetTitle.SET_SUBTITLE, this.encodeServerMessage(subtitle)));
440 		this.sendPacket(new Play.SetTitle(Play.SetTitle.SET_TIMINGS, "", fadeIn, stay, fadeOut));
441 	}
442 
443 	protected override void sendHideTitles() {
444 		this.sendPacket(new Play.SetTitle(Play.SetTitle.HIDE));
445 	}
446 
447 	protected override void sendResetTitles() {
448 		this.sendPacket(new Play.SetTitle(Play.SetTitle.RESET));
449 	}
450 
451 	public override void sendMovementUpdates(Entity[] entities) {
452 		foreach(Entity entity ; entities) {
453 			this.sendPacket(new Play.MoveEntity(entity.id, (cast(Vector3!float)(entity.position + [0, entity.eyeHeight, 0])).tuple, entity.anglePitch, entity.angleYaw, cast(Living)entity ? (cast(Living)entity).angleBodyYaw : entity.angleYaw, entity.onGround));
454 		}
455 	}
456 	
457 	public override void sendMotionUpdates(Entity[] entities) {
458 		foreach(Entity entity ; entities) {
459 			this.sendPacket(new Play.SetEntityMotion(entity.id, (cast(Vector3!float)entity.motion).tuple));
460 		}
461 	}
462 	
463 	public override void spawnToItself() {
464 		//this.sendAddList([this]);
465 	}
466 
467 	public override void sendGamemode() {
468 		this.sendPacket(new Play.SetPlayerGameType(this.gamemode == 3 ? 1 : this.gamemode));
469 		if(this.creative) {
470 			if(!this.has_creative_inventory) {
471 				this.sendPacketPayload(creative_inventory);
472 				this.has_creative_inventory = true;
473 			}
474 		} else if(this.spectator) {
475 			if(has_creative_inventory) {
476 				//TODO remove armor and inventory
477 				static if(__protocol < 120) this.sendPacket(new Play.ContainerSetContent(121, this.id));
478 				else this.sendPacket(new Play.InventoryContent(121));
479 				this.has_creative_inventory = false;
480 			}
481 		}
482 		this.sendSettingsPacket();
483 	}
484 	
485 	public override void sendSpawnPosition() {
486 		this.sendPacket(new Play.SetSpawnPosition(0, toBlockPosition(cast(Vector3!int)this.spawn), true));
487 	}
488 	
489 	public override void sendAddList(Player[] players) {
490 		Types.PlayerList[] list;
491 		foreach(Player player ; players) {
492 			list ~= Types.PlayerList(toUUID(player.uuid), player.id, player.displayName, Types.Skin(player.skin.name, player.skin.data.dup, player.skin.cape.dup, player.skin.geometryName, player.skin.geometryData.dup));
493 		}
494 		if(list.length) this.sendPacket(new Play.PlayerList().new Add(list));
495 	}
496 
497 	public override void sendUpdateLatency(Player[] players) {}
498 
499 	public override void sendRemoveList(Player[] players) {
500 		Types.McpeUuid[] uuids;
501 		foreach(Player player ; players) {
502 			uuids ~= toUUID(player.uuid);
503 		}
504 		if(uuids.length) this.sendPacket(new Play.PlayerList().new Remove(uuids));
505 	}
506 	
507 	public override void sendMetadata(Entity entity) {
508 		this.sendPacket(new Play.SetEntityData(entity.id, metadataOf(entity.metadata)));
509 	}
510 	
511 	public override void sendChunk(Chunk chunk) {
512 
513 		Types.ChunkData data;
514 
515 		auto sections = chunk.sections;
516 		size_t[] keys = sections.keys;
517 		sort(keys);
518 		ubyte top = keys.length ? to!ubyte(keys[$-1] + 1) : 0;
519 		foreach(size_t i ; 0..top) {
520 			Types.Section section;
521 			auto section_ptr = i in sections;
522 			if(section_ptr) {
523 				auto s = *section_ptr;
524 				foreach(ubyte x ; 0..16) {
525 					foreach(ubyte z ; 0..16) {
526 						foreach(ubyte y ; 0..16) {
527 							auto ptr = s[x, y, z];
528 							if(ptr) {
529 								Block block = *ptr;
530 								section.blockIds[x << 8 | z << 4 | y] = block.bedrockId != 0 ? block.bedrockId : ubyte(248);
531 								if(block.bedrockMeta != 0) section.blockMetas[x << 7 | z << 3 | y >> 1] |= to!ubyte(block.bedrockMeta << (y % 2 == 1 ? 4 : 0));
532 							}
533 						}
534 					}
535 				}
536 				static if(__protocol < 120) {
537 					section.skyLight = s.skyLight;
538 					section.blockLight = s.blocksLight;
539 				}
540 			} else {
541 				static if(__protocol < 120) {
542 					section.skyLight = 255;
543 					section.blockLight = 0;
544 				}
545 			}
546 			data.sections ~= section;
547 		}
548 		//data.heights = chunk.lights;
549 		foreach(i, biome; chunk.biomes) {
550 			data.biomes[i] = biome.id;
551 		}
552 		//TODO extra data
553 
554 		auto stream = new NetworkStream!(Endian.littleEndian)();
555 		foreach(tile ; chunk.tiles) {
556 			if(tile.pocketCompound !is null) {
557 				auto compound = tile.pocketCompound.dup;
558 				compound["id"] = new String(tile.pocketSpawnId);
559 				compound["x"] = new Int(tile.position.x);
560 				compound["y"] = new Int(tile.position.y);
561 				compound["z"] = new Int(tile.position.z);
562 				stream.writeTag(compound);
563 			}
564 		}
565 		data.blockEntities = stream.buffer;
566 
567 		this.sendPacket(new Play.FullChunkData(chunk.position.tuple, data));
568 
569 		/*if(chunk.translatable_tiles.length > 0) {
570 			foreach(Tile tile ; chunk.translatable_tiles) {
571 				if(tile.tags) this.sendTile(tile, true);
572 			}
573 		}*/
574 	}
575 	
576 	public override void unloadChunk(ChunkPosition pos) {
577 		// no UnloadChunk packet :(
578 	}
579 
580 	public override void sendChangeDimension(Dimension from, Dimension to) {
581 		//if(from == to) this.sendPacket(new Play.ChangeDimension((to + 1) % 3, typeof(Play.ChangeDimension.position)(0, 128, 0), true));
582 		if(from != to) this.sendPacket(new Play.ChangeDimension(to));
583 	}
584 	
585 	public override void sendInventory(ubyte flag=PlayerInventory.ALL, bool[] slots=[]) {
586 		//slot only
587 		foreach(ushort index, bool slot; slots) {
588 			if(slot) {
589 				//TODO if slot is in the hotbar the third argument should not be 0
590 				static if(__protocol < 120) this.sendPacket(new Play.ContainerSetSlot(0, index, 0, toSlot(this.inventory[index])));
591 				//else this.sendPacket(new Play.InventorySlot(0, index, 0, toSlot(this.inventory[index])));
592 			}
593 		}
594 		//normal inventory
595 		if((flag & PlayerInventory.INVENTORY) > 0) {
596 			static if(__protocol < 120) this.sendPacket(new Play.ContainerSetContent(0, this.id, toSlots(this.inventory[]), [9, 10, 11, 12, 13, 14, 15, 16, 17]));
597 			else this.sendPacket(new Play.InventoryContent(0, toSlots(this.inventory[])));
598 		}
599 		//armour
600 		if((flag & PlayerInventory.ARMOR) > 0) {
601 			static if(__protocol < 120) this.sendPacket(new Play.ContainerSetContent(120, this.id, toSlots(this.inventory.armor[]), new int[0]));
602 			else this.sendPacket(new Play.InventoryContent(120, toSlots(this.inventory.armor[])));
603 		}
604 		//held item
605 		if((flag & PlayerInventory.HELD) > 0) this.sendHeld();
606 	}
607 	
608 	public override void sendHeld() {
609 		static if(__protocol < 120) this.sendPacket(new Play.ContainerSetSlot(0, this.inventory.hotbar[this.inventory.selected] + 9, this.inventory.selected, toSlot(this.inventory.held)));
610 		//else this.sendPacket(new Play.InventorySlot(0, this.inventory.hotbar[this.inventory.selected] + 9, this.inventory.selected, toSlot(this.inventory.held)));
611 	}
612 	
613 	public override void sendEntityEquipment(Player player) {
614 		this.sendPacket(new Play.MobEquipment(player.id, toSlot(player.inventory.held), cast(ubyte)0, cast(ubyte)0, cast(ubyte)0));
615 	}
616 	
617 	public override void sendArmorEquipment(Player player) {
618 		this.sendPacket(new Play.MobArmorEquipment(player.id, [toSlot(player.inventory.helmet), toSlot(player.inventory.chestplate), toSlot(player.inventory.leggings), toSlot(player.inventory.boots)]));
619 	}
620 	
621 	public override void sendOpenContainer(ubyte type, ushort slots, BlockPosition position) {
622 		//TODO
623 		//this.sendPacket(new PocketContainerOpen(to!ubyte(type + 1), type, slots, position));
624 	}
625 	
626 	public override void sendHurtAnimation(Entity entity) {
627 		this.sendEntityEvent(entity, Play.EntityEvent.HURT_ANIMATION);
628 	}
629 	
630 	public override void sendDeathAnimation(Entity entity) {
631 		this.sendEntityEvent(entity, Play.EntityEvent.DEATH_ANIMATION);
632 	}
633 
634 	private void sendEntityEvent(Entity entity, typeof(Play.EntityEvent.eventId) evid) {
635 		this.sendPacket(new Play.EntityEvent(entity.id, evid));
636 	}
637 	
638 	protected override void sendDeathSequence() {
639 		this.sendPacket(new Play.SetHealth(0));
640 		this.sendRespawnPacket();
641 	}
642 	
643 	protected override @trusted void experienceUpdated() {
644 		auto attributes = [
645 			Types.Attribute(Attributes.experience.min, Attributes.experience.max, this.experience, Attributes.experience.def, Attributes.experience.name),
646 			Types.Attribute(Attributes.level.min, Attributes.level.max, this.level, Attributes.level.def, Attributes.level.name)
647 		];
648 		this.sendPacket(new Play.UpdateAttributes(this.id, attributes));
649 	}
650 
651 	protected override void sendPosition() {
652 		this.sendPacket(new Play.MovePlayer(this.id, (cast(Vector3!float)this.position).tuple, this.pitch, this.bodyYaw, this.yaw, Play.MovePlayer.TELEPORT, this.onGround));
653 	}
654 
655 	protected override void sendMotion(EntityPosition motion) {
656 		this.sendPacket(new Play.SetEntityMotion(this.id, (cast(Vector3!float)motion).tuple));
657 	}
658 
659 	public override void sendSpawnEntity(Entity entity) {
660 		if(cast(Player)entity) this.sendAddPlayer(cast(Player)entity);
661 		else if(cast(ItemEntity)entity) this.sendAddItemEntity(cast(ItemEntity)entity);
662 		else if(entity.bedrock) this.sendAddEntity(entity);
663 	}
664 
665 	public override void sendDespawnEntity(Entity entity) {
666 		this.sendPacket(new Play.RemoveEntity(entity.id));
667 	}
668 	
669 	protected void sendAddPlayer(Player player) {
670 		this.sendPacket(new Play.AddPlayer(toUUID(player.uuid), player.name, player.id, player.id, (cast(Vector3!float)player.position).tuple, (cast(Vector3!float)player.motion).tuple, player.pitch, player.bodyYaw, player.yaw, toSlot(player.inventory.held), metadataOf(player.metadata)));
671 	}
672 	
673 	protected void sendAddItemEntity(ItemEntity item) {
674 		this.sendPacket(new Play.AddItemEntity(item.id, item.id, toSlot(item.item), (cast(Vector3!float)item.position).tuple, (cast(Vector3!float)item.motion).tuple, metadataOf(item.metadata)));
675 	}
676 	
677 	protected void sendAddEntity(Entity entity) {
678 		this.sendPacket(new Play.AddEntity(entity.id, entity.id, entity.bedrockId, (cast(Vector3!float)entity.position).tuple, (cast(Vector3!float)entity.motion).tuple, entity.pitch, entity.yaw, new Types.Attribute[0], metadataOf(entity.metadata), typeof(Play.AddEntity.links).init));
679 	}
680 
681 	public override @trusted void healthUpdated() {
682 		super.healthUpdated();
683 		auto attributes = [
684 			Types.Attribute(Attributes.health.min, this.maxHealthNoAbs, this.healthNoAbs, Attributes.health.def, Attributes.health.name),
685 			Types.Attribute(Attributes.absorption.min, this.maxAbsorption, this.absorption, Attributes.absorption.def, Attributes.absorption.name)
686 		];
687 		this.sendPacket(new Play.UpdateAttributes(this.id, attributes));
688 	}
689 	
690 	public override @trusted void hungerUpdated() {
691 		super.hungerUpdated();
692 		auto attributes = [
693 			Types.Attribute(Attributes.hunger.min, Attributes.hunger.max, this.hunger, Attributes.hunger.def, Attributes.hunger.name),
694 			Types.Attribute(Attributes.saturation.min, Attributes.saturation.max, this.saturation, Attributes.saturation.def, Attributes.saturation.name)
695 		];
696 		this.sendPacket(new Play.UpdateAttributes(this.id, attributes));
697 	}
698 
699 	protected override void onEffectAdded(Effect effect, bool modified) {
700 		if(effect.bedrock) this.sendPacket(new Play.MobEffect(this.id, modified ? Play.MobEffect.MODIFY : Play.MobEffect.ADD, effect.bedrock.id, effect.level, true, cast(int)effect.duration));
701 	}
702 
703 	protected override void onEffectRemoved(Effect effect) {
704 		if(effect.bedrock) this.sendPacket(new Play.MobEffect(this.id, Play.MobEffect.REMOVE, effect.bedrock.id, effect.level));
705 	}
706 	
707 	public override void recalculateSpeed() {
708 		super.recalculateSpeed();
709 		this.sendPacket(new Play.UpdateAttributes(this.id, [Types.Attribute(Attributes.speed.min, Attributes.speed.max, this.speed, Attributes.speed.def, Attributes.speed.name)]));
710 	}
711 	
712 	public override void sendJoinPacket() {
713 		//TODO send thunders
714 		auto packet = new Play.StartGame(this.id, this.id);
715 		packet.gamemode = convertGamemode(this.gamemode);
716 		packet.position = (cast(Vector3!float)this.position).tuple;
717 		packet.yaw = this.yaw;
718 		packet.pitch = this.pitch;
719 		packet.seed = this.world.seed;
720 		packet.dimension = this.world.dimension;
721 		packet.generator = this.world.type=="flat" ? 2 : 1;
722 		packet.worldGamemode = convertGamemode(this.world.gamemode);
723 		packet.difficulty = this.world.difficulty;
724 		packet.spawnPosition = (cast(Vector3!int)this.spawn).tuple;
725 		packet.time = this.world.time.to!uint;
726 		packet.vers = this.server.config.hub.edu;
727 		packet.rainLevel = this.world.weather.raining ? this.world.weather.intensity : 0;
728 		packet.commandsEnabled = true;
729 		static if(__protocol >= 120) packet.permissionLevel = this.op ? 1 : 0;
730 		packet.levelId = Software.display;
731 		packet.worldName = this.server.name;
732 		this.sendPacket(packet);
733 	}
734 
735 	public override void sendResourcePack() {}
736 
737 	public override void sendPermissionLevel(PermissionLevel) {
738 		this.sendSettingsPacket();
739 	}
740 	
741 	public override void sendDifficulty(Difficulty difficulty) {
742 		this.sendPacket(new Play.SetDifficulty(difficulty));
743 	}
744 
745 	public override void sendWorldGamemode(Gamemode gamemode) {
746 		this.sendPacket(new Play.SetDefaultGameType(convertGamemode(gamemode)));
747 	}
748 
749 	public override void sendDoDaylightCycle(bool cycle) {
750 		this.sendGamerule(Types.Rule.DO_DAYLIGHT_CYCLE, cycle);
751 	}
752 	
753 	public override void sendTime(uint time) {
754 		this.sendPacket(new Play.SetTime(time));
755 	}
756 	
757 	public override void sendWeather(bool raining, bool thunderous, uint time, uint intensity) {
758 		if(raining) {
759 			this.sendLevelEvent(Play.LevelEvent.START_RAIN, EntityPosition(0), intensity * 24000);
760 			if(thunderous) this.sendLevelEvent(Play.LevelEvent.START_THUNDER, EntityPosition(0), time);
761 			else this.sendLevelEvent(Play.LevelEvent.STOP_THUNDER, EntityPosition(0), 0);
762 		} else {
763 			this.sendLevelEvent(Play.LevelEvent.STOP_RAIN, EntityPosition(0), 0);
764 			this.sendLevelEvent(Play.LevelEvent.STOP_THUNDER, EntityPosition(0), 0);
765 		}
766 	}
767 	
768 	public override void sendSettingsPacket() {
769 		uint flags = Play.AdventureSettings.EVP_DISABLED; // player vs environment is disabled and the animation is done by server
770 		if(this.adventure || this.spectator) flags |= Play.AdventureSettings.IMMUTABLE_WORLD;
771 		if(!this.world.pvp || this.spectator) flags |= Play.AdventureSettings.PVP_DISABLED;
772 		if(this.spectator) flags |= Play.AdventureSettings.PVM_DISABLED;
773 		if(this.creative || this.spectator) flags |= Play.AdventureSettings.ALLOW_FLIGHT;
774 		if(this.spectator) flags |= Play.AdventureSettings.NO_CLIP;
775 		if(this.spectator) flags |= Play.AdventureSettings.FLYING;
776 		uint abilities;
777 		if(this.hasPermission("minecraft.build_and_mine")) abilities |= Play.AdventureSettings.BUILD_AND_MINE;
778 		if(this.hasPermission("minecraft.doors_and_switches")) abilities |= Play.AdventureSettings.DOORS_AND_SWITCHES;
779 		if(this.hasPermission("minecraft.open_containers")) abilities |= Play.AdventureSettings.OPEN_CONTAINERS;
780 		if(this.hasPermission("minecraft.attack_players")) abilities |= Play.AdventureSettings.ATTACK_PLAYERS;
781 		if(this.hasPermission("minecraft.attack_mobs")) abilities |= Play.AdventureSettings.ATTACK_MOBS;
782 		if(this.operator) abilities |= Play.AdventureSettings.OP;
783 		if(this.hasPermission("minecraft.teleport")) abilities |= Play.AdventureSettings.TELEPORT;
784 		this.sendPacket(new Play.AdventureSettings(flags, this.permissionLevel, abilities));
785 	}
786 	
787 	public override void sendRespawnPacket() {
788 		this.sendPacket(new Play.Respawn((cast(Vector3!float)(this.spawn + [0, this.eyeHeight, 0])).tuple));
789 	}
790 	
791 	public override void setAsReadyToSpawn() {
792 		this.sendPacket(new Play.PlayStatus(Play.PlayStatus.SPAWNED));
793 		if(!this.hasResourcePack) {
794 			// require custom texture
795 			this.sendPacket(new Play.ResourcePacksInfo(true, new Types.PackWithSize[0], [Types.PackWithSize(resourcePackId, Software.fullVersion, resourcePackSize)]));
796 		} else if(resourcePackChunks.length == 0) {
797 			// no resource pack
798 			this.sendPacket(new Play.ResourcePacksInfo(false));
799 		}
800 		this.send_commands = true;
801 		this.sendCommands();
802 	}
803 
804 	private void sendLevelEvent(typeof(Play.LevelEvent.eventId) evid, EntityPosition position, uint data) {
805 		this.sendPacket(new Play.LevelEvent(evid, (cast(Vector3!float)position).tuple, data));
806 	}
807 	
808 	public override void sendLightning(Lightning lightning) {
809 		this.sendAddEntity(lightning);
810 	}
811 	
812 	public override void sendAnimation(Entity entity) {
813 		this.sendPacket(new Play.Animate(Play.Animate.BREAKING, entity.id));
814 	}
815 
816 	public override void sendBlocks(PlacedBlock[] blocks) {
817 		foreach(PlacedBlock block ; blocks) {
818 			this.sendPacket(new Play.UpdateBlock(toBlockPosition(block.position), block.bedrock.id, 176 | block.bedrock.meta));
819 		}
820 		this.broken_by_this.length = 0;
821 	}
822 	
823 	public override void sendTile(Tile tile, bool translatable) {
824 		if(translatable) {
825 			//TODO
826 			//tile.to!ITranslatable.translateStrings(this.lang);
827 		}
828 		auto packet = new Play.BlockEntityData(toBlockPosition(tile.position));
829 		if(tile.pocketCompound !is null) {
830 			auto stream = new NetworkStream!(Endian.littleEndian)();
831 			stream.writeTag(tile.pocketCompound);
832 			packet.nbt = stream.buffer;
833 		} else {
834 			packet.nbt ~= NBT_TYPE.END;
835 		}
836 		this.sendPacket(packet);
837 		/*if(translatable) {
838 			tile.to!ITranslatable.untranslateStrings();
839 		}*/
840 	}
841 	
842 	public override void sendPickupItem(Entity picker, Entity picked) {
843 		this.sendPacket(new Play.TakeItemEntity(picked.id, picker.id));
844 	}
845 	
846 	public override void sendPassenger(ubyte mode, uint passenger, uint vehicle) {
847 		this.sendPacket(new Play.SetEntityLink(passenger, vehicle, mode));
848 	}
849 	
850 	public override void sendExplosion(EntityPosition position, float radius, Vector3!byte[] updates) {
851 		Types.BlockPosition[] upd;
852 		foreach(Vector3!byte u ; updates) {
853 			upd ~= toBlockPosition(cast(Vector3!int)u);
854 		}
855 		this.sendPacket(new Play.Explode((cast(Vector3!float)position).tuple, radius, upd));
856 	}
857 	
858 	public override void sendMap(Map map) {
859 		//TODO implement this!
860 		//this.sendPacket(map.pecompression.length > 0 ? new PocketBatch(map.pecompression) : map.pocketpacket);
861 	}
862 
863 	public override void sendMusic(EntityPosition position, ubyte instrument, uint pitch) {
864 		this.sendPacket(new Play.LevelSoundEvent(Play.LevelSoundEvent.NOTE, (cast(Vector3!float)position).tuple, instrument, pitch, false));
865 	}
866 
867 	protected override void sendCommands() {
868 		this.sent_commands.clear();
869 		auto packet = new Play.AvailableCommands();
870 		ushort addValue(string value) {
871 			foreach(ushort i, v; packet.enumValues) {
872 				if(v == value) return i;
873 			}
874 			packet.enumValues ~= value;
875 			return cast(ushort)(packet.enumValues.length - 1);
876 		}
877 		uint addEnum(string name, inout(string)[] values) {
878 			foreach(uint i, enum_; packet.enums) {
879 				if(enum_.name == name) return i;
880 			}
881 			auto enum_ = Types.Enum(name);
882 			foreach(value ; values) {
883 				enum_.valuesIndexes ~= addValue(value);
884 			}
885 			packet.enums ~= enum_;
886 			return packet.enums.length.to!uint - 1;
887 		}
888 		foreach(command ; this.availableCommands) {
889 			if(!command.hidden) {
890 				Types.Command pc;
891 				pc.name = command.name;
892 				if(command.description.type == Description.TEXT) pc.description = command.description.text;
893 				else if(command.description.type == Description.TRANSLATABLE) {
894 					if(command.description.translatable.bedrock.length) pc.description = command.description.translatable.bedrock;
895 					else pc.description = this.server.lang.translate(command.description.translatable.default_, this.language);
896 				}
897 				if(command.aliases.length) {
898 					pc.aliasesEnum = addEnum(command.name ~ ".aliases", command.aliases);
899 				}
900 				foreach(overload ; command.overloads) {
901 					Types.Overload po;
902 					foreach(i, name; overload.params) {
903 						auto parameter = Types.Parameter(name, Types.Parameter.VALID, i >= overload.requiredArgs);
904 						parameter.type |= {
905 							final switch(overload.pocketTypeOf(i)) with(Types.Parameter) {
906 								case PocketType.integer: return INT;
907 								case PocketType.floating: return FLOAT;
908 								case PocketType.target: return TARGET;
909 								case PocketType..string: return STRING;
910 								case PocketType.blockpos: return POSITION;
911 								case PocketType.rawtext: return RAWTEXT;
912 								case PocketType.stringenum: return ENUM | addEnum(overload.typeOf(i), overload.enumMembers(i));
913 								case PocketType.boolean: return ENUM | addEnum("bool", ["true", "false"]);
914 							}
915 						}();
916 						po.parameters ~= parameter;
917 					}
918 					pc.overloads ~= po;
919 				}
920 				packet.commands ~= pc;
921 			}
922 		}
923 		if(packet.enumValues.length > 0 && packet.enumValues.length < 257) packet.enumValues.length = 257; //TODO fix protocol
924 		this.sendPacket(packet);
925 	}
926 
927 	// generic
928 
929 	private void sendGamerule(const string name, bool value) {
930 		this.sendPacket(new Play.GameRulesChanged([Types.Rule(name, Types.Rule.BOOLEAN, value)]));
931 	}
932 
933 	mixin generateHandlers!(Play.Packets);
934 
935 	protected void handleResourcePackClientResponsePacket(ubyte status, string[] packIds) {
936 		if(resourcePackId.length) {
937 			// only handle if the server has a resource pack to serve
938 			if(status == Play.ResourcePackClientResponse.SEND_PACKS) {
939 				this.sendPacket(new Play.ResourcePackDataInfo(resourcePackId, 4096u, resourcePackChunks.length.to!uint, resourcePackSize, resourcePackHash));
940 				foreach(uint i, chunk; resourcePackChunks) {
941 					this.sendPacket(new Play.ResourcePackChunkData(resourcePackId, i, i*4096u, chunk));
942 				}
943 			} else {
944 				//TODO
945 			}
946 		}
947 	}
948 
949 	protected void handleResourcePackChunkDataRequestPacket(string id, uint index) {
950 		//TODO send chunk
951 	}
952 
953 	protected void handleTextChatPacket(bool unknown1, string sender, string message, string xuid) {
954 		this.handleTextMessage(message);
955 	}
956 
957 	protected void handleMovePlayerPacket(long eid, typeof(Play.MovePlayer.position) position, float pitch, float bodyYaw, float yaw, ubyte mode, bool onGround, long unknown7, int unknown8, int unknown9) {
958 		position.y -= this.eyeHeight;
959 		this.handleMovementPacket(cast(EntityPosition)Vector3!float(position), yaw, bodyYaw, pitch);
960 	}
961 
962 	protected void handleRiderJumpPacket(long eid) {}
963 
964 	//protected void handleLevelSoundEventPacket(ubyte sound, typeof(Play.LevelSoundEvent.position) position, uint volume, int pitch, bool u1, bool u2) {}
965 
966 	protected void handleEntityEventPacket(long eid, ubyte evid, int unknown) {
967 		if(evid == Play.EntityEvent.USE_ITEM) {
968 			//TODO
969 		}
970 	}
971 
972 	protected void handleMobEquipmentPacket(long eid, Types.Slot item, ubyte inventorySlot, ubyte hotbarSlot, ubyte unknown) {
973 		/+if(hotbarSlot < 9) {
974 			if(inventorySlot == 255) {
975 				// empty
976 				this.inventory.hotbar[hotbarSlot] = 255;
977 			} else {
978 				inventorySlot -= 9;
979 				if(inventorySlot < this.inventory.length) {
980 					if(this.inventory.hotbar.hotbar.canFind(hotbarSlot)) {
981 						// switch item
982 						auto s = this.inventory.hotbar[hotbarSlot];
983 						log("switching ", s, " with ", inventorySlot);
984 						if(s == inventorySlot) {
985 							// just selecting
986 						} else {
987 							// idk what to do
988 						}
989 					} else {
990 						// just move
991 						this.inventory.hotbar[hotbarSlot] = inventorySlot;
992 					}
993 				}
994 			}
995 			this.inventory.selected = hotbarSlot;
996 		}
997 		foreach(i ; this.inventory.hotbar) {
998 			log(i == 255 ? "null" : to!string(this.inventory[i]));
999 		}+/
1000 	}
1001 	
1002 	//protected void handleMobArmorEquipmentPacket(long eid, Types.Slot[4] armor) {}
1003 
1004 	protected void handleInteractPacket(ubyte action, long target, typeof(Play.Interact.targetPosition) position) {
1005 		switch(action) {
1006 			case Play.Interact.LEAVE_VEHICLE:
1007 				//TODO
1008 				break;
1009 			case Play.Interact.HOVER:
1010 				//TODO
1011 				break;
1012 			default:
1013 				break;
1014 		}
1015 	}
1016 
1017 	//protected void handleUseItemPacket(Types.BlockPosition blockPosition, uint hotbarSlot, uint face, typeof(Play.UseItem.facePosition) facePosition, typeof(Play.UseItem.position) position, int slot, Types.Slot item) {}
1018 
1019 	protected void handlePlayerActionPacket(long eid, typeof(Play.PlayerAction.action) action, Types.BlockPosition position, int face) {
1020 		switch(action) {
1021 			case Play.PlayerAction.START_BREAK:
1022 				this.handleStartBlockBreaking(fromBlockPosition(position));
1023 				break;
1024 			case Play.PlayerAction.ABORT_BREAK:
1025 				this.handleAbortBlockBreaking();
1026 				break;
1027 			case Play.PlayerAction.STOP_BREAK:
1028 				this.handleBlockBreaking();
1029 				break;
1030 			case Play.PlayerAction.STOP_SLEEPING:
1031 				this.handleStopSleeping();
1032 				break;
1033 			case Play.PlayerAction.RESPAWN:
1034 				this.handleRespawn();
1035 				break;
1036 			case Play.PlayerAction.JUMP:
1037 				this.handleJump();
1038 				break;
1039 			case Play.PlayerAction.START_SPRINT:
1040 				this.handleSprinting(true);
1041 				if(Effects.speed in this) this.recalculateSpeed();
1042 				break;
1043 			case Play.PlayerAction.STOP_SPRINT:
1044 				this.handleSprinting(false);
1045 				if(Effects.speed in this) this.recalculateSpeed();
1046 				break;
1047 			case Play.PlayerAction.START_SNEAK:
1048 				this.handleSneaking(true);
1049 				break;
1050 			case Play.PlayerAction.STOP_SNEAK:
1051 				this.handleSneaking(false);
1052 				break;
1053 			case Play.PlayerAction.START_GLIDING:
1054 				//TODO
1055 				break;
1056 			case Play.PlayerAction.STOP_GLIDING:
1057 				//TODO
1058 				break;
1059 			default:
1060 				break;
1061 		}
1062 	}
1063 
1064 	//protected void handlePlayerFallPacket(float distance) {}
1065 
1066 	protected void handleAnimatePacket(uint action, long eid, float unknown2) {
1067 		if(action == Play.Animate.BREAKING) this.handleArmSwing();
1068 	}
1069 
1070 	//protected void handleDropItemPacket(ubyte type, Types.Slot slot) {}
1071 
1072 	//protected void handleInventoryActionPacket(uint action, Types.Slot item) {}
1073 
1074 	//protected void handleContainerSetSlotPacket(ubyte window, uint slot, uint hotbar_slot, Types.Slot item, ubyte unknown) {}
1075 
1076 	//protected void handleCraftingEventPacket(ubyte window, uint type, UUID uuid, Types.Slot[] input, Types.Slot[] output) {}
1077 
1078 	protected void handleAdventureSettingsPacket(uint flags, uint unknown1, uint permissions, uint permissionLevel, uint customPermissions, long eid) {
1079 		if(flags & Play.AdventureSettings.FLYING) {
1080 			if(!this.creative && !this.spectator) this.kick("Flying is not enabled on this server");
1081 			//TODO set as flying
1082 		}
1083 	}
1084 
1085 	//protected void handlePlayerInputPacket(typeof(Play.PlayerInput.motion) motion, ushort flags, bool unknown) {}
1086 
1087 	protected void handleSetPlayerGameTypePacket(int gamemode) {
1088 		if(this.op && gamemode >= 0 && gamemode <= 2) {
1089 			this.gamemode = gamemode & 0b11;
1090 		} else {
1091 			this.sendGamemode();
1092 		}
1093 	}
1094 
1095 	//protected void handleMapInfoRequestPacket(long mapId) {}
1096 
1097 	//protected void handleReplaceSelectedItemPacket(Types.Slot slot) {}
1098 
1099 	//protected void handleShowCreditsPacket(ubyte[] payload) {}
1100 
1101 	protected void handleCommandRequestPacket(string command, uint type, string requestId, uint playerId) {
1102 		if(command.startsWith("/")) command = command[1..$];
1103 		if(command.length) {
1104 			this.callCommand(command);
1105 		}
1106 	}
1107 
1108 	protected void handleCommandRequestPacket(string command, uint type, Types.McpeUuid uuid, string requestId, uint playerId, bool internal) {
1109 		this.handleCommandRequestPacket(command, type, requestId, playerId);
1110 	}
1111 	
1112 	enum string stringof = "PocketPlayer!" ~ to!string(__protocol);
1113 
1114 	private static class Compression : Player.Compression {
1115 
1116 		protected override ubyte[] compress(ubyte[] payload) {
1117 			ubyte[] data;
1118 			Compress compress = new Compress(6, HeaderFormat.deflate); //TODO smaller level for smaller payloads
1119 			data ~= cast(ubyte[])compress.compress(payload);
1120 			data ~= cast(ubyte[])compress.flush();
1121 			return data;
1122 		}
1123 
1124 	}
1125 	
1126 }