Skip to content

Commit 5f737e1

Browse files
Pesekjaksovdeeth
andauthored
Fix of the BlockLineIterator causing IllegalStateException (#7513)
* replaced Bukkit's (broken) block iterator with own implementation * checkstyle and fixed the overflowing max target block distance * fixed block duplicates, mimics the behaviour of previous block line iterator (using grid traversal algorithm instead of Bresenham's) * added documentation, changed some variable names so the code is easier to understand * fixed epsilon calculation, small changes * simplify calculations by removing Planes, add more tests * sidestep weird JOML mistake (now fixed, but not in pre 1.21.4) --------- Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>
1 parent 12c9f07 commit 5f737e1

6 files changed

Lines changed: 191 additions & 72 deletions

File tree

src/main/java/ch/njol/skript/expressions/ExprBlocks.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@
2828
import ch.njol.util.coll.iterator.ArrayIterator;
2929

3030
@Name("Blocks")
31-
@Description({"Blocks relative to other blocks or between other blocks. Can be used to get blocks relative to other blocks or for looping.",
32-
"Blocks from/to and between will return a straight line whereas blocks within will return a cuboid."})
31+
@Description({"Blocks relative to other blocks or between other blocks.",
32+
"Can be used to get blocks relative to other blocks or for looping.",
33+
"Blocks from/to and between will return a straight line whereas blocks within will return a cuboid."})
3334
@Examples({"loop blocks above the player:",
34-
"loop blocks between the block below the player and the targeted block:",
35-
"set the blocks below the player, the victim and the targeted block to air",
36-
"set all blocks within {loc1} and {loc2} to stone",
37-
"set all blocks within chunk at player to air"})
35+
"loop blocks between the block below the player and the targeted block:",
36+
"set the blocks below the player, the victim and the targeted block to air",
37+
"set all blocks within {loc1} and {loc2} to stone",
38+
"set all blocks within chunk at player to air"})
3839
@Since("1.0, 2.5.1 (within/cuboid/chunk)")
3940
public class ExprBlocks extends SimpleExpression<Block> {
4041

@@ -100,7 +101,7 @@ protected Block[] get(Event event) {
100101
return from.stream(event)
101102
.filter(Location.class::isInstance)
102103
.map(Location.class::cast)
103-
.filter(location -> {
104+
.filter(location -> {
104105
if (SUPPORTS_WORLD_LOADED)
105106
return location.isWorldLoaded();
106107
return location.getChunk().isLoaded();
@@ -129,15 +130,20 @@ public Iterator<Block> iterator(Event event) {
129130
Object object = from.getSingle(event);
130131
if (object == null)
131132
return null;
132-
Location location = object instanceof Location ? (Location) object : ((Block) object).getLocation().add(0.5, 0.5, 0.5);
133+
Location location = object instanceof Location
134+
? (Location) object
135+
: ((Block) object).getLocation().add(0.5, 0.5, 0.5);
133136
Direction direction = this.direction.getSingle(event);
134137
if (direction == null || location.getWorld() == null)
135138
return null;
136-
Vector vector = object != location ? direction.getDirection((Block) object) : direction.getDirection(location);
139+
Vector vector = object != location
140+
? direction.getDirection((Block) object)
141+
: direction.getDirection(location);
137142
// Cannot be zero.
138143
if (vector.getX() == 0 && vector.getY() == 0 && vector.getZ() == 0)
139144
return null;
140-
int distance = SkriptConfig.maxTargetBlockDistance.value();
145+
// start block + (max - 1) == max
146+
int distance = SkriptConfig.maxTargetBlockDistance.value() - 1;
141147
if (this.direction instanceof ExprDirection) {
142148
Expression<Number> numberExpression = ((ExprDirection) this.direction).amount;
143149
if (numberExpression != null) {
@@ -188,7 +194,7 @@ public String toString(@Nullable Event event, boolean debug) {
188194
return "blocks from " + from.toString(event, debug) + " to " + end.toString(event, debug);
189195
} else {
190196
assert direction != null;
191-
return "block" + (isSingle() ? "" : "s") + " " + direction.toString(event, debug) + " " + from.toString(event, debug);
197+
return "blocks " + direction.toString(event, debug) + " " + from.toString(event, debug);
192198
}
193199
}
194200

Lines changed: 107 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,132 @@
11
package ch.njol.skript.util;
22

3-
import ch.njol.skript.bukkitutil.WorldUtils;
4-
import ch.njol.util.Math2;
53
import org.bukkit.Location;
4+
import org.bukkit.World;
65
import org.bukkit.block.Block;
7-
import org.bukkit.util.BlockIterator;
86
import org.bukkit.util.Vector;
9-
import org.jetbrains.annotations.Nullable;
7+
import org.jetbrains.annotations.Contract;
8+
import org.jetbrains.annotations.NotNull;
9+
import org.joml.Vector3d;
1010

11-
import ch.njol.util.NullableChecker;
12-
import ch.njol.util.coll.iterator.StoppableIterator;
11+
import java.util.Iterator;
12+
import java.util.NoSuchElementException;
1313

14-
public class BlockLineIterator extends StoppableIterator<Block> {
14+
/**
15+
* Iterates through blocks in a straight line from a start to end location (inclusive).
16+
* <p>
17+
* Given start and end locations are always cloned but may not be block-centered.
18+
* Iterates through all blocks the line passes through in order from start to end location.
19+
*/
20+
public class BlockLineIterator implements Iterator<Block> {
21+
22+
private final Vector3d current;
23+
private final Vector3d end;
24+
private final Vector3d centeredEnd;
25+
final Vector3d step; // package private for tests
26+
private final World world;
27+
private boolean finished;
28+
29+
/**
30+
* @param start start location
31+
* @param end end location
32+
*/
33+
public BlockLineIterator(@NotNull Location start, @NotNull Location end) {
34+
this.current = start.toVector().toVector3d();
35+
this.world = start.getWorld();
36+
this.end = end.toVector().toVector3d();
37+
this.centeredEnd = centered(this.end);
38+
this.step = this.end.sub(current, new Vector3d()).normalize();
39+
}
40+
41+
/**
42+
* @param start first block
43+
* @param end last block
44+
*/
45+
public BlockLineIterator(@NotNull Block start, @NotNull Block end) {
46+
this(start.getLocation().toCenterLocation(), end.getLocation().toCenterLocation());
47+
}
1548

1649
/**
17-
* @param start
18-
* @param end
19-
* @throws IllegalStateException randomly (Bukkit bug)
50+
* @param start start location
51+
* @param direction direction to travel in
52+
* @param distance maximum distance to travel
2053
*/
21-
public BlockLineIterator(Block start, Block end) throws IllegalStateException {
22-
super(new BlockIterator(start.getWorld(), start.getLocation().toVector(),
23-
end.equals(start) ? new Vector(1, 0, 0) : end.getLocation().subtract(start.getLocation()).toVector(), // should prevent an error if start = end
24-
0, 0
25-
),
26-
new NullableChecker<Block>() {
27-
private final double overshotSq = Math.pow(start.getLocation().distance(end.getLocation()) + 2, 2);
28-
29-
@Override
30-
public boolean check(@Nullable Block block) {
31-
assert block != null;
32-
if (block.getLocation().distanceSquared(start.getLocation()) > overshotSq)
33-
throw new IllegalStateException("BlockLineIterator missed the end block!");
34-
return block.equals(end);
35-
}
36-
}, true);
54+
public BlockLineIterator(Location start, @NotNull Vector direction, double distance) {
55+
this(start, start.clone().add(direction.clone().normalize().multiply(distance)));
56+
}
57+
58+
/**
59+
* @param start first block
60+
* @param direction direction to travel in
61+
* @param distance maximum distance to travel
62+
*/
63+
public BlockLineIterator(@NotNull Block start, Vector direction, double distance) {
64+
this(start.getLocation().toCenterLocation(), direction, distance);
65+
}
66+
67+
@Override
68+
public boolean hasNext() {
69+
return !finished;
70+
}
71+
72+
@Override
73+
public Block next() {
74+
if (!hasNext()) throw new NoSuchElementException("Reached the final block destination");
75+
// sanity check (is the current->end vector pointing away from step)
76+
if (end.sub(current, new Vector3d()).dot(step) < 0) throw new NoSuchElementException("Overshot the final block!");
77+
// get block and check end
78+
Vector3d center = centered(current);
79+
Block block = getBlock(center, world);
80+
if (center.equals(centeredEnd)) finished = true;
81+
// calculate next position
82+
double t = stepsToNextFace(current, step, center) + Math.ulp(1);
83+
current.fma(t, step);
84+
return block;
3785
}
3886

3987
/**
40-
* @param start
41-
* @param direction
42-
* @param distance
43-
* @throws IllegalStateException randomly (Bukkit bug)
88+
* Calculates the number of steps to the next closest block face this ray, defined by start and step, will encounter.
89+
* Block faces are determined by the center vector, which is interpreted as the center of the block.
90+
* @param start the current location of the ray to check.
91+
* @param step the direction of the ray.
92+
* @param center the center location of the block the ray is currently within.
93+
* @return a scalar floating point number representing the number of times step must be added to start in order
94+
* to arrive at the closest block face.
4495
*/
45-
public BlockLineIterator(Location start, Vector direction, double distance) throws IllegalStateException {
46-
super(new BlockIterator(start.getWorld(), start.toVector(), direction, 0, 0), new NullableChecker<Block>() {
47-
private final double distSq = distance * distance;
48-
49-
@Override
50-
public boolean check(final @Nullable Block b) {
51-
return b != null && b.getLocation().add(0.5, 0.5, 0.5).distanceSquared(start) >= distSq;
52-
}
53-
}, false);
96+
static double stepsToNextFace(Vector3d start, @NotNull Vector3d step, Vector3d center) {
97+
Vector3d neededSteps = new Vector3d(Math.signum(step.x), Math.signum(step.y), Math.signum(step.z))
98+
.mulAdd(0.5, center)
99+
.sub(start)
100+
.div(step, new Vector3d()); // need to make new vector due to JOML method signature issue
101+
// get min component, ignoring NaN
102+
if (Double.isNaN(neededSteps.x))
103+
neededSteps.x = Double.POSITIVE_INFINITY;
104+
if (Double.isNaN(neededSteps.y))
105+
neededSteps.y = Double.POSITIVE_INFINITY;
106+
if (Double.isNaN(neededSteps.z))
107+
neededSteps.z = Double.POSITIVE_INFINITY;
108+
return neededSteps.get(neededSteps.minComponent());
54109
}
55110

56111
/**
57-
* @param start
58-
* @param direction
59-
* @param distance
60-
* @throws IllegalStateException randomly (Bukkit bug)
112+
* Creates vector at the center of a block at the coordinates provided
113+
* by {@code vector}.
114+
*
115+
* @param vector point
116+
* @return coordinates at the center of a block at given point
61117
*/
62-
public BlockLineIterator(Block start, Vector direction, double distance) throws IllegalStateException {
63-
this(start.getLocation().add(0.5, 0.5, 0.5), direction, distance);
118+
@Contract("_ -> new")
119+
private static Vector3d centered(@NotNull Vector3d vector) {
120+
return vector.floor(new Vector3d()).add(0.5, 0.5, 0.5);
64121
}
65122

66123
/**
67-
* Makes the vector fit within the world parameters.
68-
*
69-
* @param location The original starting location.
70-
* @param direction The direction of the vector that will be based on the location.
71-
* @return The newly modified Vector if needed.
124+
* @param vector the xyz coordinates of the block to get.
125+
* @param world the world which the block should be obtained from
126+
* @return the block at the given xyz coords in the given world.
72127
*/
73-
private static Vector fitInWorld(Location location, Vector direction) {
74-
int lowest = WorldUtils.getWorldMinHeight(location.getWorld());
75-
int highest = location.getWorld().getMaxHeight();
76-
Vector vector = location.toVector();
77-
int y = location.getBlockY();
78-
if (y >= lowest && y <= highest)
79-
return vector;
80-
double newY = Math2.fit(lowest, location.getY(), highest);
81-
return new Vector(location.getX(), newY, location.getZ());
128+
private static @NotNull Block getBlock(@NotNull Vector3d vector, @NotNull World world) {
129+
return Vector.fromJOML(vector).toLocation(world).getBlock();
82130
}
83131

84132
}

src/main/java/ch/njol/util/coll/iterator/StoppableIterator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import ch.njol.util.NullableChecker;
99

1010
/**
11-
* @author Peter Güttinger
11+
* @deprecated unused
1212
*/
13+
@Deprecated
1314
public class StoppableIterator<T> implements Iterator<T> {
1415

1516
private final Iterator<T> iter;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ch.njol.skript.util;
2+
3+
import org.bukkit.Location;
4+
import org.joml.Vector3d;
5+
import org.junit.Assert;
6+
import org.junit.Test;
7+
8+
import java.util.NoSuchElementException;
9+
10+
import static org.junit.Assert.assertEquals;
11+
12+
public class BlockLineIteratorTest {
13+
14+
@Test
15+
public void testStepsToNextFace() {
16+
Vector3d start = new Vector3d(0,0,0);
17+
Vector3d center = new Vector3d(0.5,0.5,0.5);
18+
Vector3d step = new Vector3d(0,0,0);
19+
assertEquals(Double.POSITIVE_INFINITY, BlockLineIterator.stepsToNextFace(start, step, center), Math.ulp(1d));
20+
step.x = 1;
21+
assertEquals(1.0, BlockLineIterator.stepsToNextFace(start, step, center), Math.ulp(1d));
22+
step.y = 1;
23+
step.normalize();
24+
assertEquals(Math.sqrt(2), BlockLineIterator.stepsToNextFace(start, step, center), Math.ulp(1d));
25+
step.x = 1;
26+
step.y = 1;
27+
step.z = 1;
28+
step.normalize();
29+
assertEquals(Math.sqrt(3), BlockLineIterator.stepsToNextFace(start, step, center), Math.ulp(1d));
30+
start.x = 0.99;
31+
assertEquals(Math.sqrt(3) / 100, BlockLineIterator.stepsToNextFace(start, step, center), Math.ulp(1d));
32+
}
33+
34+
@Test
35+
public void testOvershoot() {
36+
Location start = new Location(null, 0,0,0);
37+
Location end = new Location(null, 0,10,0);
38+
var iterator = new BlockLineIterator(start, end);
39+
iterator.step.mul(-1);
40+
// step is now wrong way, so it should trigger overshoot protection
41+
Assert.assertThrows(NoSuchElementException.class, iterator::next);
42+
}
43+
44+
}

src/test/skript/tests/regressions/5566-block iterator being 100 always.sk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ test "100 blocks fix":
44
set {_l} to test-location ~ vector(0, -1, 10)
55
set block at {_l} to air
66

7-
assert blocks 5 below {_l} contains air, grass block, dirt, dirt, bedrock and void air with "Failed to get correct blocks (got '%blocks 5 below test-location%')"
7+
set {_blocks::*} to (block at {_l}), (block at {_l} ~ vector(0, -1, 0)), (block at {_l} ~ vector(0, -2, 0)), (block at {_l} ~ vector(0, -3, 0)), (block at {_l} ~ vector(0, -4, 0)), and (block at {_l} ~ vector(0, -5, 0))
8+
assert blocks 5 below {_l} is {_blocks::*} with "Failed to get correct blocks"
89
assert size of blocks 3 below location below {_l} is 4 with "Failed to match asserted size"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
test "basic block iterator test":
2+
set {_l1} to location(0, 100, 0, world "world")
3+
set {_l2} to location(10, 100, 10, world "world")
4+
set {_origin} to location(5, 100, 5, world "world")
5+
loop blocks within {_l1} and {_l2}:
6+
set {_loc} to location of loop-value
7+
assert blocks from {_origin} to {_loc} is set with "failed to find blocks between %{_origin}% and %{_loc}%."
8+
9+
test "ensure non-centered block iterators are not equal":
10+
set {_l1} to location(0.1, 0.1, 0.1, world "world")
11+
set {_l2} to location(0.9, 0.9, 0.9, world "world")
12+
set {_dir} to vector(1, 0, -1)
13+
assert blocks {_dir} {_l1} are not blocks {_dir} {_l2} with "block iterators from offset locations returned same values."
14+
15+
test "ensure same block iterators are equal":
16+
set {_l1} to location(0.1, 0.1, 0.1, world "world")
17+
set {_l2} to location(0.9, 0.9, 0.9, world "world")
18+
set {_dir} to vector(1, 0, -1)
19+
assert blocks {_dir} (block at {_l1}) are blocks {_dir} (block at {_l2}) with "block iterators from same block returned different values."

0 commit comments

Comments
 (0)