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