diff --git a/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/Block.java b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/Block.java index f38c5c5a..6d3881bb 100644 --- a/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/Block.java +++ b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/Block.java @@ -1,7 +1,10 @@ package net.buildtheearth.terraplusplus.dataset.vector.draw; +import java.io.IOException; + import com.google.gson.annotations.JsonAdapter; import com.google.gson.stream.JsonReader; + import lombok.NonNull; import lombok.RequiredArgsConstructor; import net.buildtheearth.terraplusplus.TerraConstants; @@ -9,8 +12,6 @@ import net.buildtheearth.terraplusplus.generator.CachedChunkData; import net.minecraft.block.state.IBlockState; -import java.io.IOException; - /** * {@link DrawFunction} which sets the surface block to a fixed block state. * @@ -24,7 +25,7 @@ public final class Block implements DrawFunction { @Override public void drawOnto(@NonNull CachedChunkData.Builder data, int x, int z, int weight) { - data.surfaceBlocks()[x * 16 + z] = this.state; + data.surfaceBlocks()[x * 16 + z].setBetween(0, 1, this.state); } static class Parser extends JsonParser { diff --git a/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/DrawFunctionParser.java b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/DrawFunctionParser.java index 7fe48920..28f40f58 100644 --- a/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/DrawFunctionParser.java +++ b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/DrawFunctionParser.java @@ -1,10 +1,10 @@ package net.buildtheearth.terraplusplus.dataset.vector.draw; +import java.util.Map; + import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.buildtheearth.terraplusplus.dataset.osm.JsonParser; -import java.util.Map; - /** * @author DaPorkchop_ */ @@ -20,6 +20,7 @@ public class DrawFunctionParser extends JsonParser.Typed { TYPES.put("weight_less_than", WeightLessThan.class); TYPES.put("block", Block.class); + TYPES.put("blocks", MultiBlock.class); TYPES.put("no_trees", NoTrees.class); TYPES.put("ocean", Ocean.class); TYPES.put("water", Water.class); diff --git a/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/MultiBlock.java b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/MultiBlock.java new file mode 100644 index 00000000..a0520534 --- /dev/null +++ b/src/main/java/net/buildtheearth/terraplusplus/dataset/vector/draw/MultiBlock.java @@ -0,0 +1,75 @@ +package net.buildtheearth.terraplusplus.dataset.vector.draw; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +import lombok.NonNull; +import net.buildtheearth.terraplusplus.TerraConstants; +import net.buildtheearth.terraplusplus.dataset.osm.JsonParser; +import net.buildtheearth.terraplusplus.generator.CachedChunkData; +import net.buildtheearth.terraplusplus.generator.CachedChunkData.SurfaceColumn; +import net.daporkchop.lib.common.util.PValidation; +import net.minecraft.block.state.IBlockState; + +/** + * {@link DrawFunction} which sets the surface to a fixed surface pattern. + * + * @author SmylerMC + */ +@JsonAdapter(MultiBlock.Parser.class) +public class MultiBlock implements DrawFunction { + + private int offset; + private IBlockState[] states; + + public MultiBlock(int offset, IBlockState... states) { + this.offset = offset; + this.states = states; + } + + @Override + public void drawOnto(@NonNull CachedChunkData.Builder data, int x, int z, int weight) { + SurfaceColumn column = data.surfaceBlocks()[x * 16 + z]; + for (int i = 0; i < states.length; i++) { + column.setBetween(i + this.offset, i + this.offset + 1, this.states[i]); + } + } + + static class Parser extends JsonParser { + @Override + public MultiBlock read(JsonReader in) throws IOException { + in.beginObject(); + int offset = 0; + List states = new ArrayList<>(); + while (in.peek() != JsonToken.END_OBJECT) { + String name = in.nextName(); + switch (name) { + case "offset": + offset = in.nextInt(); + break; + case "blocks": + in.beginArray(); + while (in.peek() != JsonToken.END_ARRAY) { + states.add(TerraConstants.GSON.fromJson(in, IBlockState.class)); + } + in.endArray(); + break; + default: + throw new IllegalStateException("invalid property: " + name); + } + } + in.endObject(); + PValidation.checkState(states.size() > 0, "Illegal block state array: at least one required"); + Collections.reverse(states); + IBlockState[] blocks = states.toArray(new IBlockState[0]); + return new MultiBlock(offset, blocks); + } + } + +} diff --git a/src/main/java/net/buildtheearth/terraplusplus/generator/CachedChunkData.java b/src/main/java/net/buildtheearth/terraplusplus/generator/CachedChunkData.java index 3450539f..98faf2c3 100644 --- a/src/main/java/net/buildtheearth/terraplusplus/generator/CachedChunkData.java +++ b/src/main/java/net/buildtheearth/terraplusplus/generator/CachedChunkData.java @@ -1,13 +1,23 @@ package net.buildtheearth.terraplusplus.generator; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static net.daporkchop.lib.common.math.PMath.clamp; +import static net.daporkchop.lib.common.math.PMath.floorI; +import static net.daporkchop.lib.common.math.PMath.lerp; + +import java.util.Arrays; +import java.util.Map; + import com.google.common.collect.ImmutableMap; + import io.github.opencubicchunks.cubicchunks.api.util.Coords; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import net.buildtheearth.terraplusplus.util.CustomAttributeContainer; -import net.buildtheearth.terraplusplus.util.ImmutableCompactArray; +import net.buildtheearth.terraplusplus.util.StabbingTree; import net.daporkchop.lib.common.ref.Ref; import net.daporkchop.lib.common.ref.ThreadRef; import net.daporkchop.lib.common.util.PorkUtil; @@ -15,12 +25,6 @@ import net.minecraft.init.Biomes; import net.minecraft.world.biome.Biome; -import java.util.Arrays; -import java.util.Map; - -import static java.lang.Math.*; -import static net.daporkchop.lib.common.math.PMath.*; - /** * A collection of data cached per-column by {@link EarthGenerator}. * @@ -53,7 +57,7 @@ private static int extractActualDepth(int waterDepth) { @Getter private final byte[] biomes; - private final ImmutableCompactArray surfaceBlocks; + private final SurfaceColumn[] surfaceBlocks; private final int surfaceMinCube; private final int surfaceMaxCube; @@ -99,13 +103,16 @@ private CachedChunkData(@NonNull Builder builder, @NonNull Map c this.biomes[i] = (byte) Biome.getIdForBiome(PorkUtil.fallbackIfNull(builder.biomes[i], Biomes.DEEP_OCEAN)); } - this.surfaceBlocks = new ImmutableCompactArray<>(builder.surfaceBlocks); + this.surfaceBlocks = new SurfaceColumn[16 * 16]; + for (int i = 0; i < 16 * 16; i++) { + this.surfaceBlocks[i] = builder.surfaceBlocks[i].copy(); + } int min = Integer.MAX_VALUE; int max = Integer.MIN_VALUE; for (int i = 0; i < 16 * 16; i++) { - min = min(min, min(this.groundHeight[i], this.surfaceHeight[i])); - max = max(max, max(this.groundHeight[i], this.surfaceHeight[i])); + min = min(min, min(this.groundHeight[i], this.surfaceHeight[i]) - this.surfaceBlocks[i].lowestBlock()); + max = max(max, max(this.groundHeight[i], this.surfaceHeight[i]) + this.surfaceBlocks[i].highestBlock()); } this.surfaceMinCube = Coords.blockToCube(min) - 1; this.surfaceMaxCube = Coords.blockToCube(max) + 1; @@ -139,8 +146,8 @@ public int waterHeight(int x, int z) { return this.surfaceHeight(x, z) - 1; } - public IBlockState surfaceBlock(int x, int z) { - return this.surfaceBlocks.get(x * 16 + z); + public SurfaceColumn surfaceBlock(int x, int z) { + return this.surfaceBlocks[x * 16 + z]; } public int biome(int x, int z) { @@ -160,7 +167,7 @@ public static final class Builder extends CustomAttributeContainer implements IE private final Biome[] biomes = new Biome[16 * 16]; - protected final IBlockState[] surfaceBlocks = new IBlockState[16 * 16]; + protected final SurfaceColumn[] surfaceBlocks = new SurfaceColumn[16 * 16]; /** * @deprecated use {@link #builder()} unless you have a specific reason to invoke this constructor directly @@ -168,6 +175,9 @@ public static final class Builder extends CustomAttributeContainer implements IE @Deprecated public Builder() { super(new Object2ObjectOpenHashMap<>()); + for (int i = 0; i< this.surfaceBlocks.length; i++) { + this.surfaceBlocks[i] = new SurfaceColumn(); + } this.reset(); } @@ -203,7 +213,7 @@ public void putCustom(@NonNull String key, @NonNull Object value) { public Builder reset() { Arrays.fill(this.surfaceHeight, BLANK_HEIGHT); Arrays.fill(this.waterDepth, (byte) WATERDEPTH_DEFAULT); - Arrays.fill(this.surfaceBlocks, null); + Arrays.stream(this.surfaceBlocks).forEach(SurfaceColumn::clear); this.custom.clear(); return this; } @@ -215,4 +225,29 @@ public CachedChunkData build() { return new CachedChunkData(this, custom); } } + + /** + * This is mostly to avoid generic type shenanigans + */ + public static class SurfaceColumn extends StabbingTree { + + public SurfaceColumn copy() { + SurfaceColumn other = new SurfaceColumn(); + for (Node node: this.nodes) { + other.nodes.add(node); + } + return other; + } + + public int highestBlock() { + int size = this.nodes.size(); + return size > 0 ? this.nodes.get(size - 1).start() - 1 : 0; + } + + public int lowestBlock() { + int size = this.nodes.size(); + return size > 0 ? this.nodes.get(0).start() : 0; + } + } + } diff --git a/src/main/java/net/buildtheearth/terraplusplus/generator/EarthGenerator.java b/src/main/java/net/buildtheearth/terraplusplus/generator/EarthGenerator.java index 7a99b2b7..629f4398 100644 --- a/src/main/java/net/buildtheearth/terraplusplus/generator/EarthGenerator.java +++ b/src/main/java/net/buildtheearth/terraplusplus/generator/EarthGenerator.java @@ -1,8 +1,21 @@ package net.buildtheearth.terraplusplus.generator; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; + import io.github.opencubicchunks.cubicchunks.api.util.Coords; import io.github.opencubicchunks.cubicchunks.api.util.CubePos; import io.github.opencubicchunks.cubicchunks.api.world.ICube; @@ -29,6 +42,7 @@ import lombok.NonNull; import net.buildtheearth.terraplusplus.TerraConstants; import net.buildtheearth.terraplusplus.TerraMod; +import net.buildtheearth.terraplusplus.generator.CachedChunkData.SurfaceColumn; import net.buildtheearth.terraplusplus.generator.data.IEarthDataBaker; import net.buildtheearth.terraplusplus.generator.populate.IEarthPopulator; import net.buildtheearth.terraplusplus.projection.GeographicProjection; @@ -51,17 +65,6 @@ import net.minecraftforge.fml.common.gameevent.PlayerEvent; import net.minecraftforge.fml.common.registry.ForgeRegistries; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import static java.lang.Math.*; - public class EarthGenerator extends BasicCubeGenerator { public static final int WATER_DEPTH_OFFSET = 1; @@ -263,11 +266,20 @@ protected void generateCube(int cubeX, int cubeY, int cubeZ, CubePrimer primer, if (data.intersectsSurface(cubeY)) { //render surface blocks onto cube surface for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - int y = data.surfaceHeight(x, z) - Coords.cubeToMinBlock(cubeY); - IBlockState state; - if ((y & 0xF) == y //don't set surface blocks outside of this cube - && (state = data.surfaceBlock(x, z)) != null) { - primer.setBlockState(x, y, z, state); + SurfaceColumn column = data.surfaceBlock(x, z); + if (data != null) { + int cx = x; + int surfaceY = data.surfaceHeight(x, z) - Coords.cubeToMinBlock(cubeY); + int cz = z; + column.forEachSection(-surfaceY, -surfaceY + 16, (startNode, endNode) -> { + IBlockState state = startNode.value(); + if (state == null) return; + int start = startNode.start(); + int end = endNode.start(); + for (int y = start; y < end; y++) { + primer.setBlockState(cx, y + surfaceY, cz, state); + } + }); } } } @@ -371,7 +383,7 @@ public void populate(ICube cube) { Random random = Coords.coordsSeedRandom(this.world.getSeed(), cube.getX(), cube.getY(), cube.getZ()); Biome biome = cube.getBiome(Coords.getCubeCenter(cube)); - this.cubiccfg.expectedBaseHeight = (float) datas[0].groundHeight(15, 15); + this.cubiccfg.expectedBaseHeight = datas[0].groundHeight(15, 15); for (IEarthPopulator populator : this.populators) { populator.populate(this.world, random, cube.getCoords(), biome, datas); diff --git a/src/main/java/net/buildtheearth/terraplusplus/generator/TerrainPreview.java b/src/main/java/net/buildtheearth/terraplusplus/generator/TerrainPreview.java index 2462ebfa..b99479ed 100644 --- a/src/main/java/net/buildtheearth/terraplusplus/generator/TerrainPreview.java +++ b/src/main/java/net/buildtheearth/terraplusplus/generator/TerrainPreview.java @@ -1,22 +1,11 @@ package net.buildtheearth.terraplusplus.generator; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import lombok.NonNull; -import net.buildtheearth.terraplusplus.generator.data.TreeCoverBaker; -import net.buildtheearth.terraplusplus.projection.GeographicProjection; -import net.buildtheearth.terraplusplus.projection.OutOfProjectionBoundsException; -import net.buildtheearth.terraplusplus.util.EmptyWorld; -import net.buildtheearth.terraplusplus.util.TilePos; -import net.buildtheearth.terraplusplus.util.http.Http; -import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Bootstrap; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraftforge.client.model.pipeline.LightUtil; +import static java.lang.Math.max; +import static net.daporkchop.lib.common.math.PMath.clamp; +import static net.daporkchop.lib.common.math.PMath.floorI; +import static net.daporkchop.lib.common.math.PMath.lerpI; +import static net.daporkchop.lib.common.util.PorkUtil.uncheckedCast; -import javax.swing.JFrame; import java.awt.Canvas; import java.awt.Graphics; import java.awt.Graphics2D; @@ -31,9 +20,25 @@ import java.util.Collections; import java.util.concurrent.CompletableFuture; -import static java.lang.Math.*; -import static net.daporkchop.lib.common.math.PMath.*; -import static net.daporkchop.lib.common.util.PorkUtil.*; +import javax.swing.JFrame; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import lombok.NonNull; +import net.buildtheearth.terraplusplus.generator.CachedChunkData.SurfaceColumn; +import net.buildtheearth.terraplusplus.generator.data.TreeCoverBaker; +import net.buildtheearth.terraplusplus.projection.GeographicProjection; +import net.buildtheearth.terraplusplus.projection.OutOfProjectionBoundsException; +import net.buildtheearth.terraplusplus.util.EmptyWorld; +import net.buildtheearth.terraplusplus.util.TilePos; +import net.buildtheearth.terraplusplus.util.http.Http; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Bootstrap; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraftforge.client.model.pipeline.LightUtil; /** * @author DaPorkchop_ @@ -283,7 +288,8 @@ protected CompletableFuture baseZoomTile(int x, int z) { for (int cz = 0; cz < 16; cz++) { int c; - IBlockState state = data.surfaceBlock(cx, cz); + SurfaceColumn column = data.surfaceBlock(cx, cz); + IBlockState state = column.get(column.highestBlock()); if (state != null) { c = state.getMapColor(EmptyWorld.INSTANCE, BlockPos.ORIGIN).colorValue; } else { diff --git a/src/main/java/net/buildtheearth/terraplusplus/util/StabbingTree.java b/src/main/java/net/buildtheearth/terraplusplus/util/StabbingTree.java new file mode 100644 index 00000000..f9d0c548 --- /dev/null +++ b/src/main/java/net/buildtheearth/terraplusplus/util/StabbingTree.java @@ -0,0 +1,233 @@ +package net.buildtheearth.terraplusplus.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import com.google.common.base.Objects; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import net.daporkchop.lib.common.util.PValidation; + +/** + * A data structure to keep track of sections to which values have been assigned. + * + * @param - section key type + * @param - section value type + * + * @author SmylerMC + */ +public class StabbingTree, V> { + + protected List nodes = new ArrayList<>(); + @Getter private final V defaultValue; + + /** + * Creates a new {@link StabbingTree}. + * + * @param defaultValue - the value to return when requesting a value at a point that isn't in any section + */ + public StabbingTree(V defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Creates a new {@link StabbingTree} that returns null when requesting a value at a point that isn't in any section. + */ + public StabbingTree() { + this.defaultValue = null; + } + + /** + * Creates a new section with a given value. + * + * @param start - starting point of the section (inclusive) + * @param end - ending point of the section (exclusive) + * @param value - the value for this section + * + * @throws NullPointerException if either start or end is null + * @throws IllegalArgumentException if start is grater or equal to end + */ + public void setBetween(K start, K end, V value) { + this.validateInterval(start, end); + int nodeCount = this.nodes.size(); + if (nodeCount == 0) { + this.nodes.add(new Node(start, value)); + this.nodes.add(new Node(end, this.defaultValue)); + } else { + int lower = this.getInternalIndexFor(start); + int upper = this.getInternalIndexFor(end); + V continueVal; + if (upper < nodeCount && this.nodes.get(upper).start.equals(end)){ + // The start next of the previous section is just where ours end, do not look at the previous one + continueVal = this.nodes.get(upper).value; + } else if (upper == 0) { + // We are before the first node + continueVal = this.defaultValue; + } else { + // Once we end, we can continue the section that was there before us + continueVal = this.nodes.get(--upper).value; + } + // Remove the sections we are overwriting + for(; upper >= lower; upper--) { + this.nodes.remove(upper); + } + if (!Objects.equal(value, continueVal)) { + // We do not need to add a node if the section that follows has the same value, in that case we just merge + this.nodes.add(lower, new Node(end, continueVal)); + } + if (lower == 0 || !Objects.equal(value, this.nodes.get(lower - 1).value)) { + // If the section that precedes has the same value, we just need to extend it, no need for a starting node + this.nodes.add(lower, new Node(start, value)); + } + } + } + + /** + * Gets the value at the given point. + * If there is no section there, the default value is returned. + * + * @param index - where to get he value + * + * @return the value at the given point + */ + public V get(K index) { + int nodeCount = this.nodes.size(); + if (nodeCount >= 2) { + int i = this.getInternalIndexFor(index); + if (i < nodeCount && this.nodes.get(i).start.equals(index)) { + return this.nodes.get(i).value; + } else if (i == 0) { + // We are before the first node + return this.defaultValue; + } else { + Node node = this.nodes.get(i - 1); + return node.value; + } + } else { + return this.defaultValue; + } + } + + /** + * @return the number of section + */ + public int sections() { + int c = this.nodes.size(); + return c == 0 ? c : c - 1; + } + + /** + * Clears all sections, resets this {@link StabbingTree} as if if was just created + */ + public void clear() { + this.nodes.clear(); + } + + /** + * Calls a consumer for all sections in the tree. + * The consumer will take the start node and the end node of each section as inputs. + * The start node holds the value for the section. + * The end node holds the value for the section that follows. + * + * @param consumer - the method to call + */ + public void forEachSection(BiConsumer consumer) { + int nodeCount = this.nodes.size(); + if (nodeCount > 1) { + Node previousNode = this.nodes.get(0); + for (int i = 1; i < nodeCount; i++) { + Node node = this.nodes.get(i); + consumer.accept(previousNode, node); + previousNode = node; + } + } + } + + /** + * Calls a consumer for all sections in the tree intersecting with the given interval. + * The consumer will take the start node and the end node of each section as inputs. + * The start node holds the value for the section. + * The end node holds the value for the section that follows. + * + * @param consumer - the method to call + * + * @throws NullPointerException if either start or end is null + * @throws IllegalArgumentException if start is grater or equal to end + */ + public void forEachSection(K start, K end, BiConsumer consumer) { + this.validateInterval(start, end); + int si = this.getInternalIndexFor(start); + int ei = this.getInternalIndexFor(end); + int nodeCount = this.nodes.size(); + if (si != ei || (si != 0 && si != nodeCount)) { + // We do intersect + Node previousNode = this.nodes.get(si); + if (!previousNode.start.equals(start) && si > 0) { + // We only keep a portion of the previous section + previousNode = new Node(start, this.nodes.get(si - 1).value); + } else { + si++; + } + for (; si < ei; si++) { + Node node = this.nodes.get(si); + consumer.accept(previousNode, node); + previousNode = node; + } + Node endNode; + if (ei < nodeCount) { + endNode = this.nodes.get(ei); + if(!endNode.start.equals(end)) { + // What follows still has the same value + endNode = new Node(end, previousNode.value); + } + consumer.accept(previousNode, endNode); + } + } + } + + /** + * Finds the index at which a node would have to be inserted to keep the list sorted + * + * @param key + * + * @return the proper index for the given key + */ + private int getInternalIndexFor(K key) { + int lower = -1, upper = this.nodes.size(); + while (upper - lower > 1) { + int mid = (upper + lower) / 2; + K midKey = this.nodes.get(mid).start; + int comparison = key.compareTo(midKey); + if (comparison < 0) { + upper = mid; + } else if (comparison > 0) { + lower = mid; + } else { + return mid; + } + } + return lower + 1; + } + + private void validateInterval(@NonNull K start, @NonNull K end) { + PValidation.checkArg(start.compareTo(end) < 0, "Start of section cannot greater than or equal to the end of the section"); + } + + /** + * Represents the boundaries of sections in a {@link StabbingTree}. + * A {@link Node} is the end of a first section, in which it is excluded, + * and the beginning of the section that follows, in which it is included, + * and for which it holds the value stored in the section. + */ + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + public class Node { + private final K start; + private final V value; + } + +} diff --git a/src/test/java/net/buildtheearth/terraplusplus/util/StabbingTreeTest.java b/src/test/java/net/buildtheearth/terraplusplus/util/StabbingTreeTest.java new file mode 100644 index 00000000..66f96a5c --- /dev/null +++ b/src/test/java/net/buildtheearth/terraplusplus/util/StabbingTreeTest.java @@ -0,0 +1,234 @@ +package net.buildtheearth.terraplusplus.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class StabbingTreeTest { + + @Test + public void testSetAndGet() { + StabbingTree tree = new StabbingTree<>(); + assertNull(tree.get(0)); + + tree.setBetween(0, 100, 1); + + assertEquals(1, tree.sections()); + assertNull(tree.get(-1)); + for (int i = 0; i < 100; i++) assertEquals(1, tree.get(i).intValue()); + assertNull(tree.get(100)); + + tree.setBetween(-50, 50, 2); + + assertEquals(2, tree.sections()); + assertNull(tree.get(-51)); + for (int i = -50; i < 50; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = 50; i < 100; i++) assertEquals(1, tree.get(i).intValue()); + assertNull(tree.get(100)); + + tree.setBetween(40, 60, 3); + + assertEquals(3, tree.sections()); + assertNull(tree.get(-51)); + for (int i = -50; i < 40; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = 40; i < 60; i++) assertEquals(3, tree.get(i).intValue()); + for (int i = 60; i < 100; i++) assertEquals(1, tree.get(i).intValue()); + assertNull(tree.get(100)); + + tree.setBetween(100, 150, 4); + + assertEquals(4, tree.sections()); + assertNull(tree.get(-51)); + for (int i = -50; i < 40; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = 40; i < 60; i++) assertEquals(3, tree.get(i).intValue()); + for (int i = 60; i < 100; i++) assertEquals(1, tree.get(i).intValue()); + for (int i = 100; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(-100, -50, 5); + + assertEquals(5, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < 40; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = 40; i < 60; i++) assertEquals(3, tree.get(i).intValue()); + for (int i = 60; i < 100; i++) assertEquals(1, tree.get(i).intValue()); + for (int i = 100; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(-10, 120, 6); + + assertEquals(4, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 120; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 120; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(20, 80, 7); + + assertEquals(6, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 20; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 20; i < 80; i++) assertEquals(7, tree.get(i).intValue()); + for (int i = 80; i < 120; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 120; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(80, 100, 8); + + assertEquals(7, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 20; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 20; i < 80; i++) assertEquals(7, tree.get(i).intValue()); + for (int i = 80; i < 100; i++) assertEquals(8, tree.get(i).intValue()); + for (int i = 100; i < 120; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 120; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(0, 20, 9); + + assertEquals(8, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 0; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 0; i < 20; i++) assertEquals(9, tree.get(i).intValue()); + for (int i = 20; i < 80; i++) assertEquals(7, tree.get(i).intValue()); + for (int i = 80; i < 100; i++) assertEquals(8, tree.get(i).intValue()); + for (int i = 100; i < 120; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 120; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(60, 80, 8); + + assertEquals(8, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 0; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 0; i < 20; i++) assertEquals(9, tree.get(i).intValue()); + for (int i = 20; i < 60; i++) assertEquals(7, tree.get(i).intValue()); + for (int i = 60; i < 100; i++) assertEquals(8, tree.get(i).intValue()); + for (int i = 100; i < 120; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 120; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + + tree.setBetween(100, 130, 8); + + assertEquals(7, tree.sections()); + assertNull(tree.get(-101)); + for (int i = -100; i < -50; i++) assertEquals(5, tree.get(i).intValue()); + for (int i = -50; i < -10; i++) assertEquals(2, tree.get(i).intValue()); + for (int i = -10; i < 0; i++) assertEquals(6, tree.get(i).intValue()); + for (int i = 0; i < 20; i++) assertEquals(9, tree.get(i).intValue()); + for (int i = 20; i < 60; i++) assertEquals(7, tree.get(i).intValue()); + for (int i = 60; i < 130; i++) assertEquals(8, tree.get(i).intValue()); + for (int i = 130; i < 150; i++) assertEquals(4, tree.get(i).intValue()); + assertNull(tree.get(150)); + } + + @Test + public void testForEach() { + StabbingTree tree = new StabbingTree<>(); + this.testForEach(tree); + + tree.setBetween(0, 100, 1); + + this.testForEach(tree, + 0, 1, 100, null); + + tree.setBetween(-50, 50, 2); + + this.testForEach(tree, + -50, 2, 50, 1, + 50, 1, 100, null); + + tree.setBetween(40, 60, 3); + + this.testForEach(tree, + -50, 2, 40, 3, + 40, 3, 60, 1, + 60, 1, 100, null); + + tree.setBetween(100, 150, 4); + tree.setBetween(-100, -50, 5); + tree.setBetween(-10, 120, 6); + tree.setBetween(20, 80, 7); + tree.setBetween(80, 100, 8); + tree.setBetween(0, 20, 9); + tree.setBetween(60, 80, 8); + + this.testForEach(tree, + -100, 5, -50, 2, + -50, 2, -10, 6, + -10, 6, 0, 9, + 0, 9, 20, 7, + 20, 7, 60, 8, + 60, 8, 100, 6, + 100, 6, 120, 4, + 120, 4, 150, null); + this.testForEach(-210, -200, tree); + this.testForEach(500, 510, tree); + this.testForEach(-50, 60, tree, + -50, 2, -10, 6, + -10, 6, 0, 9, + 0, 9, 20, 7, + 20, 7, 60, 8); + this.testForEach(50, 130, tree, + 50, 7, 60, 8, + 60, 8, 100, 6, + 100, 6, 120, 4, + 120, 4, 130, 4); + this.testForEach(80, 160, tree, + 80, 8, 100, 6, + 100, 6, 120, 4, + 120, 4, 150, null); + this.testForEach(-200, 10, tree, + -100, 5, -50, 2, + -50, 2, -10, 6, + -10, 6, 0, 9, + 0, 9, 10, 9); + this.testForEach(30, 40, tree, + 30, 7, 40, 7); + + } + + private void testForEach(StabbingTree tree, Integer... values) { + List list = new ArrayList<>(); + tree.forEachSection((startNode, endNode) -> { + list.add(startNode.start()); + list.add(startNode.value()); + list.add(endNode.start()); + list.add(endNode.value()); + }); + assertEquals(values.length, list.size()); + for (int i = 0; i < values.length; i++) { + assertEquals(values[i], list.get(i)); + } + } + + private void testForEach(int start, int end, StabbingTree tree, Integer... values) { + List list = new ArrayList<>(); + tree.forEachSection(start, end, (startNode, endNode) -> { + list.add(startNode.start()); + list.add(startNode.value()); + list.add(endNode.start()); + list.add(endNode.value()); + }); + assertEquals(values.length, list.size()); + for (int i = 0; i < values.length; i++) { + assertEquals(values[i], list.get(i)); + } + } + +}