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/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 }