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 extends BlockVector3, ? extends BaseBlock> 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 extends BlockVector3, ? extends BaseBlock> 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 super BlockVector3, ? super BaseBlock> action) {
+ maps.forEach((groupKey, m) ->
+ m.forEach((innerKey, block) ->
+ action.accept(reconstructLocation(groupKey, innerKey), block)
+ )
+ );
+ }
+
+ @Override
+ public void replaceAll(BiFunction super BlockVector3, ? super BaseBlock, ? extends BaseBlock> 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 super BlockVector3, ? extends BaseBlock> mappingFunction) {
+ return cleanlyModifyMap(toGroupKey(key),
+ map -> map.computeIfAbsent(toInnerKey(key), ik -> mappingFunction.apply(key)));
+ }
+
+ @Override
+ public BaseBlock computeIfPresent(BlockVector3 key, BiFunction super BlockVector3, ? super BaseBlock, ? extends BaseBlock> remappingFunction) {
+ return cleanlyModifyMap(toGroupKey(key),
+ map -> map.computeIfPresent(toInnerKey(key), (ik, block) -> remappingFunction.apply(key, block)));
+ }
+
+ @Override
+ public BaseBlock compute(BlockVector3 key, BiFunction super BlockVector3, ? super BaseBlock, ? extends BaseBlock> 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 super BaseBlock, ? super BaseBlock, ? extends BaseBlock> 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 extends BlockVector3, ? extends BaseBlock> 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 extends LocatedBlock> 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 super Integer, ? super BaseBlock, ? extends BaseBlock> 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