mirror of https://github.com/Minestom/Minestom.git
Compare commits
9 Commits
c6bbd3716e
...
3346f301d2
Author | SHA1 | Date |
---|---|---|
Tyreece Rozycki | 3346f301d2 | |
oglass | 5c23713c03 | |
KrystilizeNevaDies | 0f75d7962c | |
KrystilizeNevaDies | 5e4e3b3647 | |
KrystilizeNevaDies | d2e66fd970 | |
KrystilizeNevaDies | 8568006fbc | |
KrystilizeNevaDies | 7ea775ff49 | |
KrystilizeNevaDies | 71b157e20c | |
KrystilizeNevaDies | 21e610827e |
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue