Implement remove on set/iter views of BlockMap

Fixes #1354.

(cherry picked from commit 637da62e34e676015aa7fbad490cf6f033e55b89)
This commit is contained in:
MattBDev 2020-06-02 18:37:04 -04:00 committed by MattBDev
parent c72f3eeb58
commit 63b81b801b
3 changed files with 97 additions and 25 deletions

View File

@ -19,7 +19,6 @@
package com.sk89q.worldedit.util.collection; package com.sk89q.worldedit.util.collection;
import com.google.common.collect.AbstractIterator;
import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BaseBlock;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@ -36,6 +35,7 @@ import java.util.AbstractSet;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -43,6 +43,7 @@ import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import static com.google.common.base.Preconditions.checkState;
import static com.sk89q.worldedit.math.BitMath.fixSign; import static com.sk89q.worldedit.math.BitMath.fixSign;
import static com.sk89q.worldedit.math.BitMath.mask; import static com.sk89q.worldedit.math.BitMath.mask;
@ -245,26 +246,65 @@ public class BlockMap<V> extends AbstractMap<BlockVector3, V> {
entrySet = es = new AbstractSet<Entry<BlockVector3, V>>() { entrySet = es = new AbstractSet<Entry<BlockVector3, V>>() {
@Override @Override
public Iterator<Entry<BlockVector3, V>> iterator() { public Iterator<Entry<BlockVector3, V>> iterator() {
return new AbstractIterator<Entry<BlockVector3, V>>() { return new Iterator<Entry<BlockVector3,V>>() {
private final ObjectIterator<Long2ObjectMap.Entry<Int2ObjectMap<V>>> primaryIterator private final ObjectIterator<Long2ObjectMap.Entry<Int2ObjectMap<V>>> primaryIterator
= Long2ObjectMaps.fastIterator(maps); = Long2ObjectMaps.fastIterator(maps);
private long currentGroupKey; private Long2ObjectMap.Entry<Int2ObjectMap<V>> currentPrimaryEntry;
private ObjectIterator<Int2ObjectMap.Entry<V>> secondaryIterator; private ObjectIterator<Int2ObjectMap.Entry<V>> secondaryIterator;
private boolean finished;
private LazyEntry next;
@Override @Override
protected Entry<BlockVector3, V> computeNext() { public boolean hasNext() {
if (finished) {
return false;
}
if (next == null) {
LazyEntry proposedNext = computeNext();
if (proposedNext == null) {
finished = true;
return false;
}
next = proposedNext;
}
return true;
}
private LazyEntry computeNext() {
if (secondaryIterator == null || !secondaryIterator.hasNext()) { if (secondaryIterator == null || !secondaryIterator.hasNext()) {
if (!primaryIterator.hasNext()) { if (!primaryIterator.hasNext()) {
return endOfData(); return null;
} }
Long2ObjectMap.Entry<Int2ObjectMap<V>> next = primaryIterator.next(); currentPrimaryEntry = primaryIterator.next();
currentGroupKey = next.getLongKey(); secondaryIterator = Int2ObjectMaps.fastIterator(currentPrimaryEntry.getValue());
secondaryIterator = Int2ObjectMaps.fastIterator(next.getValue()); // be paranoid
checkState(secondaryIterator.hasNext(),
"Should not have an empty map entry, it should have been removed!");
} }
Int2ObjectMap.Entry<V> next = secondaryIterator.next(); Int2ObjectMap.Entry<V> next = secondaryIterator.next();
return new LazyEntry(currentGroupKey, next.getIntKey(), next.getValue()); return new LazyEntry(currentPrimaryEntry.getLongKey(), next.getIntKey(), next.getValue());
}
@Override
public Entry<BlockVector3, V> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
LazyEntry tmp = next;
next = null;
return tmp;
}
@Override
public void remove() {
secondaryIterator.remove();
// ensure invariants hold
if (currentPrimaryEntry.getValue().isEmpty()) {
// the remove call cleared this map. call remove on the primary iter
primaryIterator.remove();
}
} }
}; };
} }
@ -364,13 +404,14 @@ public class BlockMap<V> extends AbstractMap<BlockVector3, V> {
@Override @Override
public V remove(Object key) { public V remove(Object key) {
BlockVector3 vec = (BlockVector3) key; BlockVector3 vec = (BlockVector3) key;
Int2ObjectMap<V> activeMap = maps.get(toGroupKey(vec)); long groupKey = toGroupKey(vec);
Int2ObjectMap<V> activeMap = maps.get(groupKey);
if (activeMap == null) { if (activeMap == null) {
return null; return null;
} }
V removed = activeMap.remove(toInnerKey(vec)); V removed = activeMap.remove(toInnerKey(vec));
if (activeMap.isEmpty()) { if (activeMap.isEmpty()) {
maps.remove(toGroupKey(vec)); maps.remove(groupKey);
} }
return removed; return removed;
} }
@ -429,7 +470,9 @@ public class BlockMap<V> extends AbstractMap<BlockVector3, V> {
} }
if (o instanceof BlockMap) { if (o instanceof BlockMap) {
// optimize by skipping entry translations: // optimize by skipping entry translations:
return maps.equals(((BlockMap) o).maps); @SuppressWarnings("unchecked")
BlockMap<V> other = (BlockMap<V>) o;
return maps.equals(other.maps);
} }
return super.equals(o); return super.equals(o);
} }

View File

@ -91,6 +91,7 @@ class Int2BaseBlockMap extends AbstractInt2ObjectMap<BaseBlock> {
= Int2IntMaps.fastIterator(commonMap); = Int2IntMaps.fastIterator(commonMap);
private final ObjectIterator<Entry<BaseBlock>> uncommonIter private final ObjectIterator<Entry<BaseBlock>> uncommonIter
= Int2ObjectMaps.fastIterator(uncommonMap); = Int2ObjectMaps.fastIterator(uncommonMap);
private boolean lastNextFromCommon = false;
@Override @Override
public boolean hasNext() { public boolean hasNext() {
@ -101,15 +102,26 @@ class Int2BaseBlockMap extends AbstractInt2ObjectMap<BaseBlock> {
public Entry<BaseBlock> next() { public Entry<BaseBlock> next() {
if (commonIter.hasNext()) { if (commonIter.hasNext()) {
Int2IntMap.Entry e = commonIter.next(); Int2IntMap.Entry e = commonIter.next();
lastNextFromCommon = true;
return new BasicEntry<>( return new BasicEntry<>(
e.getIntKey(), assumeAsBlock(e.getIntValue()) e.getIntKey(), assumeAsBlock(e.getIntValue())
); );
} }
if (uncommonIter.hasNext()) { if (uncommonIter.hasNext()) {
lastNextFromCommon = false;
return uncommonIter.next(); return uncommonIter.next();
} }
throw new NoSuchElementException(); throw new NoSuchElementException();
} }
@Override
public void remove() {
if (lastNextFromCommon) {
commonIter.remove();
} else {
uncommonIter.remove();
}
}
}; };
} }

View File

@ -45,11 +45,11 @@ import org.mockito.MockitoAnnotations;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -63,24 +63,23 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@DisplayName("An ordered block map") @DisplayName("An ordered block map")
class BlockMapTest { class BlockMapTest {
/* /*
private static Platform mockedPlatform = mock(Platform.class); private static final Platform MOCKED_PLATFORM = mock(Platform.class);
@BeforeAll @BeforeAll
static void setupFakePlatform() { static void setupFakePlatform() {
when(mockedPlatform.getRegistries()).thenReturn(new BundledRegistries() { when(MOCKED_PLATFORM.getRegistries()).thenReturn(new BundledRegistries() {
}); });
when(mockedPlatform.getCapabilities()).thenReturn(ImmutableMap.of( when(MOCKED_PLATFORM.getCapabilities()).thenReturn(ImmutableMap.of(
Capability.WORLD_EDITING, Preference.PREFERRED, Capability.WORLD_EDITING, Preference.PREFERRED,
Capability.GAME_HOOKS, Preference.PREFERRED Capability.GAME_HOOKS, Preference.PREFERRED
)); ));
PlatformManager platformManager = WorldEdit.getInstance().getPlatformManager(); PlatformManager platformManager = WorldEdit.getInstance().getPlatformManager();
platformManager.register(mockedPlatform); platformManager.register(MOCKED_PLATFORM);
registerBlock("minecraft:air"); registerBlock("minecraft:air");
registerBlock("minecraft:oak_wood"); registerBlock("minecraft:oak_wood");
@ -88,7 +87,7 @@ class BlockMapTest {
@AfterAll @AfterAll
static void tearDownFakePlatform() throws Exception { static void tearDownFakePlatform() throws Exception {
WorldEdit.getInstance().getPlatformManager().unregister(mockedPlatform); WorldEdit.getInstance().getPlatformManager().unregister(MOCKED_PLATFORM);
Field map = Registry.class.getDeclaredField("map"); Field map = Registry.class.getDeclaredField("map");
map.setAccessible(true); map.setAccessible(true);
((Map<?, ?>) map.get(BlockType.REGISTRY)).clear(); ((Map<?, ?>) map.get(BlockType.REGISTRY)).clear();
@ -98,8 +97,6 @@ class BlockMapTest {
BlockType.REGISTRY.register(id, new BlockType(id)); BlockType.REGISTRY.register(id, new BlockType(id));
} }
@Mock
private Function<? super BlockVector3, ? extends BaseBlock> function;
@Mock @Mock
private BiFunction<? super BlockVector3, ? super BaseBlock, ? extends BaseBlock> biFunction; private BiFunction<? super BlockVector3, ? super BaseBlock, ? extends BaseBlock> biFunction;
@Mock @Mock
@ -110,7 +107,7 @@ class BlockMapTest {
private final BaseBlock air = checkNotNull(BlockTypes.AIR).getDefaultState().toBaseBlock(); private final BaseBlock air = checkNotNull(BlockTypes.AIR).getDefaultState().toBaseBlock();
private final BaseBlock oakWood = checkNotNull(BlockTypes.OAK_WOOD).getDefaultState().toBaseBlock(); private final BaseBlock oakWood = checkNotNull(BlockTypes.OAK_WOOD).getDefaultState().toBaseBlock();
private BlockMap<BaseBlock> map = BlockMap.createForBaseBlock(); private final BlockMap<BaseBlock> map = BlockMap.createForBaseBlock();
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -186,14 +183,14 @@ class BlockMapTest {
@DisplayName("never calls the forEach action") @DisplayName("never calls the forEach action")
void neverCallsForEachAction() { void neverCallsForEachAction() {
map.forEach(biConsumer); map.forEach(biConsumer);
verifyZeroInteractions(biConsumer); verifyNoMoreInteractions(biConsumer);
} }
@Test @Test
@DisplayName("never calls the replaceAll function") @DisplayName("never calls the replaceAll function")
void neverCallsReplaceAllFunction() { void neverCallsReplaceAllFunction() {
map.replaceAll(biFunction); map.replaceAll(biFunction);
verifyZeroInteractions(biFunction); verifyNoMoreInteractions(biFunction);
} }
@Test @Test
@ -254,7 +251,7 @@ class BlockMapTest {
assertEquals(air, map.merge(BlockVector3.ZERO, air, mergeFunction)); assertEquals(air, map.merge(BlockVector3.ZERO, air, mergeFunction));
assertEquals(1, map.size()); assertEquals(1, map.size());
assertEquals(air, map.get(BlockVector3.ZERO)); assertEquals(air, map.get(BlockVector3.ZERO));
verifyZeroInteractions(mergeFunction); verifyNoMoreInteractions(mergeFunction);
} }
} }
@ -431,6 +428,26 @@ class BlockMapTest {
assertEquals(0, map.size()); assertEquals(0, map.size());
} }
@VariedVectorsProvider.Test
@DisplayName("keySet().remove(key) removes the entry from the map")
void keySetRemovePassesThrough(BlockVector3 vec) {
map.put(vec, air);
assertTrue(map.keySet().remove(vec));
assertEquals(0, map.size());
}
@VariedVectorsProvider.Test
@DisplayName("entrySet().iterator().remove() removes the entry from the map")
void entrySetIteratorRemovePassesThrough(BlockVector3 vec) {
map.put(vec, air);
Iterator<Map.Entry<BlockVector3, BaseBlock>> iterator = map.entrySet().iterator();
assertTrue(iterator.hasNext());
Map.Entry<BlockVector3, BaseBlock> entry = iterator.next();
assertEquals(entry.getKey(), vec);
iterator.remove();
assertEquals(0, map.size());
}
@VariedVectorsProvider.Test(provideNonMatching = true) @VariedVectorsProvider.Test(provideNonMatching = true)
@DisplayName("remove(nonMatch) returns null") @DisplayName("remove(nonMatch) returns null")
void removeNonMatchingKeyReturnsNull(BlockVector3 vec, BlockVector3 nonMatch) { void removeNonMatchingKeyReturnsNull(BlockVector3 vec, BlockVector3 nonMatch) {