diff --git a/src/main/java/ch/njol/skript/expressions/ExprHighestSolidBlock.java b/src/main/java/ch/njol/skript/expressions/ExprHighestSolidBlock.java
deleted file mode 100644
index 8c48e9121ab..00000000000
--- a/src/main/java/ch/njol/skript/expressions/ExprHighestSolidBlock.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * This file is part of Skript.
- *
- * Skript is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Skript is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Skript. If not, see .
- *
- * Copyright Peter Güttinger, SkriptLang team and contributors
- */
-package ch.njol.skript.expressions;
-
-import ch.njol.skript.Skript;
-import ch.njol.skript.doc.Description;
-import ch.njol.skript.doc.Examples;
-import ch.njol.skript.doc.Name;
-import ch.njol.skript.doc.Since;
-import ch.njol.skript.expressions.base.SimplePropertyExpression;
-import ch.njol.skript.lang.ExpressionType;
-
-import org.bukkit.Location;
-import org.bukkit.block.Block;
-import org.eclipse.jdt.annotation.Nullable;
-
-@Name("Highest Solid Block")
-@Description("Returns the highest solid block at the x and z coordinates of the world of a given location.")
-@Examples("highest block at location of arg-player")
-@Since("2.2-dev34")
-public class ExprHighestSolidBlock extends SimplePropertyExpression {
-
- static {
- Skript.registerExpression(ExprHighestSolidBlock.class, Block.class, ExpressionType.PROPERTY, "highest [(solid|non-air)] block at %locations%");
- }
-
- @Override
- protected String getPropertyName() {
- return "highest [(solid|non-air)] block";
- }
-
- @Nullable
- @Override
- public Block convert(Location location) {
- return location.getWorld().getHighestBlockAt(location);
- }
-
- @Override
- public Class extends Block> getReturnType() {
- return Block.class;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ch/njol/skript/expressions/ExprLowestHighestSolidBlock.java b/src/main/java/ch/njol/skript/expressions/ExprLowestHighestSolidBlock.java
new file mode 100644
index 00000000000..7f4914ec444
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprLowestHighestSolidBlock.java
@@ -0,0 +1,123 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.expressions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.generator.WorldInfo;
+import org.eclipse.jdt.annotation.Nullable;
+
+@Name("Lowest/Highest Solid Block")
+@Description({
+ "An expression to obtain the lowest or highest solid (impassable) block at a location.",
+ "Note that the y-coordinate of the location is not taken into account for this expression."
+})
+@Examples({
+ "teleport the player to the block above the highest block at the player",
+ "set the highest solid block at the player's location to the lowest solid block at the player's location"
+})
+@Since("2.2-dev34, INSERT VERSION (lowest solid block, 'non-air' option removed, additional syntax option)")
+public class ExprLowestHighestSolidBlock extends SimplePropertyExpression {
+
+ private static final boolean HAS_MIN_HEIGHT =
+ Skript.classExists("org.bukkit.generator.WorldInfo") &&
+ Skript.methodExists(WorldInfo.class, "getMinHeight");
+
+ private static final boolean HAS_BLOCK_IS_SOLID = Skript.methodExists(Block.class, "isSolid");
+
+ // Before 1.15, getHighestSolidBlock actually returned the block directly ABOVE the highest solid block
+ private static final boolean RETURNS_FIRST_AIR = !Skript.isRunningMinecraft(1, 15);
+
+ static {
+ Skript.registerExpression(ExprLowestHighestSolidBlock.class, Block.class, ExpressionType.PROPERTY,
+ "[the] (highest|:lowest) [solid] block (at|of) %locations%",
+ "%locations%'[s] (highest|:lowest) [solid] block"
+ );
+ }
+
+ private boolean lowest;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ lowest = parseResult.hasTag("lowest");
+ return super.init(exprs, matchedPattern, isDelayed, parseResult);
+ }
+
+ @Override
+ @Nullable
+ public Block convert(Location location) {
+ World world = location.getWorld();
+ if (world == null) {
+ return null;
+ }
+
+ if (!lowest) {
+ return getHighestBlockAt(world, location);
+ }
+
+ location = location.clone();
+ location.setY(HAS_MIN_HEIGHT ? world.getMinHeight() : 0);
+ Block block = location.getBlock();
+ int maxHeight = world.getMaxHeight();
+ while (block.getY() < maxHeight && !isSolid(block)) { // work our way up
+ block = block.getRelative(BlockFace.UP);
+ }
+ // if this block isn't solid, there are no solid blocks at this location
+ // getHighestBlockAt is apparently NotNull, so let's just mimic that behavior by returning it
+ return isSolid(block) ? block : getHighestBlockAt(world, block.getLocation());
+ }
+
+ private static Block getHighestBlockAt(World world, Location location) {
+ Block block = world.getHighestBlockAt(location);
+ if (RETURNS_FIRST_AIR) {
+ block = block.getRelative(BlockFace.DOWN);
+ if (!isSolid(block)) { // if the one right below isn't solid let's just preserve the behavior
+ block.getRelative(BlockFace.UP);
+ }
+ }
+ return block;
+ }
+
+ private static boolean isSolid(Block block) {
+ return HAS_BLOCK_IS_SOLID ? block.isSolid() : block.getType().isSolid();
+ }
+
+ @Override
+ public Class extends Block> getReturnType() {
+ return Block.class;
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return (lowest ? "lowest" : "highest") + " solid block";
+ }
+
+}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprLowestHighestSolidBlock.sk b/src/test/skript/tests/syntaxes/expressions/ExprLowestHighestSolidBlock.sk
new file mode 100644
index 00000000000..b0f90139f50
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprLowestHighestSolidBlock.sk
@@ -0,0 +1,60 @@
+test "lowest/highest solid block (old height)" when running below minecraft "1.18":
+
+ # highest solid block
+ set {_oldBlock::1} to block data of block at location(0.5, 255.5, 0.5, "world")
+ set {_oldBlock::2} to block data of block at location(0.5, 254.5, 0.5, "world")
+ set {_oldBlock::3} to block data of block at location(0.5, 253.5, 0.5, "world")
+ set block at location(0.5, 255.5, 0.5, "world") to air
+ set block at location(0.5, 254.5, 0.5, "world") to air
+ set block at location(0.5, 253.5, 0.5, "world") to dirt
+ set {_highest} to highest solid block at location(0.5, 64, 0.5, "world")
+ assert type of {_highest} is dirt with "highest block is not dirt (got '%type of {_highest}%')"
+ assert location of {_highest} is location(0.5, 253.5, 0.5, "world") with "highest block is not at 0.5,253.5,0.5 (got '%location of {_highest}%')"
+ set block at location(0.5, 255.5, 0.5, "world") to {_oldBlock::1}
+ set block at location(0.5, 254.5, 0.5, "world") to {_oldBlock::2}
+ set block at location(0.5, 253.5, 0.5, "world") to {_oldBlock::3}
+
+ # lowest solid block
+ set {_oldBlock::1} to block data of block at location(0.5, 0.5, 0.5, "world")
+ set {_oldBlock::2} to block data of block at location(0.5, 1.5, 0.5, "world")
+ set {_oldBlock::3} to block data of block at location(0.5, 2.5, 0.5, "world")
+ set block at location(0.5, 0.5, 0.5, "world") to air
+ set block at location(0.5, 1.5, 0.5, "world") to air
+ set block at location(0.5, 2.5, 0.5, "world") to dirt
+ set {_lowest} to lowest solid block at location(0.5, 64, 0.5, "world")
+ assert type of {_lowest} is dirt with "lowest block is not dirt (got '%type of {_lowest}%')"
+ assert location of {_lowest} is location(0.5, 2.5, 0.5, "world") with "lowest block is not at 0.5,2.5,0.5 (got '%location of {_lowest}%')"
+ set block at location(0.5, 0.5, 0.5, "world") to {_oldBlock::1}
+ set block at location(0.5, 1.5, 0.5, "world") to {_oldBlock::2}
+ set block at location(0.5, 2.5, 0.5, "world") to {_oldBlock::3}
+
+test "lowest/highest solid block (new height)" when running minecraft "1.18":
+
+ # highest solid block
+ set {_oldBlock::1} to block data of block at location(0.5, 319.5, 0.5, "world")
+ set {_oldBlock::2} to block data of block at location(0.5, 318.5, 0.5, "world")
+ set {_oldBlock::3} to block data of block at location(0.5, 317.5, 0.5, "world")
+ set block at location(0.5, 319.5, 0.5, "world") to air
+ set block at location(0.5, 318.5, 0.5, "world") to air
+ set block at location(0.5, 317.5, 0.5, "world") to dirt
+ set {_highest} to highest solid block at location(0.5, 64, 0.5, "world")
+ assert type of {_highest} is dirt with "highest block is not dirt (got '%type of {_highest}%')"
+ assert location of {_highest} is location(0.5, 317.5, 0.5, "world") with "highest block is not at 0.5,317.5,0.5 (got '%location of {_highest}%')"
+ set block at location(0.5, 319.5, 0.5, "world") to {_oldBlock::1}
+ set block at location(0.5, 318.5, 0.5, "world") to {_oldBlock::1}
+ set block at location(0.5, 317.5, 0.5, "world") to {_oldBlock::1}
+
+ # lowest solid block
+ set {_oldBlock::1} to block data of block at location(0.5, -63.5, 0.5, "world")
+ set {_oldBlock::2} to block data of block at location(0.5, -62.5, 0.5, "world")
+ set {_oldBlock::3} to block data of block at location(0.5, -61.5, 0.5, "world")
+ set block at location(0.5, -63.5, 0.5, "world") to air
+ set block at location(0.5, -62.5, 0.5, "world") to air
+ set block at location(0.5, -61.5, 0.5, "world") to dirt
+ set {_lowest} to lowest solid block at location(0.5, 64, 0.5, "world")
+ assert type of {_lowest} is dirt with "lowest block is not dirt (got '%type of {_lowest}%')"
+ assert location of {_lowest} is location(0.5, -61.5, 0.5, "world") with "lowest block is not at 0.5,-61.5,0.5 (got '%location of {_lowest}%')"
+ set block at location(0.5, -63.5, 0.5, "world") to {_oldBlock::1}
+ set block at location(0.5, -62.5, 0.5, "world") to {_oldBlock::2}
+ set block at location(0.5, -61.5, 0.5, "world") to {_oldBlock::3}
+