Low level generation API (#574)

This commit is contained in:
TheMode 2022-04-08 07:19:52 +02:00 committed by GitHub
parent 1644a1e790
commit a70bb15146
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1681 additions and 330 deletions

View File

@ -5,12 +5,9 @@ import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.player.PlayerLoginEvent;
import net.minestom.server.instance.*;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class MainDemo {
@ -22,7 +19,7 @@ public class MainDemo {
// Create the instance
InstanceContainer instanceContainer = instanceManager.createInstanceContainer();
// Set the ChunkGenerator
instanceContainer.setChunkGenerator(new GeneratorDemo());
instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
// Add an event callback to specify the spawning instance (and the spawn position)
GlobalEventHandler globalEventHandler = MinecraftServer.getGlobalEventHandler();
@ -36,24 +33,4 @@ public class MainDemo {
// Start the server on port 25565
minecraftServer.start("0.0.0.0", 25565);
}
private static class GeneratorDemo implements ChunkGenerator {
@Override
public void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) {
// Set chunk blocks
for (byte x = 0; x < Chunk.CHUNK_SIZE_X; x++)
for (byte z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
for (byte y = 0; y < 40; y++) {
batch.setBlock(x, y, z, Block.STONE);
}
}
}
@Override
public List<ChunkPopulator> getPopulators() {
return null;
}
}
}

View File

@ -1,8 +1,6 @@
package net.minestom.demo;
import net.kyori.adventure.text.Component;
import net.minestom.demo.generator.ChunkGeneratorDemo;
import net.minestom.demo.generator.NoiseTestGenerator;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.coordinate.Pos;
@ -31,6 +29,7 @@ import net.minestom.server.item.metadata.BundleMeta;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.world.DimensionType;
@ -40,7 +39,6 @@ import java.util.Random;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
public class PlayerInit {
@ -122,17 +120,13 @@ public class PlayerInit {
static {
InstanceManager instanceManager = MinecraftServer.getInstanceManager();
ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo();
NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator();
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD);
instanceContainer.setChunkGenerator(chunkGeneratorDemo);
instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
if (false) {
System.out.println("start");
IntStream.range(0, 5000).forEach(value -> {
instanceContainer.loadChunk(0, value).join();
});
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> instanceContainer.loadChunk(x, z).join());
System.out.println("load end");
}

View File

@ -1,28 +0,0 @@
package net.minestom.demo.generator;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.ChunkGenerator;
import net.minestom.server.instance.ChunkPopulator;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class ChunkGeneratorDemo implements ChunkGenerator {
@Override
public void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) {
for (byte x = 0; x < Chunk.CHUNK_SIZE_X; x++)
for (byte z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
for (byte y = 0; y < 40; y++) {
batch.setBlock(x, y, z, Block.STONE);
}
}
}
@Override
public List<ChunkPopulator> getPopulators() {
return null;
}
}

View File

@ -2,24 +2,39 @@ package net.minestom.demo.generator;
import de.articdive.jnoise.JNoise;
import de.articdive.jnoise.interpolation.InterpolationType;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.ChunkGenerator;
import net.minestom.server.instance.ChunkPopulator;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import net.minestom.server.instance.generator.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class NoiseTestGenerator implements ChunkGenerator {
public class NoiseTestGenerator implements Generator {
private Random random = new Random();
private JNoise jNoise = JNoise.newBuilder().perlin().setInterpolation(InterpolationType.LINEAR).setSeed(random.nextInt()).setFrequency(0.4).build();
private JNoise jNoise2 = JNoise.newBuilder().perlin().setInterpolation(InterpolationType.LINEAR).setSeed(random.nextInt()).setFrequency(0.6).build();
private TreePopulator treeGen = new TreePopulator();
public NoiseTestGenerator() {
treeNoise.getNoise(0, 0);
jNoise.getNoise(0, 0);
}
private final JNoise treeNoise = JNoise.newBuilder()
.fastSimplex()
.setFrequency(999)
.setSeed(123)
.build();
private final JNoise jNoise = JNoise.newBuilder()
.perlin()
.setInterpolation(InterpolationType.LINEAR)
.setSeed(123)
.setFrequency(0.4).build();
public int getHeight(int x, int z) {
double preHeight = jNoise.getNoise(x / 16.0, z / 16.0);
@ -27,143 +42,111 @@ public class NoiseTestGenerator implements ChunkGenerator {
}
@Override
public void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) {
public void generate(@NotNull GenerationUnit unit) {
Point start = unit.absoluteStart();
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
final int height = getHeight(x + chunkX * 16, z + chunkZ * 16);
for (int y = 0; y < height; y++) {
if (y == 0) {
batch.setBlock(x, y, z, Block.BEDROCK);
} else if (y == height - 1) {
batch.setBlock(x, y, z, Block.GRASS_BLOCK);
} else if (y > height - 7) {
batch.setBlock(x, y, z, Block.DIRT);
} else {
batch.setBlock(x, y, z, Block.STONE);
}
Point pos;
{
int absX = start.blockX() + x;
int absZ = start.blockZ() + z;
final int height = getHeight(absX, absZ);
pos = new Vec(absX, height, absZ);
}
if (height < 61) {
batch.setBlock(x, height - 1, z, Block.DIRT);
for (int y = 0; y < 61 - height; y++) {
batch.setBlock(x, y + height, z, Block.WATER);
}
Point posp1 = pos.add(1, 0, 1);
// Water
if (pos.y() < 61) {
unit.modifier().fill(pos, posp1.withY(61), Block.WATER);
unit.modifier().fill(pos.withY(0), posp1, Block.AIR);
return;
}
// Regular terrain
unit.modifier().fill(pos.withY(0), posp1, Block.STONE);
unit.modifier().fill(pos.withY(pos.y() - 7), posp1, Block.DIRT);
unit.modifier().fill(pos.withY(pos.y() - 1), posp1, Block.GRASS_BLOCK);
unit.modifier().fill(pos.withY(0), posp1.withY(1), Block.BEDROCK);
if (treeNoise.getNoise(pos.x(), pos.z()) > 0.8) {
TreePopulator.populate(pos, unit);
}
}
}
}
@Override
public List<ChunkPopulator> getPopulators() {
List<ChunkPopulator> list = new ArrayList<>();
list.add(treeGen);
return list;
}
private class TreePopulator implements ChunkPopulator {
final Structure tree;
public TreePopulator() {
tree = new Structure();
tree.addBlock(Block.DIRT, 0, -1, 0);
tree.addBlock(Block.OAK_LOG, 0, 0, 0);
tree.addBlock(Block.OAK_LOG, 0, 1, 0);
tree.addBlock(Block.OAK_LOG, 0, 2, 0);
tree.addBlock(Block.OAK_LOG, 0, 3, 0);
tree.addBlock(Block.OAK_LEAVES, 1, 1, 0);
tree.addBlock(Block.OAK_LEAVES, 2, 1, 0);
tree.addBlock(Block.OAK_LEAVES, -1, 1, 0);
tree.addBlock(Block.OAK_LEAVES, -2, 1, 0);
tree.addBlock(Block.OAK_LEAVES, 1, 1, 1);
tree.addBlock(Block.OAK_LEAVES, 2, 1, 1);
tree.addBlock(Block.OAK_LEAVES, 0, 1, 1);
tree.addBlock(Block.OAK_LEAVES, -1, 1, 1);
tree.addBlock(Block.OAK_LEAVES, -2, 1, 1);
tree.addBlock(Block.OAK_LEAVES, 1, 1, 2);
tree.addBlock(Block.OAK_LEAVES, 2, 1, 2);
tree.addBlock(Block.OAK_LEAVES, 0, 1, 2);
tree.addBlock(Block.OAK_LEAVES, -1, 1, 2);
tree.addBlock(Block.OAK_LEAVES, -2, 1, 2);
tree.addBlock(Block.OAK_LEAVES, 1, 1, -1);
tree.addBlock(Block.OAK_LEAVES, 2, 1, -1);
tree.addBlock(Block.OAK_LEAVES, 0, 1, -1);
tree.addBlock(Block.OAK_LEAVES, -1, 1, -1);
tree.addBlock(Block.OAK_LEAVES, -2, 1, -1);
tree.addBlock(Block.OAK_LEAVES, 1, 1, -2);
tree.addBlock(Block.OAK_LEAVES, 2, 1, -2);
tree.addBlock(Block.OAK_LEAVES, 0, 1, -2);
tree.addBlock(Block.OAK_LEAVES, -1, 1, -2);
tree.addBlock(Block.OAK_LEAVES, -2, 1, -2);
tree.addBlock(Block.OAK_LEAVES, 1, 2, 0);
tree.addBlock(Block.OAK_LEAVES, 2, 2, 0);
tree.addBlock(Block.OAK_LEAVES, -1, 2, 0);
tree.addBlock(Block.OAK_LEAVES, -2, 2, 0);
tree.addBlock(Block.OAK_LEAVES, 1, 2, 1);
tree.addBlock(Block.OAK_LEAVES, 2, 2, 1);
tree.addBlock(Block.OAK_LEAVES, 0, 2, 1);
tree.addBlock(Block.OAK_LEAVES, -1, 2, 1);
tree.addBlock(Block.OAK_LEAVES, -2, 2, 1);
tree.addBlock(Block.OAK_LEAVES, 1, 2, 2);
tree.addBlock(Block.OAK_LEAVES, 2, 2, 2);
tree.addBlock(Block.OAK_LEAVES, 0, 2, 2);
tree.addBlock(Block.OAK_LEAVES, -1, 2, 2);
tree.addBlock(Block.OAK_LEAVES, -2, 2, 2);
tree.addBlock(Block.OAK_LEAVES, 1, 2, -1);
tree.addBlock(Block.OAK_LEAVES, 2, 2, -1);
tree.addBlock(Block.OAK_LEAVES, 0, 2, -1);
tree.addBlock(Block.OAK_LEAVES, -1, 2, -1);
tree.addBlock(Block.OAK_LEAVES, -2, 2, -1);
tree.addBlock(Block.OAK_LEAVES, 1, 2, -2);
tree.addBlock(Block.OAK_LEAVES, 2, 2, -2);
tree.addBlock(Block.OAK_LEAVES, 0, 2, -2);
tree.addBlock(Block.OAK_LEAVES, -1, 2, -2);
tree.addBlock(Block.OAK_LEAVES, -2, 2, -2);
tree.addBlock(Block.OAK_LEAVES, 1, 3, 0);
tree.addBlock(Block.OAK_LEAVES, -1, 3, 0);
tree.addBlock(Block.OAK_LEAVES, 1, 3, 1);
tree.addBlock(Block.OAK_LEAVES, 0, 3, 1);
tree.addBlock(Block.OAK_LEAVES, -1, 3, 1);
tree.addBlock(Block.OAK_LEAVES, 1, 3, -1);
tree.addBlock(Block.OAK_LEAVES, 0, 3, -1);
tree.addBlock(Block.OAK_LEAVES, -1, 3, -1);
tree.addBlock(Block.OAK_LEAVES, 1, 4, 0);
tree.addBlock(Block.OAK_LEAVES, 0, 4, 0);
tree.addBlock(Block.OAK_LEAVES, -1, 4, 0);
tree.addBlock(Block.OAK_LEAVES, 0, 4, 1);
tree.addBlock(Block.OAK_LEAVES, 0, 4, -1);
tree.addBlock(Block.OAK_LEAVES, -1, 4, -1);
private static class TreePopulator {
private static void populate(Point origin, GenerationUnit unit) {
unit.fork(setter -> {
setter.setBlock(origin.add(0, -1, 0), Block.DIRT);
setter.setBlock(origin.add(0, -1, 0), Block.DIRT);
setter.setBlock(origin.add(0, 0, 0), Block.OAK_LOG);
setter.setBlock(origin.add(0, 1, 0), Block.OAK_LOG);
setter.setBlock(origin.add(0, 2, 0), Block.OAK_LOG);
setter.setBlock(origin.add(0, 3, 0), Block.OAK_LOG);
setter.setBlock(origin.add(1, 1, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 1, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 1, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 1, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 1, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 1, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 1, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 1, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 1, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 1, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 1, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 1, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 1, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 1, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 1, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 1, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 1, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 1, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 1, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 1, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 1, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 1, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 1, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 1, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 2, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 2, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 2, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 2, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 2, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 2, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 2, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 2, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 2, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 2, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 2, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 2, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 2, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 2, 2), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 2, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 2, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 2, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 2, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 2, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 2, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(2, 2, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 2, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 2, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(-2, 2, -2), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 3, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 3, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 3, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 3, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 3, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 3, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 3, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 3, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(1, 4, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 4, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 4, 0), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 4, 1), Block.OAK_LEAVES);
setter.setBlock(origin.add(0, 4, -1), Block.OAK_LEAVES);
setter.setBlock(origin.add(-1, 4, -1), Block.OAK_LEAVES);
});
}
//todo improve
@Override
public void populateChunk(ChunkBatch batch, Chunk chunk) {
for (int i = -2; i < 18; i++) {
for (int j = -2; j < 18; j++) {
if (jNoise2.getNoise(i + chunk.getChunkX() * 16, j + chunk.getChunkZ() * 16) > 0.75) {
int y = getHeight(i + chunk.getChunkX() * 16, j + chunk.getChunkZ() * 16);
tree.build(batch, new Vec(i, y, j));
}
}
}
}
}
}

View File

@ -1,30 +0,0 @@
package net.minestom.demo.generator;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
import java.util.HashMap;
import java.util.Map;
public class Structure {
private final Map<Point, Block> blocks = new HashMap<>();
public void build(ChunkBatch batch, Point pos) {
blocks.forEach((bPos, block) -> {
if (bPos.x() + pos.x() >= Chunk.CHUNK_SIZE_X || bPos.x() + pos.x() < 0)
return;
if (bPos.z() + pos.z() >= Chunk.CHUNK_SIZE_Z || bPos.z() + pos.z() < 0)
return;
batch.setBlock(bPos.add(pos), block);
});
}
public void addBlock(Block block, int localX, int localY, int localZ) {
blocks.put(new Vec(localX, localY, localZ), block);
}
}

View File

@ -11,7 +11,9 @@ import java.util.List;
* Responsible for the {@link Chunk} generation, can be set using {@link Instance#setChunkGenerator(ChunkGenerator)}.
* <p>
* Called if the instance {@link IChunkLoader} hasn't been able to load the chunk.
* @deprecated Replaced by {@link net.minestom.server.instance.generator.Generator}
*/
@Deprecated
public interface ChunkGenerator {
/**

View File

@ -0,0 +1,42 @@
package net.minestom.server.instance;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import net.minestom.server.instance.generator.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Provides full compatibility for the deprecated {@link ChunkGenerator}
*/
@SuppressWarnings("deprecation")
record ChunkGeneratorCompatibilityLayer(@NotNull ChunkGenerator chunkGenerator) implements Generator {
@Override
public void generate(@NotNull GenerationUnit unit) {
if (!(unit instanceof GeneratorImpl.UnitImpl impl) ||
!(impl.modifier() instanceof GeneratorImpl.AreaModifierImpl modifier && modifier.chunk() != null)) {
throw new IllegalArgumentException("Invalid unit");
}
final int startY = unit.absoluteStart().blockY();
ChunkBatch batch = new ChunkBatch() {
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
unit.modifier().setRelative(x, y - startY, z, block);
}
};
final Point start = unit.absoluteStart();
chunkGenerator.generateChunkData(batch, start.chunkX(), start.chunkZ());
final List<ChunkPopulator> populators = chunkGenerator.getPopulators();
final boolean hasPopulator = populators != null && !populators.isEmpty();
if (hasPopulator) {
for (ChunkPopulator chunkPopulator : populators) {
chunkPopulator.populateChunk(batch, modifier.chunk());
}
}
}
}

View File

@ -2,6 +2,7 @@ package net.minestom.server.instance;
import net.minestom.server.instance.batch.ChunkBatch;
@Deprecated
public interface ChunkPopulator {
void populateChunk(ChunkBatch batch, Chunk chunk);

View File

@ -0,0 +1,518 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
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.GenerationUnit;
import net.minestom.server.instance.generator.UnitModifier;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static net.minestom.server.utils.chunk.ChunkUtils.*;
final class GeneratorImpl {
private static final Vec SECTION_SIZE = new Vec(16);
static GenerationUnit section(Section section, int sectionX, int sectionY, int sectionZ,
boolean fork) {
final Vec start = SECTION_SIZE.mul(sectionX, sectionY, sectionZ);
final Vec end = start.add(SECTION_SIZE);
final UnitModifier modifier = new SectionModifierImpl(SECTION_SIZE, start, end,
section.blockPalette(), section.biomePalette(), new Int2ObjectOpenHashMap<>(0), fork);
return unit(modifier, start, end, null);
}
static GenerationUnit section(Section section, int sectionX, int sectionY, int sectionZ) {
return section(section, sectionX, sectionY, sectionZ, false);
}
static UnitImpl chunk(Chunk chunk, int minSection, int maxSection,
List<Section> chunkSections, int chunkX, int chunkZ) {
final int minY = minSection * 16;
AtomicInteger sectionCounterY = new AtomicInteger(minSection);
List<GenerationUnit> sections = chunkSections.stream()
.map(section -> section(section, chunkX, sectionCounterY.getAndIncrement(), chunkZ))
.toList();
final Vec size = new Vec(16, (maxSection - minSection) * 16, 16);
final Vec start = new Vec(chunkX * 16, minY, chunkZ * 16);
final Vec end = new Vec(chunkX * 16 + 16, size.y() + minY, chunkZ * 16 + 16);
final UnitModifier modifier = new AreaModifierImpl(chunk,
size, start, end, 1, sections.size(), 1, sections);
return unit(modifier, start, end, sections);
}
static UnitImpl chunk(int minSection, int maxSection,
List<Section> chunkSections, int chunkX, int chunkZ) {
return chunk(null, minSection, maxSection, chunkSections, chunkX, chunkZ);
}
static UnitImpl chunk(Chunk chunk) {
return chunk(chunk, chunk.minSection, chunk.maxSection, chunk.getSections(), chunk.getChunkX(), chunk.getChunkZ());
}
static UnitImpl unit(UnitModifier modifier, Point start, Point end,
List<GenerationUnit> divided) {
if (start.x() > end.x() || start.y() > end.y() || start.z() > end.z()) {
throw new IllegalArgumentException("absoluteStart must be before absoluteEnd");
}
if (start.x() % 16 != 0 || start.y() % 16 != 0 || start.z() % 16 != 0) {
throw new IllegalArgumentException("absoluteStart must be a multiple of 16");
}
if (end.x() % 16 != 0 || end.y() % 16 != 0 || end.z() % 16 != 0) {
throw new IllegalArgumentException("absoluteEnd must be a multiple of 16");
}
final Point size = end.sub(start);
return new UnitImpl(modifier, size, start, end, divided, new CopyOnWriteArrayList<>());
}
static final class DynamicFork implements Block.Setter {
Vec minSection;
int width, height, depth;
List<GenerationUnit> sections;
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
resize(x, y, z);
GenerationUnit section = findAbsolute(sections, minSection, width, height, depth, x, y, z);
assert section.absoluteStart().chunkX() == getChunkCoordinate(x) &&
section.absoluteStart().section() == getChunkCoordinate(y) &&
section.absoluteStart().chunkZ() == getChunkCoordinate(z) :
"Invalid section " + section.absoluteStart() + " for " + x + ", " + y + ", " + z;
section.modifier().setBlock(x, y, z, block);
}
private void resize(int x, int y, int z) {
final int sectionX = getChunkCoordinate(x);
final int sectionY = getChunkCoordinate(y);
final int sectionZ = getChunkCoordinate(z);
if (sections == null) {
this.minSection = new Vec(sectionX * 16, sectionY * 16, sectionZ * 16);
this.width = 1;
this.height = 1;
this.depth = 1;
this.sections = List.of(section(new Section(), sectionX, sectionY, sectionZ, true));
} else if (x < minSection.x() || y < minSection.y() || z < minSection.z() ||
x >= minSection.x() + width * 16 || y >= minSection.y() + height * 16 || z >= minSection.z() + depth * 16) {
// Resize necessary
final Vec newMin = new Vec(Math.min(minSection.x(), sectionX * 16),
Math.min(minSection.y(), sectionY * 16),
Math.min(minSection.z(), sectionZ * 16));
final Vec newMax = new Vec(Math.max(minSection.x() + width * 16, sectionX * 16 + 16),
Math.max(minSection.y() + height * 16, sectionY * 16 + 16),
Math.max(minSection.z() + depth * 16, sectionZ * 16 + 16));
final int newWidth = getChunkCoordinate(newMax.x() - newMin.x());
final int newHeight = getChunkCoordinate(newMax.y() - newMin.y());
final int newDepth = getChunkCoordinate(newMax.z() - newMin.z());
// Resize
GenerationUnit[] newSections = new GenerationUnit[newWidth * newHeight * newDepth];
// Copy old sections
for (GenerationUnit s : sections) {
final Point start = s.absoluteStart();
final int newX = getChunkCoordinate(start.x() - newMin.x());
final int newY = getChunkCoordinate(start.y() - newMin.y());
final int newZ = getChunkCoordinate(start.z() - newMin.z());
final int index = findIndex(newWidth, newHeight, newDepth, newX, newY, newZ);
newSections[index] = s;
}
// Fill new sections
final int startX = newMin.chunkX();
final int startY = newMin.section();
final int startZ = newMin.chunkZ();
for (int i = 0; i < newSections.length; i++) {
if (newSections[i] == null) {
final Point coordinates = to3D(i, newWidth, newHeight, newDepth);
final int newX = coordinates.blockX() + startX;
final int newY = coordinates.blockY() + startY;
final int newZ = coordinates.blockZ() + startZ;
final GenerationUnit unit = section(new Section(), newX, newY, newZ, true);
newSections[i] = unit;
}
}
this.sections = List.of(newSections);
this.minSection = newMin;
this.width = newWidth;
this.height = newHeight;
this.depth = newDepth;
}
}
}
record UnitImpl(UnitModifier modifier, Point size,
Point absoluteStart, Point absoluteEnd,
List<GenerationUnit> divided,
List<UnitImpl> forks) implements GenerationUnit {
@Override
public @NotNull GenerationUnit fork(@NotNull Point start, @NotNull Point end) {
final int minSectionX = floorSection(start.blockX()) / 16;
final int minSectionY = floorSection(start.blockY()) / 16;
final int minSectionZ = floorSection(start.blockZ()) / 16;
final int maxSectionX = ceilSection(end.blockX()) / 16;
final int maxSectionY = ceilSection(end.blockY()) / 16;
final int maxSectionZ = ceilSection(end.blockZ()) / 16;
final int width = maxSectionX - minSectionX;
final int height = maxSectionY - minSectionY;
final int depth = maxSectionZ - minSectionZ;
GenerationUnit[] units = new GenerationUnit[width * height * depth];
int index = 0;
for (int sectionX = minSectionX; sectionX < maxSectionX; sectionX++) {
for (int sectionY = minSectionY; sectionY < maxSectionY; sectionY++) {
for (int sectionZ = minSectionZ; sectionZ < maxSectionZ; sectionZ++) {
final GenerationUnit unit = section(new Section(), sectionX, sectionY, sectionZ, true);
units[index++] = unit;
}
}
}
final List<GenerationUnit> sections = List.of(units);
final Point startSection = new Vec(minSectionX * 16, minSectionY * 16, minSectionZ * 16);
return registerFork(startSection, sections, width, height, depth);
}
@Override
public void fork(@NotNull Consumer<Block.@NotNull Setter> consumer) {
DynamicFork dynamicFork = new DynamicFork();
consumer.accept(dynamicFork);
final int width = dynamicFork.width;
final int height = dynamicFork.height;
final int depth = dynamicFork.depth;
final Point startSection = dynamicFork.minSection;
final List<GenerationUnit> sections = dynamicFork.sections;
registerFork(startSection, sections, width, height, depth);
}
@Override
public @NotNull List<GenerationUnit> subdivide() {
return Objects.requireNonNullElseGet(divided, GenerationUnit.super::subdivide);
}
private GenerationUnit registerFork(Point start, List<GenerationUnit> sections,
int width, int height, int depth) {
final Point end = start.add(width * 16, height * 16, depth * 16);
final Point size = end.sub(start);
final AreaModifierImpl modifier = new AreaModifierImpl(null,
size, start, end, width, height, depth, sections);
final UnitImpl fork = new UnitImpl(modifier, size, start, end, sections, forks);
forks.add(fork);
return fork;
}
}
record SectionModifierImpl(Point size, Point start, Point end,
Palette blockPalette, Palette biomePalette,
Int2ObjectMap<Block> cache, boolean fork) implements GenericModifier {
@Override
public void setBiome(int x, int y, int z, @NotNull Biome biome) {
if (fork) throw new IllegalStateException("Cannot modify biomes of a fork");
this.biomePalette.set(
toSectionRelativeCoordinate(x) / 4,
toSectionRelativeCoordinate(y) / 4,
toSectionRelativeCoordinate(z) / 4, biome.id());
}
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
final int localX = toSectionRelativeCoordinate(x);
final int localY = toSectionRelativeCoordinate(y);
final int localZ = toSectionRelativeCoordinate(z);
handleCache(localX, localY, localZ, block);
this.blockPalette.set(localX, localY, localZ, retrieveBlockId(block));
}
@Override
public void setRelative(int x, int y, int z, @NotNull Block block) {
handleCache(x, y, z, block);
this.blockPalette.set(x, y, z, retrieveBlockId(block));
}
@Override
public void setAllRelative(@NotNull Supplier supplier) {
this.blockPalette.setAll((x, y, z) -> {
final Block block = supplier.get(x, y, z);
handleCache(x, y, z, block);
return retrieveBlockId(block);
});
}
@Override
public void fill(@NotNull Block block) {
if (requireCache(block)) {
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
this.cache.put(getBlockIndex(x, y, z), block);
}
}
}
}
this.blockPalette.fill(retrieveBlockId(block));
}
@Override
public void fillBiome(@NotNull Biome biome) {
if (fork) throw new IllegalStateException("Cannot modify biomes of a fork");
this.biomePalette.fill(biome.id());
}
private int retrieveBlockId(Block block) {
return fork ? block.stateId() + 1 : block.stateId();
}
private void handleCache(int x, int y, int z, Block block) {
if (requireCache(block)) {
this.cache.put(getBlockIndex(x, y, z), block);
} else if (!cache.isEmpty()) {
this.cache.remove(getBlockIndex(x, y, z));
}
}
private boolean requireCache(Block block) {
return block.hasNbt() || block.handler() != null || block.registry().isBlockEntity();
}
}
record AreaModifierImpl(Chunk chunk,
Point size, Point start, Point end,
int width, int height, int depth,
List<GenerationUnit> sections) implements GenericModifier {
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
checkBorder(x, y, z);
final GenerationUnit section = findAbsoluteSection(x, y, z);
y -= start.y();
section.modifier().setBlock(x, y, z, block);
}
@Override
public void setBiome(int x, int y, int z, @NotNull Biome biome) {
checkBorder(x, y, z);
final GenerationUnit section = findAbsoluteSection(x, y, z);
y -= start.y();
section.modifier().setBiome(x, y, z, biome);
}
@Override
public void setRelative(int x, int y, int z, @NotNull Block block) {
if (x < 0 || x >= size.x() || y < 0 || y >= size.y() || z < 0 || z >= size.z()) {
throw new IllegalArgumentException("x, y and z must be in the chunk: " + x + ", " + y + ", " + z);
}
final GenerationUnit section = findRelativeSection(x, y, z);
x = toSectionRelativeCoordinate(x);
y = toSectionRelativeCoordinate(y);
z = toSectionRelativeCoordinate(z);
section.modifier().setBlock(x, y, z, block);
}
@Override
public void setAll(@NotNull Supplier supplier) {
for (GenerationUnit section : sections) {
final var start = section.absoluteStart();
final int startX = start.blockX();
final int startY = start.blockY();
final int startZ = start.blockZ();
section.modifier().setAllRelative((x, y, z) ->
supplier.get(x + startX, y + startY, z + startZ));
}
}
@Override
public void setAllRelative(@NotNull Supplier supplier) {
final Point start = this.start;
for (GenerationUnit section : sections) {
final Point sectionStart = section.absoluteStart();
final int offsetX = sectionStart.blockX() - start.blockX();
final int offsetY = sectionStart.blockY() - start.blockY();
final int offsetZ = sectionStart.blockZ() - start.blockZ();
section.modifier().setAllRelative((x, y, z) ->
supplier.get(x + offsetX, y + offsetY, z + offsetZ));
}
}
@Override
public void fill(@NotNull Block block) {
for (GenerationUnit section : sections) {
section.modifier().fill(block);
}
}
@Override
public void fillBiome(@NotNull Biome biome) {
for (GenerationUnit section : sections) {
section.modifier().fillBiome(biome);
}
}
@Override
public void fillHeight(int minHeight, int maxHeight, @NotNull Block block) {
final Point start = this.start;
final int width = this.width;
final int depth = this.depth;
final int startX = start.blockX();
final int startZ = start.blockZ();
final int minMultiple = floorSection(minHeight);
final int maxMultiple = ceilSection(maxHeight);
final boolean startOffset = minMultiple != minHeight;
final boolean endOffset = maxMultiple != maxHeight;
if (startOffset || endOffset) {
final int firstFill = Math.min(minMultiple + 16, maxHeight);
final int lastFill = floorSection(maxHeight);
for (int x = 0; x < width; x++) {
for (int z = 0; z < depth; z++) {
final int sectionX = startX + x * 16;
final int sectionZ = startZ + z * 16;
// Fill start
if (startOffset) {
final GenerationUnit section = findAbsoluteSection(sectionX, minMultiple, sectionZ);
section.modifier().fillHeight(minHeight, firstFill, block);
}
// Fill end
if (endOffset) {
final GenerationUnit section = findAbsoluteSection(sectionX, maxHeight, sectionZ);
section.modifier().fillHeight(lastFill, maxHeight, block);
}
}
}
}
// Middle sections (to fill)
final int startSection = (minMultiple) / 16 + (startOffset ? 1 : 0);
final int endSection = (maxMultiple) / 16 + (endOffset ? -1 : 0);
for (int i = startSection; i < endSection; i++) {
for (int x = 0; x < width; x++) {
for (int z = 0; z < depth; z++) {
final GenerationUnit section = findAbsoluteSection(startX + x * 16, i * 16, startZ + z * 16);
section.modifier().fill(block);
}
}
}
}
private GenerationUnit findAbsoluteSection(int x, int y, int z) {
return findAbsolute(sections, start, width, height, depth, x, y, z);
}
private GenerationUnit findRelativeSection(int x, int y, int z) {
final int sectionX = getChunkCoordinate(x);
final int sectionY = getChunkCoordinate(y);
final int sectionZ = getChunkCoordinate(z);
final int index = sectionZ + sectionY * depth + sectionX * depth * height;
return sections.get(index);
}
private void checkBorder(int x, int y, int z) {
if (x < start.x() || x >= end.x() ||
y < start.y() || y >= end.y() ||
z < start.z() || z >= end.z()) {
final String format = String.format("Invalid coordinates: %d, %d, %d for area %s %s", x, y, z, start, end);
throw new IllegalArgumentException(format);
}
}
}
sealed interface GenericModifier extends UnitModifier
permits AreaModifierImpl, SectionModifierImpl {
Point size();
Point start();
Point end();
@Override
default void setAll(@NotNull Supplier supplier) {
final Point start = start();
final Point end = end();
final int endX = end.blockX();
final int endY = end.blockY();
final int endZ = end.blockZ();
for (int x = start.blockX(); x < endX; x++) {
for (int y = start.blockY(); y < endY; y++) {
for (int z = start.blockZ(); z < endZ; z++) {
setBlock(x, y, z, supplier.get(x, y, z));
}
}
}
}
@Override
default void setAllRelative(@NotNull Supplier supplier) {
final Point size = size();
final int endX = size.blockX();
final int endY = size.blockY();
final int endZ = size.blockZ();
for (int x = 0; x < endX; x++) {
for (int y = 0; y < endY; y++) {
for (int z = 0; z < endZ; z++) {
setRelative(x, y, z, supplier.get(x, y, z));
}
}
}
}
@Override
default void fill(@NotNull Block block) {
fill(start(), end(), block);
}
@Override
default void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block) {
final int endX = end.blockX();
final int endY = end.blockY();
final int endZ = end.blockZ();
for (int x = start.blockX(); x < endX; x++) {
for (int y = start.blockY(); y < endY; y++) {
for (int z = start.blockZ(); z < endZ; z++) {
setBlock(x, y, z, block);
}
}
}
}
@Override
default void fillHeight(int minHeight, int maxHeight, @NotNull Block block) {
final Point start = start();
final Point end = end();
final int startY = start.blockY();
final int endY = end.blockY();
if (startY >= minHeight && endY <= maxHeight) {
// Fast path if the unit is fully contained in the height range
fill(start, end, block);
} else {
// Slow path if the unit is not fully contained in the height range
fill(start.withY(Math.max(minHeight, startY)), end.withY(Math.min(maxHeight, endY)), block);
}
}
}
private static GenerationUnit findAbsolute(List<GenerationUnit> units, Point start,
int width, int height, int depth,
int x, int y, int z) {
final int sectionX = getChunkCoordinate(x - start.x());
final int sectionY = getChunkCoordinate(y - start.y());
final int sectionZ = getChunkCoordinate(z - start.z());
final int index = findIndex(width, height, depth, sectionX, sectionY, sectionZ);
return units.get(index);
}
private static int findIndex(int width, int height, int depth,
int x, int y, int z) {
return (z * width * height) + (y * width) + x;
}
private static Point to3D(int idx, int width, int height, int depth) {
final int z = idx / (width * height);
idx -= (z * width * height);
final int y = idx / width;
final int x = idx % width;
return new Vec(x, y, z);
}
}

View File

@ -16,6 +16,7 @@ import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.instance.InstanceTickEvent;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.instance.generator.Generator;
import net.minestom.server.network.packet.server.play.BlockActionPacket;
import net.minestom.server.network.packet.server.play.TimeUpdatePacket;
import net.minestom.server.snapshot.ChunkSnapshot;
@ -251,19 +252,30 @@ public abstract class Instance implements Block.Getter, Block.Setter, Tickable,
*/
public abstract @NotNull CompletableFuture<Void> saveChunksToStorage();
/**
* Gets the instance {@link ChunkGenerator}.
*
* @return the {@link ChunkGenerator} of the instance
*/
public abstract @Nullable ChunkGenerator getChunkGenerator();
/**
* Changes the instance {@link ChunkGenerator}.
*
* @param chunkGenerator the new {@link ChunkGenerator} of the instance
* @deprecated Use {@link #setGenerator(Generator)}
*/
public abstract void setChunkGenerator(@Nullable ChunkGenerator chunkGenerator);
@Deprecated
public void setChunkGenerator(@Nullable ChunkGenerator chunkGenerator) {
setGenerator(chunkGenerator != null ? new ChunkGeneratorCompatibilityLayer(chunkGenerator) : null);
}
/**
* Gets the generator associated with the instance
*
* @return the generator if any
*/
public abstract @Nullable Generator generator();
/**
* Changes the generator of the instance
*
* @param generator the new generator, or null to disable generation
*/
public abstract void setGenerator(@Nullable Generator generator);
/**
* Gets all the instance's loaded chunks.

View File

@ -1,5 +1,6 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
@ -9,10 +10,11 @@ import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.instance.InstanceChunkLoadEvent;
import net.minestom.server.event.instance.InstanceChunkUnloadEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.instance.batch.ChunkGenerationBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.instance.block.rule.BlockPlacementRule;
import net.minestom.server.instance.generator.Generator;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.network.packet.server.play.BlockChangePacket;
import net.minestom.server.network.packet.server.play.BlockEntityDataPacket;
import net.minestom.server.network.packet.server.play.EffectPacket;
@ -22,6 +24,7 @@ import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.block.BlockUtils;
import net.minestom.server.utils.chunk.ChunkCache;
import net.minestom.server.utils.chunk.ChunkSupplier;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.DimensionType;
import org.jetbrains.annotations.ApiStatus;
@ -32,7 +35,9 @@ import space.vectrix.flare.fastutil.Long2ObjectSyncMap;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
@ -48,7 +53,7 @@ public class InstanceContainer extends Instance {
private final List<SharedInstance> sharedInstances = new CopyOnWriteArrayList<>();
// the chunk generator used, can be null
private ChunkGenerator chunkGenerator;
private volatile Generator generator;
// (chunk index -> chunk) map, contains all the chunks in the instance
// used as a monitor when access is required
private final Long2ObjectSyncMap<Chunk> chunks = Long2ObjectSyncMap.hashmap();
@ -271,6 +276,10 @@ public class InstanceContainer extends Instance {
final CompletableFuture<Chunk> future = this.loadingChunks.remove(index);
assert future == completableFuture;
future.complete(chunk);
})
.exceptionally(throwable -> {
MinecraftServer.getExceptionManager().handleException(throwable);
return null;
});
if (loader.supportsParallelLoading()) {
CompletableFuture.runAsync(retriever);
@ -280,20 +289,106 @@ public class InstanceContainer extends Instance {
return completableFuture;
}
Map<Long, List<GeneratorImpl.SectionModifierImpl>> generationForks = new ConcurrentHashMap<>();
protected @NotNull CompletableFuture<@NotNull Chunk> createChunk(int chunkX, int chunkZ) {
final ChunkGenerator generator = this.chunkGenerator;
final Chunk chunk = chunkSupplier.createChunk(this, chunkX, chunkZ);
Check.notNull(chunk, "Chunks supplied by a ChunkSupplier cannot be null.");
Generator generator = generator();
if (generator != null && chunk.shouldGenerate()) {
// Execute the chunk generator to populate the chunk
final ChunkGenerationBatch chunkBatch = new ChunkGenerationBatch(this, chunk);
return chunkBatch.generate(generator);
CompletableFuture<Chunk> resultFuture = new CompletableFuture<>();
// TODO: virtual thread once Loom is available
ForkJoinPool.commonPool().submit(() -> {
var chunkUnit = GeneratorImpl.chunk(chunk);
try {
// Generate block/biome palette
generator.generate(chunkUnit);
// Apply nbt/handler
if (chunkUnit.modifier() instanceof GeneratorImpl.AreaModifierImpl chunkModifier) {
for (var section : chunkModifier.sections()) {
if (section.modifier() instanceof GeneratorImpl.SectionModifierImpl sectionModifier) {
applyGenerationData(chunk, sectionModifier);
}
}
}
// Register forks or apply locally
for (var fork : chunkUnit.forks()) {
var sections = ((GeneratorImpl.AreaModifierImpl) fork.modifier()).sections();
for (var section : sections) {
if (section.modifier() instanceof GeneratorImpl.SectionModifierImpl sectionModifier) {
if (sectionModifier.blockPalette().count() == 0)
continue;
final Point start = section.absoluteStart();
final Chunk forkChunk = start.chunkX() == chunkX && start.chunkZ() == chunkZ ? chunk : getChunkAt(start);
if (forkChunk != null) {
applyFork(forkChunk, sectionModifier);
} else {
final long index = ChunkUtils.getChunkIndex(start);
this.generationForks.compute(index, (i, sectionModifiers) -> {
if (sectionModifiers == null) sectionModifiers = new ArrayList<>();
sectionModifiers.add(sectionModifier);
return sectionModifiers;
});
}
}
}
}
// Apply awaiting forks
processFork(chunk);
} catch (Throwable e) {
MinecraftServer.getExceptionManager().handleException(e);
} finally {
// End generation
chunk.sendChunk();
refreshLastBlockChangeTime();
resultFuture.complete(chunk);
}
});
return resultFuture;
} else {
// No chunk generator, execute the callback with the empty chunk
processFork(chunk);
return CompletableFuture.completedFuture(chunk);
}
}
private void processFork(Chunk chunk) {
this.generationForks.compute(ChunkUtils.getChunkIndex(chunk), (aLong, sectionModifiers) -> {
if (sectionModifiers != null) {
for (var sectionModifier : sectionModifiers) {
applyFork(chunk, sectionModifier);
}
}
return null;
});
}
private void applyFork(Chunk chunk, GeneratorImpl.SectionModifierImpl sectionModifier) {
synchronized (chunk) {
Section section = chunk.getSectionAt(sectionModifier.start().blockY());
Palette currentBlocks = section.blockPalette();
// -1 is necessary because forked units handle explicit changes by changing AIR 0 to 1
sectionModifier.blockPalette().getAllPresent((x, y, z, value) -> currentBlocks.set(x, y, z, value - 1));
applyGenerationData(chunk, sectionModifier);
}
}
private void applyGenerationData(Chunk chunk, GeneratorImpl.SectionModifierImpl section) {
var cache = section.cache();
if (cache.isEmpty()) return;
final int height = section.start().blockY();
synchronized (chunk) {
Int2ObjectMaps.fastForEach(cache, blockEntry -> {
final int index = blockEntry.getIntKey();
final Block block = blockEntry.getValue();
final int x = ChunkUtils.blockIndexToChunkPositionX(index);
final int y = ChunkUtils.blockIndexToChunkPositionY(index) + height;
final int z = ChunkUtils.blockIndexToChunkPositionZ(index);
chunk.setBlock(x, y, z, block);
});
}
}
@Override
public void enableAutoChunkLoad(boolean enable) {
this.autoChunkLoad = enable;
@ -419,13 +514,13 @@ public class InstanceContainer extends Instance {
}
@Override
public ChunkGenerator getChunkGenerator() {
return chunkGenerator;
public @Nullable Generator generator() {
return generator;
}
@Override
public void setChunkGenerator(ChunkGenerator chunkGenerator) {
this.chunkGenerator = chunkGenerator;
public void setGenerator(@Nullable Generator generator) {
this.generator = generator;
}
/**

View File

@ -4,7 +4,9 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.instance.generator.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.UUID;
@ -73,13 +75,13 @@ public class SharedInstance extends Instance {
}
@Override
public void setChunkGenerator(ChunkGenerator chunkGenerator) {
this.instanceContainer.setChunkGenerator(chunkGenerator);
public @Nullable Generator generator() {
return instanceContainer.generator();
}
@Override
public ChunkGenerator getChunkGenerator() {
return instanceContainer.getChunkGenerator();
public void setGenerator(@Nullable Generator generator) {
instanceContainer.setGenerator(generator);
}
@NotNull

View File

@ -1,60 +0,0 @@
package net.minestom.server.instance.batch;
import net.minestom.server.instance.*;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.chunk.ChunkCallback;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class ChunkGenerationBatch extends ChunkBatch {
private final InstanceContainer instance;
private final Chunk chunk;
public ChunkGenerationBatch(InstanceContainer instance, Chunk chunk) {
super(new BatchOption());
this.instance = instance;
this.chunk = chunk;
}
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
chunk.setBlock(x, y, z, block);
}
public @NotNull CompletableFuture<@NotNull Chunk> generate(@NotNull ChunkGenerator chunkGenerator) {
final CompletableFuture<Chunk> completableFuture = new CompletableFuture<>();
BLOCK_BATCH_POOL.execute(() -> {
synchronized (chunk) {
final List<ChunkPopulator> populators = chunkGenerator.getPopulators();
final boolean hasPopulator = populators != null && !populators.isEmpty();
chunkGenerator.generateChunkData(this, chunk.getChunkX(), chunk.getChunkZ());
if (hasPopulator) {
for (ChunkPopulator chunkPopulator : populators) {
chunkPopulator.populateChunk(this, chunk);
}
}
// Update the chunk.
this.chunk.sendChunk();
this.instance.refreshLastBlockChangeTime();
completableFuture.complete(chunk);
}
});
return completableFuture;
}
@Override
public void clear() {
throw new IllegalStateException("#clear is not supported for chunk generation batch.");
}
@Override
protected ChunkBatch apply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback, boolean safeCallback) {
throw new IllegalStateException("#apply is not supported for chunk generation batch.");
}
}

View File

@ -0,0 +1,63 @@
package net.minestom.server.instance.generator;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.function.Consumer;
public interface GenerationUnit {
/**
* This unit's modifier, used to place blocks and biomes within this unit.
*
* @return the modifier
*/
@NotNull UnitModifier modifier();
/**
* The size of this unit in blocks.
*
* @return the size of this unit
*/
@NotNull Point size();
/**
* The absolute start (min x, y, z) of this unit.
*
* @return the absolute start
*/
@NotNull Point absoluteStart();
/**
* The absolute end (max x, y, z) of this unit.
*
* @return the absolute end
*/
@NotNull Point absoluteEnd();
/**
* Creates a fork of this unit, which will be applied to the instance whenever possible.
*
* @param start the start of the fork
* @param end the end of the fork
* @return the fork
*/
@NotNull GenerationUnit fork(@NotNull Point start, @NotNull Point end);
/**
* Creates a fork of this unit depending on the blocks placed within the consumer.
*
* @param consumer the consumer
*/
void fork(@NotNull Consumer<Block.@NotNull Setter> consumer);
/**
* Divides this unit into the smallest independent units.
*
* @return an immutable list of independent units
*/
default @NotNull List<GenerationUnit> subdivide() {
return List.of(this);
}
}

View File

@ -0,0 +1,25 @@
package net.minestom.server.instance.generator;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
@FunctionalInterface
public interface Generator {
/**
* This method is called when this generator is requesting this unit to be filled with blocks or biomes.
*
* @param unit the unit to fill
*/
void generate(@NotNull GenerationUnit unit);
/**
* Runs {@link #generate(GenerationUnit)} on each unit in the collection.
*
* @param units the list of units to fill
*/
default void generateAll(@NotNull Collection<@NotNull GenerationUnit> units) {
units.forEach(this::generate);
}
}

View File

@ -0,0 +1,68 @@
package net.minestom.server.instance.generator;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
public interface UnitModifier extends Block.Setter, Biome.Setter {
/**
* Sets the block relative to the absolute position of the unit.
*
* @param x the x coordinate
* @param y the y coordinate
* @param z the z coordinate
* @param block the block to set
*/
void setRelative(int x, int y, int z, @NotNull Block block);
/**
* Sets all blocks within the unit to the block given by the supplier.
*
* @param supplier the supplier of the block to set
*/
void setAll(@NotNull Supplier supplier);
/**
* Sets all blocks within the unit to the block given by the supplier, relative to the absolute position of the unit.
*
* @param supplier the supplier of the block to set
*/
void setAllRelative(@NotNull Supplier supplier);
/**
* Fills the unit with the given block.
*
* @param block the block to fill
*/
void fill(@NotNull Block block);
/**
* Fills the 3d rectangular area with the given block.
*
* @param start the start (min) point of the area
* @param end the end (max) point of the area
* @param block the block to fill
*/
void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block);
/**
* Fills the 3d rectangular area with the given biome.
*
* @param minHeight the minimum height of the area
* @param maxHeight the maximum height of the area
* @param block the block to fill
*/
void fillHeight(int minHeight, int maxHeight, @NotNull Block block);
/**
* Fills the 3d rectangular area with the given biome.
*
* @param biome the biome to fill
*/
void fillBiome(@NotNull Biome biome);
interface Supplier {
@NotNull Block get(int x, int y, int z);
}
}

View File

@ -262,4 +262,12 @@ public final class ChunkUtils {
public static int toSectionRelativeCoordinate(int xyz) {
return xyz & 0xF;
}
public static int floorSection(int coordinate) {
return coordinate - (coordinate & 0xF);
}
public static int ceilSection(int coordinate) {
return ((coordinate - 1) | 15) + 1;
}
}

View File

@ -5,16 +5,11 @@ import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventFilter;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.ChunkGenerator;
import net.minestom.server.instance.ChunkPopulator;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.function.BooleanSupplier;
public interface Env {
@ -49,22 +44,7 @@ public interface Env {
default @NotNull Instance createFlatInstance() {
var instance = process().instance().createInstanceContainer();
instance.setChunkGenerator(new ChunkGenerator() {
@Override
public void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) {
for (byte x = 0; x < Chunk.CHUNK_SIZE_X; x++)
for (byte z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
for (byte y = 0; y < 40; y++) {
batch.setBlock(x, y, z, Block.STONE);
}
}
}
@Override
public List<ChunkPopulator> getPopulators() {
return null;
}
});
instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
return instance;
}
}

View File

@ -30,6 +30,7 @@ public class CoordinateTest {
@Test
public void chunkCoordinate() {
assertEquals(0, getChunkCoordinate(15));
assertEquals(1, getChunkCoordinate(16));
assertEquals(-1, getChunkCoordinate(-16));
assertEquals(3, getChunkCoordinate(48));

View File

@ -0,0 +1,162 @@
package net.minestom.server.instance;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@EnvTest
public class GeneratorForkConsumerIntegrationTest {
@Test
public void local(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.fork(setter -> {
var dynamic = (GeneratorImpl.DynamicFork) setter;
assertNull(dynamic.minSection);
assertEquals(0, dynamic.width);
assertEquals(0, dynamic.height);
assertEquals(0, dynamic.depth);
setter.setBlock(unit.absoluteStart(), Block.STONE);
assertEquals(unit.absoluteStart(), dynamic.minSection);
assertEquals(1, dynamic.width);
assertEquals(1, dynamic.height);
assertEquals(1, dynamic.depth);
});
});
instance.loadChunk(0, 0).join();
assertEquals(Block.STONE, instance.getBlock(0, -64, 0));
}
@Test
public void doubleLocal(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.fork(setter -> {
setter.setBlock(unit.absoluteStart(), Block.STONE);
setter.setBlock(unit.absoluteStart().add(1, 0, 0), Block.STONE);
});
});
instance.loadChunk(0, 0).join();
assertEquals(Block.STONE, instance.getBlock(0, -64, 0));
assertEquals(Block.STONE, instance.getBlock(1, -64, 0));
}
@Test
public void neighborZ(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.fork(setter -> {
var dynamic = (GeneratorImpl.DynamicFork) setter;
assertNull(dynamic.minSection);
assertEquals(0, dynamic.width);
assertEquals(0, dynamic.height);
assertEquals(0, dynamic.depth);
setter.setBlock(unit.absoluteStart(), Block.STONE);
setter.setBlock(unit.absoluteStart().add(0, 0, 16), Block.GRASS);
assertEquals(unit.absoluteStart(), dynamic.minSection);
assertEquals(1, dynamic.width);
assertEquals(1, dynamic.height);
assertEquals(2, dynamic.depth);
});
});
instance.loadChunk(0, 0).join();
instance.setGenerator(null);
instance.loadChunk(0, 1).join();
assertEquals(Block.STONE, instance.getBlock(0, -64, 0));
assertEquals(Block.GRASS, instance.getBlock(0, -64, 16));
}
@Test
public void neighborX(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.fork(setter -> {
var dynamic = (GeneratorImpl.DynamicFork) setter;
assertNull(dynamic.minSection);
assertEquals(0, dynamic.width);
assertEquals(0, dynamic.height);
assertEquals(0, dynamic.depth);
setter.setBlock(unit.absoluteStart(), Block.STONE);
setter.setBlock(unit.absoluteStart().add(16, 0, 0), Block.GRASS);
assertEquals(unit.absoluteStart(), dynamic.minSection);
assertEquals(2, dynamic.width);
assertEquals(1, dynamic.height);
assertEquals(1, dynamic.depth);
});
});
instance.loadChunk(0, 0).join();
instance.setGenerator(null);
instance.loadChunk(1, 0).join();
assertEquals(Block.STONE, instance.getBlock(0, -64, 0));
assertEquals(Block.GRASS, instance.getBlock(16, -64, 0));
}
@Test
public void neighborY(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.fork(setter -> {
var dynamic = (GeneratorImpl.DynamicFork) setter;
assertNull(dynamic.minSection);
assertEquals(0, dynamic.width);
assertEquals(0, dynamic.height);
assertEquals(0, dynamic.depth);
setter.setBlock(unit.absoluteStart(), Block.STONE);
setter.setBlock(unit.absoluteStart().add(0, 16, 0), Block.GRASS);
assertEquals(unit.absoluteStart(), dynamic.minSection);
assertEquals(1, dynamic.width);
assertEquals(2, dynamic.height);
assertEquals(1, dynamic.depth);
});
});
instance.loadChunk(0, 0).join();
assertEquals(Block.STONE, instance.getBlock(0, -64, 0));
assertEquals(Block.GRASS, instance.getBlock(0, -48, 0));
}
@Test
public void verticalAndHorizontalSectionBorders(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
Set<Point> points = ConcurrentHashMap.newKeySet();
instance.setGenerator(unit -> {
final Point start = unit.absoluteStart().withY(96);
unit.fork(setter -> {
var dynamic = (GeneratorImpl.DynamicFork) setter;
for (int i = 0; i < 16; i++) {
setter.setBlock(start.add(i, 0, 0), Block.STONE);
setter.setBlock(start.add(-i, 0, 0), Block.STONE);
setter.setBlock(start.add(0, i, 0), Block.STONE);
setter.setBlock(start.add(0, -i, 0), Block.STONE);
points.add(start.add(i, 0, 0));
points.add(start.add(-i, 0, 0));
points.add(start.add(0, i, 0));
points.add(start.add(0, -i, 0));
}
assertEquals(2, dynamic.width);
assertEquals(2, dynamic.height);
assertEquals(1, dynamic.depth);
});
});
instance.loadChunk(0, 0).join();
for (Point point : points) {
if (!instance.isChunkLoaded(point)) continue;
assertEquals(Block.STONE, instance.getBlock(point), point.toString());
}
}
}

View File

@ -0,0 +1,108 @@
package net.minestom.server.instance;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import net.minestom.server.world.biomes.Biome;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class GeneratorForkIntegrationTest {
@Test
public void local(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
var block = Block.STONE;
instance.setGenerator(unit -> {
var u = unit.fork(unit.absoluteStart(), unit.absoluteEnd());
assertEquals(unit.absoluteStart(), u.absoluteStart());
assertEquals(unit.absoluteEnd(), u.absoluteEnd());
u.modifier().setRelative(0, 0, 0, Block.STONE);
});
instance.loadChunk(0, 0).join();
assertEquals(block, instance.getBlock(0, -64, 0));
}
@Test
public void size(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
// Set the Generator
instance.setGenerator(unit -> {
Point start = unit.absoluteStart();
GenerationUnit fork = unit.fork(start, start.add(18, 18, 18));
assertDoesNotThrow(() -> fork.modifier().setBlock(start.add(17, 17, 17), Block.STONE));
});
// Load the chunks
instance.loadChunk(0, 0).join();
instance.setGenerator(null);
instance.loadChunk(1, 1).join();
assertEquals(Block.STONE, instance.getBlock(17, -64 + 17, 17));
}
@Test
public void signal(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
var block = Block.STONE;
instance.setGenerator(unit -> {
var u = unit.fork(unit.absoluteStart(), unit.absoluteEnd().add(16, 0, 16));
assertEquals(unit.absoluteStart(), u.absoluteStart());
assertEquals(unit.absoluteEnd().add(16, 0, 16), u.absoluteEnd());
u.modifier().setRelative(16, 0, 0, Block.STONE);
u.modifier().setRelative(16, 33, 0, Block.STONE);
});
instance.loadChunk(0, 0).join();
instance.setGenerator(null);
instance.loadChunk(1, 0).join();
assertEquals(block, instance.getBlock(16, -64, 0));
assertEquals(block, instance.getBlock(16, -31, 0));
}
@Test
public void air(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
var u = unit.fork(unit.absoluteStart(), unit.absoluteEnd().add(16, 0, 16));
u.modifier().setRelative(16, 39 + 64, 0, Block.AIR);
});
instance.loadChunk(0, 0).join();
instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
instance.loadChunk(1, 0).join();
assertEquals(Block.AIR, instance.getBlock(16, 39, 0));
}
@Test
public void fillHeight(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
var u = unit.fork(unit.absoluteStart(), unit.absoluteEnd().add(16, 0, 16));
u.modifier().fillHeight(0, 40, Block.STONE);
});
instance.loadChunk(0, 0).join();
instance.setGenerator(null);
instance.loadChunk(1, 0).join();
for (int y = 0; y < 40; y++) {
assertEquals(Block.STONE, instance.getBlock(16, y, 0), "y=" + y);
}
}
@Test
public void biome(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
var u = unit.fork(unit.absoluteStart(), unit.absoluteEnd().add(16, 0, 16));
assertThrows(IllegalStateException.class, () -> u.modifier().setBiome(16, 0, 0, Biome.PLAINS));
assertThrows(IllegalStateException.class, () -> u.modifier().fillBiome(Biome.PLAINS));
});
instance.loadChunk(0, 0).join();
}
}

View File

@ -0,0 +1,67 @@
package net.minestom.server.instance;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.instance.block.Block;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
@EnvTest
public class GeneratorIntegrationTest {
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void loader(boolean data, Env env) {
var manager = env.process().instance();
var block = data ? Block.STONE.withNbt(NBT.Compound(Map.of("key", NBT.String("value")))) : Block.STONE;
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> unit.modifier().fill(block));
instance.loadChunk(0, 0).join();
assertEquals(block, instance.getBlock(0, 0, 0));
assertEquals(block, instance.getBlock(15, 0, 0));
assertEquals(block, instance.getBlock(0, 15, 0));
assertEquals(block, instance.getBlock(0, 0, 15));
}
@Test
public void exceptionCatch(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
var ref = new AtomicReference<Throwable>();
env.process().exception().setExceptionHandler(ref::set);
var exception = new RuntimeException();
instance.setGenerator(unit -> {
unit.modifier().fill(Block.STONE);
throw exception;
});
instance.loadChunk(0, 0).join();
assertSame(exception, ref.get());
}
@Test
public void fillHeightNegative(Env env) {
var manager = env.process().instance();
var instance = manager.createInstanceContainer();
instance.setGenerator(unit -> {
unit.modifier().fillHeight(-64, -60, Block.STONE);
});
instance.loadChunk(0, 0).join();
for (int y = -64; y < -60; y++) {
assertEquals(Block.STONE, instance.getBlock(0, y, 0));
}
for (int y = -60; y < 100; y++) {
assertEquals(Block.AIR, instance.getBlock(0, y, 0));
}
}
}

View File

@ -0,0 +1,361 @@
package net.minestom.server.instance;
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.GenerationUnit;
import net.minestom.server.instance.generator.Generator;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.world.biomes.Biome;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static net.minestom.server.instance.GeneratorImpl.unit;
import static net.minestom.server.utils.chunk.ChunkUtils.ceilSection;
import static net.minestom.server.utils.chunk.ChunkUtils.floorSection;
import static org.junit.jupiter.api.Assertions.*;
public class GeneratorTest {
@Test
public void unitSize() {
assertDoesNotThrow(() -> dummyUnit(Vec.ZERO, new Vec(16)));
assertDoesNotThrow(() -> dummyUnit(new Vec(16), new Vec(32)));
assertThrows(IllegalArgumentException.class, () -> dummyUnit(new Vec(15), Vec.ZERO));
assertThrows(IllegalArgumentException.class, () -> dummyUnit(new Vec(15), new Vec(32)));
assertThrows(IllegalArgumentException.class, () -> dummyUnit(new Vec(15), new Vec(31)));
assertThrows(IllegalArgumentException.class, () -> dummyUnit(Vec.ZERO, new Vec(15)));
}
@ParameterizedTest
@MethodSource("sectionFloorParam")
public void sectionFloor(int expected, int input) {
assertEquals(expected, floorSection(input), "floorSection(" + input + ")");
}
private static Stream<Arguments> sectionFloorParam() {
return Stream.of(Arguments.of(-32, -32),
Arguments.of(-32, -31),
Arguments.of(-32, -17),
Arguments.of(-16, -16),
Arguments.of(-16, -15),
Arguments.of(0, 0),
Arguments.of(0, 1),
Arguments.of(0, 2),
Arguments.of(16, 16),
Arguments.of(16, 17));
}
@ParameterizedTest
@MethodSource("sectionCeilParam")
public void sectionCeil(int expected, int input) {
assertEquals(expected, ceilSection(input), "ceilSection(" + input + ")");
}
private static Stream<Arguments> sectionCeilParam() {
return Stream.of(Arguments.of(-32, -32),
Arguments.of(-16, -31),
Arguments.of(-16, -17),
Arguments.of(-16, -16),
Arguments.of(-0, -15),
Arguments.of(0, 0),
Arguments.of(16, 1),
Arguments.of(16, 2),
Arguments.of(16, 16),
Arguments.of(32, 17));
}
@Test
public void chunkSize() {
final int minSection = 0;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
GenerationUnit chunk = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
assertEquals(new Vec(16, sectionCount * 16, 16), chunk.size());
assertEquals(new Vec(chunkX * 16, minSection * 16, chunkZ * 16), chunk.absoluteStart());
assertEquals(new Vec(chunkX * 16 + 16, maxSection * 16, chunkZ * 16 + 16), chunk.absoluteEnd());
}
@Test
public void chunkSizeNeg() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
GenerationUnit chunk = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
assertEquals(new Vec(16, sectionCount * 16, 16), chunk.size());
assertEquals(new Vec(chunkX * 16, minSection * 16, chunkZ * 16), chunk.absoluteStart());
assertEquals(new Vec(chunkX * 16 + 16, maxSection * 16, chunkZ * 16 + 16), chunk.absoluteEnd());
}
@Test
public void sectionSize() {
final int sectionX = 3;
final int sectionY = -5;
final int sectionZ = -2;
GenerationUnit section = GeneratorImpl.section(new Section(), sectionX, sectionY, sectionZ);
assertEquals(new Vec(16), section.size());
assertEquals(new Vec(sectionX * 16, sectionY * 16, sectionZ * 16), section.absoluteStart());
assertEquals(new Vec(sectionX * 16 + 16, sectionY * 16 + 16, sectionZ * 16 + 16), section.absoluteEnd());
}
@Test
public void chunkSubdivide() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
GenerationUnit chunk = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
var subUnits = chunk.subdivide();
assertEquals(sectionCount, subUnits.size());
for (int i = 0; i < sectionCount; i++) {
var subUnit = subUnits.get(i);
assertEquals(new Vec(16, 16, 16), subUnit.size());
assertEquals(new Vec(chunkX * 16, (i + minSection) * 16, chunkZ * 16), subUnit.absoluteStart());
assertEquals(subUnit.absoluteStart().add(16), subUnit.absoluteEnd());
}
}
@Test
public void chunkAbsolute() {
final int minSection = 0;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
assertThrows(Exception.class, () -> modifier.setBlock(0, 0, 0, Block.STONE), "Block outside of chunk");
modifier.setBlock(56, 0, -25, Block.STONE);
modifier.setBlock(56, 17, -25, Block.STONE);
};
generator.generate(chunkUnits);
assertEquals(Block.STONE.stateId(), sections[0].blockPalette().get(8, 0, 7));
assertEquals(Block.STONE.stateId(), sections[1].blockPalette().get(8, 1, 7));
}
@Test
public void chunkAbsoluteAll() {
final int minSection = 0;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
Set<Point> points = new HashSet<>();
modifier.setAll((x, y, z) -> {
assertTrue(points.add(new Vec(x, y, z)), "Duplicate point: " + x + ", " + y + ", " + z);
assertEquals(chunkX, ChunkUtils.getChunkCoordinate(x));
assertEquals(chunkZ, ChunkUtils.getChunkCoordinate(z));
return Block.STONE;
});
assertEquals(16 * 16 * 16 * sectionCount, points.size());
};
generator.generate(chunkUnits);
for (var section : sections) {
section.blockPalette().getAll((x, y, z, value) ->
assertEquals(Block.STONE.stateId(), value));
}
}
@Test
public void chunkRelative() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
assertThrows(Exception.class, () -> modifier.setRelative(-1, 0, 0, Block.STONE));
assertThrows(Exception.class, () -> modifier.setRelative(16, 0, 0, Block.STONE));
assertThrows(Exception.class, () -> modifier.setRelative(17, 0, 0, Block.STONE));
assertThrows(Exception.class, () -> modifier.setRelative(0, -1, 0, Block.STONE));
assertThrows(Exception.class, () -> modifier.setRelative(0, 96, 0, Block.STONE));
modifier.setRelative(0, 0, 0, Block.STONE);
modifier.setRelative(0, 16, 2, Block.STONE);
modifier.setRelative(5, 33, 5, Block.STONE);
};
generator.generate(chunkUnits);
assertEquals(Block.STONE.stateId(), sections[0].blockPalette().get(0, 0, 0));
assertEquals(Block.STONE.stateId(), sections[1].blockPalette().get(0, 0, 2));
assertEquals(Block.STONE.stateId(), sections[2].blockPalette().get(5, 1, 5));
}
@Test
public void chunkRelativeAll() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
Set<Point> points = new HashSet<>();
modifier.setAllRelative((x, y, z) -> {
assertTrue(MathUtils.isBetween(x, 0, 16), "x out of bounds: " + x);
assertTrue(MathUtils.isBetween(y, 0, sectionCount * 16), "y out of bounds: " + y);
assertTrue(MathUtils.isBetween(z, 0, 16), "z out of bounds: " + z);
assertTrue(points.add(new Vec(x, y, z)), "Duplicate point: " + x + ", " + y + ", " + z);
return Block.STONE;
});
assertEquals(16 * 16 * 16 * sectionCount, points.size());
};
generator.generate(chunkUnits);
for (var section : sections) {
section.blockPalette().getAll((x, y, z, value) ->
assertEquals(Block.STONE.stateId(), value));
}
}
@Test
public void chunkBiomeSet() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
modifier.setBiome(48, 0, -32, Biome.PLAINS);
modifier.setBiome(48 + 8, 0, -32, Biome.PLAINS);
};
generator.generate(chunkUnits);
assertEquals(Biome.PLAINS.id(), sections[0].biomePalette().get(0, 0, 0));
assertEquals(0, sections[0].biomePalette().get(1, 0, 0));
assertEquals(Biome.PLAINS.id(), sections[0].biomePalette().get(2, 0, 0));
}
@Test
public void chunkBiomeFill() {
final int minSection = -1;
final int maxSection = 5;
final int chunkX = 3;
final int chunkZ = -2;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), chunkX, chunkZ);
Generator generator = chunk -> {
var modifier = chunk.modifier();
modifier.fillBiome(Biome.PLAINS);
};
generator.generate(chunkUnits);
for (var section : sections) {
section.biomePalette().getAll((x, y, z, value) ->
assertEquals(Biome.PLAINS.id(), value));
}
}
@Test
public void chunkFillHeightExact() {
final int minSection = -1;
final int maxSection = 5;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), 3, -2);
Generator generator = chunk -> chunk.modifier().fillHeight(0, 32, Block.STONE);
generator.generate(chunkUnits);
AtomicInteger index = new AtomicInteger(minSection);
for (var section : sections) {
section.blockPalette().getAll((x, y, z, value) -> {
if (index.get() == 0 || index.get() == 1) {
assertEquals(Block.STONE.stateId(), value, "filling failed for section " + index.get());
} else {
assertEquals(0, value);
}
});
index.incrementAndGet();
}
}
@Test
public void chunkFillHeightOneOff() {
final int minSection = -1;
final int maxSection = 5;
final int sectionCount = maxSection - minSection;
Section[] sections = new Section[sectionCount];
Arrays.setAll(sections, i -> new Section());
var chunkUnits = GeneratorImpl.chunk(minSection, maxSection, List.of(sections), 3, -2);
Generator generator = chunk -> chunk.modifier().fillHeight(1, 33, Block.STONE);
generator.generate(chunkUnits);
AtomicInteger index = new AtomicInteger(minSection);
for (var section : sections) {
section.blockPalette().getAll((x, y, z, value) -> {
Block expected;
if (index.get() == 0) {
if (y > 0) {
expected = Block.STONE;
} else {
expected = Block.AIR;
}
} else if (index.get() == 1) {
expected = Block.STONE;
} else if (index.get() == 2) {
if (y == 0) {
expected = Block.STONE;
} else {
expected = Block.AIR;
}
} else {
expected = Block.AIR;
}
assertEquals(expected.stateId(), value, "fail for coordinate: " + x + "," + y + "," + z + " for index " + index.get());
});
index.incrementAndGet();
}
}
@Test
public void sectionFill() {
Section section = new Section();
var chunkUnit = GeneratorImpl.section(section, -1, -1, 0);
Generator generator = chunk -> chunk.modifier().fill(Block.STONE);
generator.generate(chunkUnit);
section.blockPalette().getAll((x, y, z, value) ->
assertEquals(Block.STONE.stateId(), value));
}
static GenerationUnit dummyUnit(Point start, Point end) {
return unit(null, start, end, null);
}
}