mirror of
https://github.com/Creators-of-Create/Create.git
synced 2024-12-15 15:43:42 +01:00
Spicy light update listening api.
- Round 1, no profiling done yet, not everything uses it. - WeakHashSet could be useful elsewhere, too.
This commit is contained in:
parent
1310b88828
commit
20189a86fc
11 changed files with 400 additions and 27 deletions
|
@ -1,10 +1,15 @@
|
|||
package com.simibubi.create.content.contraptions.components.structureMovement;
|
||||
|
||||
import net.minecraft.world.ILightReader;
|
||||
import net.minecraft.world.LightType;
|
||||
|
||||
import com.simibubi.create.content.contraptions.components.structureMovement.render.RenderedContraption;
|
||||
import com.simibubi.create.foundation.render.backend.light.GridAlignedBB;
|
||||
import com.simibubi.create.foundation.render.backend.light.LightUpdateListener;
|
||||
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
|
||||
import com.simibubi.create.foundation.render.backend.light.LightVolume;
|
||||
|
||||
public abstract class ContraptionLighter<C extends Contraption> {
|
||||
public abstract class ContraptionLighter<C extends Contraption> implements LightUpdateListener {
|
||||
protected final C contraption;
|
||||
public final LightVolume lightVolume;
|
||||
|
||||
|
@ -21,14 +26,8 @@ public abstract class ContraptionLighter<C extends Contraption> {
|
|||
|
||||
lightVolume.initialize(contraption.entity.world);
|
||||
scheduleRebuild = true;
|
||||
}
|
||||
|
||||
protected GridAlignedBB contraptionBoundsToVolume(GridAlignedBB bounds) {
|
||||
bounds.grow(1); // so we have at least enough data on the edges to avoid artifacts and have smooth lighting
|
||||
bounds.minY = Math.max(bounds.minY, 0);
|
||||
bounds.maxY = Math.min(bounds.maxY, 255);
|
||||
|
||||
return bounds;
|
||||
startListening();
|
||||
}
|
||||
|
||||
public void tick(RenderedContraption owner) {
|
||||
|
@ -39,4 +38,26 @@ public abstract class ContraptionLighter<C extends Contraption> {
|
|||
}
|
||||
|
||||
public abstract GridAlignedBB getContraptionBounds();
|
||||
|
||||
@Override
|
||||
public void onLightUpdate(ILightReader world, LightType type, GridAlignedBB changed) {
|
||||
lightVolume.notifyLightUpdate(world, type, changed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
|
||||
lightVolume.notifyLightPacket(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
protected void startListening() {
|
||||
LightUpdater.getInstance().startListening(bounds, this);
|
||||
}
|
||||
|
||||
protected GridAlignedBB contraptionBoundsToVolume(GridAlignedBB bounds) {
|
||||
bounds.grow(1); // so we have at least enough data on the edges to avoid artifacts and have smooth lighting
|
||||
bounds.minY = Math.max(bounds.minY, 0);
|
||||
bounds.maxY = Math.min(bounds.maxY, 255);
|
||||
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ public class NonStationaryLighter<C extends Contraption> extends ContraptionLigh
|
|||
if (!contraptionBounds.sameAs(bounds)) {
|
||||
lightVolume.move(contraption.entity.world, contraptionBoundsToVolume(contraptionBounds));
|
||||
bounds = contraptionBounds;
|
||||
|
||||
startListening();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,6 @@ public abstract class ActorInstance {
|
|||
public void beginFrame() { }
|
||||
|
||||
protected int localBlockLight() {
|
||||
return modelManager.contraption.renderWorld.getLightLevel(LightType.BLOCK, context.localPos);
|
||||
return modelManager.getContraption().renderWorld.getLightLevel(LightType.BLOCK, context.localPos);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,16 +20,17 @@ import net.minecraft.world.gen.feature.template.Template;
|
|||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ContraptionKineticRenderer extends InstancedTileRenderer<ContraptionProgram> {
|
||||
|
||||
protected ArrayList<com.simibubi.create.content.contraptions.components.structureMovement.render.ActorInstance> actors = new ArrayList<>();
|
||||
|
||||
public final RenderedContraption contraption;
|
||||
private final WeakReference<RenderedContraption> contraption;
|
||||
|
||||
ContraptionKineticRenderer(RenderedContraption contraption) {
|
||||
this.contraption = contraption;
|
||||
this.contraption = new WeakReference<>(contraption);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -77,6 +78,10 @@ public class ContraptionKineticRenderer extends InstancedTileRenderer<Contraptio
|
|||
return getMaterial(KineticRenderMaterials.ACTORS);
|
||||
}
|
||||
|
||||
public RenderedContraption getContraption() {
|
||||
return contraption.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos getOriginCoordinate() {
|
||||
return BlockPos.ZERO;
|
||||
|
|
|
@ -54,12 +54,6 @@ public class ContraptionRenderDispatcher {
|
|||
public static final Compartment<Pair<Contraption, Integer>> CONTRAPTION = new Compartment<>();
|
||||
protected static PlacementSimulationWorld renderWorld;
|
||||
|
||||
public static void notifyLightUpdate(ILightReader world, LightType type, SectionPos pos) {
|
||||
for (RenderedContraption renderer : renderers.values()) {
|
||||
renderer.getLighter().lightVolume.notifyLightUpdate(world, type, pos);
|
||||
}
|
||||
}
|
||||
|
||||
public static void notifyLightPacket(ILightReader world, int chunkX, int chunkZ) {
|
||||
for (RenderedContraption renderer : renderers.values()) {
|
||||
renderer.getLighter().lightVolume.notifyLightPacket(world, chunkX, chunkZ);
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
|
||||
import com.simibubi.create.content.contraptions.components.structureMovement.render.ContraptionRenderDispatcher;
|
||||
import com.simibubi.create.foundation.render.backend.light.ILightListener;
|
||||
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientChunkProvider;
|
||||
import net.minecraft.util.math.SectionPos;
|
||||
|
@ -54,6 +55,6 @@ public abstract class LightUpdateMixin extends AbstractChunkProvider {
|
|||
});
|
||||
}
|
||||
|
||||
ContraptionRenderDispatcher.notifyLightUpdate(world, type, pos);
|
||||
LightUpdater.getInstance().onLightUpdate(world, type, pos.asLong());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package com.simibubi.create.foundation.mixin;
|
||||
|
||||
import com.simibubi.create.CreateClient;
|
||||
import com.simibubi.create.content.contraptions.components.structureMovement.render.ContraptionRenderDispatcher;
|
||||
import com.simibubi.create.foundation.render.backend.RenderWork;
|
||||
import com.simibubi.create.foundation.render.backend.light.ILightListener;
|
||||
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.network.play.ClientPlayNetHandler;
|
||||
import net.minecraft.client.world.ClientWorld;
|
||||
import net.minecraft.network.play.server.SUpdateLightPacket;
|
||||
import net.minecraft.util.math.SectionPos;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
import java.util.Map;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
|
@ -30,14 +36,16 @@ public class NetworkLightUpdateMixin {
|
|||
|
||||
if (chunk != null) {
|
||||
chunk.getTileEntityMap()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(tile -> tile instanceof ILightListener)
|
||||
.map(tile -> (ILightListener) tile)
|
||||
.forEach(ILightListener::onChunkLightUpdate);
|
||||
.values()
|
||||
.forEach(tile -> {
|
||||
CreateClient.kineticRenderer.get(world).onLightUpdate(tile);
|
||||
|
||||
if (tile instanceof ILightListener)
|
||||
((ILightListener) tile).onChunkLightUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
ContraptionRenderDispatcher.notifyLightPacket(world, chunkX, chunkZ);
|
||||
LightUpdater.getInstance().onLightPacket(world, chunkX, chunkZ);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.simibubi.create.foundation.render.backend.light;
|
||||
|
||||
import net.minecraft.world.ILightReader;
|
||||
import net.minecraft.world.LightType;
|
||||
|
||||
/**
|
||||
* Anything can implement this, implementors should call {@link LightUpdater#startListening}
|
||||
* appropriately to make sure they get the updates they want.
|
||||
*/
|
||||
public interface LightUpdateListener {
|
||||
|
||||
/**
|
||||
* Called when a light updates in a chunk the implementor cares about.
|
||||
*/
|
||||
void onLightUpdate(ILightReader world, LightType type, GridAlignedBB changed);
|
||||
|
||||
/**
|
||||
* Called when the server sends light data to the client.
|
||||
*/
|
||||
default void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
|
||||
GridAlignedBB changedVolume = GridAlignedBB.fromChunk(chunkX, chunkZ);
|
||||
|
||||
onLightUpdate(world, LightType.BLOCK, changedVolume);
|
||||
onLightUpdate(world, LightType.SKY, changedVolume);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package com.simibubi.create.foundation.render.backend.light;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.SectionPos;
|
||||
import net.minecraft.world.ILightReader;
|
||||
import net.minecraft.world.LightType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.LongConsumer;
|
||||
import com.simibubi.create.foundation.utility.WeakHashSet;
|
||||
|
||||
/**
|
||||
* By using WeakReferences we can automatically remove listeners when they are garbage collected.
|
||||
* This allows us to easily be more clever about how we store the listeners. Each listener is associated
|
||||
* with 2 sets of longs indicating what chunks and sections each listener is in. Additionally, a reverse
|
||||
* mapping is created to allow for fast lookups when light updates. The reverse mapping is more interesting,
|
||||
* but {@link #listenersToSections}, and {@link #listenersToChunks} are used to know what sections and
|
||||
* chunks we need to remove the listeners from if they re-subscribe. Otherwise, listeners could get updates
|
||||
* they no longer care about. This is done in {@link #clearSections} and {@link #clearChunks}
|
||||
*/
|
||||
public class LightUpdater {
|
||||
|
||||
private static LightUpdater instance;
|
||||
|
||||
public static LightUpdater getInstance() {
|
||||
if (instance == null)
|
||||
instance = new LightUpdater();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final Long2ObjectMap<WeakHashSet<LightUpdateListener>> sections;
|
||||
private final WeakHashMap<LightUpdateListener, LongRBTreeSet> listenersToSections;
|
||||
|
||||
private final Long2ObjectMap<WeakHashSet<LightUpdateListener>> chunks;
|
||||
private final WeakHashMap<LightUpdateListener, LongRBTreeSet> listenersToChunks;
|
||||
|
||||
public LightUpdater() {
|
||||
sections = new Long2ObjectOpenHashMap<>();
|
||||
listenersToSections = new WeakHashMap<>();
|
||||
|
||||
chunks = new Long2ObjectOpenHashMap<>();
|
||||
listenersToChunks = new WeakHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener associated with the given {@link BlockPos}.
|
||||
*
|
||||
* When a light update occurs in the chunk the position is contained in,
|
||||
* {@link LightUpdateListener#onLightUpdate} will be called.
|
||||
*
|
||||
* @param pos The position in the world that the listener cares about.
|
||||
* @param listener The object that wants to receive light update notifications.
|
||||
*/
|
||||
public void startListening(BlockPos pos, LightUpdateListener listener) {
|
||||
LongRBTreeSet sections = clearSections(listener);
|
||||
LongRBTreeSet chunks = clearChunks(listener);
|
||||
|
||||
long sectionPos = worldToSection(pos);
|
||||
addToSection(sectionPos, listener);
|
||||
sections.add(sectionPos);
|
||||
|
||||
long chunkPos = sectionToChunk(sectionPos);
|
||||
addToChunk(chunkPos, listener);
|
||||
chunks.add(chunkPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener associated with the given {@link GridAlignedBB}.
|
||||
*
|
||||
* When a light update occurs in any chunk spanning the given volume,
|
||||
* {@link LightUpdateListener#onLightUpdate} will be called.
|
||||
*
|
||||
* @param volume The volume in the world that the listener cares about.
|
||||
* @param listener The object that wants to receive light update notifications.
|
||||
*/
|
||||
public void startListening(GridAlignedBB volume, LightUpdateListener listener) {
|
||||
LongRBTreeSet sections = clearSections(listener);
|
||||
LongRBTreeSet chunks = clearSections(listener);
|
||||
|
||||
int minX = SectionPos.toChunk(volume.minX);
|
||||
int minY = SectionPos.toChunk(volume.minY);
|
||||
int minZ = SectionPos.toChunk(volume.minZ);
|
||||
int maxX = SectionPos.toChunk(volume.maxX);
|
||||
int maxY = SectionPos.toChunk(volume.maxY);
|
||||
int maxZ = SectionPos.toChunk(volume.maxZ);
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
long sectionPos = SectionPos.asLong(x, y, z);
|
||||
addToSection(sectionPos, listener);
|
||||
sections.add(sectionPos);
|
||||
}
|
||||
long chunkPos = SectionPos.asLong(x, 0, z);
|
||||
addToChunk(chunkPos, listener);
|
||||
chunks.add(chunkPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch light updates to all registered {@link LightUpdateListener}s.
|
||||
*
|
||||
* @param world The world in which light was updated.
|
||||
* @param type The type of light that changed.
|
||||
* @param sectionPos A long representing the section position where light changed.
|
||||
*/
|
||||
public void onLightUpdate(ILightReader world, LightType type, long sectionPos) {
|
||||
WeakHashSet<LightUpdateListener> set = sections.get(sectionPos);
|
||||
|
||||
if (set == null || set.isEmpty()) return;
|
||||
|
||||
GridAlignedBB chunkBox = GridAlignedBB.fromSection(SectionPos.from(sectionPos));
|
||||
|
||||
for (LightUpdateListener listener : set) {
|
||||
listener.onLightUpdate(world, type, chunkBox.copy());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch light updates to all registered {@link LightUpdateListener}s
|
||||
* when the server sends lighting data for an entire chunk.
|
||||
*
|
||||
* @param world The world in which light was updated.
|
||||
*/
|
||||
public void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
|
||||
|
||||
long chunkPos = SectionPos.asLong(chunkX, 0, chunkZ);
|
||||
|
||||
WeakHashSet<LightUpdateListener> set = chunks.get(chunkPos);
|
||||
|
||||
if (set == null || set.isEmpty()) return;
|
||||
|
||||
for (LightUpdateListener listener : set) {
|
||||
listener.onLightPacket(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LongRBTreeSet clearChunks(LightUpdateListener listener) {
|
||||
return clear(listener, listenersToChunks, chunks);
|
||||
}
|
||||
|
||||
private LongRBTreeSet clearSections(LightUpdateListener listener) {
|
||||
return clear(listener, listenersToSections, sections);
|
||||
}
|
||||
|
||||
private LongRBTreeSet clear(LightUpdateListener listener, WeakHashMap<LightUpdateListener, LongRBTreeSet> listeners, Long2ObjectMap<WeakHashSet<LightUpdateListener>> lookup) {
|
||||
LongRBTreeSet set = listeners.get(listener);
|
||||
|
||||
if (set == null) {
|
||||
set = new LongRBTreeSet();
|
||||
listeners.put(listener, set);
|
||||
} else {
|
||||
set.forEach((LongConsumer) l -> {
|
||||
WeakHashSet<LightUpdateListener> listeningSections = lookup.get(l);
|
||||
|
||||
if (listeningSections != null) listeningSections.remove(listener);
|
||||
});
|
||||
|
||||
set.clear();
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private void addToSection(long sectionPos, LightUpdateListener listener) {
|
||||
getOrCreate(sections, sectionPos).add(listener);
|
||||
}
|
||||
|
||||
private void addToChunk(long chunkPos, LightUpdateListener listener) {
|
||||
getOrCreate(chunks, chunkPos).add(listener);
|
||||
}
|
||||
|
||||
private WeakHashSet<LightUpdateListener> getOrCreate(Long2ObjectMap<WeakHashSet<LightUpdateListener>> sections, long chunkPos) {
|
||||
WeakHashSet<LightUpdateListener> set = sections.get(chunkPos);
|
||||
|
||||
if (set == null) {
|
||||
set = new WeakHashSet<>();
|
||||
sections.put(chunkPos, set);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
public static long worldToSection(BlockPos pos) {
|
||||
return SectionPos.asLong(pos.getX(), pos.getY(), pos.getZ());
|
||||
}
|
||||
|
||||
public static long sectionToChunk(long sectionPos) {
|
||||
return sectionPos & 0xFFFFFFFFFFF_00000L;
|
||||
}
|
||||
}
|
|
@ -123,17 +123,21 @@ public class LightVolume {
|
|||
}
|
||||
}
|
||||
|
||||
public void notifyLightUpdate(ILightReader world, LightType type, SectionPos location) {
|
||||
GridAlignedBB changedVolume = GridAlignedBB.fromSection(location);
|
||||
public void notifyLightUpdate(ILightReader world, LightType type, GridAlignedBB changedVolume) {
|
||||
if (removed)
|
||||
return;
|
||||
|
||||
if (!changedVolume.intersects(sampleVolume))
|
||||
return;
|
||||
changedVolume.intersectAssign(sampleVolume); // compute the region contained by us that has dirty lighting data.
|
||||
changedVolume = changedVolume.intersect(sampleVolume); // compute the region contained by us that has dirty lighting data.
|
||||
|
||||
if (type == LightType.BLOCK) copyBlock(world, changedVolume);
|
||||
else if (type == LightType.SKY) copySky(world, changedVolume);
|
||||
}
|
||||
|
||||
public void notifyLightPacket(ILightReader world, int chunkX, int chunkZ) {
|
||||
if (removed) return;
|
||||
|
||||
GridAlignedBB changedVolume = GridAlignedBB.fromChunk(chunkX, chunkZ);
|
||||
if (!changedVolume.intersects(sampleVolume))
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package com.simibubi.create.foundation.utility;
|
||||
|
||||
import net.minecraft.util.Unit;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class WeakHashSet<T> extends AbstractSet<T> {
|
||||
|
||||
WeakHashMap<T, Unit> map;
|
||||
|
||||
public WeakHashSet() {
|
||||
map = new WeakHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new set containing the elements in the specified
|
||||
* collection. The <tt>HashMap</tt> is created with default load factor
|
||||
* (0.75) and an initial capacity sufficient to contain the elements in
|
||||
* the specified collection.
|
||||
*
|
||||
* @param c the collection whose elements are to be placed into this set
|
||||
* @throws NullPointerException if the specified collection is null
|
||||
*/
|
||||
public WeakHashSet(Collection<? extends T> c) {
|
||||
map = new WeakHashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
|
||||
addAll(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
|
||||
* the specified initial capacity and the specified load factor.
|
||||
*
|
||||
* @param initialCapacity the initial capacity of the hash map
|
||||
* @param loadFactor the load factor of the hash map
|
||||
* @throws IllegalArgumentException if the initial capacity is less
|
||||
* than zero, or if the load factor is nonpositive
|
||||
*/
|
||||
public WeakHashSet(int initialCapacity, float loadFactor) {
|
||||
map = new WeakHashMap<>(initialCapacity, loadFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
|
||||
* the specified initial capacity and default load factor (0.75).
|
||||
*
|
||||
* @param initialCapacity the initial capacity of the hash table
|
||||
* @throws IllegalArgumentException if the initial capacity is less
|
||||
* than zero
|
||||
*/
|
||||
public WeakHashSet(int initialCapacity) {
|
||||
map = new WeakHashMap<>(initialCapacity);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return map.keySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(T t) {
|
||||
return map.put(t, Unit.INSTANCE) == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
return map.remove((T) o) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return map.containsKey((T) o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
return map.keySet().toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> c) {
|
||||
return c.stream().allMatch(map::containsKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends T> c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue