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/block/tile.d, selery/block/tile.d)
28  */
29 module selery.block.tile;
30 
31 import std.algorithm : canFind;
32 import std.conv : to;
33 import std.json;
34 import std.traits : isAbstractClass;
35 import std.typecons : Tuple;
36 import std.typetuple : TypeTuple;
37 
38 import sel.nbt.tags;
39 
40 import selery.about;
41 import selery.block.block;
42 import selery.block.blocks : Blocks;
43 import selery.block.solid : Facing;
44 import selery.entity.entity : Entity;
45 import selery.item.item : Item;
46 import selery.item.items : Items;
47 import selery.item.slot : Slot;
48 import selery.math.vector : BlockPosition, entityPosition;
49 import selery.player.player : Player;
50 import selery.world.world : World;
51 
52 static import sul.blocks;
53 
54 mixin("import sul.protocol.java" ~ newestJavaProtocol.to!string ~ ".clientbound : UpdateBlockEntity;");
55 
56 /**
57  * A special block that contains additional data.
58  */
59 abstract class Tile : Block {
60 
61 	private static uint count = 0;
62 
63 	private immutable uint n_tid;
64 
65 	private bool n_placed = false;
66 	private World n_world;
67 	private BlockPosition n_position;
68 
69 	public this(sul.blocks.Block data) {
70 		super(data);
71 		this.n_tid = ++count;
72 	}
73 
74 	public final pure nothrow @property @safe @nogc uint tid() {
75 		return this.n_tid;
76 	}
77 
78 	/**
79 	 * Gets the tile's spawn id for Minecraft and Minecraft: Pocket
80 	 * Edition.
81 	 * They're usually in snake case in Minecraft (flower_pot) and
82 	 * in pascal case in Minecraft: Pocket Edition (FlowerPot).
83 	 */
84 	public abstract pure nothrow @property @safe string javaSpawnId();
85 
86 	/// ditto
87 	public abstract pure nothrow @property @safe string pocketSpawnId();
88 
89 	/**
90 	 * Gets the named binary tag.
91 	 * The tag may be null if the tile does not exists in the game's
92 	 * version or when the tile is in its inital state (or empty).
93 	 */
94 	public abstract @property Compound javaCompound();
95 
96 	/// ditto
97 	public abstract @property Compound pocketCompound();
98 
99 	/**
100 	 * Parses a non-null compound saved in the Minecraft's Anvil
101 	 * format.
102 	 */
103 	public abstract void parseJavaCompound(Compound compound);
104 
105 	/**
106 	 * Parses a non-null compound saved from a Minecraft: Pocket
107 	 * Edition's LevelDB format.
108 	 */
109 	public abstract void parsePocketCompound(Compound compound);
110 
111 	public void place(World world, BlockPosition position) {
112 		this.n_placed = true;
113 		this.n_world = world;
114 		this.n_position = position;
115 		this.update();
116 	}
117 
118 	public void unplace() {
119 		this.n_placed = false;
120 		this.n_world = null;
121 		this.n_position = BlockPosition.init;
122 	}
123 
124 	/**
125 	 * Indicates whether the tile has been placed in a world.
126 	 * Example:
127 	 * ---
128 	 * if(tile.placed) {
129 	 *    assert(tile.world !is null);
130 	 * }
131 	 * ---
132 	 */
133 	public final pure nothrow @property @safe @nogc bool placed() {
134 		return this.n_placed;
135 	}
136 	/**
137 	 * Gets the world the tile is placed in, if placed is true.
138 	 */
139 	public final pure nothrow @property @safe @nogc World world() {
140 		return this.n_world;
141 	}
142 
143 	/**
144 	 * Gets the tile's position in the world, if placed.
145 	 * Example:
146 	 * ---
147 	 * if(tile.placed) {
148 	 *    assert(tile.tid == tile.world.tileAt(tile.position).tid);
149 	 * }
150 	 * ---
151 	 */
152 	public final pure nothrow @property @safe @nogc BlockPosition position() {
153 		return this.n_position;
154 	}
155 
156 	/**
157 	 * Gets the action type of the tile, used in Minecraft's
158 	 * UpdateBlockEntity packet.
159 	 */
160 	public abstract pure nothrow @property @safe @nogc ubyte action();
161 
162 	// function called when the custom data changes and the viewers should be updated.
163 	protected void update() {
164 		if(this.placed) {
165 			this.world.updateTile(this, this.position);
166 		}
167 	}
168 
169 }
170 
171 /**
172  * Sign with methods to get and set the text.
173  * Example:
174  * ---
175  * auto sign = world.tileAt!Sign(10, 44, 90);
176  * if(sign !is null) {
177  *    sign[1] = "Click to go to";
178  *    sign[2] = "world";
179  *    assert(sign[] == ["", "Click to go to", "world", ""]);
180  * }
181  * ---
182  */
183 abstract class Sign : Tile {
184 	
185 	/// Indicates the line.
186 	public static immutable size_t FIRST_LINE = 0;
187 	
188 	/// ditto
189 	public static immutable size_t SECOND_LINE = 1;
190 	
191 	/// ditto
192 	public static immutable size_t THIRD_LINE = 2;
193 	
194 	/// ditto
195 	public static immutable size_t FOURTH_LINE = 3;
196 
197 	private Compound n_compound;
198 	private Named!String[4] texts;
199 
200 	private Compound java_compound;
201 	private Named!String[4] java_texts;
202 
203 	public this(sul.blocks.Block data, string a, string b, string c, string d) {
204 		super(data);
205 		foreach(i ; TypeTuple!(0, 1, 2, 3)) {
206 			enum text = "Text" ~ to!string(i + 1);
207 			this.texts[i] = new Named!String(text, "");
208 			this.java_texts[i] = new Named!String(text, "");
209 		}
210 		this.n_compound = new Compound(this.texts[0], this.texts[1], this.texts[2], this.texts[3]);
211 		this.java_compound = new Compound(this.java_texts[0], this.java_texts[1], this.java_texts[2], this.java_texts[3]);
212 		this.setImpl(0, a);
213 		this.setImpl(1, b);
214 		this.setImpl(2, c);
215 		this.setImpl(3, d);
216 	}
217 
218 	public this(sul.blocks.Block data) {
219 		this(data, "", "", "", "");
220 	}
221 
222 	public this(sul.blocks.Block data, string[uint] texts) {
223 		auto a = 0 in texts;
224 		auto b = 1 in texts;
225 		auto c = 2 in texts;
226 		auto d = 3 in texts;
227 		this(data, a ? *a : "", b ? *b : "", c ? *c : "", d ? *d : "");
228 	}
229 	
230 	public override pure nothrow @property @safe string javaSpawnId() {
231 		return "sign";
232 	}
233 	
234 	public override pure nothrow @property @safe string pocketSpawnId() {
235 		return "Sign";
236 	}
237 
238 	/**
239 	 * Gets the array with the text in the four lines.
240 	 * Example:
241 	 * ---
242 	 * sign[2] = "test";
243 	 * assert(sign[] == ["", "", "test", ""]);
244 	 * ---
245 	 */
246 	public @safe @nogc string[4] opIndex() {
247 		string[4] ret;
248 		foreach(i, text; this.texts) {
249 			ret[i] = text.value;
250 		}
251 		return ret;
252 	}
253 
254 	/**
255 	 * Gets the text at the given line.
256 	 * Params:
257 	 * 		index = the line of the sign
258 	 * Returns: a string with the text that has been written at the given line
259 	 * Throws: RangeError if index is not on the range 0..4
260 	 * Example:
261 	 * ---
262 	 * d("First line of sign is: ", sign[Sign.FIRST_LINE]);
263 	 * ---
264 	 */
265 	public @safe string opIndex(size_t index) {
266 		return this.texts[index].value;
267 	}
268 
269 	private @trusted void setImpl(size_t index, string data) {
270 		this.texts[index].value = data;
271 		this.java_texts[index].value = JSONValue(["text": data]).toString();
272 	}
273 
274 	/**
275 	 * Sets all the four lines of the sign.
276 	 * Params:
277 	 * 		texts = four strings to be set in sign's lines
278 	 * Example:
279 	 * ---
280 	 * sign[] = ["a", "b", "", "d"];
281 	 * assert(sign[0] == "a");
282 	 * ---
283 	 */
284 	public void opIndexAssign(string[4] texts) {
285 		foreach(i ; 0..4) {
286 			this.setImpl(i, texts[i]);
287 		}
288 		this.update();
289 	}
290 
291 	/**
292 	 * Sets the given texts in every line of the sign.
293 	 * Params:
294 	 * 		text = the text to be set in every line
295 	 * Example:
296 	 * ---
297 	 * sign[] = "line";
298 	 * assert(sign[] == ["line", "line", "line", "line"]);
299 	 * ---
300 	 */
301 	public void opIndexAssign(string text) {
302 		foreach(i ; 0..4) {
303 			this.setImpl(i, text);
304 		}
305 		this.update();
306 	}
307 
308 	/**
309 	 * Sets the text at the given line.
310 	 * Params:
311 	 * 		text = the new text for the given line
312 	 * 		index = the line to place the text into
313 	 * Throws: RangeError if index is not on the range 0..4
314 	 * Example:
315 	 * ---
316 	 * string text = "New text for line 2";
317 	 * sign[Sign.SECOND_LINE] = text;
318 	 * assert(sign[Sign.SECOND_LINE] == text);
319 	 * ---
320 	 */
321 	public void opIndexAssign(string text, size_t index) {
322 		this.setImpl(index, text);
323 		this.update();
324 	}
325 
326 	/**
327 	 * Checks whether or not every sign's line is
328 	 * an empty string.
329 	 * Example:
330 	 * ---
331 	 * if(!sign.empty) {
332 	 *    sign[] = "";
333 	 *    assert(sign.empty);
334 	 * }
335 	 * ---
336 	 */
337 	public final @property @safe bool empty() {
338 		return this[0].length == 0 && this[1].length == 0 && this[2].length == 0 && this[3].length == 0;
339 	}
340 
341 	public override @property Compound javaCompound() {
342 		return this.java_compound;
343 	}
344 
345 	public override @property Compound pocketCompound() {
346 		return this.n_compound;
347 	}
348 
349 	public override void parseJavaCompound(Compound compound) {
350 		void parse(size_t i, string data) {
351 			auto json = parseJSON(data);
352 			if(json.type == JSON_TYPE.OBJECT) {
353 				auto text = "text" in json;
354 				if(text && (*text).type == JSON_TYPE.STRING) {
355 					this.setImpl(i, (*text).str);
356 				}
357 			}
358 		}
359 		foreach(i ; TypeTuple!(0, 1, 2, 3)) {
360 			mixin("auto text = \"Text" ~ to!string(i) ~ "\" in compound;");
361 			if(text && cast(String)*text) parse(i, cast(String)*text);
362 		}
363 	}
364 
365 	public override void parsePocketCompound(Compound compound) {
366 		foreach(i ; TypeTuple!(0, 1, 2, 3)) {
367 			mixin("auto text = \"Text" ~ to!string(i) ~ "\" in compound;");
368 			if(text && cast(String)*text) this.setImpl(i, cast(String)*text);
369 		}
370 	}
371 	
372 	public override @property @safe @nogc ubyte action() {
373 		static if(is(typeof(UpdateBlockEntity.SIGN_TEXT))) {
374 			return UpdateBlockEntity.SIGN_TEXT;
375 		} else {
376 			return 0;
377 		}
378 	}
379 
380 	public override Slot[] drops(World world, Player player, Item item) {
381 		return [Slot(world.items.get(Items.sign), 1)];
382 	}
383 
384 }
385 
386 class SignBlock : Sign {
387 	
388 	public this(E...)(E args) {
389 		super(args);
390 	}
391 
392 	public override void onUpdated(World world, BlockPosition position, Update update) {
393 		if(!world[position - [0, 1, 0]].solid) {
394 			world.drop(this, position);
395 			world[position] = Blocks.air;
396 		}
397 	}
398 
399 }
400 
401 class WallSignBlock(ubyte facing) : Sign if(facing < 4) {
402 
403 	public this(E...)(E args) {
404 		super(args);
405 	}
406 
407 	public override void onUpdated(World world, BlockPosition position, Update update) {
408 		static if(facing == Facing.north) {
409 			BlockPosition pc = position + [0, 0, 1];
410 		} else static if(facing == Facing.south) {
411 			BlockPosition pc = position - [0, 0, 1];
412 		} else static if(facing == Facing.west) {
413 			BlockPosition pc = position + [1, 0, 0];
414 		} else {
415 			BlockPosition pc = position - [1, 0, 0];
416 		}
417 		if(!world[pc].solid) {
418 			world.drop(this, position);
419 			world[pc] = Blocks.air;
420 		}
421 	}
422 
423 }
424 
425 /**
426  * A pot that can contain a plant.
427  */
428 class FlowerPot : Tile {
429 
430 	private enum javaItems = cast(string[ushort])[
431 		6: "sapling",
432 		31: "tallgrass",
433 		32: "deadbush",
434 		37: "yellow_flower",
435 		38: "red_flower",
436 		39: "brown_mushroom",
437 		40: "red_mushroom",
438 		81: "cactus",
439 	];
440 
441 	private Item m_item;
442 
443 	private Compound pocket_compound, java_compound;
444 
445 	public this(sul.blocks.Block data, Item item=null) {
446 		super(data);
447 		if(item !is null) this.item = item;
448 	}
449 	
450 	public override pure nothrow @property @safe string javaSpawnId() {
451 		return "flower_pot";
452 	}
453 	
454 	public override pure nothrow @property @safe string pocketSpawnId() {
455 		return "FlowerPot";
456 	}
457 
458 	/**
459 	 * Gets the current item placed in the pot.
460 	 * It may be null if the pot is empty.
461 	 * Example:
462 	 * ---
463 	 * if(pot.item is null) {
464 	 *    pot.item = new Items.OxeyeDaisy();
465 	 * }
466 	 * ---
467 	 */
468 	public pure nothrow @property @safe @nogc Item item() {
469 		return this.m_item;
470 	}
471 
472 	/**
473 	 * Places or removes an item from the pot.
474 	 * Example:
475 	 * ---
476 	 * pot.item = new Items.OxeyeDaisy(); // add
477 	 * pot.item = null; // remove
478 	 * ---
479 	 */
480 	public @property Item item(Item item) {
481 		if(item !is null) {
482 			item.clear(); // remove enchantments and custom name
483 			this.pocket_compound = new Compound(new Named!Short("item", item.bedrockId), new Named!Int("mData", item.bedrockMeta));
484 			this.java_compound = new Compound(new Named!String("Item", (){ auto ret=item.javaId in javaItems; return ret ? "minecraft:"~(*ret) : ""; }()), new Named!Int("Data", item.javaMeta));
485 		} else {
486 			this.pocket_compound = null;
487 			this.java_compound = null;
488 		}
489 		this.update();
490 		return this.m_item = item;
491 	}
492 
493 	public override bool onInteract(Player player, Item item, BlockPosition position, ubyte face) {
494 		if(this.item !is null) {
495 			// drop
496 			if(player.inventory.held.empty) player.inventory.held = Slot(this.item, ubyte(1));
497 			else if(player.inventory.held.item == this.item && !player.inventory.held.full) player.inventory.held = Slot(this.item, cast(ubyte)(player.inventory.held.count + 1));
498 			else if(!(player.inventory += Slot(this.item, 1)).empty) player.world.drop(Slot(this.item, 1), position.entityPosition + [.5, .375, .5]);
499 			this.item = null;
500 			return true;
501 		} else if(item !is null && item.javaId in javaItems) {
502 			// place
503 			this.item = item;
504 			ubyte c = player.inventory.held.count;
505 			player.inventory.held = --c ? Slot(item, c) : Slot(null);
506 			return true;
507 		}
508 		return false;
509 	}
510 
511 	public override @property Compound javaCompound() {
512 		return this.java_compound;
513 	}
514 
515 	public override @property Compound pocketCompound() {
516 		return this.pocket_compound;
517 	}
518 
519 	public override void parseJavaCompound(Compound compound) {
520 		if(this.world !is null) {
521 			auto item = "Item" in compound;
522 			auto meta = "Data" in compound;
523 			if(item && cast(String)*item) {
524 				immutable name = (cast(String)*item).value;
525 				foreach(id, n; javaItems) {
526 					if(name == n) {
527 						this.item = this.world.items.fromJava(cast(ushort)id, cast(ushort)(meta && cast(Int)*meta ? cast(Int)*meta : 0));
528 						break;
529 					}
530 				}
531 			}
532 		}
533 	}
534 
535 	public override void parsePocketCompound(Compound compound) {
536 		if(this.world !is null) {
537 			auto id = "item" in compound;
538 			auto meta = "mData" in compound;
539 			if(id && cast(Short)*id) {
540 				this.item = this.world.items.fromBedrock(cast(Short)*id, meta && cast(Int)*meta ? cast(ushort)cast(Int)*meta : 0);
541 			}
542 		}
543 	}
544 
545 	public override ubyte action() {
546 		static if(is(typeof(UpdateBlockEntity.FLOWER_POT_FLOWER))) {
547 			return UpdateBlockEntity.FLOWER_POT_FLOWER;
548 		} else {
549 			return 0;
550 		}
551 	}
552 
553 }
554 
555 //TODO
556 abstract class Container : Tile {
557 
558 	public this(sul.blocks.Block data) {
559 		super(data);
560 	}
561 
562 }
563 
564 template TileImpl(sul.blocks.Block data, T:Tile) {
565 
566 	class TileImpl : T {
567 
568 		public this(E...)(E args) {
569 			super(data, args);
570 		}
571 
572 	}
573 
574 }
575 
576 interface Tiles {
577 
578 	alias FlowerPot = TileImpl!(sul.blocks.Blocks.flowerPot, selery.block.tile.FlowerPot);
579 
580 }