Batch API

This commit is contained in:
TheMode 2022-10-27 12:19:45 +02:00
parent 93718276c5
commit 3155c99e2b
7 changed files with 506 additions and 0 deletions

View File

@ -0,0 +1,22 @@
package net.minestom.server.instance.batch;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.UnitModifier;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ApiStatus.Experimental
public sealed interface BatchPlace
permits BatchPlaceImpl {
static @NotNull BatchPlace batch(@NotNull Point size,
@NotNull Consumer<@NotNull UnitModifier> modifier) {
return BatchPlaceImpl.batch(size, modifier);
}
void forEachBlock(int originX, int originY, int originZ,
@NotNull BiConsumer<@NotNull Block, @NotNull Point> consumer);
}

View File

@ -0,0 +1,122 @@
package net.minestom.server.instance.batch;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.UnitModifier;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
record BatchPlaceImpl(Point size,
Map<Point, Block> relativeBlocks,
Map<Point, Block> absoluteBlocks) implements BatchPlace {
public BatchPlaceImpl {
relativeBlocks = Map.copyOf(relativeBlocks);
absoluteBlocks = Map.copyOf(absoluteBlocks);
}
static BatchPlace batch(Point size, Consumer<UnitModifier> consumer) {
if (size.x() < 1 || size.y() < 1 || size.z() < 1) {
throw new IllegalArgumentException("Size must be positive: " + size);
}
BatchModifier modifier = new BatchModifier(size);
consumer.accept(modifier);
return new BatchPlaceImpl(size, modifier.relativeBlocks, modifier.absoluteBlocks);
}
@Override
public void forEachBlock(int originX, int originY, int originZ, @NotNull BiConsumer<@NotNull Block, @NotNull Point> consumer) {
this.relativeBlocks.forEach((point, block) -> consumer.accept(block, point.add(originX, originY, originZ)));
this.absoluteBlocks.forEach((point, block) -> {
final int x = point.blockX();
final int y = point.blockY();
final int z = point.blockZ();
if (x < 0 || y < 0 || z < 0 || x >= size.x()+originX || y >= size.y()+originY || z >= size.z()+originZ) {
throw new IllegalArgumentException("Out of bounds: " + new Vec(x, y, z));
}
consumer.accept(block, point);
});
}
private static final class BatchModifier implements UnitModifier {
private final Point size;
private final Map<Point, Block> relativeBlocks = new HashMap<>();
private final Map<Point, Block> absoluteBlocks = new HashMap<>();
private BatchModifier(Point size) {
this.size = size;
}
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
this.absoluteBlocks.put(new Vec(x, y, z), block);
}
@Override
public void setRelative(int x, int y, int z, @NotNull Block block) {
if (x < 0 || y < 0 || z < 0 || x >= size.x() || y >= size.y() || z >= size.z()) {
throw new IllegalArgumentException("Out of bounds: " + x + ", " + y + ", " + z);
}
this.relativeBlocks.put(new Vec(x, y, z), block);
}
@Override
public void setAll(@NotNull Supplier supplier) {
throw new UnsupportedOperationException();
}
@Override
public void setAllRelative(@NotNull Supplier supplier) {
for (int x = 0; x < size.x(); x++) {
for (int y = 0; y < size.y(); y++) {
for (int z = 0; z < size.z(); z++) {
this.relativeBlocks.put(new Vec(x, y, z), supplier.get(x, y, z));
}
}
}
}
@Override
public void fill(@NotNull Block block) {
for (int x = 0; x < size.x(); x++) {
for (int y = 0; y < size.y(); y++) {
for (int z = 0; z < size.z(); z++) {
this.relativeBlocks.put(new Vec(x, y, z), block);
}
}
}
}
@Override
public void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block) {
for(int x = start.blockX(); x < end.blockX(); x++) {
for(int y = start.blockY(); y < end.blockY(); y++) {
for(int z = start.blockZ(); z < end.blockZ(); z++) {
this.absoluteBlocks.put(new Vec(x, y, z), block);
}
}
}
}
@Override
public void fillHeight(int minHeight, int maxHeight, @NotNull Block block) {
throw new UnsupportedOperationException();
}
@Override
public void fillBiome(@NotNull Biome biome) {
throw new UnsupportedOperationException();
}
@Override
public void setBiome(int x, int y, int z, @NotNull Biome biome) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -0,0 +1,44 @@
package net.minestom.server.instance.batch;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@ApiStatus.Experimental
public sealed interface BatchQuery
permits BatchQueryImpl {
static @NotNull Result fallback(Block.@NotNull Getter getter, int x, int y, int z,
Block.Getter.@NotNull Condition condition, @NotNull BatchQuery query) {
return BatchQueryImpl.fallback(getter, x, y, z, condition, query);
}
static @NotNull Builder builder(int radius) {
return new BatchQueryImpl.Builder(radius);
}
static @NotNull BatchQuery radius(int radius) {
return builder(radius).build();
}
interface Result extends Block.Getter {
/**
* Gets the number of blocks successfully queried.
*
* @return block count
*/
int count();
}
sealed interface Builder
permits BatchQueryImpl.Builder {
@Contract("_ -> this")
@NotNull Builder type(@NotNull Block @NotNull ... blocks);
@Contract("_ -> this")
@NotNull Builder exact(@NotNull Block @NotNull ... blocks);
@NotNull BatchQuery build();
}
}

View File

@ -0,0 +1,94 @@
package net.minestom.server.instance.batch;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
record BatchQueryImpl(int radius,
@NotNull Predicate<Block> predicate) implements BatchQuery {
static Result fallback(Block.Getter getter, int x, int y, int z,
Block.Getter.Condition condition, BatchQuery query) {
Map<Point, Block> blocks = new HashMap<>();
BatchQueryImpl queryImpl = (BatchQueryImpl) query;
final int radius = queryImpl.radius;
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
for (int k = -radius; k <= radius; k++) {
final int blockX = x + i;
final int blockY = y + j;
final int blockZ = z + k;
final Block block = getter.getBlock(blockX, blockY, blockZ);
if (!queryImpl.predicate.test(block)) {
continue;
}
blocks.put(new Vec(blockX, blockY, blockZ), block);
}
}
}
return new FallbackResult(blocks);
}
record FallbackResult(Map<Point, Block> blocks) implements Result {
public FallbackResult {
blocks = Map.copyOf(blocks);
}
@Override
public @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) {
return blocks.get(new Vec(x, y, z));
}
@Override
public int count() {
return blocks.size();
}
}
static final class Builder implements BatchQuery.Builder {
private final int radius;
private Set<Integer> type;
private Set<Block> exact;
Builder(Integer radius) {
this.radius = radius;
}
@Override
public BatchQuery.@NotNull Builder type(@NotNull Block @NotNull ... blocks) {
this.type = Arrays.stream(blocks).map(Block::id).collect(Collectors.toUnmodifiableSet());
return this;
}
@Override
public BatchQuery.@NotNull Builder exact(@NotNull Block @NotNull ... blocks) {
this.exact = Set.of(blocks);
return this;
}
@Override
public @NotNull BatchQuery build() {
var type = this.type != null ? Set.copyOf(this.type) : null;
var exact = this.exact != null ? Set.copyOf(this.exact) : null;
return new BatchQueryImpl(radius,
block -> {
if (type != null && !type.contains(block.id()))
return false;
if (exact != null && !exact.contains(block))
return false;
return true;
});
}
}
}

View File

@ -3,6 +3,8 @@ package net.minestom.server.instance.block;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.batch.Batch;
import net.minestom.server.instance.batch.BatchPlace;
import net.minestom.server.instance.batch.BatchQuery;
import net.minestom.server.registry.ProtocolObject;
import net.minestom.server.registry.Registry;
import net.minestom.server.tag.Tag;
@ -209,6 +211,17 @@ public sealed interface Block extends ProtocolObject, TagReadable, Blocks permit
default void setBlock(@NotNull Point blockPosition, @NotNull Block block) {
setBlock(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(), block);
}
@ApiStatus.Experimental
default void setBlocks(int x, int y, int z, @NotNull BatchPlace batch) {
batch.forEachBlock(x, y, z, (block, position) ->
setBlock(position.blockX(), position.blockY(), position.blockZ(), block));
}
@ApiStatus.Experimental
default void setBlocks(@NotNull Point blockPosition, @NotNull BatchPlace batch) {
setBlocks(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(), batch);
}
}
interface Getter {
@ -226,6 +239,28 @@ public sealed interface Block extends ProtocolObject, TagReadable, Blocks permit
return Objects.requireNonNull(getBlock(point, Condition.NONE));
}
@ApiStatus.Experimental
default BatchQuery.@NotNull Result getBlocks(int x, int y, int z, @NotNull Condition condition,
@NotNull BatchQuery query) {
return BatchQuery.fallback(this, x, y, z, condition, query);
}
@ApiStatus.Experimental
default BatchQuery.@NotNull Result getBlocks(@NotNull Point point, @NotNull Condition condition,
@NotNull BatchQuery query) {
return getBlocks(point.blockX(), point.blockY(), point.blockZ(), condition, query);
}
@ApiStatus.Experimental
default BatchQuery.@NotNull Result getBlocks(int x, int y, int z, @NotNull BatchQuery query) {
return getBlocks(x, y, z, Condition.NONE, query);
}
@ApiStatus.Experimental
default BatchQuery.@NotNull Result getBlocks(@NotNull Point point, @NotNull BatchQuery query) {
return getBlocks(point, Condition.NONE, query);
}
/**
* Represents a hint to retrieve blocks more efficiently.
* Implementing interfaces do not have to honor this.

View File

@ -0,0 +1,100 @@
package net.minestom.server.instance.batch;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class BatchPlaceIntegrationTest {
@Test
public void subSectionBatchFill(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchPlace.batch(new Vec(4, 4, 4), modifier -> modifier.fill(Block.STONE));
instance.setBlocks(0, 0, 0, batch);
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
for (int z = 0; z < 4; z++) {
assertEquals(Block.STONE, instance.getBlock(x, y, z));
}
}
}
for (int x = 4; x < 16; x++) {
for (int y = 4; y < 16; y++) {
for (int z = 4; z < 16; z++) {
assertEquals(Block.AIR, instance.getBlock(x, y, z), "Block at " + x + ", " + y + ", " + z + " was not air");
}
}
}
}
@Test
public void setAbsolute(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchPlace.batch(new Vec(16, 16, 16), modifier -> {
modifier.setBlock(0, 0, 0, Block.STONE);
modifier.setBlock(15, 15, 15, Block.STONE);
});
instance.setBlocks(0, 0, 0, batch);
assertEquals(Block.STONE, instance.getBlock(0, 0, 0));
assertEquals(Block.STONE, instance.getBlock(15, 15, 15));
assertEquals(Block.AIR, instance.getBlock(1, 0, 0));
}
@Test
public void setAbsoluteBound(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchPlace.batch(new Vec(16, 16, 16),
modifier -> modifier.setBlock(32, 32, 32, Block.STONE));
instance.setBlocks(32, 32, 32, batch);
assertEquals(Block.STONE, instance.getBlock(32, 32, 32));
assertNull(instance.getChunk(0, 0));
instance.loadChunk(0, 0).join();
assertEquals(Block.AIR, instance.getBlock(0, 0, 0));
instance.setBlock(32, 32, 32, Block.AIR);
assertThrows(Exception.class, () -> instance.setBlocks(0, 0, 0, batch), "Block outside of chunk");
instance.setBlock(32, 32, 32, Block.AIR);
assertEquals(Block.AIR, instance.getBlock(0, 0, 0));
}
@Test
public void setRelative(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchPlace.batch(new Vec(16, 16, 16), modifier -> {
modifier.setRelative(0, 0, 0, Block.STONE);
modifier.setRelative(15, 15, 15, Block.STONE);
});
instance.setBlocks(0, 0, 0, batch);
assertEquals(Block.STONE, instance.getBlock(0, 0, 0));
assertEquals(Block.STONE, instance.getBlock(15, 15, 15));
assertEquals(Block.AIR, instance.getBlock(1, 0, 0));
}
@Test
public void fillSection(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchPlace.batch(new Vec(16, 16, 16), modifier -> modifier.fill(Block.STONE));
instance.setBlocks(0, 0, 0, batch);
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
assertEquals(Block.STONE, instance.getBlock(x, y, z));
}
}
}
}
}

View File

@ -0,0 +1,89 @@
package net.minestom.server.instance.batch;
import net.minestom.server.instance.block.Block;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@EnvTest
public class BatchQueryIntegrationTest {
@Test
public void radius(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchQuery.radius(5);
instance.loadChunk(0, 0).join();
for (int x = 3; x < 13; x++) {
for (int y = 3; y < 13; y++) {
for (int z = 3; z < 13; z++) {
instance.setBlock(x, y, z, Block.STONE);
}
}
}
var result = instance.getBlocks(8, 8, 8, batch);
assertEquals(1331, result.count());
for (int x = 3; x < 13; x++) {
for (int y = 3; y < 13; y++) {
for (int z = 3; z < 13; z++) {
assertEquals(instance.getBlock(x, y, z), result.getBlock(x, y, z),
"Block at " + x + ", " + y + ", " + z + " was not equal");
}
}
}
}
@Test
public void type(Env env) {
var instance = env.process().instance().createInstanceContainer();
var batch = BatchQuery.builder(3).type(Block.STONE).build();
instance.loadChunk(0, 0).join();
// Fill section with grass
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
instance.setBlock(x, y, z, Block.GRASS);
}
}
}
// Place stone in the middle
for (int x = 7; x < 10; x++) {
for (int y = 7; y < 10; y++) {
for (int z = 7; z < 10; z++) {
instance.setBlock(x, y, z, Block.STONE);
}
}
}
var result = instance.getBlocks(8, 8, 8, batch);
assertEquals(27, result.count());
for (int x = 5; x < 11; x++) {
for (int y = 5; y < 11; y++) {
for (int z = 5; z < 11; z++) {
if (x >= 7 && y >= 7 && z >= 7
&& x < 10 && y < 10 && z < 10) {
assertEquals(instance.getBlock(x, y, z), result.getBlock(x, y, z),
"Block at " + x + ", " + y + ", " + z + " was not equal");
} else {
int finalX = x;
int finalY = y;
int finalZ = z;
assertThrows(Exception.class, () -> result.getBlock(finalX, finalY, finalZ),
"Block at " + x + ", " + y + ", " + z + " was not air");
}
}
}
}
}
}