From f472c20bfbb125361834cfeb36650f493b17f0c0 Mon Sep 17 00:00:00 2001 From: Kenzie Togami Date: Mon, 12 Aug 2019 05:06:40 -0700 Subject: [PATCH] Memory optimizations (#505) * Remove LocatedBlock overhead in LBL map * Add new space-efficient block map, with thourough testing * Drop ordering property, add full insertion test * Add licenses * Fix mocked platform conflicts * Disable full block map testing for faster builds * Re-implement BlockMap with fastutil maps * Re-write chunk batching to be memory efficient * Make MultiStageReorder use BlockMap * Increase LBL load factor, fix long-pack limit detection * Fix infinite loop in chunk batching * Save memory in history by cleaning up MSR * Re-implement LocatedBlockList in BlockMap * Fix data race with BlockType lazy fields * Make IDs ALWAYS present, only runtime-consistent. Use for memory efficiency in BlockMap * Remap inner structure of BlockMap for smaller maps * Remove containedBlocks fields, not very efficient * Fix minor de-optimizing bug in stage reorder * Make long packed y signed * Add extended Y limit configuration option * Add licenses * Store 3 ints for unoptimized BV list * Add final to BitMath * Correct int-cast for long-packing --- buildSrc/src/main/kotlin/PlatformConfig.kt | 3 + buildSrc/src/main/kotlin/Versions.kt | 1 + config/checkstyle/import-control.xml | 1 + worldedit-bukkit/build.gradle.kts | 3 + worldedit-core/build.gradle.kts | 2 +- .../sk89q/worldedit/LocalConfiguration.java | 1 + .../extent/reorder/ChunkBatchingExtent.java | 63 +- .../extent/reorder/MultiStageReorder.java | 38 +- .../function/operation/SetBlockMap.java | 60 ++ .../internal/block/BlockStateIdAccess.java | 33 +- .../com/sk89q/worldedit/math/BitMath.java | 52 ++ .../sk89q/worldedit/math/BlockVector3.java | 34 + .../math/RegionOptimizedChunkComparator.java | 41 ++ .../math/RegionOptimizedComparator.java | 36 ++ .../util/PropertiesConfiguration.java | 1 + .../worldedit/util/YAMLConfiguration.java | 1 + .../worldedit/util/collection/BlockMap.java | 427 +++++++++++++ .../util/collection/LocatedBlockList.java | 42 +- .../util/collection/LongPositionList.java | 91 +++ .../util/collection/PositionList.java | 47 ++ .../util/collection/SubBlockMap.java | 194 ++++++ .../util/collection/VectorPositionList.java | 98 +++ .../util/concurrency/LazyReference.java | 75 +++ .../worldedit/world/block/BlockState.java | 3 - .../worldedit/world/block/BlockType.java | 63 +- .../internal/expression/ExpressionTest.java | 15 +- .../worldedit/util/VariedVectorsProvider.java | 133 ++++ .../util/collection/BlockMapTest.java | 588 ++++++++++++++++++ .../test/resources/junit-platform.properties | 5 + .../config/ConfigurateConfiguration.java | 2 + 30 files changed, 2014 insertions(+), 139 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/function/operation/SetBlockMap.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/math/BitMath.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedChunkComparator.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedComparator.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/BlockMap.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LongPositionList.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/PositionList.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/SubBlockMap.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/VectorPositionList.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/concurrency/LazyReference.java create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/util/VariedVectorsProvider.java create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java create mode 100644 worldedit-core/src/test/resources/junit-platform.properties diff --git a/buildSrc/src/main/kotlin/PlatformConfig.kt b/buildSrc/src/main/kotlin/PlatformConfig.kt index c5deea857..beb159945 100644 --- a/buildSrc/src/main/kotlin/PlatformConfig.kt +++ b/buildSrc/src/main/kotlin/PlatformConfig.kt @@ -46,6 +46,9 @@ fun Project.applyPlatformAndCoreConfiguration() { dependencies { "testImplementation"("org.junit.jupiter:junit-jupiter-api:${Versions.JUNIT}") + "testImplementation"("org.junit.jupiter:junit-jupiter-params:${Versions.JUNIT}") + "testImplementation"("org.mockito:mockito-core:${Versions.MOCKITO}") + "testImplementation"("org.mockito:mockito-junit-jupiter:${Versions.MOCKITO}") "testRuntime"("org.junit.jupiter:junit-jupiter-engine:${Versions.JUNIT}") } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index e0c2b4763..b460f77eb 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,4 +4,5 @@ object Versions { const val PISTON = "0.4.3" const val AUTO_VALUE = "1.6.5" const val JUNIT = "5.5.0" + const val MOCKITO = "3.0.0" } diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 27b054a03..e26e9a388 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -40,6 +40,7 @@ + diff --git a/worldedit-bukkit/build.gradle.kts b/worldedit-bukkit/build.gradle.kts index 1343ff2be..cb859fb6a 100644 --- a/worldedit-bukkit/build.gradle.kts +++ b/worldedit-bukkit/build.gradle.kts @@ -63,6 +63,9 @@ tasks.named("shadowJar") { relocate("io.papermc.lib", "com.sk89q.worldedit.bukkit.paperlib") { include(dependency("io.papermc:paperlib:1.0.2")) } + relocate("it.unimi.dsi.fastutil", "com.sk89q.worldedit.bukkit.fastutil") { + include(dependency("it.unimi.dsi:fastutil")) + } } } diff --git a/worldedit-core/build.gradle.kts b/worldedit-core/build.gradle.kts index 274cf2e06..a20b254f8 100644 --- a/worldedit-core/build.gradle.kts +++ b/worldedit-core/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { "compile"("com.google.code.findbugs:jsr305:1.3.9") "compile"("com.google.code.gson:gson:2.8.0") "compile"("org.slf4j:slf4j-api:1.7.26") + "compile"("it.unimi.dsi:fastutil:8.2.1") "compileOnly"(project(":worldedit-libs:core:ap")) "annotationProcessor"(project(":worldedit-libs:core:ap")) @@ -28,7 +29,6 @@ dependencies { "annotationProcessor"("com.google.guava:guava:21.0") "compileOnly"("com.google.auto.value:auto-value-annotations:${Versions.AUTO_VALUE}") "annotationProcessor"("com.google.auto.value:auto-value:${Versions.AUTO_VALUE}") - "testCompile"("org.mockito:mockito-core:1.9.0-rc1") } tasks.withType().configureEach { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java index d814bf48b..0b042c8bb 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java @@ -75,6 +75,7 @@ public abstract class LocalConfiguration { public int butcherMaxRadius = -1; public boolean allowSymlinks = false; public boolean serverSideCUI = true; + public boolean extendedYLimit = false; protected String[] getDefaultDisallowedBlocks() { List blockTypes = Lists.newArrayList( diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/ChunkBatchingExtent.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/ChunkBatchingExtent.java index a44017fd1..591cb936b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/ChunkBatchingExtent.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/ChunkBatchingExtent.java @@ -19,25 +19,21 @@ package com.sk89q.worldedit.extent.reorder; -import com.google.common.collect.Table; -import com.google.common.collect.TreeBasedTable; +import com.google.common.collect.ImmutableSortedSet; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.extent.AbstractBufferingExtent; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.operation.Operation; import com.sk89q.worldedit.function.operation.RunContext; -import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.math.RegionOptimizedComparator; +import com.sk89q.worldedit.util.collection.BlockMap; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockStateHolder; -import java.util.Comparator; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; /** * A special extent that batches changes into Minecraft chunks. This helps @@ -47,17 +43,7 @@ import java.util.Set; */ public class ChunkBatchingExtent extends AbstractBufferingExtent { - /** - * Comparator optimized for sorting chunks by the region file they reside - * in. This allows for file caches to be used while loading the chunk. - */ - private static final Comparator REGION_OPTIMIZED_SORT = - Comparator.comparing((BlockVector2 vec) -> vec.shr(5), BlockVector2.COMPARING_GRID_ARRANGEMENT) - .thenComparing(BlockVector2.COMPARING_GRID_ARRANGEMENT); - - private final Table batches = - TreeBasedTable.create(REGION_OPTIMIZED_SORT, BlockVector3.sortByCoordsYzx()); - private final Set containedBlocks = new HashSet<>(); + private final BlockMap blockMap = BlockMap.create(); private boolean enabled; public ChunkBatchingExtent(Extent extent) { @@ -81,32 +67,18 @@ public class ChunkBatchingExtent extends AbstractBufferingExtent { return enabled; } - private BlockVector2 getChunkPos(BlockVector3 location) { - return location.shr(4).toBlockVector2(); - } - - private BlockVector3 getInChunkPos(BlockVector3 location) { - return BlockVector3.at(location.getX() & 15, location.getY(), location.getZ() & 15); - } - @Override public > boolean setBlock(BlockVector3 location, B block) throws WorldEditException { if (!enabled) { return setDelegateBlock(location, block); } - BlockVector2 chunkPos = getChunkPos(location); - BlockVector3 inChunkPos = getInChunkPos(location); - batches.put(chunkPos, inChunkPos, block.toBaseBlock()); - containedBlocks.add(location); + blockMap.put(location, block.toBaseBlock()); return true; } @Override protected Optional getBufferedBlock(BlockVector3 position) { - if (!containedBlocks.contains(position)) { - return Optional.empty(); - } - return Optional.of(batches.get(getChunkPos(position), getInChunkPos(position))); + return Optional.ofNullable(blockMap.get(position)); } @Override @@ -117,24 +89,21 @@ public class ChunkBatchingExtent extends AbstractBufferingExtent { return new Operation() { // we get modified between create/resume -- only create this on resume to prevent CME - private Iterator>> batchIterator; + private Iterator iterator; @Override public Operation resume(RunContext run) throws WorldEditException { - if (batchIterator == null) { - batchIterator = batches.rowMap().entrySet().iterator(); + if (iterator == null) { + iterator = ImmutableSortedSet.copyOf(RegionOptimizedComparator.INSTANCE, + blockMap.keySet()).iterator(); } - if (!batchIterator.hasNext()) { - return null; + while (iterator.hasNext()) { + BlockVector3 position = iterator.next(); + BaseBlock block = blockMap.get(position); + getExtent().setBlock(position, block); } - Map.Entry> next = batchIterator.next(); - BlockVector3 chunkOffset = next.getKey().toBlockVector3().shl(4); - for (Map.Entry block : next.getValue().entrySet()) { - getExtent().setBlock(block.getKey().add(chunkOffset), block.getValue()); - containedBlocks.remove(block.getKey()); - } - batchIterator.remove(); - return this; + blockMap.clear(); + return null; } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/MultiStageReorder.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/MultiStageReorder.java index 011640709..733d6c7de 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/MultiStageReorder.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/reorder/MultiStageReorder.java @@ -24,9 +24,10 @@ import com.sk89q.worldedit.extent.AbstractBufferingExtent; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.operation.Operation; import com.sk89q.worldedit.function.operation.OperationQueue; -import com.sk89q.worldedit.function.operation.SetLocatedBlocks; +import com.sk89q.worldedit.function.operation.RunContext; +import com.sk89q.worldedit.function.operation.SetBlockMap; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.util.collection.LocatedBlockList; +import com.sk89q.worldedit.util.collection.BlockMap; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockCategories; import com.sk89q.worldedit.world.block.BlockState; @@ -36,12 +37,10 @@ import com.sk89q.worldedit.world.block.BlockTypes; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; /** * Re-orders blocks into several stages. @@ -143,8 +142,7 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde priorityMap.put(BlockTypes.MOVING_PISTON, PlacementPriority.FINAL); } - private final Set containedBlocks = new HashSet<>(); - private Map stages = new HashMap<>(); + private Map stages = new HashMap<>(); private boolean enabled; @@ -178,7 +176,7 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde this.enabled = enabled; for (PlacementPriority priority : PlacementPriority.values()) { - stages.put(priority, new LocatedBlockList()); + stages.put(priority, BlockMap.create()); } } @@ -220,7 +218,7 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde return setDelegateBlock(location, block); } - BlockState existing = getBlock(location); + BlockState existing = getExtent().getBlock(location); PlacementPriority priority = getPlacementPriority(block); PlacementPriority srcPriority = getPlacementPriority(existing); @@ -229,13 +227,13 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde switch (srcPriority) { case FINAL: - stages.get(PlacementPriority.CLEAR_FINAL).add(location, replacement); + stages.get(PlacementPriority.CLEAR_FINAL).put(location, replacement); break; case LATE: - stages.get(PlacementPriority.CLEAR_LATE).add(location, replacement); + stages.get(PlacementPriority.CLEAR_LATE).put(location, replacement); break; case LAST: - stages.get(PlacementPriority.CLEAR_LAST).add(location, replacement); + stages.get(PlacementPriority.CLEAR_LAST).put(location, replacement); break; } @@ -244,16 +242,12 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde } } - stages.get(priority).add(location, block); - containedBlocks.add(location); + stages.get(priority).put(location, block.toBaseBlock()); return !existing.equalsFuzzy(block); } @Override protected Optional getBufferedBlock(BlockVector3 position) { - if (!containedBlocks.contains(position)) { - return Optional.empty(); - } return stages.values().stream() .map(blocks -> blocks.get(position)) .filter(Objects::nonNull) @@ -267,7 +261,17 @@ public class MultiStageReorder extends AbstractBufferingExtent implements Reorde } List operations = new ArrayList<>(); for (PlacementPriority priority : PlacementPriority.values()) { - operations.add(new SetLocatedBlocks(getExtent(), stages.get(priority))); + BlockMap blocks = stages.get(priority); + operations.add(new SetBlockMap(getExtent(), blocks) { + @Override + public Operation resume(RunContext run) throws WorldEditException { + Operation operation = super.resume(run); + if (operation == null) { + blocks.clear(); + } + return operation; + } + }); } return new OperationQueue(operations); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/operation/SetBlockMap.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/operation/SetBlockMap.java new file mode 100644 index 000000000..863aadd03 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/operation/SetBlockMap.java @@ -0,0 +1,60 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.function.operation; + +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.LocatedBlock; +import com.sk89q.worldedit.util.collection.BlockMap; +import com.sk89q.worldedit.world.block.BaseBlock; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SetBlockMap implements Operation { + + private final Extent extent; + private final BlockMap blocks; + + public SetBlockMap(Extent extent, BlockMap blocks) { + this.extent = checkNotNull(extent); + this.blocks = checkNotNull(blocks); + } + + @Override + public Operation resume(RunContext run) throws WorldEditException { + for (Map.Entry entry : blocks.entrySet()) { + extent.setBlock(entry.getKey(), entry.getValue()); + } + return null; + } + + @Override + public void cancel() { + } + + @Override + public void addStatusMessages(List messages) { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/block/BlockStateIdAccess.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/block/BlockStateIdAccess.java index dcd8210b0..c62b96225 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/block/BlockStateIdAccess.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/block/BlockStateIdAccess.java @@ -22,10 +22,10 @@ package com.sk89q.worldedit.internal.block; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.registry.BlockRegistry; import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.Map; +import java.util.BitSet; import java.util.OptionalInt; import static com.google.common.base.Preconditions.checkState; @@ -43,19 +43,32 @@ public final class BlockStateIdAccess { return ASSIGNED_IDS.inverse().get(id); } + /** + * For platforms that don't have an internal ID system, + * {@link BlockRegistry#getInternalBlockStateId(BlockState)} will return + * {@link OptionalInt#empty()}. In those cases, we will use our own ID system, + * since it's useful for other entries as well. + * @return an unused ID in WorldEdit's ID tracker + */ + private static int provideUnusedWorldEditId() { + return usedIds.nextClearBit(0); + } + + private static final BitSet usedIds = new BitSet(); + public static void register(BlockState blockState, OptionalInt id) { - if (id.isPresent()) { - int i = id.getAsInt(); - BlockState existing = ASSIGNED_IDS.inverse().get(i); - checkState(existing == null || existing == blockState, - "BlockState %s is using the same block ID (%s) as BlockState %s", - blockState, i, existing); - ASSIGNED_IDS.put(blockState, i); - } + int i = id.orElseGet(BlockStateIdAccess::provideUnusedWorldEditId); + BlockState existing = ASSIGNED_IDS.inverse().get(i); + checkState(existing == null || existing == blockState, + "BlockState %s is using the same block ID (%s) as BlockState %s", + blockState, i, existing); + ASSIGNED_IDS.put(blockState, i); + usedIds.set(i); } public static void clear() { ASSIGNED_IDS.clear(); + usedIds.clear(); } private BlockStateIdAccess() { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/BitMath.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BitMath.java new file mode 100644 index 000000000..46fa66e73 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BitMath.java @@ -0,0 +1,52 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.math; + +public final class BitMath { + + public static int mask(int bits) { + return ~(~0 << bits); + } + + public static int unpackX(long packed) { + return extractSigned(packed, 0, 26); + } + + public static int unpackZ(long packed) { + return extractSigned(packed, 26, 26); + } + + public static int unpackY(long packed) { + return extractSigned(packed, 26 + 26, 12); + } + + public static int extractSigned(long i, int shift, int bits) { + return fixSign((int) (i >> shift) & mask(bits), bits); + } + + public static int fixSign(int i, int bits) { + // Using https://stackoverflow.com/a/29266331/436524 + return i << (32 - bits) >> (32 - bits); + } + + private BitMath() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java index e8192a128..ec1aaf1d8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java @@ -24,6 +24,10 @@ import com.sk89q.worldedit.math.transform.AffineTransform; import java.util.Comparator; import static com.google.common.base.Preconditions.checkArgument; +import static com.sk89q.worldedit.math.BitMath.mask; +import static com.sk89q.worldedit.math.BitMath.unpackX; +import static com.sk89q.worldedit.math.BitMath.unpackY; +import static com.sk89q.worldedit.math.BitMath.unpackZ; /** * An immutable 3-dimensional vector. @@ -61,6 +65,31 @@ public final class BlockVector3 { return new BlockVector3(x, y, z); } + private static final int WORLD_XZ_MINMAX = 30_000_000; + private static final int WORLD_Y_MAX = 4095; + + private static boolean isHorizontallyInBounds(int h) { + return -WORLD_XZ_MINMAX <= h && h <= WORLD_XZ_MINMAX; + } + + public static boolean isLongPackable(BlockVector3 location) { + return isHorizontallyInBounds(location.getX()) && + isHorizontallyInBounds(location.getZ()) && + 0 <= location.getY() && location.getY() <= WORLD_Y_MAX; + } + + public static void checkLongPackable(BlockVector3 location) { + checkArgument(isLongPackable(location), + "Location exceeds long packing limits: %s", location); + } + + private static final long BITS_26 = mask(26); + private static final long BITS_12 = mask(12); + + public static BlockVector3 fromLongPackedForm(long packed) { + return at(unpackX(packed), unpackY(packed), unpackZ(packed)); + } + // thread-safe initialization idiom private static final class YzxOrderComparator { private static final Comparator YZX_ORDER = @@ -94,6 +123,11 @@ public final class BlockVector3 { this.z = z; } + public long toLongPackedForm() { + checkLongPackable(this); + return (x & BITS_26) | ((z & BITS_26) << 26) | (((y & (long) BITS_12) << (26 + 26))); + } + /** * Get the X coordinate. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedChunkComparator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedChunkComparator.java new file mode 100644 index 000000000..d6d2d89d6 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedChunkComparator.java @@ -0,0 +1,41 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.math; + +import java.util.Comparator; + +import static com.sk89q.worldedit.math.BlockVector2.COMPARING_GRID_ARRANGEMENT; + +/** + * Sort block positions by region, then chunk. + */ +public class RegionOptimizedChunkComparator { + + private static final Comparator CHUNK_COMPARATOR = + Comparator.comparing((BlockVector2 chunkPos) -> chunkPos.shr(5), COMPARING_GRID_ARRANGEMENT) + .thenComparing(COMPARING_GRID_ARRANGEMENT); + + public static final Comparator INSTANCE + = Comparator.comparing(blockPos -> blockPos.toBlockVector2().shr(4), CHUNK_COMPARATOR); + + private RegionOptimizedChunkComparator() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedComparator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedComparator.java new file mode 100644 index 000000000..ed715d703 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/RegionOptimizedComparator.java @@ -0,0 +1,36 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.math; + +import java.util.Comparator; + +/** + * Sort block positions by region, chunk, and finally Y-Z-X. + */ +public class RegionOptimizedComparator { + + public static final Comparator INSTANCE + = RegionOptimizedChunkComparator.INSTANCE + .thenComparing(BlockVector3.sortByCoordsYzx()); + + private RegionOptimizedComparator() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java index 766d7d99c..7953c4d31 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java @@ -118,6 +118,7 @@ public class PropertiesConfiguration extends LocalConfiguration { butcherMaxRadius = getInt("butcher-max-radius", butcherMaxRadius); allowSymlinks = getBool("allow-symbolic-links", allowSymlinks); serverSideCUI = getBool("server-side-cui", serverSideCUI); + extendedYLimit = getBool("extended-y-limit", extendedYLimit); LocalSession.MAX_HISTORY_SIZE = Math.max(15, getInt("history-size", 15)); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java index 5347eae97..be676b132 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java @@ -124,6 +124,7 @@ public class YAMLConfiguration extends LocalConfiguration { String type = config.getString("shell-save-type", "").trim(); shellSaveType = type.isEmpty() ? null : type; + extendedYLimit = config.getBoolean("compat.extended-y-limit", false); } public void unload() { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/BlockMap.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/BlockMap.java new file mode 100644 index 000000000..2620a906e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/BlockMap.java @@ -0,0 +1,427 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.google.common.collect.AbstractIterator; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BaseBlock; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static com.sk89q.worldedit.math.BitMath.fixSign; +import static com.sk89q.worldedit.math.BitMath.mask; + +/** + * A space-efficient map implementation for block locations. + */ +public class BlockMap extends AbstractMap { + + /* ========================= + IF YOU MAKE CHANGES TO THIS CLASS + Re-run BlockMapTest with the blockmap.fulltesting=true system property. + Or just temporarily remove the annotation disabling the related tests. + ========================= */ + + public static BlockMap create() { + return new BlockMap(); + } + + public static BlockMap copyOf(Map source) { + return new BlockMap(source); + } + + /* + * Stores blocks by sub-dividing them into smaller groups. + * A block location is 26 bits long for x + z, and usually + * 8 bits for y, although mods such as cubic chunks may + * expand this to infinite. We support up to 32 bits of y. + * + * Grouping key stores 20 bits x + z, 24 bits y. + * Inner key stores 6 bits x + z, 8 bits y. + * Order (lowest to highest) is x-z-y. + */ + + private static final long BITS_24 = mask(24); + private static final long BITS_20 = mask(20); + private static final int BITS_8 = mask(8); + private static final int BITS_6 = mask(6); + + private static long toGroupKey(BlockVector3 location) { + return ((location.getX() >>> 6) & BITS_20) + | (((location.getZ() >>> 6) & BITS_20) << 20) + | (((location.getY() >>> 8) & BITS_24) << (20 + 20)); + } + + private static int toInnerKey(BlockVector3 location) { + return (location.getX() & BITS_6) + | ((location.getZ() & BITS_6) << 6) + | ((location.getY() & BITS_8) << (6 + 6)); + } + + private static final long GROUP_X = BITS_20; + private static final long GROUP_Z = BITS_20 << 20; + private static final long GROUP_Y = BITS_24 << (20 + 20); + private static final int INNER_X = BITS_6; + private static final int INNER_Z = BITS_6 << 6; + private static final int INNER_Y = BITS_8 << (6 + 6); + + private static BlockVector3 reconstructLocation(long group, int inner) { + int groupX = (int) ((group & GROUP_X) << 6); + int x = fixSign(groupX | (inner & INNER_X), 26); + int groupZ = (int) ((group & GROUP_Z) >>> (20 - 6)); + int z = fixSign(groupZ | ((inner & INNER_Z) >>> 6), 26); + int groupY = (int) ((group & GROUP_Y) >>> (20 + 20 - 8)); + int y = groupY | ((inner & INNER_Y) >>> (6 + 6)); + return BlockVector3.at(x, y, z); + } + + private final Long2ObjectMap maps = new Long2ObjectOpenHashMap<>(4, 1f); + private Set> entrySet; + private Collection values; + + private BlockMap() { + } + + private BlockMap(Map source) { + putAll(source); + } + + private SubBlockMap getOrCreateMap(long groupKey) { + return maps.computeIfAbsent(groupKey, k -> new SubBlockMap()); + } + + private SubBlockMap getOrEmptyMap(long groupKey) { + return maps.getOrDefault(groupKey, SubBlockMap.EMPTY); + } + + /** + * Apply the function the the map at {@code groupKey}, and if the function empties the map, + * delete it from {@code maps}. + */ + private R cleanlyModifyMap(long groupKey, Function, R> func) { + SubBlockMap map = maps.get(groupKey); + if (map != null) { + R result = func.apply(map); + if (map.isEmpty()) { + maps.remove(groupKey); + } + return result; + } + map = new SubBlockMap(); + R result = func.apply(map); + if (!map.isEmpty()) { + maps.put(groupKey, map); + } + return result; + } + + @Override + public BaseBlock put(BlockVector3 key, BaseBlock value) { + return getOrCreateMap(toGroupKey(key)).put(toInnerKey(key), value); + } + + @Override + public BaseBlock getOrDefault(Object key, BaseBlock defaultValue) { + BlockVector3 vec = (BlockVector3) key; + return getOrEmptyMap(toGroupKey(vec)) + .getOrDefault(toInnerKey(vec), defaultValue); + } + + @Override + public void forEach(BiConsumer action) { + maps.forEach((groupKey, m) -> + m.forEach((innerKey, block) -> + action.accept(reconstructLocation(groupKey, innerKey), block) + ) + ); + } + + @Override + public void replaceAll(BiFunction function) { + maps.forEach((groupKey, m) -> + m.replaceAll((innerKey, block) -> + function.apply(reconstructLocation(groupKey, innerKey), block) + ) + ); + } + + @Override + public BaseBlock putIfAbsent(BlockVector3 key, BaseBlock value) { + return getOrCreateMap(toGroupKey(key)).putIfAbsent(toInnerKey(key), value); + } + + @Override + public boolean remove(Object key, Object value) { + BlockVector3 vec = (BlockVector3) key; + return cleanlyModifyMap(toGroupKey(vec), + map -> map.remove(toInnerKey(vec), value)); + } + + @Override + public boolean replace(BlockVector3 key, BaseBlock oldValue, BaseBlock newValue) { + return cleanlyModifyMap(toGroupKey(key), + map -> map.replace(toInnerKey(key), oldValue, newValue)); + } + + @Override + public BaseBlock replace(BlockVector3 key, BaseBlock value) { + return getOrCreateMap(toGroupKey(key)).replace(toInnerKey(key), value); + } + + @Override + public BaseBlock computeIfAbsent(BlockVector3 key, Function mappingFunction) { + return cleanlyModifyMap(toGroupKey(key), + map -> map.computeIfAbsent(toInnerKey(key), ik -> mappingFunction.apply(key))); + } + + @Override + public BaseBlock computeIfPresent(BlockVector3 key, BiFunction remappingFunction) { + return cleanlyModifyMap(toGroupKey(key), + map -> map.computeIfPresent(toInnerKey(key), (ik, block) -> remappingFunction.apply(key, block))); + } + + @Override + public BaseBlock compute(BlockVector3 key, BiFunction remappingFunction) { + return cleanlyModifyMap(toGroupKey(key), + map -> map.compute(toInnerKey(key), (ik, block) -> remappingFunction.apply(key, block))); + } + + @Override + public BaseBlock merge(BlockVector3 key, BaseBlock value, BiFunction remappingFunction) { + return cleanlyModifyMap(toGroupKey(key), + map -> map.merge(toInnerKey(key), value, remappingFunction)); + } + + @Override + public Set> entrySet() { + Set> es = entrySet; + if (es == null) { + entrySet = es = new AbstractSet>() { + @Override + public Iterator> iterator() { + return new AbstractIterator>() { + + private final ObjectIterator> primaryIterator + = Long2ObjectMaps.fastIterator(maps); + private long currentGroupKey; + private ObjectIterator> secondaryIterator; + + @Override + protected Entry computeNext() { + if (secondaryIterator == null || !secondaryIterator.hasNext()) { + if (!primaryIterator.hasNext()) { + return endOfData(); + } + + Long2ObjectMap.Entry next = primaryIterator.next(); + currentGroupKey = next.getLongKey(); + secondaryIterator = Int2ObjectMaps.fastIterator(next.getValue()); + } + Int2ObjectMap.Entry next = secondaryIterator.next(); + return new LazyEntry(currentGroupKey, next.getIntKey(), next.getValue()); + } + }; + } + + @Override + public int size() { + return BlockMap.this.size(); + } + }; + } + return es; + } + + private final class LazyEntry implements Map.Entry { + + private final long groupKey; + private final int innerKey; + private BlockVector3 lazyKey; + private BaseBlock value; + + private LazyEntry(long groupKey, int innerKey, BaseBlock value) { + this.groupKey = groupKey; + this.innerKey = innerKey; + this.value = value; + } + + @Override + public BlockVector3 getKey() { + BlockVector3 result = lazyKey; + if (result == null) { + lazyKey = result = reconstructLocation(groupKey, innerKey); + } + return result; + } + + @Override + public BaseBlock getValue() { + return value; + } + + @Override + public BaseBlock setValue(BaseBlock value) { + this.value = value; + return getOrCreateMap(groupKey).put(innerKey, value); + } + + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + if (o instanceof LazyEntry) { + LazyEntry otherE = (LazyEntry) o; + return otherE.groupKey == groupKey + && otherE.innerKey == innerKey + && Objects.equals(value, e.getValue()); + } + return Objects.equals(getKey(), e.getKey()) && Objects.equals(value, e.getValue()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey()) ^ Objects.hashCode(value); + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } + } + + @Override + public boolean containsValue(Object value) { + return maps.values().stream().anyMatch(m -> m.containsValue(value)); + } + + @Override + public boolean containsKey(Object key) { + BlockVector3 vec = (BlockVector3) key; + Map activeMap = maps.get(toGroupKey(vec)); + if (activeMap == null) { + return false; + } + return activeMap.containsKey(toInnerKey(vec)); + } + + @Override + public BaseBlock get(Object key) { + BlockVector3 vec = (BlockVector3) key; + Map activeMap = maps.get(toGroupKey(vec)); + if (activeMap == null) { + return null; + } + return activeMap.get(toInnerKey(vec)); + } + + @Override + public BaseBlock remove(Object key) { + BlockVector3 vec = (BlockVector3) key; + Map activeMap = maps.get(toGroupKey(vec)); + if (activeMap == null) { + return null; + } + BaseBlock removed = activeMap.remove(toInnerKey(vec)); + if (activeMap.isEmpty()) { + maps.remove(toGroupKey(vec)); + } + return removed; + } + + @Override + public void putAll(Map m) { + if (m instanceof BlockMap) { + // optimize insertions: + ((BlockMap) m).maps.forEach((groupKey, map) -> + getOrCreateMap(groupKey).putAll(map) + ); + } else { + super.putAll(m); + } + } + + @Override + public void clear() { + maps.clear(); + } + + @Override + public int size() { + return maps.values().stream().mapToInt(Map::size).sum(); + } + + // no keySet override, since we can't really optimize it. + // we can optimize values access though, by skipping BV construction. + + @Override + public Collection values() { + Collection vs = values; + if (vs == null) { + values = vs = new AbstractCollection() { + @Override + public Iterator iterator() { + return maps.values().stream() + .flatMap(m -> m.values().stream()) + .iterator(); + } + + @Override + public int size() { + return BlockMap.this.size(); + } + }; + } + return vs; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof BlockMap) { + // optimize by skipping entry translations: + return maps.equals(((BlockMap) o).maps); + } + return super.equals(o); + } + + // satisfy checkstyle + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LocatedBlockList.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LocatedBlockList.java index b558a7102..e8d11d2e2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LocatedBlockList.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LocatedBlockList.java @@ -19,74 +19,74 @@ package com.sk89q.worldedit.util.collection; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.util.LocatedBlock; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockStateHolder; import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; /** * Wrapper around a list of blocks located in the world. */ public class LocatedBlockList implements Iterable { - private final Map map = new LinkedHashMap<>(); + private final BlockMap blocks = BlockMap.create(); + private final PositionList order = PositionList.create( + WorldEdit.getInstance().getConfiguration().extendedYLimit + ); public LocatedBlockList() { } public LocatedBlockList(Collection collection) { for (LocatedBlock locatedBlock : collection) { - map.put(locatedBlock.getLocation(), locatedBlock); + add(locatedBlock.getLocation(), locatedBlock.getBlock()); } } public void add(LocatedBlock setBlockCall) { checkNotNull(setBlockCall); - map.put(setBlockCall.getLocation(), setBlockCall); + add(setBlockCall.getLocation(), setBlockCall.getBlock()); } public > void add(BlockVector3 location, B block) { - add(new LocatedBlock(location, block.toBaseBlock())); + blocks.put(location, block.toBaseBlock()); + order.add(location); } public boolean containsLocation(BlockVector3 location) { - return map.containsKey(location); + return blocks.containsKey(location); } public @Nullable BaseBlock get(BlockVector3 location) { - return map.get(location).getBlock(); + return blocks.get(location); } public int size() { - return map.size(); + return order.size(); } public void clear() { - map.clear(); + blocks.clear(); + order.clear(); } @Override public Iterator iterator() { - return map.values().iterator(); + return Iterators.transform(order.iterator(), position -> + new LocatedBlock(position, blocks.get(position))); } public Iterator reverseIterator() { - List data = new ArrayList<>(map.values()); - Collections.reverse(data); - return data.iterator(); + return Iterators.transform(order.reverseIterator(), position -> + new LocatedBlock(position, blocks.get(position))); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LongPositionList.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LongPositionList.java new file mode 100644 index 000000000..8d5c0b72a --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/LongPositionList.java @@ -0,0 +1,91 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.google.common.collect.AbstractIterator; +import com.sk89q.worldedit.math.BlockVector3; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; +import it.unimi.dsi.fastutil.longs.LongListIterator; + +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.function.ToLongFunction; + +class LongPositionList implements PositionList { + + private final LongList delegate = new LongArrayList(); + + @Override + public BlockVector3 get(int index) { + return BlockVector3.fromLongPackedForm(delegate.getLong(index)); + } + + @Override + public void add(BlockVector3 vector) { + delegate.add(vector.toLongPackedForm()); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Iterator iterator() { + return new PositionIterator(delegate.iterator(), + LongListIterator::hasNext, + LongListIterator::nextLong); + } + + @Override + public Iterator reverseIterator() { + return new PositionIterator(delegate.listIterator(size()), + LongListIterator::hasPrevious, + LongListIterator::previousLong); + } + + private static final class PositionIterator extends AbstractIterator { + + private final LongListIterator iterator; + private final Predicate hasNext; + private final ToLongFunction next; + + private PositionIterator(LongListIterator iterator, + Predicate hasNext, + ToLongFunction next) { + this.iterator = iterator; + this.hasNext = hasNext; + this.next = next; + } + + @Override + protected BlockVector3 computeNext() { + return hasNext.test(iterator) + ? BlockVector3.fromLongPackedForm(next.applyAsLong(iterator)) + : endOfData(); + } + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/PositionList.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/PositionList.java new file mode 100644 index 000000000..793f0fef3 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/PositionList.java @@ -0,0 +1,47 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.sk89q.worldedit.math.BlockVector3; + +import java.util.Iterator; + +interface PositionList { + + static PositionList create(boolean extendedYLimit) { + if (extendedYLimit) { + return new VectorPositionList(); + } + return new LongPositionList(); + } + + BlockVector3 get(int index); + + void add(BlockVector3 vector); + + int size(); + + void clear(); + + Iterator iterator(); + + Iterator reverseIterator(); + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/SubBlockMap.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/SubBlockMap.java new file mode 100644 index 000000000..440f52eee --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/SubBlockMap.java @@ -0,0 +1,194 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.sk89q.worldedit.internal.block.BlockStateIdAccess; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockState; +import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntMaps; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.AbstractObjectSet; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ObjectSet; + +import java.util.NoSuchElementException; +import java.util.function.BiFunction; + +/** + * Int-to-BaseBlock map, but with optimizations for common cases. + */ +class SubBlockMap extends AbstractInt2ObjectMap { + + private static boolean hasInt(BlockState b) { + return BlockStateIdAccess.getBlockStateId(b).isPresent(); + } + + private static boolean isUncommon(BaseBlock block) { + return block.hasNbtData() || !hasInt(block.toImmutableState()); + } + + private static int assumeAsInt(BlockState b) { + return BlockStateIdAccess.getBlockStateId(b) + .orElseThrow(() -> new IllegalStateException("Block state " + b + " did not have an ID")); + } + + private static BaseBlock assumeAsBlock(int id) { + if (id == Integer.MIN_VALUE) { + return null; + } + BlockState state = BlockStateIdAccess.getBlockStateById(id); + if (state == null) { + throw new IllegalStateException("No state for ID " + id); + } + return state.toBaseBlock(); + } + + static final SubBlockMap EMPTY = new SubBlockMap(); + + private final Int2IntMap commonMap = new Int2IntOpenHashMap(64, 1f); + private final Int2ObjectMap uncommonMap = new Int2ObjectOpenHashMap<>(1, 1f); + + { + commonMap.defaultReturnValue(Integer.MIN_VALUE); + } + + @Override + public int size() { + return commonMap.size() + uncommonMap.size(); + } + + @Override + public ObjectSet> int2ObjectEntrySet() { + return new AbstractObjectSet>() { + @Override + public ObjectIterator> iterator() { + return new ObjectIterator>() { + + private final ObjectIterator commonIter + = Int2IntMaps.fastIterator(commonMap); + private final ObjectIterator> uncommonIter + = Int2ObjectMaps.fastIterator(uncommonMap); + + @Override + public boolean hasNext() { + return commonIter.hasNext() || uncommonIter.hasNext(); + } + + @Override + public Entry next() { + if (commonIter.hasNext()) { + Int2IntMap.Entry e = commonIter.next(); + return new BasicEntry<>( + e.getIntKey(), assumeAsBlock(e.getIntValue()) + ); + } + if (uncommonIter.hasNext()) { + return uncommonIter.next(); + } + throw new NoSuchElementException(); + } + }; + } + + @Override + public int size() { + return SubBlockMap.this.size(); + } + }; + } + + @Override + public BaseBlock get(int key) { + int oldId = commonMap.get(key); + if (oldId == Integer.MIN_VALUE) { + return uncommonMap.get(key); + } + return assumeAsBlock(oldId); + } + + @Override + public boolean containsKey(int k) { + return commonMap.containsKey(k) || uncommonMap.containsKey(k); + } + + @Override + public boolean containsValue(Object v) { + BaseBlock block = (BaseBlock) v; + if (isUncommon(block)) { + return uncommonMap.containsValue(block); + } + return commonMap.containsValue(assumeAsInt(block.toImmutableState())); + } + + @Override + public BaseBlock put(int key, BaseBlock value) { + if (isUncommon(value)) { + BaseBlock old = uncommonMap.put(key, value); + if (old == null) { + // ensure common doesn't have the entry too + int oldId = commonMap.remove(key); + return assumeAsBlock(oldId); + } + return old; + } + int oldId = commonMap.put(key, assumeAsInt(value.toImmutableState())); + return assumeAsBlock(oldId); + } + + @Override + public BaseBlock remove(int key) { + int removed = commonMap.remove(key); + if (removed == Integer.MIN_VALUE) { + return uncommonMap.remove(key); + } + return assumeAsBlock(removed); + } + + @Override + public void replaceAll(BiFunction function) { + for (ObjectIterator iter = Int2IntMaps.fastIterator(commonMap); + iter.hasNext(); ) { + Int2IntMap.Entry next = iter.next(); + BaseBlock value = function.apply(next.getIntKey(), assumeAsBlock(next.getIntValue())); + if (isUncommon(value)) { + uncommonMap.put(next.getIntKey(), value); + iter.remove(); + } else { + next.setValue(assumeAsInt(value.toImmutableState())); + } + } + for (ObjectIterator> iter = Int2ObjectMaps.fastIterator(uncommonMap); + iter.hasNext(); ) { + Int2ObjectMap.Entry next = iter.next(); + BaseBlock value = function.apply(next.getIntKey(), next.getValue()); + if (isUncommon(value)) { + next.setValue(value); + } else { + commonMap.put(next.getIntKey(), assumeAsInt(value.toImmutableState())); + iter.remove(); + } + } + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/VectorPositionList.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/VectorPositionList.java new file mode 100644 index 000000000..6c8d27ddf --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/VectorPositionList.java @@ -0,0 +1,98 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.google.common.collect.AbstractIterator; +import com.sk89q.worldedit.math.BlockVector3; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntListIterator; + +import java.util.Iterator; + +class VectorPositionList implements PositionList { + + private final IntList delegate = new IntArrayList(); + + @Override + public BlockVector3 get(int index) { + int ri = index * 3; + return BlockVector3.at( + delegate.getInt(ri), + delegate.getInt(ri + 1), + delegate.getInt(ri + 2)); + } + + @Override + public void add(BlockVector3 vector) { + delegate.add(vector.getX()); + delegate.add(vector.getY()); + delegate.add(vector.getZ()); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Iterator iterator() { + return new AbstractIterator() { + + private final IntIterator iterator = delegate.iterator(); + + @Override + protected BlockVector3 computeNext() { + if (!iterator.hasNext()) { + return endOfData(); + } + return BlockVector3.at( + iterator.nextInt(), + iterator.nextInt(), + iterator.nextInt()); + } + }; + } + + @Override + public Iterator reverseIterator() { + return new AbstractIterator() { + + private final IntListIterator iterator = delegate.listIterator(delegate.size()); + + @Override + protected BlockVector3 computeNext() { + if (!iterator.hasPrevious()) { + return endOfData(); + } + return BlockVector3.at( + iterator.previousInt(), + iterator.previousInt(), + iterator.previousInt()); + } + }; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/concurrency/LazyReference.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/concurrency/LazyReference.java new file mode 100644 index 000000000..720ca0161 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/concurrency/LazyReference.java @@ -0,0 +1,75 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.concurrency; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +/** + * Thread-safe lazy reference. + */ +public class LazyReference { + + public static LazyReference from(Supplier valueComputation) { + return new LazyReference<>(valueComputation); + } + + // Memory saving technique: hold the computation info in the same reference field that we'll + // put the value into, so the memory possibly retained by those parts is GC'able as soon as + // it's no longer needed. + + private static final class RefInfo { + private final Lock lock = new ReentrantLock(); + private final Supplier valueComputation; + + private RefInfo(Supplier valueComputation) { + this.valueComputation = valueComputation; + } + } + + private Object value; + + private LazyReference(Supplier valueComputation) { + this.value = new RefInfo<>(valueComputation); + } + + // casts are safe, value is either RefInfo or T + @SuppressWarnings("unchecked") + public T getValue() { + Object v = value; + if (!(v instanceof RefInfo)) { + return (T) v; + } + RefInfo refInfo = (RefInfo) v; + refInfo.lock.lock(); + try { + v = value; + if (!(v instanceof RefInfo)) { + return (T) v; + } + value = v = refInfo.valueComputation.get(); + return (T) v; + } finally { + refInfo.lock.unlock(); + } + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java index 34ffee81c..9cd5435f7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java @@ -26,9 +26,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Table; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.registry.state.Property; -import com.sk89q.worldedit.world.registry.BlockRegistry; import java.util.Collections; import java.util.Comparator; @@ -60,7 +58,6 @@ public class BlockState implements BlockStateHolder { } static Map, Object>, BlockState> generateStateMap(BlockType blockType) { - BlockRegistry registry = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getRegistries().getBlockRegistry(); Map, Object>, BlockState> stateMap = new LinkedHashMap<>(); List> properties = blockType.getProperties(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java index e86ee7e83..66f286ca5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java @@ -19,28 +19,26 @@ package com.sk89q.worldedit.world.block; -import static com.google.common.base.Preconditions.checkArgument; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.registry.Keyed; import com.sk89q.worldedit.registry.NamespacedRegistry; import com.sk89q.worldedit.registry.state.Property; +import com.sk89q.worldedit.util.concurrency.LazyReference; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.registry.BlockMaterial; import com.sk89q.worldedit.world.registry.LegacyMapper; -import java.util.ArrayList; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import java.util.function.Supplier; -import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; public class BlockType implements Keyed { @@ -48,11 +46,18 @@ public class BlockType implements Keyed { private final String id; private final Function values; - private final AtomicReference defaultState = new AtomicReference<>(); - private final AtomicReference emptyFuzzy = new AtomicReference<>(); - private final AtomicReference>> properties = new AtomicReference<>(); - private final AtomicReference blockMaterial = new AtomicReference<>(); - private final AtomicReference, Object>, BlockState>> blockStatesMap = new AtomicReference<>(); + private final LazyReference defaultState + = LazyReference.from(this::computeDefaultState); + private final LazyReference emptyFuzzy + = LazyReference.from(() -> new FuzzyBlockState(this)); + private final LazyReference>> properties + = LazyReference.from(() -> ImmutableMap.copyOf(WorldEdit.getInstance().getPlatformManager() + .queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getProperties(this))); + private final LazyReference blockMaterial + = LazyReference.from(() -> WorldEdit.getInstance().getPlatformManager() + .queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getMaterial(this)); + private final LazyReference, Object>, BlockState>> blockStatesMap + = LazyReference.from(() -> BlockState.generateStateMap(this)); public BlockType(String id) { this(id, null); @@ -67,24 +72,16 @@ public class BlockType implements Keyed { this.values = values; } - private T updateField(AtomicReference field, Supplier value) { - T result = field.get(); - if (result == null) { - // swap in new value, if someone doesn't beat us - T update = value.get(); - if (field.compareAndSet(null, update)) { - // use ours - result = update; - } else { - // update to real value - result = field.get(); - } + private BlockState computeDefaultState() { + BlockState defaultState = Iterables.getFirst(getBlockStatesMap().values(), null); + if (values != null) { + defaultState = values.apply(defaultState); } - return result; + return defaultState; } private Map, Object>, BlockState> getBlockStatesMap() { - return updateField(blockStatesMap, () -> BlockState.generateStateMap(this)); + return blockStatesMap.getValue(); } /** @@ -117,8 +114,7 @@ public class BlockType implements Keyed { * @return The properties map */ public Map> getPropertyMap() { - return updateField(properties, () -> ImmutableMap.copyOf(WorldEdit.getInstance().getPlatformManager() - .queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getProperties(this))); + return properties.getValue(); } /** @@ -150,17 +146,11 @@ public class BlockType implements Keyed { * @return The default state */ public BlockState getDefaultState() { - return updateField(defaultState, () -> { - BlockState defaultState = new ArrayList<>(getBlockStatesMap().values()).get(0); - if (values != null) { - defaultState = values.apply(defaultState); - } - return defaultState; - }); + return defaultState.getValue(); } public FuzzyBlockState getFuzzyMatcher() { - return updateField(emptyFuzzy, () -> new FuzzyBlockState(this)); + return emptyFuzzy.getValue(); } /** @@ -208,8 +198,7 @@ public class BlockType implements Keyed { * @return The material */ public BlockMaterial getMaterial() { - return updateField(blockMaterial, () -> WorldEdit.getInstance().getPlatformManager() - .queryCapability(Capability.GAME_HOOKS).getRegistries().getBlockRegistry().getMaterial(this)); + return blockMaterial.getValue(); } /** diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java index d26d955d0..de9cce3cc 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java @@ -27,9 +27,9 @@ import com.sk89q.worldedit.internal.expression.parser.ParserException; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import static java.lang.Math.atan2; import static java.lang.Math.sin; @@ -37,12 +37,16 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ExpressionTest { + + private Platform mockPlat = mock(Platform.class); + @BeforeEach public void setup() { - Platform mockPlat = Mockito.mock(Platform.class); - Mockito.when(mockPlat.getConfiguration()).thenReturn(new LocalConfiguration() { + when(mockPlat.getConfiguration()).thenReturn(new LocalConfiguration() { @Override public void load() { } @@ -50,6 +54,11 @@ public class ExpressionTest { WorldEdit.getInstance().getPlatformManager().register(mockPlat); } + @AfterEach + public void tearDown() { + WorldEdit.getInstance().getPlatformManager().unregister(mockPlat); + } + @Test public void testEvaluate() throws ExpressionException { // check diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/util/VariedVectorsProvider.java b/worldedit-core/src/test/java/com/sk89q/worldedit/util/VariedVectorsProvider.java new file mode 100644 index 000000000..10a835392 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/util/VariedVectorsProvider.java @@ -0,0 +1,133 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util; + +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import com.sk89q.worldedit.math.BlockVector3; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Argument provider for various vectors. + */ +public final class VariedVectorsProvider implements ArgumentsProvider, AnnotationConsumer { + + @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @ArgumentsSource(VariedVectorsProvider.class) + @ParameterizedTest(name = ParameterizedTest.ARGUMENTS_PLACEHOLDER) + public @interface Test { + + /** + * If {@code true}, provide a non-matching vector from + * the existing vectors set as well. This will nearly + * square the number of tests executed, since it will + * test every non-matching vector. + */ + boolean provideNonMatching() default false; + + } + + private static final int WORLD_XZ_MINMAX = 30_000_000; + private static final long WORLD_Y_MAX = Integer.MAX_VALUE; + private static final long WORLD_Y_MIN = Integer.MIN_VALUE; + + // For better coverage assurance, increase these values for a local Gradle run. + // Don't do it for IntelliJ, it'll probably run out of memory. + private static final int DIVISIONS_XZ = Integer.getInteger("variedvecs.divisions.xz", 5); + private static final int DIVISIONS_Y = Integer.getInteger("variedvecs.divisions.y", 5); + + private static final int XZ_STEP = (WORLD_XZ_MINMAX * 2) / DIVISIONS_XZ; + private static final long Y_STEP = (WORLD_Y_MAX * 2) / DIVISIONS_Y; + + private static final Set ALWAYS_INCLUDE = + ImmutableSet.of(BlockVector3.ZERO, BlockVector3.ONE, + BlockVector3.at(-WORLD_XZ_MINMAX, 0, -WORLD_XZ_MINMAX), + BlockVector3.at(WORLD_XZ_MINMAX, 0, WORLD_XZ_MINMAX), + BlockVector3.at(-WORLD_XZ_MINMAX, WORLD_Y_MAX, -WORLD_XZ_MINMAX), + BlockVector3.at(WORLD_XZ_MINMAX, WORLD_Y_MAX, WORLD_XZ_MINMAX)); + + private boolean provideNonMatching; + + @Override + public void accept(Test test) { + provideNonMatching = test.provideNonMatching(); + } + + @Override + public Stream provideArguments(ExtensionContext context) { + if (provideNonMatching) { + return makeVectorsStream() + .flatMap(vec -> makeVectorsStream().filter(v -> !v.equals(vec)) + .map(v -> Arguments.of(vec, v))); + } + return makeVectorsStream().map(Arguments::of); + } + + public static Stream makeVectorsStream() { + return Stream.concat( + ALWAYS_INCLUDE.stream(), + Streams.stream(generateVectors()).filter(v -> !ALWAYS_INCLUDE.contains(v)) + ); + } + + private static Iterator generateVectors() { + return new AbstractIterator() { + + private int x = -WORLD_XZ_MINMAX + 1; + private int z = -WORLD_XZ_MINMAX + 1; + private long y = WORLD_Y_MAX; + + @Override + protected BlockVector3 computeNext() { + if (x > WORLD_XZ_MINMAX) { + return endOfData(); + } + BlockVector3 newVector = BlockVector3.at(x, (int) y, z); + y += Y_STEP; + if (y > WORLD_Y_MAX) { + y = 0; + z += XZ_STEP; + if (z > WORLD_XZ_MINMAX) { + z = -WORLD_XZ_MINMAX; + x += XZ_STEP; + } + } + return newVector; + } + }; + } +} diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java new file mode 100644 index 000000000..64b18c84e --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java @@ -0,0 +1,588 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.collection; + +import com.google.common.collect.ImmutableMap; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extension.platform.PlatformManager; +import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.registry.Registry; +import com.sk89q.worldedit.util.VariedVectorsProvider; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.registry.BundledRegistries; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.AbstractMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@DisplayName("An ordered block map") +class BlockMapTest { + + private static Platform mockedPlatform = mock(Platform.class); + + @BeforeAll + static void setupFakePlatform() { + when(mockedPlatform.getRegistries()).thenReturn(new BundledRegistries() { + }); + when(mockedPlatform.getCapabilities()).thenReturn(ImmutableMap.of( + Capability.WORLD_EDITING, Preference.PREFERRED, + Capability.GAME_HOOKS, Preference.PREFERRED + )); + PlatformManager platformManager = WorldEdit.getInstance().getPlatformManager(); + platformManager.register(mockedPlatform); + + registerBlock("minecraft:air"); + registerBlock("minecraft:oak_wood"); + } + + @AfterAll + static void tearDownFakePlatform() throws Exception { + WorldEdit.getInstance().getPlatformManager().unregister(mockedPlatform); + Field map = Registry.class.getDeclaredField("map"); + map.setAccessible(true); + ((Map) map.get(BlockType.REGISTRY)).clear(); + } + + private static void registerBlock(String id) { + BlockType.REGISTRY.register(id, new BlockType(id)); + } + + @Mock + private Function function; + @Mock + private BiFunction biFunction; + @Mock + private BiFunction mergeFunction; + @Mock + private BiConsumer biConsumer; + + private final BaseBlock air = checkNotNull(BlockTypes.AIR).getDefaultState().toBaseBlock(); + private final BaseBlock oakWood = checkNotNull(BlockTypes.OAK_WOOD).getDefaultState().toBaseBlock(); + + private BlockMap map = BlockMap.create(); + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + } + + @AfterEach + void tearDown() { + map.clear(); + } + + @Test + @DisplayName("throws ClassCastException if invalid argument to get") + void throwsFromGetOnInvalidArgument() { + assertThrows(ClassCastException.class, () -> map.get("")); + } + + @Nested + @DisplayName("when created") + class WhenCreated { + + @Test + @DisplayName("is empty") + void isEmpty() { + assertEquals(0, map.size()); + } + + @Test + @DisplayName("is equal to another empty map") + void isEqualToEmptyMap() { + assertEquals(ImmutableMap.of(), map); + } + + @Test + @DisplayName("has the same hashCode as another empty map") + void isHashCodeEqualToEmptyMap() { + assertEquals(ImmutableMap.of().hashCode(), map.hashCode()); + } + + @Test + @DisplayName("returns `null` from get") + void returnsNullFromGet() { + assertNull(map.get(BlockVector3.ZERO)); + } + + @Test + @DisplayName("contains no keys") + void containsNoKeys() { + assertEquals(0, map.keySet().size()); + assertFalse(map.containsKey(BlockVector3.ZERO)); + } + + @Test + @DisplayName("contains no values") + void containsNoValues() { + assertEquals(0, map.values().size()); + assertFalse(map.containsValue(air)); + } + + @Test + @DisplayName("contains no entries") + void containsNoEntries() { + assertEquals(0, map.entrySet().size()); + } + + @Test + @DisplayName("returns the default value from getOrDefault") + void returnsDefaultFromGetOrDefault() { + assertEquals(air, map.getOrDefault(BlockVector3.ZERO, air)); + } + + @Test + @DisplayName("never calls the forEach action") + void neverCallsForEachAction() { + map.forEach(biConsumer); + verifyZeroInteractions(biConsumer); + } + + @Test + @DisplayName("never calls the replaceAll function") + void neverCallsReplaceAllFunction() { + map.replaceAll(biFunction); + verifyZeroInteractions(biFunction); + } + + @Test + @DisplayName("inserts on putIfAbsent") + void insertOnPutIfAbsent() { + assertNull(map.putIfAbsent(BlockVector3.ZERO, air)); + assertEquals(1, map.size()); + assertEquals(air, map.get(BlockVector3.ZERO)); + } + + @Test + @DisplayName("remove(key) returns null") + void removeKeyReturnsNull() { + assertNull(map.remove(BlockVector3.ZERO)); + } + + @Test + @DisplayName("remove(key, value) returns false") + void removeKeyValueReturnsFalse() { + assertFalse(map.remove(BlockVector3.ZERO, air)); + } + + @Test + @DisplayName("does nothing on replace") + void doesNothingOnReplace() { + assertNull(map.replace(BlockVector3.ZERO, air)); + assertEquals(0, map.size()); + assertFalse(map.replace(BlockVector3.ZERO, null, air)); + assertEquals(0, map.size()); + } + + @Test + @DisplayName("inserts on computeIfAbsent") + void insertOnComputeIfAbsent() { + assertEquals(air, map.computeIfAbsent(BlockVector3.ZERO, k -> air)); + assertEquals(1, map.size()); + assertEquals(air, map.get(BlockVector3.ZERO)); + } + + @Test + @DisplayName("inserts on compute") + void insertOnCompute() { + assertEquals(air, map.compute(BlockVector3.ZERO, (k, v) -> air)); + assertEquals(1, map.size()); + assertEquals(air, map.get(BlockVector3.ZERO)); + } + + @Test + @DisplayName("does nothing on computeIfPresent") + void doesNothingOnComputeIfPresent() { + assertNull(map.computeIfPresent(BlockVector3.ZERO, (k, v) -> air)); + assertEquals(0, map.size()); + } + + @Test + @DisplayName("inserts on merge, without calling merge function") + void insertsOnMerge() { + assertEquals(air, map.merge(BlockVector3.ZERO, air, mergeFunction)); + assertEquals(1, map.size()); + assertEquals(air, map.get(BlockVector3.ZERO)); + verifyZeroInteractions(mergeFunction); + } + + } + + @Nested + @DisplayName("after having an entry added") + @EnabledIfSystemProperty(named = "blockmap.fulltesting", matches = "true") + class AfterEntryAdded { + + // Note: This section of tests would really benefit from + // being able to parameterize classes. It's not part of JUnit + // yet though: https://github.com/junit-team/junit5/issues/878 + + @VariedVectorsProvider.Test + @DisplayName("has a size of one") + void hasSizeOne(BlockVector3 vec) { + map.put(vec, air); + assertEquals(1, map.size()); + } + + @VariedVectorsProvider.Test + @DisplayName("is equal to another map with the same entry") + void isEqualToSimilarMap(BlockVector3 vec) { + map.put(vec, air); + assertEquals(ImmutableMap.of(vec, air), map); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("is not equal to another map with a different key") + void isNotEqualToDifferentKeyMap(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertNotEquals(ImmutableMap.of(nonMatch, air), map); + } + + @VariedVectorsProvider.Test + @DisplayName("is not equal to another map with a different value") + void isNotEqualToDifferentValueMap(BlockVector3 vec) { + map.put(vec, air); + assertNotEquals(ImmutableMap.of(vec, oakWood), map); + } + + @VariedVectorsProvider.Test + @DisplayName("is not equal to an empty map") + void isNotEqualToEmptyMap(BlockVector3 vec) { + map.put(vec, air); + assertNotEquals(ImmutableMap.of(), map); + } + + @VariedVectorsProvider.Test + @DisplayName("has the same hashCode as another map with the same entry") + void isHashCodeEqualToSimilarMap(BlockVector3 vec) { + map.put(vec, air); + assertEquals(ImmutableMap.of(vec, air).hashCode(), map.hashCode()); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("has a different hashCode from another map with a different key") + void isHashCodeNotEqualToDifferentKeyMap(BlockVector3 vec, BlockVector3 nonMatch) { + assumeFalse(vec.hashCode() == nonMatch.hashCode(), + "Vectors have equivalent hashCodes, maps will too."); + map.put(vec, air); + assertNotEquals(ImmutableMap.of(nonMatch, air).hashCode(), map.hashCode()); + } + + @VariedVectorsProvider.Test + @DisplayName("has a different hashCode from another map with a different value") + void isHashCodeNotEqualToDifferentValueMap(BlockVector3 vec) { + map.put(vec, air); + assertNotEquals(ImmutableMap.of(vec, oakWood).hashCode(), map.hashCode()); + } + + @VariedVectorsProvider.Test + @DisplayName("has a different hashCode from an empty map") + void isHashCodeNotEqualToEmptyMap(BlockVector3 vec) { + map.put(vec, air); + assertNotEquals(ImmutableMap.of().hashCode(), map.hashCode()); + } + + @VariedVectorsProvider.Test + @DisplayName("returns value from get") + void returnsValueFromGet(BlockVector3 vec) { + map.put(vec, air); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("returns `null` from get with different key") + void returnsValueFromGet(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertNotEquals(air, map.get(nonMatch)); + } + + @VariedVectorsProvider.Test + @DisplayName("contains the key") + void containsTheKey(BlockVector3 vec) { + map.put(vec, air); + assertEquals(1, map.keySet().size()); + assertTrue(map.keySet().contains(vec)); + assertTrue(map.containsKey(vec)); + } + + @VariedVectorsProvider.Test + @DisplayName("contains the value") + void containsTheValue(BlockVector3 vec) { + map.put(vec, air); + assertEquals(1, map.values().size()); + assertTrue(map.values().contains(air)); + assertTrue(map.containsValue(air)); + } + + @VariedVectorsProvider.Test + @DisplayName("contains the entry") + void containsTheEntry(BlockVector3 vec) { + map.put(vec, air); + assertEquals(1, map.entrySet().size()); + assertEquals(new AbstractMap.SimpleImmutableEntry<>(vec, air), map.entrySet().iterator().next()); + } + + @VariedVectorsProvider.Test + @DisplayName("returns the provided value from getOrDefault") + void returnsProvidedFromGetOrDefault(BlockVector3 vec) { + map.put(vec, air); + assertEquals(air, map.getOrDefault(vec, oakWood)); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("returns the default value from getOrDefault with a different key") + void returnsDefaultFromGetOrDefaultWrongKey(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertEquals(oakWood, map.getOrDefault(nonMatch, oakWood)); + } + + @VariedVectorsProvider.Test + @DisplayName("calls the forEach action once") + void neverCallsForEachAction(BlockVector3 vec) { + map.put(vec, air); + map.forEach(biConsumer); + verify(biConsumer).accept(vec, air); + verifyNoMoreInteractions(biConsumer); + } + + @VariedVectorsProvider.Test + @DisplayName("replaces value using replaceAll") + void neverCallsReplaceAllFunction(BlockVector3 vec) { + map.put(vec, air); + map.replaceAll((v, b) -> oakWood); + assertEquals(oakWood, map.get(vec)); + } + + @VariedVectorsProvider.Test + @DisplayName("does not insert on `putIfAbsent`") + void noInsertOnPutIfAbsent(BlockVector3 vec) { + map.put(vec, air); + assertEquals(air, map.putIfAbsent(vec, oakWood)); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("inserts on `putIfAbsent` to a different key") + void insertOnPutIfAbsentDifferentKey(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertNull(map.putIfAbsent(nonMatch, oakWood)); + assertEquals(2, map.size()); + assertEquals(air, map.get(vec)); + assertEquals(oakWood, map.get(nonMatch)); + } + + @VariedVectorsProvider.Test + @DisplayName("remove(key) returns the old value") + void removeKeyReturnsOldValue(BlockVector3 vec) { + map.put(vec, air); + assertEquals(air, map.remove(vec)); + assertEquals(0, map.size()); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("remove(nonMatch) returns null") + void removeNonMatchingKeyReturnsNull(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertNull(map.remove(nonMatch)); + assertEquals(1, map.size()); + } + + @VariedVectorsProvider.Test + @DisplayName("remove(key, value) returns true") + void removeKeyValueReturnsTrue(BlockVector3 vec) { + map.put(vec, air); + assertTrue(map.remove(vec, air)); + assertEquals(0, map.size()); + } + + @VariedVectorsProvider.Test + @DisplayName("remove(key, value) returns false for wrong value") + void removeKeyValueReturnsFalseWrongValue(BlockVector3 vec) { + map.put(vec, air); + assertFalse(map.remove(vec, oakWood)); + assertEquals(1, map.size()); + } + + @VariedVectorsProvider.Test + @DisplayName("replaces value at key") + void replacesValueAtKey(BlockVector3 vec) { + map.put(vec, air); + + assertEquals(air, map.replace(vec, oakWood)); + assertEquals(1, map.size()); + assertEquals(oakWood, map.get(vec)); + + assertTrue(map.replace(vec, oakWood, air)); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("does not replace value at different key") + void doesNotReplaceAtDifferentKey(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + + assertNull(map.replace(nonMatch, oakWood)); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + + assertFalse(map.replace(nonMatch, air, oakWood)); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test + @DisplayName("does not insert on computeIfAbsent") + void doesNotInsertComputeIfAbsent(BlockVector3 vec) { + map.put(vec, air); + assertEquals(air, map.computeIfAbsent(vec, k -> { + assertEquals(vec, k); + return oakWood; + })); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("inserts on computeIfAbsent with different key") + void insertsOnComputeIfAbsentDifferentKey(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertEquals(oakWood, map.computeIfAbsent(nonMatch, k -> { + assertEquals(nonMatch, k); + return oakWood; + })); + assertEquals(2, map.size()); + assertEquals(air, map.get(vec)); + assertEquals(oakWood, map.get(nonMatch)); + } + + @VariedVectorsProvider.Test + @DisplayName("replaces on compute") + void replaceOnCompute(BlockVector3 vec) { + map.put(vec, air); + assertEquals(oakWood, map.compute(vec, (k, v) -> { + assertEquals(vec, k); + assertEquals(air, v); + return oakWood; + })); + assertEquals(1, map.size()); + assertEquals(oakWood, map.get(vec)); + assertNull(map.compute(vec, (k, v) -> null)); + assertEquals(0, map.size()); + } + + @VariedVectorsProvider.Test(provideNonMatching = true) + @DisplayName("inserts on compute with different key") + void insertOnComputeDifferentKey(BlockVector3 vec, BlockVector3 nonMatch) { + map.put(vec, air); + assertEquals(oakWood, map.compute(nonMatch, (k, v) -> { + assertEquals(nonMatch, k); + assertNull(v); + return oakWood; + })); + assertEquals(2, map.size()); + assertEquals(air, map.get(vec)); + assertEquals(oakWood, map.get(nonMatch)); + assertNull(map.compute(nonMatch, (k, v) -> null)); + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + } + + @VariedVectorsProvider.Test + @DisplayName("replaces on computeIfPresent") + void replacesOnComputeIfPresent(BlockVector3 vec) { + map.put(vec, air); + assertEquals(oakWood, map.computeIfPresent(vec, (k, v) -> { + assertEquals(vec, k); + assertEquals(air, v); + return oakWood; + })); + assertEquals(1, map.size()); + assertEquals(oakWood, map.get(vec)); + assertNull(map.computeIfPresent(vec, (k, v) -> null)); + assertEquals(0, map.size()); + } + + @VariedVectorsProvider.Test + @DisplayName("inserts on merge, with call to merge function") + void insertsOnMerge(BlockVector3 vec) { + map.put(vec, air); + assertEquals(oakWood, map.merge(vec, oakWood, (o, n) -> { + assertEquals(air, o); + assertEquals(oakWood, n); + return n; + })); + assertEquals(1, map.size()); + assertEquals(oakWood, map.get(vec)); + } + + } + + @Test + @DisplayName("contains all inserted vectors") + void containsAllInsertedVectors() { + Set allVectors = VariedVectorsProvider.makeVectorsStream().collect(Collectors.toSet()); + for (BlockVector3 vec : allVectors) { + map.put(vec, air); + } + assertEquals(allVectors.size(), map.size()); + assertEquals(allVectors, map.keySet()); + for (Map.Entry entry : map.entrySet()) { + assertTrue(allVectors.contains(entry.getKey())); + assertEquals(air, entry.getValue()); + } + } + +} diff --git a/worldedit-core/src/test/resources/junit-platform.properties b/worldedit-core/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..ee7c4fad3 --- /dev/null +++ b/worldedit-core/src/test/resources/junit-platform.properties @@ -0,0 +1,5 @@ +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=same_thread +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.config.dynamic.factor=4 diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java index a3bc382f4..377633000 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/config/ConfigurateConfiguration.java @@ -129,5 +129,7 @@ public class ConfigurateConfiguration extends LocalConfiguration { String type = node.getNode("shell-save-type").getString("").trim(); shellSaveType = type.equals("") ? null : type; + + extendedYLimit = node.getNode("compat", "extended-y-limit").getBoolean(false); } } \ No newline at end of file