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 }