Compare commits

...

9 Commits

Author SHA1 Message Date
Tyreece Rozycki 3346f301d2
Merge 0f75d7962c into 5c23713c03 2024-04-25 22:17:26 +09:00
oglass 5c23713c03
Use PlayerInstanceEvent instead of PlayerEvent (#2102) 2024-04-24 16:27:42 +00:00
KrystilizeNevaDies 0f75d7962c Introduce discrimination 2022-11-22 19:51:10 +10:00
KrystilizeNevaDies 5e4e3b3647 start Area api integration 2022-11-22 19:50:56 +10:00
KrystilizeNevaDies d2e66fd970 Implement new block tracker api
New block tracker api designed to help optimize things such as section palettes and world updates

Note that the chunk tracker test is expected to fail at the moment.
2022-11-22 19:50:56 +10:00
KrystilizeNevaDies 8568006fbc Introduce more optimizations + seal the area interface 2022-11-22 19:50:09 +10:00
KrystilizeNevaDies 7ea775ff49 More simplification, and start on optimized queries 2022-11-22 18:30:04 +10:00
KrystilizeNevaDies 71b157e20c Simplify AreaImpl and add tests 2022-11-22 17:32:23 +10:00
KrystilizeNevaDies 21e610827e new area api 2022-11-18 02:07:05 +10:00
10 changed files with 816 additions and 7 deletions

View File

@ -0,0 +1,130 @@
package net.minestom.server.coordinate;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* An area is a spatially connected set of block positions.
* These areas can be used for optimizations such as instance block queries, and pathfinding domains.
*/
public sealed interface Area extends Iterable<Point> permits AreaImpl.Fill, AreaImpl.SetArea, AreaImpl.Union {
/**
* Creates a new area from a collection of block positions. Note that these points will be block-aligned.
* @param collection the collection of block positions
* @return a new area
* @throws IllegalStateException if the resulting area is not fully connected
*/
static @NotNull Area collection(@NotNull Collection<? extends Point> collection) {
return AreaImpl.fromCollection(collection);
}
/**
* Creates a new rectangular prism area from two points.
* @param point1 the first (min) point
* @param point2 the second (max) point
* @return a new area
*/
static @NotNull Area fill(@NotNull Point point1, @NotNull Point point2) {
return new AreaImpl.Fill(point1, point2);
}
/**
* Creates a union of multiple areas.
* @param areas the areas to union
* @return a new area
*/
static @NotNull Area union(@NotNull Area... areas) {
return new AreaImpl.Union(List.of(areas));
}
/**
* Creates an intersection of multiple areas.
* @param areas the areas to intersect
* @return a new area
*/
static @NotNull Area intersection(@NotNull Area... areas) {
return AreaImpl.intersection(areas);
}
/**
* Starts a path pointer used to construct an area. This is useful for pathfinding purposes.
* @return a new path pointer
*/
static @NotNull Area.Path path() {
return new AreaImpl.Path();
}
/**
* The minimum point of this area
* @return the minimum point
*/
@NotNull Point min();
/**
* The maximum point of this area
* @return the maximum point
*/
@NotNull Point max();
/**
* Moves this area by an offset
* @param offset the offset
* @return a new area
*/
default @NotNull Area move(@NotNull Point offset) {
Set<Point> points = StreamSupport.stream(spliterator(), false)
.map(point -> point.add(offset))
.collect(Collectors.toUnmodifiableSet());
return AreaImpl.fromCollection(points);
}
interface Path {
@NotNull Area.Path north(double factor);
@NotNull Area.Path south(double factor);
@NotNull Area.Path east(double factor);
@NotNull Area.Path west(double factor);
@NotNull Area.Path up(double factor);
@NotNull Area.Path down(double factor);
@NotNull Area end();
default @NotNull Area.Path north() {
return north(1);
}
default @NotNull Area.Path south() {
return south(1);
}
default @NotNull Area.Path east() {
return east(1);
}
default @NotNull Area.Path west() {
return west(1);
}
default @NotNull Area.Path up() {
return up(1);
}
default @NotNull Area.Path down() {
return down(1);
}
}
sealed interface HasChildren permits AreaImpl.Union {
@NotNull Collection<Area> children();
}
}

View File

@ -0,0 +1,233 @@
package net.minestom.server.coordinate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
class AreaImpl {
static Area fromCollection(Collection<? extends Point> collection) {
// Detect any nested nxnxn areas, and create them
Set<Point> points = collection.stream()
.map(point -> new Vec(point.blockX(), point.blockY(), point.blockZ()))
.collect(Collectors.toSet());
return new SetArea(points);
}
private static Point findMin(Collection<Point> children) {
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE;
for (Point point : children) {
minX = Math.min(minX, point.blockX());
minY = Math.min(minY, point.blockY());
minZ = Math.min(minZ, point.blockZ());
}
return new Vec(minX, minY, minZ);
}
private static Point findMax(Collection<Point> children) {
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
for (Point point : children) {
maxX = Math.max(maxX, point.blockX());
maxY = Math.max(maxY, point.blockY());
maxZ = Math.max(maxZ, point.blockZ());
}
return new Vec(maxX, maxY, maxZ);
}
static Area intersection(Area[] children) {
if (children.length == 0) {
throw new IllegalArgumentException("Must have at least one child");
}
Set<Point> points = new HashSet<>(StreamSupport.stream(children[0].spliterator(), false).toList());
for (Area child : children) {
points.retainAll(StreamSupport.stream(child.spliterator(), false).toList());
}
return fromCollection(points);
}
record SetArea(Set<Point> points, Point min, Point max) implements Area {
public SetArea(Set<Point> points) {
this(points, findMin(points), findMax(points));
if (!isFullyConnected()) {
throw new IllegalArgumentException("Points must be fully connected");
}
}
public boolean isFullyConnected() {
if (points.size() == 1) return true;
Set<Point> connected = new HashSet<>();
for (Point point : points) {
connected.add(point.add(1, 0, 0));
connected.add(point.add(-1, 0, 0));
connected.add(point.add(0, 1, 0));
connected.add(point.add(0, -1, 0));
connected.add(point.add(0, 0, 1));
connected.add(point.add(0, 0, -1));
}
return connected.containsAll(points);
}
@NotNull
@Override
public Iterator<Point> iterator() {
return points.iterator();
}
}
static final class Fill implements Area {
private final Point min, max;
Fill(Point pos1, Point pos2) {
this.min = new Vec(Math.min(pos1.x(), pos2.x()),
Math.min(pos1.y(), pos2.y()),
Math.min(pos1.z(), pos2.z()));
this.max = new Vec(Math.max(pos1.x(), pos2.x()),
Math.max(pos1.y(), pos2.y()),
Math.max(pos1.z(), pos2.z()));
}
@Override
public @NotNull Point min() {
return min;
}
@Override
public @NotNull Point max() {
return max;
}
public boolean contains(Point pos) {
return pos.x() >= min.x() && pos.x() <= max.x() &&
pos.y() >= min.y() && pos.y() <= max.y() &&
pos.z() >= min.z() && pos.z() <= max.z();
}
@NotNull
@Override
public Iterator<Point> iterator() {
return new Iterator<>() {
private int x = min.blockX();
private int y = min.blockY();
private int z = min.blockZ();
@Override
public boolean hasNext() {
return x < max.blockX() && y < max.blockY() && z < max.blockZ();
}
@Override
public Point next() {
Point point = new Vec(x, y, z);
z++;
if (z >= max.blockZ()) {
z = min.blockZ();
y++;
if (y >= max.blockY()) {
y = min.blockY();
x++;
}
}
return point;
}
};
}
}
static class Path implements Area.Path {
private final List<Point> positions = new ArrayList<>();
private Point currentPosition;
@Override
public Area.@NotNull Path north(double factor) {
return with(blockPosition -> blockPosition.add(0, 0, -factor));
}
@Override
public Area.@NotNull Path south(double factor) {
return with(blockPosition -> blockPosition.add(0, 0, factor));
}
@Override
public Area.@NotNull Path east(double factor) {
return with(blockPosition -> blockPosition.add(factor, 0, 0));
}
@Override
public Area.@NotNull Path west(double factor) {
return with(blockPosition -> blockPosition.add(-factor, 0, 0));
}
@Override
public Area.@NotNull Path up(double factor) {
return with(blockPosition -> blockPosition.add(0, factor, 0));
}
@Override
public Area.@NotNull Path down(double factor) {
return with(blockPosition -> blockPosition.add(0, -factor, 0));
}
@Override
public @NotNull Area end() {
return fromCollection(positions);
}
private Area.Path with(UnaryOperator<Point> operator) {
this.currentPosition = operator.apply(currentPosition);
this.positions.add(currentPosition);
return this;
}
}
record Union(Collection<Area> children, Point min, Point max) implements Area, Area.HasChildren {
public Union(Collection<Area> children) {
this(children,
findMin(children.stream().map(Area::min).toList()),
findMax(children.stream().map(Area::max).toList()));
}
@Override
public @NotNull Iterator<Point> iterator() {
return new Iterator<>() {
private final Iterator<Area> areaIterator = children.iterator();
private Iterator<Point> currentIterator = areaIterator.next().iterator();
@Override
public boolean hasNext() {
if (currentIterator.hasNext()) {
return true;
}
while (areaIterator.hasNext()) {
currentIterator = areaIterator.next().iterator();
if (currentIterator.hasNext()) {
return true;
}
}
return false;
}
@Override
public Point next() {
return currentIterator.next();
}
};
}
}
}

View File

@ -0,0 +1,113 @@
package net.minestom.server.coordinate;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
public interface AreaQuery {
static boolean contains(Area area, Point point) {
if (area.min().x() > point.x() || area.max().x() < point.x() ||
area.min().y() > point.y() || area.max().y() < point.y() ||
area.min().z() > point.z() || area.max().z() < point.z()) {
return false;
}
Class<?> areaClass = area.getClass();
if (areaClass == AreaImpl.Fill.class) {
// We can return true here as we know the point is within the (filled) area
return true;
}
if (areaClass == AreaImpl.SetArea.class) {
// Just check if the point is in the set
return ((AreaImpl.SetArea) area).points().contains(point);
}
// Attempt to subdivide the area for a faster check
if (area instanceof Area.HasChildren parent) {
return parent.children()
.stream()
.anyMatch(child -> contains(child, point));
}
// Last resort, iterate over all points (Very slow)
for (Point pos : area) {
if (pos.equals(point)) {
return true;
}
}
return false;
}
static boolean hasOverlap(Area a, Area b) {
if (a.min().x() > b.max().x() || a.max().x() < b.min().x() ||
a.min().y() > b.max().y() || a.max().y() < b.min().y() ||
a.min().z() > b.max().z() || a.max().z() < b.min().z()) {
return false;
}
// TODO: Add optimizations for specific area types
Class<?> aClass = a.getClass();
Class<?> bClass = b.getClass();
if (aClass == AreaImpl.Fill.class) {
if (bClass == AreaImpl.Fill.class && hasOverlapFillFill((AreaImpl.Fill) a, (AreaImpl.Fill) b)) return true;
if (bClass == AreaImpl.SetArea.class && hasOverlapFillSet((AreaImpl.Fill) a, (AreaImpl.SetArea) b)) return true;
}
if (aClass == AreaImpl.SetArea.class) {
if (bClass == AreaImpl.Fill.class && hasOverlapFillSet((AreaImpl.Fill) b, (AreaImpl.SetArea) a)) return true;
if (bClass == AreaImpl.SetArea.class && hasOverlapSetSet((AreaImpl.SetArea) a, (AreaImpl.SetArea) b)) return true;
}
// Attempt to subdivide the area for a faster check
if (a instanceof Area.HasChildren parentA) {
if (b instanceof Area.HasChildren parentB) {
return parentA.children()
.stream()
.anyMatch(childA ->
parentB.children()
.stream()
.anyMatch(childB -> hasOverlap(childA, childB)));
} else {
return parentA.children()
.stream()
.anyMatch(childA -> hasOverlap(childA, b));
}
} else if (b instanceof Area.HasChildren parentB) {
return parentB.children()
.stream()
.anyMatch(childB -> hasOverlap(a, childB));
}
// Last resort, iterate over all points (Very slow)
for (Point pointA : a) {
for (Point pointB : b) {
if (pointA.equals(pointB)) {
return true;
}
}
}
return false;
}
static boolean hasOverlapSetSet(AreaImpl.SetArea a, AreaImpl.SetArea b) {
// We want A to be the smallest set (for performance reasons)
if (a.points().size() > b.points().size()) {
return hasOverlapSetSet(b, a);
}
return a.points().stream().anyMatch(b.points()::contains);
}
static boolean hasOverlapFillSet(AreaImpl.Fill a, AreaImpl.SetArea b) {
return b.points().stream().anyMatch(a::contains);
}
static boolean hasOverlapFillFill(AreaImpl.Fill a, AreaImpl.Fill b) {
// Fill areas can be checked for overlap by checking if the min/max points are within the other area
return a.contains(b.min()) || a.contains(b.max());
}
}

View File

@ -1,15 +1,14 @@
package net.minestom.server.event.player;
import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.PlayerEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.instance.Instance;
import org.jetbrains.annotations.NotNull;
/**
* Called when a new instance is set for a player.
*/
public class PlayerSpawnEvent implements PlayerEvent {
public class PlayerSpawnEvent implements PlayerInstanceEvent {
private final Player player;
private final Instance spawnInstance;
private final boolean firstSpawn;
@ -21,11 +20,12 @@ public class PlayerSpawnEvent implements PlayerEvent {
}
/**
* Gets the entity new instance.
* Gets the player's new instance.
*
* @return the instance
*/
@NotNull
@Deprecated
public Instance getSpawnInstance() {
return spawnInstance;
}

View File

@ -64,7 +64,7 @@ import java.util.stream.Collectors;
* with {@link InstanceManager#registerInstance(Instance)}, and
* you need to be sure to signal the {@link ThreadDispatcher} of every partition/element changes.
*/
public abstract class Instance implements Block.Getter, Block.Setter,
public abstract class Instance implements Block.Getter, Block.Setter, Block.Trackable,
Tickable, Schedulable, Snapshotable, EventHandler<InstanceEvent>, Taggable, PacketGroupingAudience {
private boolean registered;

View File

@ -2,6 +2,8 @@ package net.minestom.server.instance;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Area;
import net.minestom.server.coordinate.AreaQuery;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
@ -45,6 +47,7 @@ import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static net.minestom.server.utils.chunk.ChunkUtils.*;
@ -70,6 +73,10 @@ public class InstanceContainer extends Instance {
private final Long2ObjectSyncMap<Chunk> chunks = Long2ObjectSyncMap.hashmap();
private final Map<Long, CompletableFuture<Chunk>> loadingChunks = new ConcurrentHashMap<>();
// Block tracking
private final Map<Area, Set<Block.Tracker>> blockTrackers = new HashMap<>();
private final Set<Block.Tracker> globalBlockTrackers = new HashSet<>();
private final Lock changingBlockLock = new ReentrantLock();
private final Map<Point, Block> currentlyChangingBlocks = new HashMap<>();
@ -176,6 +183,31 @@ public class InstanceContainer extends Instance {
// Set the block
chunk.setBlock(x, y, z, block, placement, destroy);
// Update the block trackers
// TODO: Write tests for duplicate block updates (two updates after another where the block has not changed)?
synchronized (blockTrackers) {
@NotNull Block newBlock = block;
if (blockTrackers.size() != 0) {
for (var entry : blockTrackers.entrySet()) {
Area area = entry.getKey();
Set<Block.Tracker> trackers = entry.getValue();
if (trackers.stream().noneMatch(Block.Tracker::trackBlockPlacement)) continue;
if (AreaQuery.contains(area, blockPosition)) {
trackers.stream()
.filter(Block.Tracker::trackBlockPlacement)
.forEach(tracker -> tracker.updateBlock(blockPosition, newBlock));
}
}
}
}
synchronized (globalBlockTrackers) {
@NotNull Block newBlock = block;
globalBlockTrackers.stream()
.filter(Block.Tracker::trackBlockPlacement)
.forEach(tracker -> tracker.updateBlock(blockPosition, newBlock));
}
// Refresh neighbors since a new block has been placed
if (doBlockUpdates) {
executeNeighboursBlockPlacementRule(blockPosition, updateDistance);
@ -308,6 +340,12 @@ public class InstanceContainer extends Instance {
chunk.onLoad();
EventDispatcher.call(new InstanceChunkLoadEvent(this, chunk));
// Update the block trackers
synchronized (chunk) {
updateBlockTrackersForChunk(chunk);
}
final CompletableFuture<Chunk> future = this.loadingChunks.remove(index);
assert future == completableFuture : "Invalid future: " + future;
completableFuture.complete(chunk);
@ -324,6 +362,68 @@ public class InstanceContainer extends Instance {
return completableFuture;
}
private void updateBlockTrackersForChunk(Chunk chunk) {
if (blockTrackers.isEmpty()) return;
if (blockTrackers.values()
.stream()
.flatMap(Collection::stream)
.noneMatch(Block.Tracker::trackGeneration)) return;
int chunkX = chunk.getChunkX();
int chunkZ = chunk.getChunkZ();
int minX = chunkX * Chunk.CHUNK_SIZE_X;
int minY = getDimensionType().getMinY();
int minZ = chunkZ * Chunk.CHUNK_SIZE_Z;
int maxX = minX + Chunk.CHUNK_SIZE_X;
int maxY = getDimensionType().getMaxY();
int maxZ = minZ + Chunk.CHUNK_SIZE_Z;
// Scan through the chunk and collect blocks that need to be tracked
// TODO: Optimize this using the generation api where possible
// Optimizing the block trackers using the generation api means saving a list of all the operations as fill
// areas, and using them here.
Map<Block, Set<Point>> block2points = new HashMap<>();
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
Block block = chunk.getBlock(x, y, z);
block2points.computeIfAbsent(block, b -> new HashSet<>()).add(new Vec(x, y, z));
}
}
}
Map<Block, Area> areas = block2points.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> Area.collection(e.getValue())));
// Update the block trackers
synchronized (blockTrackers) {
areas.forEach((block, area) -> {
blockTrackers.forEach((trackerArea, trackers) -> {
// Exit early if none of the trackers are tracking generation
if (trackers.stream().noneMatch(Block.Tracker::trackGeneration)) return;
if (AreaQuery.hasOverlap(area, trackerArea)) {
Area intersection = Area.intersection(trackerArea, area);
trackers.stream()
.filter(Block.Tracker::trackGeneration)
.forEach(tracker -> tracker.updateBlocks(intersection, block));
}
});
});
}
synchronized (globalBlockTrackers) {
areas.forEach((block, area) -> globalBlockTrackers.stream()
.filter(Block.Tracker::trackGeneration)
.forEach(tracker -> tracker.updateBlocks(area, block)));
}
}
Map<Long, List<GeneratorImpl.SectionModifierImpl>> generationForks = new ConcurrentHashMap<>();
protected @NotNull CompletableFuture<@NotNull Chunk> createChunk(int chunkX, int chunkZ) {
@ -666,4 +766,18 @@ public class InstanceContainer extends Instance {
var dispatcher = MinecraftServer.process().dispatcher();
dispatcher.createPartition(chunk);
}
@Override
public void trackBlocks(@NotNull Area area, Block.@NotNull Tracker tracker) {
synchronized (blockTrackers) {
blockTrackers.computeIfAbsent(area, a -> new HashSet<>()).add(tracker);
}
}
@Override
public void trackAllBlocks(Block.@NotNull Tracker tracker) {
synchronized (globalBlockTrackers) {
globalBlockTrackers.add(tracker);
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.instance;
import net.minestom.server.coordinate.Area;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block;
@ -125,4 +126,14 @@ public class SharedInstance extends Instance {
public @NotNull InstanceContainer getInstanceContainer() {
return instanceContainer;
}
@Override
public void trackBlocks(@NotNull Area area, Block.@NotNull Tracker tracker) {
}
@Override
public void trackAllBlocks(Block.@NotNull Tracker tracker) {
}
}

View File

@ -1,6 +1,8 @@
package net.minestom.server.instance.block;
import net.minestom.server.coordinate.Area;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.batch.Batch;
import net.minestom.server.registry.StaticProtocolObject;
@ -12,9 +14,11 @@ import org.jetbrains.annotations.*;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
/**
* Represents a block that can be placed anywhere.
@ -252,4 +256,64 @@ public sealed interface Block extends StaticProtocolObject, TagReadable, Blocks
TYPE
}
}
/**
* Represents an object in which blocks changes may be tracked.
*/
interface Trackable {
/**
* Tracks the given area, listening for any block changes.
* All changes given to the tracker object are guaranteed to be within this area.
* @param area the area to track
* @param tracker the tracker object
*/
void trackBlocks(@NotNull Area area, @NotNull Tracker tracker);
/**
* Tracks all block changes.
* @param tracker the tracker object
*/
void trackAllBlocks(@NotNull Tracker tracker);
/**
* Tracks this specific block position, running the given update consumer when the block changes.
* @param point the block position
* @param newBlock the update consumer
*/
default void trackBlock(Point point, @NotNull Consumer<Block> newBlock) {
trackBlocks(Area.collection(List.of(point)), (pos, block) -> newBlock.accept(block));
}
}
/**
* Represents a target for where block-trackable objects can update their changes to.
*/
interface Tracker {
/**
* Called when the block at the given position has changed.
* @param point the position
* @param block the new block
*/
void updateBlock(Point point, @NotNull Block block);
/**
* Updates the block at the given rectangular prism.
* @param area the area
* @param block the new block
*/
default void updateBlocks(Area area, @NotNull Block block) {
for (Point pos : area) {
updateBlock(pos, block);
}
}
default boolean trackGeneration() {
return true;
}
default boolean trackBlockPlacement() {
return true;
}
}
}

View File

@ -0,0 +1,107 @@
package net.minestom.server.coordinate;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.junit.jupiter.api.Assertions.*;
public class AreaTest {
@Test
public void testSingleFillArea() {
Area area = Area.fill(new Vec(0, 0, 0), new Vec(1, 1, 1));
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 1)), "Point(0, 0, 1) should not be in the area");
}
@Test
public void testSmallFillArea() {
Area area = Area.fill(new Vec(0, 0, 0), new Vec(2, 2, 2));
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertTrue(points.contains(new Vec(1, 1, 1)), "Point(1, 1, 1) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 3)), "Point(0, 0, 3) should not be in the area");
}
@Test
public void testLargeFillArea() {
Area area = Area.fill(new Vec(0, 0, 0), new Vec(100, 100, 100));
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertTrue(points.contains(new Vec(50, 50, 50)), "Point(50, 50, 50) should be in the area");
assertTrue(points.contains(new Vec(99, 99, 99)), "Point(99, 99, 99) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 101)), "Point(0, 0, 101) should not be in the area");
}
@Test
public void testSingleCollectionArea() {
Area area = Area.collection(List.of(new Vec(0, 0, 0)));
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 1)), "Point(0, 0, 1) should not be in the area");
}
@Test
public void testSmallCollectionArea() {
Area area = Area.collection(List.of(new Vec(0, 0, 0), new Vec(1, 0, 0)));
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertTrue(points.contains(new Vec(1, 0, 0)), "Point(1, 0, 0) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 1)), "Point(0, 0, 1) should not be in the area");
assertFalse(points.contains(new Vec(2, 0, 0)), "Point(2, 0, 0) should not be in the area");
}
@Test
public void testUnionFillCollection() {
Area area = Area.union(
Area.fill(new Vec(0, 0, 0), new Vec(2, 2, 2)),
Area.collection(List.of(new Vec(2, 1, 1)))
);
Set<Point> points = points(area);
assertTrue(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should be in the area");
assertTrue(points.contains(new Vec(1, 1, 1)), "Point(1, 1, 1) should be in the area");
assertTrue(points.contains(new Vec(2, 1, 1)), "Point(2, 1, 1) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 3)), "Point(0, 0, 3) should not be in the area");
assertFalse(points.contains(new Vec(2, 0, 0)), "Point(2, 0, 0) should not be in the area");
assertFalse(points.contains(new Vec(3, 0, 0)), "Point(3, 0, 0) should not be in the area");
}
@Test
public void testIntersectionFillCollection() {
Area area = Area.intersection(
Area.fill(new Vec(0, 0, 0), new Vec(2, 2, 2)),
Area.collection(List.of(new Vec(1, 1, 1)))
);
Set<Point> points = points(area);
assertFalse(points.contains(new Vec(0, 0, 0)), "Point(0, 0, 0) should not be in the area");
assertTrue(points.contains(new Vec(1, 1, 1)), "Point(1, 1, 1) should be in the area");
assertFalse(points.contains(new Vec(0, 0, 1)), "Point(0, 0, 1) should not be in the area");
assertFalse(points.contains(new Vec(1, 0, 0)), "Point(1, 0, 0) should not be in the area");
assertFalse(points.contains(new Vec(0, 1, 0)), "Point(0, 1, 0) should not be in the area");
assertFalse(points.contains(new Vec(0, 0, 2)), "Point(0, 0, 2) should not be in the area");
}
private Set<Point> points(Area area) {
return StreamSupport.stream(area.spliterator(), false)
.collect(Collectors.toSet());
}
}

View File

@ -7,8 +7,9 @@ import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class InstanceBlockIntegrationTest {
@ -73,4 +74,40 @@ public class InstanceBlockIntegrationTest {
instance.setBlock(point, Block.GRASS_BLOCK.withTag(tag, 8));
assertEquals(8, instance.getBlock(point).getTag(tag));
}
@Test
public void basicTracker(Env env) {
var instance = env.createFlatInstance();
instance.loadChunk(0, 0).join();
AtomicBoolean called = new AtomicBoolean(false);
instance.trackBlock(new Vec(0, 0, 0), block -> called.set(true));
instance.setBlock(0, 0, 0, Block.GRASS);
assertEquals(Block.GRASS, instance.getBlock(0, 0, 0), "Block not set");
assertTrue(called.get(), "Tracker not called");
}
@Test
public void singleLoadChunkTracker(Env env) {
var instance = env.createFlatInstance();
AtomicBoolean called = new AtomicBoolean(false);
instance.trackBlock(new Vec(0, 0, 0), block -> called.set(true));
instance.loadChunk(0, 0).join();
assertTrue(called.get(), "Tracker not called");
}
@Test
public void singleGenerateChunkTracker(Env env) {
var instance = env.createFlatInstance();
instance.setGenerator(unit -> unit.modifier().fill(Block.STONE));
AtomicBoolean called = new AtomicBoolean(false);
instance.trackBlock(new Vec(0, 0, 0), block -> called.set(block == Block.STONE));
instance.loadChunk(0, 0).join();
assertTrue(called.get(), "Tracker not called with the correct block.");
}
}