diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
index 7089b80c64a..51bc664ff82 100644
--- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
+++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
@@ -9,7 +9,6 @@
import ch.njol.skript.lang.ParseContext;
import ch.njol.skript.lang.util.SimpleLiteral;
import ch.njol.skript.registrations.Classes;
-import ch.njol.skript.util.BlockUtils;
import ch.njol.yggdrasil.Fields;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.world.MoonPhase;
@@ -21,7 +20,6 @@
import org.bukkit.block.BlockState;
import org.bukkit.block.DoubleChest;
import org.bukkit.block.banner.PatternType;
-import org.bukkit.block.data.BlockData;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.*;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
@@ -85,67 +83,6 @@ public BukkitClasses() {}
.defaultExpression(new EventValueExpression<>(Projectile.class))
.changer(DefaultChangers.nonLivingEntityChanger));
- Classes.registerClass(new ClassInfo<>(BlockData.class, "blockdata")
- .user("block ?datas?")
- .name("Block Data")
- .description("Block data is the detailed information about a block, referred to in Minecraft as BlockStates, " +
- "allowing for the manipulation of different aspects of the block, including shape, waterlogging, direction the block is facing, " +
- "and so much more. Information regarding each block's optional data can be found on Minecraft's Wiki. Find the block you're " +
- "looking for and scroll down to 'Block States'. Different states must be separated by a semicolon (see examples). " +
- "The 'minecraft:' namespace is optional, as well as are underscores.")
- .examples("set block at player to campfire[lit=false]",
- "set target block of player to oak stairs[facing=north;waterlogged=true]",
- "set block at player to grass_block[snowy=true]",
- "set loop-block to minecraft:chest[facing=north]",
- "set block above player to oak_log[axis=y]",
- "set target block of player to minecraft:oak_leaves[distance=2;persistent=false]")
- .after("itemtype")
- .since("2.5")
- .parser(new Parser<>() {
- @Nullable
- @Override
- public BlockData parse(String input, ParseContext context) {
- return BlockUtils.createBlockData(input);
- }
-
- @Override
- public String toString(BlockData o, int flags) {
- return o.getAsString().replace(",", ";");
- }
-
- @Override
- public String toVariableNameString(BlockData o) {
- return "blockdata:" + o.getAsString();
- }
- })
- .serializer(new Serializer<>() {
- @Override
- public Fields serialize(BlockData blockData) {
- return Fields.singletonObject("blockdata", blockData.getAsString());
- }
-
- @Override
- protected BlockData deserialize(Fields fields) throws StreamCorruptedException {
- String data = fields.getObject("blockdata", String.class);
- assert data != null;
- try {
- return Bukkit.createBlockData(data);
- } catch (IllegalArgumentException ex) {
- throw new StreamCorruptedException("Invalid block data: " + data);
- }
- }
-
- @Override
- public boolean mustSyncDeserialization() {
- return true;
- }
-
- @Override
- protected boolean canBeInstantiated() {
- return false;
- }
- }).cloner(BlockData::clone));
-
Classes.registerClass(new ClassInfo<>(World.class, "world")
.user("worlds?")
.name("World")
diff --git a/src/main/java/ch/njol/skript/expressions/ExprBlockData.java b/src/main/java/ch/njol/skript/expressions/ExprBlockData.java
deleted file mode 100644
index 64ba08c6b61..00000000000
--- a/src/main/java/ch/njol/skript/expressions/ExprBlockData.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package ch.njol.skript.expressions;
-
-import ch.njol.skript.classes.Changer.ChangeMode;
-import ch.njol.skript.doc.Description;
-import ch.njol.skript.doc.Example;
-import ch.njol.skript.doc.Name;
-import ch.njol.skript.doc.Since;
-import ch.njol.skript.expressions.base.SimplePropertyExpression;
-import ch.njol.util.coll.CollectionUtils;
-import org.bukkit.block.Block;
-import org.bukkit.block.data.BlockData;
-import org.bukkit.entity.BlockDisplay;
-import org.bukkit.entity.FallingBlock;
-import org.bukkit.event.Event;
-import org.jetbrains.annotations.Nullable;
-
-@Name("Block Data")
-@Description({
- "Get the block data associated with a block.",
- "This data can also be used to set blocks."
-})
-@Example("set {_data} to block data of target block")
-@Example("set block at player to {_data}")
-@Example("set block data of target block to oak_stairs[facing=south;waterlogged=true]")
-@Since("2.5, 2.5.2 (set), 2.10 (block displays)")
-public class ExprBlockData extends SimplePropertyExpression {
-
- static {
- register(ExprBlockData.class, BlockData.class, "block[ ]data", "blocks/displays/entities");
- }
-
- @Override
- public @Nullable BlockData convert(Object object) {
- if (object instanceof Block block)
- return block.getBlockData();
- if (object instanceof BlockDisplay blockDisplay)
- return blockDisplay.getBlock();
- if (object instanceof FallingBlock fallingBlock)
- return fallingBlock.getBlockData();
- return null;
-
- }
-
- @Override
- public Class> @Nullable [] acceptChange(ChangeMode mode) {
- if (mode == ChangeMode.SET)
- return CollectionUtils.array(BlockData.class);
- return null;
- }
-
- @Override
- public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
- assert delta != null; // reset/delete not supported
- BlockData blockData = ((BlockData) delta[0]);
- for (Object object : getExpr().getArray(event)) {
- if (object instanceof Block block) {
- block.setBlockData(blockData);
- } else if (object instanceof BlockDisplay blockDisplay) {
- blockDisplay.setBlock(blockData);
- } else if (object instanceof FallingBlock fallingBlock) {
- fallingBlock.setBlockData(blockData);
- }
- }
- }
-
- @Override
- public Class extends BlockData> getReturnType() {
- return BlockData.class;
- }
-
- @Override
- protected String getPropertyName() {
- return "block data";
- }
-
-}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprBlocks.java b/src/main/java/ch/njol/skript/expressions/ExprBlocks.java
index d4acbcc7af4..7acb0525497 100644
--- a/src/main/java/ch/njol/skript/expressions/ExprBlocks.java
+++ b/src/main/java/ch/njol/skript/expressions/ExprBlocks.java
@@ -1,16 +1,5 @@
package ch.njol.skript.expressions;
-import java.util.Iterator;
-
-import org.bukkit.Chunk;
-import org.bukkit.Location;
-import org.bukkit.block.Block;
-import org.bukkit.event.Event;
-import org.bukkit.util.Vector;
-import org.jetbrains.annotations.Nullable;
-
-import com.google.common.collect.Lists;
-
import ch.njol.skript.Skript;
import ch.njol.skript.SkriptConfig;
import ch.njol.skript.doc.Description;
@@ -26,6 +15,16 @@
import ch.njol.skript.util.Direction;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.iterator.ArrayIterator;
+import com.google.common.collect.Lists;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.event.Event;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.misc.elements.expressions.ExprDirection;
+
+import java.util.Iterator;
@Name("Blocks")
@Description({"Blocks relative to other blocks or between other blocks.",
@@ -144,8 +143,8 @@ public Iterator iterator(Event event) {
return null;
// start block + (max - 1) == max
int distance = SkriptConfig.maxTargetBlockDistance.value() - 1;
- if (this.direction instanceof ExprDirection) {
- Expression numberExpression = ((ExprDirection) this.direction).amount;
+ if (this.direction instanceof ExprDirection exprDirection) {
+ Expression numberExpression = exprDirection.getAmount();
if (numberExpression != null) {
Number number = numberExpression.getSingle(event);
if (number != null)
diff --git a/src/main/java/ch/njol/skript/expressions/ExprDirection.java b/src/main/java/ch/njol/skript/expressions/ExprDirection.java
deleted file mode 100644
index b0a709accc8..00000000000
--- a/src/main/java/ch/njol/skript/expressions/ExprDirection.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package ch.njol.skript.expressions;
-
-import org.bukkit.Location;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockFace;
-import org.bukkit.entity.Entity;
-import org.bukkit.event.Event;
-import org.bukkit.util.Vector;
-import org.jetbrains.annotations.Nullable;
-
-import ch.njol.skript.Skript;
-import ch.njol.skript.doc.Description;
-import ch.njol.skript.doc.Example;
-import ch.njol.skript.doc.Name;
-import ch.njol.skript.doc.Since;
-import ch.njol.skript.lang.Expression;
-import ch.njol.skript.lang.ExpressionType;
-import ch.njol.skript.lang.SkriptParser.ParseResult;
-import ch.njol.skript.lang.util.SimpleExpression;
-import ch.njol.skript.util.Direction;
-import ch.njol.util.Kleenean;
-import ch.njol.util.Math2;
-
-/**
- * @author Peter Güttinger
- */
-@Name("Direction")
-@Description("A helper expression for the direction type .")
-@Example("thrust the player upwards")
-@Example("set the block behind the player to water")
-@Example("""
- loop blocks above the player:
- set {_rand} to a random integer between 1 and 10
- set the block {_rand} meters south east of the loop-block to stone
- """)
-@Example("block in horizontal facing of the clicked entity from the player is air")
-@Example("spawn a creeper 1.5 meters horizontally behind the player")
-@Example("spawn a TNT 5 meters above and 2 meters horizontally behind the player")
-@Example("thrust the last spawned TNT in the horizontal direction of the player with speed 0.2")
-@Example("push the player upwards and horizontally forward at speed 0.5")
-@Example("push the clicked entity in in the direction of the player at speed -0.5")
-@Example("open the inventory of the block 2 blocks below the player to the player")
-@Example("teleport the clicked entity behind the player")
-@Example("grow a regular tree 2 meters horizontally behind the player")
-@Since("1.0 (basic), 2.0 (extended)")
-public class ExprDirection extends SimpleExpression {
-
- private final static BlockFace[] byMark = new BlockFace[] {
- BlockFace.UP, BlockFace.DOWN,
- BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST,
- BlockFace.NORTH_EAST, BlockFace.NORTH_WEST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_WEST};
- private final static int UP = 0, DOWN = 1,
- NORTH = 2, SOUTH = 3, EAST = 4, WEST = 5,
- NORTH_EAST = 6, NORTH_WEST = 7, SOUTH_EAST = 8, SOUTH_WEST = 9;
-
- static {
- // TODO think about parsing statically & dynamically (also in general)
- // "at": see LitAt
- // TODO direction of %location% (from|relative to) %location%
- Skript.registerExpression(ExprDirection.class, Direction.class, ExpressionType.COMBINED,
- "[%-number% [(block|met(er|re))[s]] [to the]] (" +
- NORTH + "¦north[(-| |)(" + (NORTH_EAST ^ NORTH) + "¦east|" + (NORTH_WEST ^ NORTH) + "¦west)][(ward(s|ly|)|er(n|ly|))] [of]" +
- "|" + SOUTH + "¦south[(-| |)(" + (SOUTH_EAST ^ SOUTH) + "¦east|" + (SOUTH_WEST ^ SOUTH) + "¦west)][(ward(s|ly|)|er(n|ly|))] [of]" +
- "|(" + EAST + "¦east|" + WEST + "¦west)[(ward(s|ly|)|er(n|ly|))] [of]" +
- "|" + UP + "¦above|" + UP + "¦over|(" + UP + "¦up|" + DOWN + "¦down)[ward(s|ly|)]|" + DOWN + "¦below|" + DOWN + "¦under[neath]|" + DOWN + "¦beneath" +
- ") [%-direction%]",
- "[%-number% [(block|met(er|re))[s]]] in [the] (0¦direction|1¦horizontal direction|2¦facing|3¦horizontal facing) of %entity/block% (of|from|)",
- "[%-number% [(block|met(er|re))[s]]] in %entity/block%'[s] (0¦direction|1¦horizontal direction|2¦facing|3¦horizontal facing) (of|from|)",
- "[%-number% [(block|met(er|re))[s]]] (0¦in[ ]front [of]|0¦forward[s]|2¦behind|2¦backwards|[to the] (1¦right|-1¦left) [of])",
- "[%-number% [(block|met(er|re))[s]]] horizontal[ly] (0¦in[ ]front [of]|0¦forward[s]|2¦behind|2¦backwards|to the (1¦right|-1¦left) [of])");
- }
-
- @Nullable
- Expression amount;
-
- @Nullable
- private Vector direction;
- @Nullable
- private ExprDirection next;
-
- @Nullable
- private Expression> relativeTo;
- boolean horizontal;
- boolean facing;
-
- private double yaw;
-
- @SuppressWarnings("unchecked")
- @Override
- public boolean init(final Expression>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) {
- amount = (Expression) exprs[0];
- switch (matchedPattern) {
- case 0:
- direction = new Vector(byMark[parseResult.mark].getModX(), byMark[parseResult.mark].getModY(), byMark[parseResult.mark].getModZ());
- if (exprs[1] != null) {
- if (!(exprs[1] instanceof ExprDirection) || ((ExprDirection) exprs[1]).direction == null)
- return false;
- next = (ExprDirection) exprs[1];
- }
- break;
- case 1:
- case 2:
- relativeTo = exprs[1];
- horizontal = parseResult.mark % 2 != 0;
- facing = parseResult.mark >= 2;
- break;
- case 3:
- case 4:
- yaw = Math.PI / 2 * parseResult.mark;
- horizontal = matchedPattern == 4;
- }
- return true;
- }
-
- @Override
- @Nullable
- protected Direction[] get(final Event e) {
- final Number n = amount != null ? amount.getSingle(e) : 1;
- if (n == null)
- return new Direction[0];
- final double ln = n.doubleValue();
- if (direction != null) {
- final Vector v = direction.clone().multiply(ln);
- ExprDirection d = next;
- while (d != null) {
- final Number n2 = d.amount != null ? d.amount.getSingle(e) : 1;
- if (n2 == null)
- return new Direction[0];
- assert d.direction != null; // checked in init()
- v.add(d.direction.clone().multiply(n2.doubleValue()));
- d = d.next;
- }
- assert v != null;
- return new Direction[] {new Direction(v)};
- } else if (relativeTo != null) {
- final Object o = relativeTo.getSingle(e);
- if (o == null)
- return new Direction[0];
- if (o instanceof Block) {
- final BlockFace f = Direction.getFacing((Block) o);
- if (f == BlockFace.SELF || horizontal && (f == BlockFace.UP || f == BlockFace.DOWN))
- return new Direction[] {Direction.ZERO};
- return new Direction[] {new Direction(f, ln)};
- } else {
- final Location l = ((Entity) o).getLocation();
- if (!horizontal) {
- if (!facing) {
- final Vector v = l.getDirection().normalize().multiply(ln);
- assert v != null;
- return new Direction[] {new Direction(v)};
- }
- final double pitch = Direction.pitchToRadians(l.getPitch());
- assert pitch >= -Math.PI / 2 && pitch <= Math.PI / 2;
- if (pitch > Math.PI / 4)
- return new Direction[] {new Direction(new double[] {0, ln, 0})};
- if (pitch < -Math.PI / 4)
- return new Direction[] {new Direction(new double[] {0, -ln, 0})};
- }
- double yaw = Direction.yawToRadians(l.getYaw());
- if (horizontal && !facing) {
- return new Direction[] {new Direction(new double[] {Math.cos(yaw) * ln, 0, Math.sin(yaw) * ln})};
- }
- yaw = Math2.mod(yaw, 2 * Math.PI);
- if (yaw >= Math.PI / 4 && yaw < 3 * Math.PI / 4)
- return new Direction[] {new Direction(new double[] {0, 0, ln})};
- if (yaw >= 3 * Math.PI / 4 && yaw < 5 * Math.PI / 4)
- return new Direction[] {new Direction(new double[] {-ln, 0, 0})};
- if (yaw >= 5 * Math.PI / 4 && yaw < 7 * Math.PI / 4)
- return new Direction[] {new Direction(new double[] {0, 0, -ln})};
- assert yaw >= 0 && yaw < Math.PI / 4 || yaw >= 7 * Math.PI / 4 && yaw < 2 * Math.PI;
- return new Direction[] {new Direction(new double[] {ln, 0, 0})};
- }
- } else {
- return new Direction[] {new Direction(horizontal ? Direction.IGNORE_PITCH : 0, yaw, ln)};
- }
- }
-
- @Override
- public boolean isSingle() {
- return true;
- }
-
- @Override
- public Class extends Direction> getReturnType() {
- return Direction.class;
- }
-
- @Override
- public String toString(final @Nullable Event e, final boolean debug) {
- final Expression> relativeTo = this.relativeTo;
- return (amount != null ? amount.toString(e, debug) + " meter(s) " : "") + (direction != null ? Direction.toString(direction) :
- relativeTo != null ? " in " + (horizontal ? "horizontal " : "") + (facing ? "facing" : "direction") + " of " + relativeTo.toString(e, debug) :
- (horizontal ? "horizontally " : "") + Direction.toString(0, yaw, 1));
- }
-
-}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprVectorFromDirection.java b/src/main/java/ch/njol/skript/expressions/ExprVectorFromDirection.java
index b75881118c8..90af64f8ba8 100644
--- a/src/main/java/ch/njol/skript/expressions/ExprVectorFromDirection.java
+++ b/src/main/java/ch/njol/skript/expressions/ExprVectorFromDirection.java
@@ -14,6 +14,7 @@
import org.bukkit.event.Event;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.misc.elements.expressions.ExprDirection;
@Name("Vectors - Create from Direction")
@Description({
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/BlockModule.java b/src/main/java/org/skriptlang/skript/bukkit/block/BlockModule.java
index a3180678d40..d8b4dd4ed94 100644
--- a/src/main/java/org/skriptlang/skript/bukkit/block/BlockModule.java
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/BlockModule.java
@@ -3,6 +3,7 @@
import org.skriptlang.skript.addon.AddonModule;
import org.skriptlang.skript.addon.HierarchicalAddonModule;
import org.skriptlang.skript.addon.SkriptAddon;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataModule;
import org.skriptlang.skript.bukkit.block.furnace.FurnaceModule;
import org.skriptlang.skript.bukkit.block.sign.SignModule;
@@ -17,6 +18,7 @@ public BlockModule(AddonModule parentModule) {
@Override
public Iterable children() {
return List.of(
+ new BlockDataModule(this),
new FurnaceModule(this),
new SignModule(this)
);
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataClassInfo.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataClassInfo.java
new file mode 100644
index 00000000000..456163b8642
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataClassInfo.java
@@ -0,0 +1,91 @@
+package org.skriptlang.skript.bukkit.block.blockdata;
+
+import ch.njol.skript.classes.ClassInfo;
+import ch.njol.skript.classes.Parser;
+import ch.njol.skript.classes.Serializer;
+import ch.njol.skript.lang.ParseContext;
+import ch.njol.skript.util.BlockUtils;
+import ch.njol.yggdrasil.Fields;
+import org.bukkit.Bukkit;
+import org.bukkit.block.data.BlockData;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.StreamCorruptedException;
+
+public class BlockDataClassInfo extends ClassInfo {
+
+ public BlockDataClassInfo() {
+ super(BlockData.class, "blockdata");
+ this.user("block ?datas?")
+ .name("Block Data")
+ .description("""
+ Block data is the detailed information about a block, referred to in Minecraft as BlockStates, \
+ allowing for the manipulation of different aspects of the block, including shape, waterlogging, \
+ direction the block is facing, and so much more.
+ Information regarding each block's optional data can be found on Minecraft's Wiki. Find the block you're \
+ looking for and scroll down to 'Block States'. Different states must be separated by a semicolon (see examples).
+ The 'minecraft:' namespace is optional, as well as are underscores.
+ """)
+ .examples("set block at player to campfire[lit=false]",
+ "set target block of player to oak stairs[facing=north;waterlogged=true]",
+ "set block at player to grass_block[snowy=true]",
+ "set loop-block to minecraft:chest[facing=north]",
+ "set block above player to oak_log[axis=y]",
+ "set target block of player to minecraft:oak_leaves[distance=2;persistent=false]")
+ .after("itemtype")
+ .since("2.5")
+ .parser(new BlockDataParser())
+ .serializer(new BlockDataSerializer())
+ .cloner(BlockData::clone);
+ }
+
+ private static class BlockDataParser extends Parser {
+ //
+ @Override
+ public @Nullable BlockData parse(String input, ParseContext context) {
+ return BlockUtils.createBlockData(input);
+ }
+
+ @Override
+ public String toString(BlockData blockData, int flags) {
+ return blockData.getAsString().replace(",", ";");
+ }
+
+ @Override
+ public String toVariableNameString(BlockData blockData) {
+ return "blockdata:" + blockData.getAsString();
+ }
+ //
+ }
+
+ private static class BlockDataSerializer extends Serializer {
+ //
+ @Override
+ public Fields serialize(BlockData blockData) {
+ return Fields.singletonObject("blockdata", blockData.getAsString());
+ }
+
+ @Override
+ protected BlockData deserialize(Fields fields) throws StreamCorruptedException {
+ String data = fields.getObject("blockdata", String.class);
+ assert data != null;
+ try {
+ return Bukkit.createBlockData(data);
+ } catch (IllegalArgumentException ex) {
+ throw new StreamCorruptedException("Invalid block data: " + data);
+ }
+ }
+
+ @Override
+ public boolean mustSyncDeserialization() {
+ return true;
+ }
+
+ @Override
+ protected boolean canBeInstantiated() {
+ return false;
+ }
+ //
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataHolder.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataHolder.java
new file mode 100644
index 00000000000..de4d789d3db
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataHolder.java
@@ -0,0 +1,230 @@
+package org.skriptlang.skript.bukkit.block.blockdata;
+
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.lang.SyntaxElement;
+import ch.njol.util.StringUtils;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.entity.BlockDisplay;
+import org.bukkit.entity.FallingBlock;
+import org.bukkit.inventory.meta.BlockDataMeta;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class for listing types that contain {@link BlockData}.
+ * @param The object type that contains {@link BlockData}.
+ */
+public interface BlockDataHolder {
+
+ /**
+ * {@link BlockDataHolder} for {@link Block}s.
+ */
+ BlockDataHolder BLOCK = new BlockDataHolder<>() {
+ //
+ @Override
+ public String getPattern(boolean plural) {
+ return plural ? "blocks" : "block";
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return Block.class;
+ }
+
+ @Override
+ public BlockData getBlockData(Block block) {
+ return block.getBlockData();
+ }
+
+ @Override
+ public void setBlockData(Block block, BlockData blockData) {
+ block.setBlockData(blockData);
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataHolder} for {@link BlockData}s.
+ */
+ BlockDataHolder BLOCK_DATA = new BlockDataHolder<>() {
+ //
+ @Override
+ public String getPattern(boolean plural) {
+ return plural ? "blockdatas" : "blockdata";
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return BlockData.class;
+ }
+
+ @Override
+ public BlockData getBlockData(BlockData blockData) {
+ return blockData;
+ }
+
+ @Override
+ public void setBlockData(BlockData blockData, BlockData blockData2) {
+ blockData2.copyTo(blockData);
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataHolder} for {@link ItemType}s.
+ */
+ BlockDataHolder ITEMTYPE = new BlockDataHolder<>() {
+ //
+ @Override
+ public String getPattern(boolean plural) {
+ return plural ? "itemtypes" : "itemtype";
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return ItemType.class;
+ }
+
+ @Override
+ public boolean isType(Object object) {
+ return object instanceof ItemType itemType && itemType.getItemMeta() instanceof BlockDataMeta && itemType.getMaterial().isBlock();
+ }
+
+ @Override
+ public BlockData getBlockData(ItemType itemType) {
+ return ((BlockDataMeta) itemType.getItemMeta()).getBlockData(itemType.getMaterial());
+ }
+
+ @Override
+ public void setBlockData(ItemType itemType, BlockData blockData) {
+ BlockDataMeta blockDataMeta = (BlockDataMeta) itemType.getItemMeta();
+ blockDataMeta.setBlockData(blockData);
+ itemType.setItemMeta(blockDataMeta);
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataHolder} for {@link BlockDisplay}s.
+ */
+ BlockDataHolder BLOCK_DISPLAY = new BlockDataHolder<>() {
+ //
+ @Override
+ public String getPattern(boolean plural) {
+ return plural ? "displays" : "display";
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return BlockDisplay.class;
+ }
+
+ @Override
+ public BlockData getBlockData(BlockDisplay blockDisplay) {
+ return blockDisplay.getBlock();
+ }
+
+ @Override
+ public void setBlockData(BlockDisplay blockDisplay, BlockData blockData) {
+ blockDisplay.setBlock(blockData);
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataHolder} for {@link FallingBlock}s.
+ */
+ BlockDataHolder FALLING_BLOCK = new BlockDataHolder<>() {
+ //
+ @Override
+ public String getPattern(boolean plural) {
+ return plural ? "entities" : "entity";
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return FallingBlock.class;
+ }
+
+ @Override
+ public BlockData getBlockData(FallingBlock fallingBlock) {
+ return fallingBlock.getBlockData();
+ }
+
+ @Override
+ public void setBlockData(FallingBlock fallingBlock, BlockData blockData) {
+ fallingBlock.setBlockData(blockData);
+ }
+ //
+ };
+
+ /**
+ * List of all {@link BlockDataHolder}s currently supported.
+ */
+ List> HOLDERS = List.of(BLOCK, BLOCK_DATA, ITEMTYPE, BLOCK_DISPLAY, FALLING_BLOCK);
+
+ /**
+ * Combined pattern of all {@link BlockDataHolder}s plural type with "/".
+ * Used for patterns when registering a {@link SyntaxElement}.
+ */
+ String PLURAL_PATTERN_TYPES = StringUtils.join(HOLDERS.stream()
+ .map(holder -> holder.getPattern(true)).collect(Collectors.toSet()),
+ "/");
+
+ /**
+ * Retrieves the {@link BlockDataHolder} that handles {@code object}.
+ * @param object The {@link Object} to get a {@link BlockDataHolder} for.
+ * @return The resulting {@link BlockDataHolder} if found, otherwise {@code null}.
+ */
+ static @Nullable BlockDataHolder> getHolder(Object object) {
+ for (BlockDataHolder> holder : HOLDERS) {
+ if (holder.isType(object))
+ return holder;
+ }
+ return null;
+ }
+
+ /**
+ * @return The singular pattern used for registering {@link SyntaxElement}s.
+ */
+ default String getPattern() {
+ return getPattern(false);
+ }
+
+ /**
+ * @param plural Whether the returned pattern should be plural or singular.
+ * @return The resulting singular or plural pattern used for registering {@link SyntaxElement}s.
+ */
+ String getPattern(boolean plural);
+
+ /**
+ * @return The {@link Class} {@code this} handles.
+ */
+ Class getTypeClass();
+
+ /**
+ * Whether {@code object} is handled by {@code this}.
+ * @param object The {@link Object} to check.
+ * @return {@code true} if handled, otherwise {@code false}.
+ */
+ default boolean isType(Object object) {
+ return getTypeClass().isInstance(object);
+ }
+
+ /**
+ * @param type The object to get the {@link BlockData} from.
+ * @return The {@link BlockData} of {@code type}.
+ */
+ BlockData getBlockData(Type type);
+
+ /**
+ * Sets the {@link BlockData} on {@code type} to {@code blockData}.
+ * @param type The object to change the {@link BlockData} of.
+ * @param blockData The {@link BlockData} to change to.
+ */
+ void setBlockData(Type type, BlockData blockData);
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataModule.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataModule.java
new file mode 100644
index 00000000000..91c87469eea
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataModule.java
@@ -0,0 +1,38 @@
+package org.skriptlang.skript.bukkit.block.blockdata;
+
+import ch.njol.skript.registrations.Classes;
+import org.skriptlang.skript.addon.AddonModule;
+import org.skriptlang.skript.addon.HierarchicalAddonModule;
+import org.skriptlang.skript.addon.SkriptAddon;
+import org.skriptlang.skript.bukkit.block.blockdata.elements.CondBlockDataTag;
+import org.skriptlang.skript.bukkit.block.blockdata.elements.ExprBlockData;
+import org.skriptlang.skript.bukkit.block.blockdata.elements.ExprBlockDataTags;
+import org.skriptlang.skript.bukkit.block.blockdata.elements.ExprBlockDataValues;
+
+public class BlockDataModule extends HierarchicalAddonModule {
+
+ public BlockDataModule(AddonModule parent) {
+ super(parent);
+ }
+
+ @Override
+ protected void initSelf(SkriptAddon addon) {
+ Classes.registerClass(new BlockDataClassInfo());
+ }
+
+ @Override
+ protected void loadSelf(SkriptAddon addon) {
+ register(addon,
+ CondBlockDataTag::register,
+ ExprBlockData::register,
+ ExprBlockDataTags::register,
+ ExprBlockDataValues::register
+ );
+ }
+
+ @Override
+ public String name() {
+ return "blockdata";
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataTag.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataTag.java
new file mode 100644
index 00000000000..d9766944d70
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataTag.java
@@ -0,0 +1,238 @@
+package org.skriptlang.skript.bukkit.block.blockdata;
+
+import org.bukkit.Bukkit;
+import org.bukkit.block.data.BlockData;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Container for holding the key, raw value, and typed value object of a {@link BlockData} tag.
+ * @see BlockDataValueType for typed value objects other than a string.
+ */
+public class BlockDataTag {
+
+ private static final Pattern BLOCKDATA_PATTERN = Pattern.compile(".*\\[(?(?:.+=.+,?)+)?]");
+
+ /**
+ * Gets a {@link BlockDataTag} matching {@code key} from {@code blockData}.
+ * @param blockData The {@link BlockData} to get the {@link BlockDataTag} from.
+ * @param key The key of the tag to retrieve.
+ * @return The resulting {@link BlockDataTag} if found, otherwise {@code null}.
+ */
+ public static @Nullable BlockDataTag of(BlockData blockData, String key) {
+ BlockDataTag[] tags = of(blockData);
+ if (tags == null)
+ return null;
+ for (BlockDataTag tag : tags) {
+ if (tag.key.equals(key))
+ return tag;
+ }
+ return null;
+ }
+
+ /**
+ * Gets all {@link BlockDataTag}s matching any of the {@code keys} from {@code blockData}.
+ * @param blockData The {@link BlockData} to get the {@link BlockDataTag}s from.
+ * @param keys The keys of the tags to retrieve.
+ * @return The resulting {@link BlockDataTag}s if any are found, otherwise {@code null}.
+ */
+ public static BlockDataTag @Nullable [] of(BlockData blockData, Collection keys) {
+ return of(blockData, keys.toArray(String[]::new));
+ }
+
+ /**
+ * Gets all {@link BlockDataTag}s matching any of the {@code keys} from {@code blockData}.
+ * @param blockData The {@link BlockData} to get the {@link BlockDataTag}s from.
+ * @param keys The keys of the tags to retrieve.
+ * @return The resulting {@link BlockDataTag}s if any are found, otherwise {@code null}.
+ */
+ public static BlockDataTag @Nullable [] of(BlockData blockData, String... keys) {
+ BlockDataTag[] tags = of(blockData);
+ if (tags == null)
+ return null;
+ return Arrays.stream(tags)
+ .filter(tag -> Arrays.stream(keys).anyMatch(key -> key.equalsIgnoreCase(tag.getKey())))
+ .toArray(BlockDataTag[]::new);
+ }
+
+ /**
+ * Gets all {@link BlockDataTag}s from {@code blockData}.
+ * @param blockData The {@link BlockData} to get the {@link BlockDataTag}s from.
+ * @return The resulting {@link BlockDataTag}s if any exist, otherwise {@code null}.
+ */
+ public static BlockDataTag @Nullable [] of(BlockData blockData) {
+ String[] tags = getTags(blockData);
+ if (tags == null || tags.length == 0)
+ return null;
+ List dataTags = new ArrayList<>();
+ for (String tag : tags) {
+ String[] split = tag.split("=");
+ assert split.length >= 2;
+ dataTags.add(new BlockDataTag(split[0], split[1]));
+ }
+ return dataTags.toArray(BlockDataTag[]::new);
+ }
+
+ /**
+ * Gets all the tags of {@code blockData}.
+ * @param blockData The {@link BlockData} to get the tags from.
+ * @return The resulting {@link String} array of the tags in the form of "key=value".
+ */
+ private static String @Nullable [] getTags(BlockData blockData) {
+ String dataString = blockData.getAsString(false);
+ Matcher matcher = BLOCKDATA_PATTERN.matcher(dataString);
+ if (!matcher.matches())
+ return null;
+ String tagGroup = matcher.group("tags");
+ if (tagGroup == null || tagGroup.isBlank())
+ return null;
+ return tagGroup.split(",");
+ }
+
+ private final String key;
+ private String rawValue;
+ private Object value;
+ private BlockDataValueType valueType = BlockDataValueType.STRING;
+
+ /**
+ * Construct a new {@link BlockDataTag} with the data from a {@link BlockData}'s tag.
+ * @param key The key of a {@link BlockData} tag.
+ * @param rawValue The raw/string value of a {@link BlockData} tag.
+ */
+ public BlockDataTag(String key, String rawValue) {
+ this.key = key.toLowerCase(Locale.ENGLISH);
+ this.rawValue = rawValue;
+ this.value = rawValue;
+ setValueType();
+ }
+
+ /**
+ * Checks if the {@link #rawValue} can be parsed as one of the {@link BlockDataValueType}s.
+ */
+ private void setValueType() {
+ if (rawValue == null)
+ return;
+ for (BlockDataValueType> type : BlockDataValueType.TYPES) {
+ if (type == BlockDataValueType.STRING)
+ continue;
+ Object newValue = type.parse(rawValue);
+ if (newValue != null) {
+ value = newValue;
+ valueType = type;
+ return;
+ }
+ }
+ }
+
+ /**
+ * @return The key of the {@link BlockData} tag used to construct {@code this}.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * @return The raw/string value used for {@link #key}.
+ */
+ public String getRawValue() {
+ return rawValue;
+ }
+
+ /**
+ * @return The value for {@link #key}. Can be a {@link String} or any of the types in {@link BlockDataValueType}.
+ */
+ public Object getValue() {
+ return value;
+ }
+
+ /**
+ * @return The {@link BlockDataValueType} if the {@link #rawValue} successfully parsed, otherwise {@code null}.
+ */
+ public @Nullable BlockDataValueType> getValueType() {
+ return valueType;
+ }
+
+ /**
+ * Attempts to change the value of {@code this} by ensuring {@code value} can be parsed with {@link #valueType} if not {@code null}.
+ * @param value The value to change to.
+ * @return {@code true} if the change was successful, otherwise {@code false}.
+ */
+ public boolean attemptValueChange(@Nullable Object value) {
+ // Regardless of type, value can be changed to null
+ if (value == null) {
+ this.value = null;
+ this.rawValue = null;
+ return true;
+ }
+ // Parses the value as the intended type
+ Object newValue = valueType.parse(value);
+ if (newValue != null) {
+ this.value = newValue;
+ this.rawValue = newValue.toString();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Whether the {@link #valueType} of {@code this} can be checked to ensure a value is valid.
+ * @return {@code true} if can be checked, otherwise {@code false}.
+ */
+ public boolean hasValidityCheck() {
+ return valueType.hasValidityCheck();
+ }
+
+ /**
+ * Checks the validity of {@link #value} to ensure {@code blockData} supports it.
+ * @param blockData The {@link BlockData} to check if it supports {@link #value}.
+ * @return {@code true} if it's supported, otherwise {@code false}.
+ */
+ public boolean checkValidity(BlockData blockData) {
+ String dataString = blockData.getMaterial().getKey() + "[" + this + "]";
+ try {
+ Bukkit.createBlockData(dataString);
+ return true;
+ } catch (Exception ignored) {}
+ return false;
+ }
+
+ /**
+ * @return {@link #rawValue} or string representation of the converted value defined by {@link BlockDataValueType#toStringConversion(Object)}.
+ */
+ public String getConversionString() {
+ if (rawValue == null)
+ return "";
+ if (valueType.requiresConversion()) {
+ //noinspection unchecked
+ return valueType.toStringConversion(value);
+ }
+ return rawValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return valueType.hashCode() + key.hashCode() + value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof BlockDataTag other))
+ return false;
+ return valueType == other.valueType && key.equalsIgnoreCase(other.key) && value.equals(other.value);
+ }
+
+ @Override
+ public String toString() {
+ if (rawValue == null)
+ return "";
+ return key + "=" + getConversionString();
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataValueType.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataValueType.java
new file mode 100644
index 00000000000..82484d5a3f9
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/BlockDataValueType.java
@@ -0,0 +1,192 @@
+package org.skriptlang.skript.bukkit.block.blockdata;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser;
+import ch.njol.skript.lang.util.ContextlessEvent;
+import ch.njol.skript.log.RetainingLogHandler;
+import ch.njol.skript.log.SkriptLogger;
+import ch.njol.skript.util.Direction;
+import ch.njol.util.coll.iterator.SingleItemIterator;
+import org.bukkit.Bukkit;
+import org.bukkit.block.data.BlockData;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.misc.elements.expressions.ExprDirection;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Helper class for determining the value of a {@link BlockData} tag can be an object other than a {@link String}.
+ * @param The type of value
+ */
+public interface BlockDataValueType {
+
+ /**
+ * {@link BlockDataValueType} for {@link BlockData} tags with {@link Integer} values.
+ */
+ BlockDataValueType INTEGER = new BlockDataValueType<>() {
+ //
+ @Override
+ public Class getTypeClass() {
+ return Integer.class;
+ }
+
+ @Override
+ public @Nullable Integer parse(String string) {
+ return string.matches("\\d+") ? Integer.parseInt(string) : null;
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataValueType} for {@link BlockData} tags with {@link Boolean} values.
+ */
+ BlockDataValueType BOOLEAN = new BlockDataValueType<>() {
+ //
+ @Override
+ public Class getTypeClass() {
+ return Boolean.class;
+ }
+
+ @Override
+ public @Nullable Boolean parse(String string) {
+ return (string.equalsIgnoreCase("true") || string.equalsIgnoreCase("false"))
+ ? Boolean.valueOf(string) : null;
+ }
+
+ @Override
+ public boolean hasValidityCheck() {
+ return false;
+ }
+ //
+ };
+
+ /**
+ * {@link BlockDataValueType} for {@link BlockData} tags with {@link Direction} values.
+ */
+ BlockDataValueType DIRECTION = new BlockDataValueType<>() {
+ //
+ @Override
+ public Class getTypeClass() {
+ return Direction.class;
+ }
+
+ @Override
+ public @Nullable Direction parse(String string) {
+ RetainingLogHandler logHandler = SkriptLogger.startRetainingLog();
+ Expression> expr = null;
+ try {
+ expr = SkriptParser.parseStatic(string.replace("_", " "), new SingleItemIterator<>(ExprDirection.syntaxInfo), "");
+ } finally {
+ logHandler.clear();
+ logHandler.printErrors();
+ }
+ if (expr == null || !expr.getReturnType().equals(Direction.class))
+ return null;
+ return (Direction) expr.getSingle(ContextlessEvent.get());
+ }
+
+ @Override
+ public boolean requiresConversion() {
+ return true;
+ }
+
+ @Override
+ public String toStringConversion(Direction direction) {
+ return Direction.toNearestBlockFace(direction.getDirection()).toString().toLowerCase(Locale.ENGLISH);
+ }
+ //
+ };
+
+
+ /**
+ * {@link BlockDataValueType} for {@link BlockData} tags with {@link String} values.
+ * This is the default value type and will match against any value.
+ */
+ BlockDataValueType STRING = new BlockDataValueType<>() {
+ //
+ @Override
+ public Class getTypeClass() {
+ return String.class;
+ }
+
+ @Override
+ public @Nullable String parse(Object object) {
+ if (object instanceof String string)
+ return string;
+ return object.toString();
+ }
+
+ @Override
+ public @Nullable String parse(String string) {
+ return string;
+ }
+ //
+ };
+
+ /**
+ * List of all {@link BlockDataValueType}s currently supported.
+ */
+ List> TYPES = List.of(INTEGER, BOOLEAN, DIRECTION, STRING);
+
+ /**
+ * List of all {@link Class}es that all {@link BlockDataValueType}s handle.
+ */
+ Class>[] TYPE_CLASSES = TYPES.stream().map(BlockDataValueType::getTypeClass).toArray(Class[]::new);
+
+ /**
+ * @return The {@link Class} {@code this} is bound to.
+ */
+ Class getTypeClass();
+
+ /**
+ * Checks if {@code object} is instance of {@code TYPE} or a {@link String} that can be parsed into {@code TYPE}.
+ * @param object The {@link Object} to parse.
+ * @return The parsed {@code TYPE} if successful, otherwise {@code null}.
+ */
+ @SuppressWarnings("unchecked")
+ default @Nullable Type parse(Object object) {
+ if (getTypeClass().isInstance(object)) {
+ return (Type) object;
+ } else if (object instanceof String string) {
+ return parse(string);
+ }
+ return null;
+ }
+
+ /**
+ * Attempts to parse {@code string} into {@code TYPE}.
+ * @param string The {@link String} to parse.
+ * @return The parsed {@code TYPE} if successful, otherwise {@code null}.
+ */
+ @Nullable Type parse(String string);
+
+ /**
+ * Whether {@code this} requires converting {@code Type} for stringification
+ * via {@link #toStringConversion(Object)}
+ * @return {@code true} if conversion required, otherwise {@code false}.
+ */
+ default boolean requiresConversion() {
+ return false;
+ }
+
+ /**
+ * Converts {@code type} for specified stringification.
+ * Primarily for converting {@link Skript} types into {@link Bukkit} types and to be used in {@link BlockData}.
+ * @param type The {@code Type} handled by {@code this}.
+ * @return The converted string representation/
+ */
+ default @Nullable String toStringConversion(Type type) {
+ return null;
+ }
+
+ /**
+ * Whether {@code this} can be checked to ensure a value is valid.
+ * @return {@code true} if can be checked, otherwise {@code false}.
+ */
+ default boolean hasValidityCheck() {
+ return true;
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/CondBlockDataTag.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/CondBlockDataTag.java
new file mode 100644
index 00000000000..0d21b0b6014
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/CondBlockDataTag.java
@@ -0,0 +1,88 @@
+package org.skriptlang.skript.bukkit.block.blockdata.elements;
+
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataHolder;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataTag;
+import org.skriptlang.skript.registration.SyntaxInfo;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+import java.util.Arrays;
+
+@Name("Has Block Data")
+@Description("Whether the blockdata of a block or block related object has the specified tag.")
+@Example("""
+ if {_block} has blockdata "waterlogged":
+ set the blockdata "waterlogged" of {_block} to true
+ """)
+@Example("""
+ if {_stairs} is tagged with "facing" blockdata:
+ set the blockdata tag "facing" of {_stairs} to "north"
+ """)
+@Since("INSERT VERSISON")
+public class CondBlockDataTag extends Condition {
+
+ public static void register(SyntaxRegistry registry) {
+ String types = "%" + BlockDataHolder.PLURAL_PATTERN_TYPES + "%";
+ registry.register(SyntaxRegistry.CONDITION, SyntaxInfo.simple(
+ CondBlockDataTag.class,
+ CondBlockDataTag::new,
+ types + " (has|have) [the] block[ ]data [tag[s]] %strings%",
+ types + " (is|are) tagged with [the] block[ ]data [tag[s]] %strings%",
+ types + "(is|are) tagged with %strings% block[ ]data",
+ types + " (does not|doesn't|do not| don't) have [the] block[ ]data [tag[s]] %strings%",
+ types + " (is not|isn't|are not|aren't) tagged with [the] block[ ]data [tag[s]] %strings%",
+ types + " (is not|isn't|are not|aren't) tagged with %strings% block[ ]data"
+ ));
+ }
+
+ private Expression> objects;
+ private Expression strings;
+ private boolean negate;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ objects = exprs[0];
+ //noinspection unchecked
+ strings = (Expression) exprs[1];
+ negate = matchedPattern >= 3;
+ return true;
+ }
+
+ @Override
+ public boolean check(Event event) {
+ String[] strings = this.strings.getArray(event);
+ return objects.check(event, object -> {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ return false;
+ //noinspection unchecked
+ BlockDataTag[] tags = BlockDataTag.of(holder.getBlockData(object), strings);
+ if (tags == null)
+ return negate;
+ return SimpleExpression.check(strings, string -> Arrays.stream(tags).anyMatch(tag -> tag.getKey().equalsIgnoreCase(string)),
+ negate, this.strings.getAnd());
+ });
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return new SyntaxStringBuilder(event, debug)
+ .append(objects)
+ .append(objects.isSingle() ? "does" : "do")
+ .appendIf(negate, "not")
+ .append("have blockdata tags", strings)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockData.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockData.java
new file mode 100644
index 00000000000..cecc82833db
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockData.java
@@ -0,0 +1,95 @@
+package org.skriptlang.skript.bukkit.block.blockdata.elements;
+
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataHolder;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+@Name("Block Data")
+@Description("""
+ The block data associated with a block or block related objects. \
+ (i.e. blocks, block displays, falling blocks, items)
+ """)
+@Example("set {_data} to block data of target block")
+@Example("set block at player to {_data}")
+@Example("set block data of target block to oak_stairs[facing=south;waterlogged=true]")
+@Example("reset the blockdata of {_block}")
+@Since({
+ "2.5",
+ "2.5.2 (set)",
+ "2.10 (block displays)",
+ "INSERT VERSION (items, reset)"
+})
+public class ExprBlockData extends SimplePropertyExpression {
+
+ public static void register(SyntaxRegistry registry) {
+ registry.register(SyntaxRegistry.EXPRESSION, infoBuilder(
+ ExprBlockData.class,
+ BlockData.class,
+ "block[ ]data",
+ BlockDataHolder.PLURAL_PATTERN_TYPES,
+ false
+ ).supplier(ExprBlockData::new)
+ .build());
+ }
+
+ @Override
+ public @Nullable BlockData convert(Object object) {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ return null;
+ //noinspection unchecked
+ return holder.getBlockData(object);
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET) {
+ return CollectionUtils.array(BlockData.class);
+ } else if (mode == ChangeMode.RESET) {
+ return CollectionUtils.array();
+ }
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ BlockData newBlockData = delta == null ? null : ((BlockData) delta[0]);
+ for (Object object : getExpr().getArray(event)) {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ continue;
+ if (newBlockData != null)
+ holder.setBlockData(object, newBlockData);
+ if (mode != ChangeMode.RESET)
+ continue;
+ BlockData blockData = holder.getBlockData(object);
+ String dataString = blockData.getMaterial().getKey() + "[]";
+ try {
+ BlockData newData = Bukkit.createBlockData(dataString);
+ holder.setBlockData(object, newData);
+ } catch (Exception ignored) {}
+ }
+ }
+
+ @Override
+ public Class extends BlockData> getReturnType() {
+ return BlockData.class;
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "block data";
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataTags.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataTags.java
new file mode 100644
index 00000000000..fd35b1bf2df
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataTags.java
@@ -0,0 +1,109 @@
+package org.skriptlang.skript.bukkit.block.blockdata.elements;
+
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataHolder;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataTag;
+import org.skriptlang.skript.registration.SyntaxInfo;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Name("Block Data Tags")
+@Description("All the tags of a block or block related object's blockdata.")
+@Example("set {_tags::*} to all of the blockdata tags of block at location(0, 0, 0)")
+@Example("set {_tags::*} to all of the blockdata tags of an oak slab")
+@Example("reset the blockdata tags of {_block}")
+@Since("INSERT VERSION")
+public class ExprBlockDataTags extends SimpleExpression {
+
+ public static void register(SyntaxRegistry registry) {
+ registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.simple(
+ ExprBlockDataTags.class,
+ ExprBlockDataTags::new,
+ String.class,
+ "[all [of the]|the] block[ ]data tags of %" + BlockDataHolder.PLURAL_PATTERN_TYPES + "%"
+ ));
+ }
+
+ private Expression> objects;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ objects = exprs[0];
+ return true;
+ }
+
+ @Override
+ protected String @Nullable [] get(Event event) {
+ List dataTags = new ArrayList<>();
+ this.objects.stream(event).forEach(object -> {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ return;
+ //noinspection unchecked
+ BlockDataTag[] tags = BlockDataTag.of(holder.getBlockData(object));
+ if (tags == null)
+ return;
+ Arrays.stream(tags).forEach(tag -> dataTags.add(tag.getKey()));
+ });
+ return dataTags.toArray(String[]::new);
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.RESET)
+ return CollectionUtils.array();
+ return null;
+ }
+
+ @Override
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ for (Object object : objects.getArray(event)) {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ continue;
+ //noinspection unchecked
+ BlockData blockData = holder.getBlockData(object);
+ String dataString = blockData.getMaterial().getKey() + "[]";
+ try {
+ BlockData newData = Bukkit.createBlockData(dataString);
+ //noinspection unchecked
+ holder.setBlockData(object, newData);
+ } catch (Exception ignored) {}
+ }
+ }
+
+ @Override
+ public boolean isSingle() {
+ return false;
+ }
+
+ @Override
+ public Class extends String> getReturnType() {
+ return String.class;
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return new SyntaxStringBuilder(event, debug)
+ .append("all of the blockdata tags of", objects)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataValues.java b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataValues.java
new file mode 100644
index 00000000000..6cbfb86fe6b
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/block/blockdata/elements/ExprBlockDataValues.java
@@ -0,0 +1,240 @@
+package org.skriptlang.skript.bukkit.block.blockdata.elements;
+
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.skript.registrations.Classes;
+import ch.njol.skript.util.Utils;
+import ch.njol.util.Kleenean;
+import ch.njol.util.StringUtils;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataHolder;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataTag;
+import org.skriptlang.skript.bukkit.block.blockdata.BlockDataValueType;
+import org.skriptlang.skript.registration.SyntaxInfo;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Name("Block Data Values")
+@Description("""
+ The value of a blockdata tag on a block or block related object.
+ Note that specific blockdata tags can return, and be changed to, objects other than strings.
+ Examples:
+ - tag "waterlogged" will always return a boolean value, and can be changed to a boolean value or string of a boolean: "true"
+ - tag "facing" will always return a direction value, and can be changed to a direction value or string of a direction: "north"
+ - tag "pickles" will always return an integer value, and can be changed to an integer value or string of an integer: "1"
+ """)
+@Example("""
+ set blockdata "waterlogged" of {_campfire} to false
+ set blockdata "waterlogged" of {_campfire} to "true"
+ if blockdata "waterlogged" of {_campfire} is "true":
+ # FAILS
+ else if blockdata "waterlogged" of {_campfire} is true:
+ # PASSES
+ """)
+@Example("""
+ set blockdata "facing" of {_oakStairs} to north
+ set blockdata "facing" of {_oakStairs} to "west"
+ if blockdata "facing" of {_oakStairs} is "west":
+ # FAILS
+ else if blockdata "facing" of {_oakStairs} is west:
+ # PASSES
+ """)
+@Example("""
+ set blockdata "pickles" of {_seaPickle} to 1
+ set blockdata "pickles" of {_seaPickle} to "5"
+ if blockdata value "pickles" of {_seaPickle} is "5":
+ # FAILS
+ else if blockdata tag value "pickles" of {_seaPickle} is 5:
+ # PASSES
+ """)
+@Example("""
+ set blockdata "half" of {_oakStairs} to "top"
+ if blockdata "half" of {_oakStairs} is "top":
+ # PASSES
+ """)
+@Example("""
+ reset blockdata "waterlogged", "facing", "half" and "shape" of {_oakStairs}
+ """)
+@Since("INSERT VERSION")
+public class ExprBlockDataValues extends SimpleExpression {
+
+ public static void register(SyntaxRegistry registry) {
+ registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.simple(
+ ExprBlockDataValues.class,
+ ExprBlockDataValues::new,
+ Object.class,
+ "[the] block[ ]data [tag[s]] [value[s]] %strings% of %" + BlockDataHolder.PLURAL_PATTERN_TYPES + "%"
+ ));
+ }
+
+ private Expression strings;
+ private Expression> objects;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ //noinspection unchecked
+ strings = (Expression) exprs[0];
+ objects = exprs[1];
+ return true;
+ }
+
+ @Override
+ protected Object @Nullable [] get(Event event) {
+ String[] strings = this.strings.getArray(event);
+ List values = new ArrayList<>();
+ objects.stream(event).map(object -> {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ return null;
+ //noinspection unchecked
+ return BlockDataTag.of(holder.getBlockData(object), strings);
+ })
+ .filter(Objects::nonNull)
+ .forEach(tags -> values.addAll(Arrays.stream(tags).map(BlockDataTag::getValue).toList()));
+ return values.toArray();
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET) {
+ return BlockDataValueType.TYPE_CLASSES;
+ } else if (mode == ChangeMode.RESET) {
+ return CollectionUtils.array();
+ }
+ return null;
+ }
+
+ @Override
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ Object change = delta != null ? delta[0] : null;
+ Set strings = this.strings.stream(event).map(String::toLowerCase).collect(Collectors.toSet());
+ Map, List> rejected = new HashMap<>();
+ Map, Map>> invalid = new HashMap<>();
+
+ for (Object object : objects.getArray(event)) {
+ BlockDataHolder holder = BlockDataHolder.getHolder(object);
+ if (holder == null)
+ continue;
+ //noinspection unchecked
+ BlockData blockData = holder.getBlockData(object);
+ BlockDataTag[] tags;
+ if (mode == ChangeMode.SET) {
+ tags = BlockDataTag.of(blockData, strings);
+ } else {
+ tags = BlockDataTag.of(blockData);
+ }
+ if (tags == null)
+ continue;
+ for (BlockDataTag tag : tags) {
+ if (!strings.contains(tag.getKey()))
+ continue;
+ if (!tag.attemptValueChange(change)) {
+ BlockDataValueType> valueType = tag.getValueType();
+ assert valueType != null;
+ rejected.computeIfAbsent(valueType, list -> new ArrayList<>()).add(tag.getKey());
+ strings.remove(tag.getKey());
+ }
+ }
+ String dataString = blockData.getMaterial().getKey() + "["
+ + StringUtils.join(Arrays.stream(tags).filter(tag -> tag.getRawValue() != null).toList(), ",") + "]";
+ try {
+ BlockData newData = Bukkit.createBlockData(dataString);
+ //noinspection unchecked
+ holder.setBlockData(object, mode == ChangeMode.SET ? blockData.merge(newData) : newData);
+ } catch (Exception exception) {
+ for (BlockDataTag tag : tags) {
+ if (!tag.hasValidityCheck())
+ continue;
+ if (tag.checkValidity(blockData))
+ continue;
+ invalid.computeIfAbsent(tag.getValueType(), map -> new HashMap<>())
+ .computeIfAbsent(tag, list -> new ArrayList<>())
+ .add(blockData.getMaterial());
+ }
+ }
+ }
+ if (!rejected.isEmpty()) {
+ List messages = new ArrayList<>();
+ for (Entry, List> entry : rejected.entrySet()) {
+ BlockDataValueType> valueType = entry.getKey();
+ List tags = entry.getValue();
+ if (tags == null || tags.isEmpty())
+ continue;
+ String message = "The blockdata tag";
+ if (tags.size() > 1)
+ message += "s";
+ String classInfoName = Classes.getSuperClassInfo(valueType.getTypeClass()).getName().toString();
+ message += " '" + StringUtils.join(tags, ", ", ", and ") + "' can only be changed to " + Utils.a(classInfoName)
+ + " value.";
+ messages.add(message);
+ }
+ if (!messages.isEmpty())
+ error(StringUtils.join(messages, "\n\t "));
+ }
+ if (!invalid.isEmpty()) {
+ List messages = new ArrayList<>();
+ for (Entry, Map>> entry : invalid.entrySet()) {
+ BlockDataValueType> valueType = entry.getKey();
+ Map> map = entry.getValue();
+ String classInfoName = Classes.getSuperClassInfo(valueType.getTypeClass()).getName().toString();
+ for (Entry> tagEntry : map.entrySet()) {
+ BlockDataTag tag = tagEntry.getKey();
+ List materials = tagEntry.getValue();
+ String message = "The blockdata tag '" + tag.getKey() + "' does not support the " + classInfoName
+ + " value '" + tag.getConversionString() + "' for the block type" + (materials.size() > 1 ? "s" : "") + ": "
+ + StringUtils.join(materials.stream()
+ .map(ItemType::new)
+ .toList(), ", ", ", and ");
+ messages.add(message);
+ }
+ }
+ if (!messages.isEmpty())
+ error(StringUtils.join(messages, "\n\t "));
+ }
+ }
+
+ @Override
+ public boolean isSingle() {
+ return strings.isSingle() && objects.isSingle();
+ }
+
+ @Override
+ public Class>[] possibleReturnTypes() {
+ return BlockDataValueType.TYPE_CLASSES;
+ }
+
+ @Override
+ public Class> getReturnType() {
+ return Object.class;
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return new SyntaxStringBuilder(event, debug)
+ .append("the blockdata tag", strings, "of", objects)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/misc/MiscModule.java b/src/main/java/org/skriptlang/skript/bukkit/misc/MiscModule.java
index 8557a4c9d69..7566e4dd8a6 100644
--- a/src/main/java/org/skriptlang/skript/bukkit/misc/MiscModule.java
+++ b/src/main/java/org/skriptlang/skript/bukkit/misc/MiscModule.java
@@ -18,6 +18,7 @@ protected void loadSelf(SkriptAddon addon) {
EffRotate::register,
ExprBroadcastMessage::register,
ExprColorOf::register,
+ ExprDirection::register,
ExprItemOfEntity::register,
ExprMOTD::register,
ExprQuaternionAxisAngle::register,
diff --git a/src/main/java/org/skriptlang/skript/bukkit/misc/elements/expressions/ExprDirection.java b/src/main/java/org/skriptlang/skript/bukkit/misc/elements/expressions/ExprDirection.java
new file mode 100644
index 00000000000..fd6abd9edcd
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/misc/elements/expressions/ExprDirection.java
@@ -0,0 +1,216 @@
+package org.skriptlang.skript.bukkit.misc.elements.expressions;
+
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.skript.util.Direction;
+import ch.njol.util.Kleenean;
+import ch.njol.util.Math2;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.entity.Entity;
+import org.bukkit.event.Event;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.registration.DefaultSyntaxInfos;
+import org.skriptlang.skript.registration.SyntaxInfo;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+/**
+ * @author Peter Güttinger
+ */
+@Name("Direction")
+@Description("A helper expression for the direction type .")
+@Example("thrust the player upwards")
+@Example("set the block behind the player to water")
+@Example("""
+ loop blocks above the player:
+ set {_rand} to a random integer between 1 and 10
+ set the block {_rand} meters south east of the loop-block to stone
+ """)
+@Example("block in horizontal facing of the clicked entity from the player is air")
+@Example("spawn a creeper 1.5 meters horizontally behind the player")
+@Example("spawn a TNT 5 meters above and 2 meters horizontally behind the player")
+@Example("thrust the last spawned TNT in the horizontal direction of the player with speed 0.2")
+@Example("push the player upwards and horizontally forward at speed 0.5")
+@Example("push the clicked entity in in the direction of the player at speed -0.5")
+@Example("open the inventory of the block 2 blocks below the player to the player")
+@Example("teleport the clicked entity behind the player")
+@Example("grow a regular tree 2 meters horizontally behind the player")
+@Since("1.0 (basic), 2.0 (extended)")
+public class ExprDirection extends SimpleExpression {
+
+ public static DefaultSyntaxInfos.Expression syntaxInfo;
+
+ private final static BlockFace[] byMark = new BlockFace[] {
+ BlockFace.UP, BlockFace.DOWN,
+ BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST,
+ BlockFace.NORTH_EAST, BlockFace.NORTH_WEST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_WEST};
+
+ private final static int UP = 0, DOWN = 1,
+ NORTH = 2, SOUTH = 3, EAST = 4, WEST = 5,
+ NORTH_EAST = 6, NORTH_WEST = 7, SOUTH_EAST = 8, SOUTH_WEST = 9;
+
+ public static void register(SyntaxRegistry registry) {
+ // TODO think about parsing statically & dynamically (also in general)
+ // "at": see LitAt
+ // TODO direction of %location% (from|relative to) %location%
+
+ syntaxInfo = SyntaxInfo.Expression.simple(
+ ExprDirection.class,
+ ExprDirection::new,
+ Direction.class,
+ "[%-number% [(block|met(er|re))[s]] [to the]] (" +
+ NORTH + "¦north[(-| |)(" + (NORTH_EAST ^ NORTH) + "¦east|" + (NORTH_WEST ^ NORTH) + "¦west)][(ward(s|ly|)|er(n|ly|))] [of]" +
+ "|" + SOUTH + "¦south[(-| |)(" + (SOUTH_EAST ^ SOUTH) + "¦east|" + (SOUTH_WEST ^ SOUTH) + "¦west)][(ward(s|ly|)|er(n|ly|))] [of]" +
+ "|(" + EAST + "¦east|" + WEST + "¦west)[(ward(s|ly|)|er(n|ly|))] [of]" +
+ "|" + UP + "¦above|" + UP + "¦over|(" + UP + "¦up|" + DOWN + "¦down)[ward(s|ly|)]|" + DOWN + "¦below|" + DOWN + "¦under[neath]|" + DOWN + "¦beneath" +
+ ") [%-direction%]",
+ "[%-number% [(block|met(er|re))[s]]] in [the] (0¦direction|1¦horizontal direction|2¦facing|3¦horizontal facing) of %entity/block% (of|from|)",
+ "[%-number% [(block|met(er|re))[s]]] in %entity/block%'[s] (0¦direction|1¦horizontal direction|2¦facing|3¦horizontal facing) (of|from|)",
+ "[%-number% [(block|met(er|re))[s]]] (0¦in[ ]front [of]|0¦forward[s]|2¦behind|2¦backwards|[to the] (1¦right|-1¦left) [of])",
+ "[%-number% [(block|met(er|re))[s]]] horizontal[ly] (0¦in[ ]front [of]|0¦forward[s]|2¦behind|2¦backwards|to the (1¦right|-1¦left) [of])"
+ );
+
+ registry.register(SyntaxRegistry.EXPRESSION, syntaxInfo);
+ }
+
+ @Nullable Expression amount;
+
+ private @Nullable Vector direction;
+ private @Nullable ExprDirection next;
+
+ private @Nullable Expression> relativeTo;
+ boolean horizontal;
+ boolean facing;
+
+ private double yaw;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ //noinspection unchecked
+ amount = (Expression) exprs[0];
+ switch (matchedPattern) {
+ case 0 -> {
+ direction = new Vector(byMark[parseResult.mark].getModX(), byMark[parseResult.mark].getModY(), byMark[parseResult.mark].getModZ());
+ if (exprs[1] != null) {
+ if (!(exprs[1] instanceof ExprDirection exprDirection) || (exprDirection.direction == null))
+ return false;
+ next = (ExprDirection) exprs[1];
+ }
+ }
+ case 1, 2 -> {
+ relativeTo = exprs[1];
+ horizontal = parseResult.mark % 2 != 0;
+ facing = parseResult.mark >= 2;
+ }
+ case 3, 4 -> {
+ yaw = Math.PI / 2 * parseResult.mark;
+ horizontal = matchedPattern == 4;
+ }
+ }
+ return true;
+ }
+
+ public @Nullable Expression getAmount() {
+ return amount;
+ }
+
+ @Override
+ protected Direction @Nullable [] get(Event event) {
+ Number number = amount != null ? amount.getSingle(event) : 1;
+ if (number == null)
+ return new Direction[0];
+ double doubleValue = number.doubleValue();
+ if (direction != null) {
+ Vector vector = direction.clone().multiply(doubleValue);
+ ExprDirection exprDirection = next;
+ while (exprDirection != null) {
+ Number number1 = exprDirection.amount != null ? exprDirection.amount.getSingle(event) : 1;
+ if (number1 == null)
+ return new Direction[0];
+ assert exprDirection.direction != null; // checked in init()
+ vector.add(exprDirection.direction.clone().multiply(number1.doubleValue()));
+ exprDirection = exprDirection.next;
+ }
+ return new Direction[] {new Direction(vector)};
+ } else if (relativeTo != null) {
+ Object object = relativeTo.getSingle(event);
+ if (object == null)
+ return new Direction[0];
+ if (object instanceof Block block) {
+ BlockFace blockFace = Direction.getFacing(block);
+ if (blockFace == BlockFace.SELF || horizontal && (blockFace == BlockFace.UP || blockFace == BlockFace.DOWN))
+ return new Direction[] {Direction.ZERO};
+ return new Direction[] {new Direction(blockFace, doubleValue)};
+ } else {
+ Location location = ((Entity) object).getLocation();
+ if (!horizontal) {
+ if (!facing) {
+ Vector vector = location.getDirection().normalize().multiply(doubleValue);
+ assert vector != null;
+ return new Direction[] {new Direction(vector)};
+ }
+ double pitch = Direction.pitchToRadians(location.getPitch());
+ assert pitch >= -Math.PI / 2 && pitch <= Math.PI / 2;
+ if (pitch > Math.PI / 4)
+ return new Direction[] {new Direction(new double[] {0, doubleValue, 0})};
+ if (pitch < -Math.PI / 4)
+ return new Direction[] {new Direction(new double[] {0, -doubleValue, 0})};
+ }
+ double yaw = Direction.yawToRadians(location.getYaw());
+ if (horizontal && !facing) {
+ return new Direction[] {new Direction(new double[] {Math.cos(yaw) * doubleValue, 0, Math.sin(yaw) * doubleValue})};
+ }
+ yaw = Math2.mod(yaw, 2 * Math.PI);
+ if (yaw >= Math.PI / 4 && yaw < 3 * Math.PI / 4)
+ return new Direction[] {new Direction(new double[] {0, 0, doubleValue})};
+ if (yaw >= 3 * Math.PI / 4 && yaw < 5 * Math.PI / 4)
+ return new Direction[] {new Direction(new double[] {-doubleValue, 0, 0})};
+ if (yaw >= 5 * Math.PI / 4 && yaw < 7 * Math.PI / 4)
+ return new Direction[] {new Direction(new double[] {0, 0, -doubleValue})};
+ assert yaw >= 0 && yaw < Math.PI / 4 || yaw >= 7 * Math.PI / 4 && yaw < 2 * Math.PI;
+ return new Direction[] {new Direction(new double[] {doubleValue, 0, 0})};
+ }
+ } else {
+ return new Direction[] {new Direction(horizontal ? Direction.IGNORE_PITCH : 0, yaw, doubleValue)};
+ }
+ }
+
+ @Override
+ public boolean isSingle() {
+ return true;
+ }
+
+ @Override
+ public Class extends Direction> getReturnType() {
+ return Direction.class;
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ Expression> relativeTo = this.relativeTo;
+ SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug)
+ .appendIf(amount != null, amount, "meter(s)");
+ if (direction != null) {
+ builder.append(Direction.toString(direction));
+ } else {
+ if (relativeTo != null) {
+ builder.append("in")
+ .appendIf(horizontal, "horizontal")
+ .append(facing ? "facing" : "direction", "of", relativeTo);
+ } else {
+ builder.appendIf(horizontal, "horizontally")
+ .append(Direction.toString(0, yaw, 1));
+ }
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprBlockData.sk b/src/test/skript/tests/syntaxes/expressions/ExprBlockData.sk
index 799e8286bba..00a3c54ff33 100644
--- a/src/test/skript/tests/syntaxes/expressions/ExprBlockData.sk
+++ b/src/test/skript/tests/syntaxes/expressions/ExprBlockData.sk
@@ -1,22 +1,106 @@
+using error catching
+
+test "invalid block data":
+ set {_block} to campfire[]
+ catch runtime errors:
+ set blockdata "waterlogged" of {_block} to "1"
+ set {_error} to "The blockdata tag 'waterlogged' can only be changed to a boolean (yes/no) value."
+ assert last caught runtime errors contains {_error} with "Boolean BlockData should error with non-boolean value"
+
+ set {_block} to sea pickles[]
+ catch runtime errors:
+ set blockdata "pickles" of {_block} to "a"
+ set {_error} to "The blockdata tag 'pickles' can only be changed to an integer value."
+ assert last caught runtime errors contains {_error} with "Integer BlockData should error with non-integer value"
+
+ set {_block} to oak stairs[]
+ catch runtime errors:
+ set blockdata "facing" of {_block} to 1
+ set {_error} to "The blockdata tag 'facing' can only be changed to a direction value."
+ assert last caught runtime errors contains {_error} with "Direction BlockData should error with non-direction value"
+
+ catch runtime errors:
+ set blockdata "facing" of {_block} to north east
+ set {_error} to "The blockdata tag 'facing' does not support the direction value 'north_east' for the block type: oak stairs"
+ assert last caught runtime errors contains {_error} with "Direction BlockData should error with non-supported direction value for 'facing'"
+
+ catch runtime errors:
+ set blockdata "half" of {_block} to "left"
+ set {_error} to "The blockdata tag 'half' does not support the text value 'left' for the block type: oak stairs"
+ assert last caught runtime errors contains {_error} with "String BlockData should error with non-supported string value for 'half'"
+
test "block data":
- set {_b} to block at test-location
- set block at {_b} to campfire[lit=false;waterlogged=true]
- assert block at {_b} is campfire[lit=false] with "block at spawn should be an unlit campfire"
+ set {_loc} to test-location
+ set block at {_loc} to campfire[lit=false;waterlogged=true]
+
+ assert block at {_loc} is campfire[lit=false] with "Block should match with partial BlockData - lit"
+ assert block at {_loc} is campfire[waterlogged=true] with "Block should match with partial BlockData - waterlogged"
+ assert block at {_loc} is campfire[lit=false;waterlogged=true] with "Block should match full BlockData"
+ assert block at {_loc} is campfire[] with "Block should match with no specified BlockData"
+ assert block at {_loc} is not campfire[lit=true;waterlogged=false] with "Block should not match with inverted BlockData"
+
+ set {_data} to blockdata of block at {_loc}
+ assert "%{_data}%" contains "campfire", "lit=false" and "waterlogged=true" with "BlockData string did not contain specified parts"
+
+ assert {_data} is campfire[lit=false] with "BlockData should match with partial BlockData - lit"
+ assert {_data} is campfire[waterlogged=true] with "BlockData should match with partial BlockData - waterlogged"
+ assert {_data} is campfire[lit=false;waterlogged=true] with "BlockData should match full BlockData"
+ assert {_data} is campfire[] with "BlockData should match with no specified BlockData"
+ assert {_data} is not campfire[lit=true;waterlogged=false] with "BlockData should not match with inverted BlockData"
- assert block at {_b} = campfire[lit=false;waterlogged=true] with "block should have been an unlit, waterlogged campfire"
- assert block at {_b} = campfire[waterlogged=true] with "block should have been a waterlogged campfire"
- assert block at {_b} = campfire[] with "block should have been a campfire"
- assert block at {_b} != campfire[lit=true;waterlogged=false] with "block should not have been an unlit, waterlogged campfire"
+ set {_tags::*} to "lit" and "waterlogged"
+ loop {_tags::*}:
+ assert blockdata (loop-value) of (block at {_loc}) is set with "BlockData '" + (loop-value) +"' was not found on Block"
+ assert (block at {_loc}) has blockdata (loop-value) with "Block did not have BlockData '" + (loop-value) + "'"
+ assert blockdata (loop-value) of {_data} is set with "BlockData '" + (loop-value) +"' was not found on BlockData"
+ assert {_data} has blockdata (loop-value) with "BlockData did not have BlockData '" + (loop-value) + "'"
+ assert (block at {_loc}) is tagged with blockdata {_tags::*} with "Block did not have all BlockData tags"
+ assert {_data} is tagged with blockdata {_tags::*} with "BlockData did not have all BlockData tags"
- set {_data} to block data of block at {_b}
- assert "%{_data}%" contains "campfire", "lit=false" and "waterlogged=true" with "block data for campfire did not match"
+ assert blockdata tags of (block at {_loc}) contains {_tags::*} with "Block's BlockData tags did not have all specified tags"
+ assert blockdata tags of {_data} contains {_tags::*} with "BlockData's tags did not have all specified tags"
- set block at {_b} to air
+ set blockdata "waterlogged" of (block at {_loc}) to false
+ assert blockdata "waterlogged" of (block at {_loc}) is false with "BlockData 'waterlogged' was not set to false (boolean)"
+ set blockdata "lit" of (block at {_loc}) to "true"
+ assert blockdata "lit" of (block at {_loc}) is true with "BlockData 'lit' was not set to true (string)"
+
+ set block at {_loc} to air
test "falling block block data":
- spawn a falling block at test-location:
- set {_e} to entity
- assert block data of {_e} is stone[] with "Falling block should default to stone"
- set block data of {_e} to sand[]
- assert block data of {_e} is sand[] with "Falling block should update to sand"
- clear entity within {_e}
+ spawn a falling block at test-location:
+ set {_entity} to entity
+ assert block data of {_entity} is stone[] with "FallingBlock should default to stone"
+ set block data of {_entity} to oak stairs[]
+ assert block data of {_entity} is oak stairs[] with "FallingBlock should update to oak stairs"
+
+ set {_tags::*} to "facing", "shape", "half" and "waterlogged"
+ assert {_entity} has blockdata {_tags::*} with "FallingBlock BlockData did not have specified tags"
+ set blockdata "facing" of {_entity} to east
+ assert blockdata "facing" of {_entity} is east with "FallingBlock 'facing' was not set to 'east'"
+ assert blockdata of {_entity} is oak stairs[facing=east;waterlogged=false] with "FallingBlock should match with partial BlockData"
+
+ clear entity within {_entity}
+
+test "block display block data":
+ spawn a block display at test-location:
+ set {_entity} to entity
+ set block data of {_entity} to oak stairs[]
+ assert block data of {_entity} is oak stairs[] with "BlockDisplay should update to oak stairs"
+
+ set {_tags::*} to "facing", "shape", "half" and "waterlogged"
+ assert {_entity} has blockdata {_tags::*} with "BlockDisplay BlockData did not have specified tags"
+ set blockdata "facing" of {_entity} to east
+ assert blockdata "facing" of {_entity} is east with "BlockDisplay 'facing' was not set to 'east'"
+ assert blockdata of {_entity} is oak stairs[facing=east;waterlogged=false] with "BlockDisplay should match with partial BlockData"
+
+ clear entity within {_entity}
+
+test "itemtype block data":
+ set {_item} to oak stairs
+ set {_tags::*} to "facing", "shape", "half" and "waterlogged"
+ assert {_item} has blockdata {_tags::*} with "ItemType BlockData did not have specified tags"
+ set blockdata "facing" of {_item} to east
+ assert blockdata "facing" of {_item} is east with "ItemType 'facing' was not set to 'east'"
+ assert blockdata of {_item} is oak stairs[facing=east;waterlogged=false] with "ItemType should match with partial BlockData"
+