/* * Copyright 2013 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.world.chunks.localChunkProvider; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import com.google.common.collect.Sets; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import gnu.trove.map.TShortObjectMap; import gnu.trove.map.hash.TShortObjectHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.entitySystem.Component; import org.terasology.entitySystem.entity.EntityManager; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.entitySystem.entity.EntityStore; import org.terasology.entitySystem.prefab.Prefab; import org.terasology.math.ChunkMath; import org.terasology.math.Region3i; import org.terasology.math.Side; import org.terasology.math.TeraMath; import org.terasology.math.geom.Vector3i; import org.terasology.monitoring.PerformanceMonitor; import org.terasology.monitoring.chunk.ChunkMonitor; import org.terasology.persistence.ChunkStore; import org.terasology.persistence.StorageManager; import org.terasology.utilities.concurrency.TaskMaster; import org.terasology.world.BlockEntityRegistry; import org.terasology.world.biomes.BiomeManager; import org.terasology.world.block.BeforeDeactivateBlocks; import org.terasology.world.block.Block; import org.terasology.world.block.BlockManager; import org.terasology.world.block.OnActivatedBlocks; import org.terasology.world.block.OnAddedBlocks; import org.terasology.world.chunks.Chunk; import org.terasology.world.chunks.ChunkBlockIterator; import org.terasology.world.chunks.ChunkConstants; import org.terasology.world.chunks.ChunkRegionListener; import org.terasology.world.chunks.ManagedChunk; import org.terasology.world.chunks.event.BeforeChunkUnload; import org.terasology.world.chunks.event.OnChunkGenerated; import org.terasology.world.chunks.event.OnChunkLoaded; import org.terasology.world.chunks.event.PurgeWorldEvent; import org.terasology.world.chunks.internal.ChunkImpl; import org.terasology.world.chunks.internal.ChunkRelevanceRegion; import org.terasology.world.chunks.internal.GeneratingChunkProvider; import org.terasology.world.chunks.internal.ReadyChunkInfo; import org.terasology.world.chunks.pipeline.AbstractChunkTask; import org.terasology.world.chunks.pipeline.ChunkGenerationPipeline; import org.terasology.world.chunks.pipeline.ChunkTask; import org.terasology.world.generation.impl.EntityBufferImpl; import org.terasology.world.generator.WorldGenerator; import org.terasology.world.internal.ChunkViewCore; import org.terasology.world.internal.ChunkViewCoreImpl; import org.terasology.world.propagation.light.InternalLightProcessor; import org.terasology.world.propagation.light.LightMerger; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** */ public class LocalChunkProvider implements GeneratingChunkProvider { private static final Logger logger = LoggerFactory.getLogger(LocalChunkProvider.class); private static final int UNLOAD_PER_FRAME = 64; private static final Vector3i UNLOAD_LEEWAY = Vector3i.one(); private StorageManager storageManager; private final EntityManager entityManager; private ChunkGenerationPipeline pipeline; private TaskMaster<ChunkUnloadRequest> unloadRequestTaskMaster; private WorldGenerator generator; private Map<EntityRef, ChunkRelevanceRegion> regions = Maps.newHashMap(); private Map<Vector3i, Chunk> nearCache = Maps.newConcurrentMap(); private final Set<Vector3i> preparingChunks = Sets.newHashSet(); private final BlockingQueue<ReadyChunkInfo> readyChunks = Queues.newLinkedBlockingQueue(); private List<ReadyChunkInfo> sortedReadyChunks = Lists.newArrayList(); private final BlockingQueue<TShortObjectMap<TIntList>> deactivateBlocksQueue = Queues.newLinkedBlockingQueue(); private EntityRef worldEntity = EntityRef.NULL; private ReadWriteLock regionLock = new ReentrantReadWriteLock(); private BlockManager blockManager; private BiomeManager biomeManager; private BlockEntityRegistry registry; private LightMerger<ReadyChunkInfo> lightMerger = new LightMerger<>(this); public LocalChunkProvider(StorageManager storageManager, EntityManager entityManager, WorldGenerator generator, BlockManager blockManager, BiomeManager biomeManager) { this.storageManager = storageManager; this.entityManager = entityManager; this.generator = generator; this.blockManager = blockManager; this.biomeManager = biomeManager; this.pipeline = new ChunkGenerationPipeline(new ChunkTaskRelevanceComparator()); this.unloadRequestTaskMaster = TaskMaster.createFIFOTaskMaster("Chunk-Unloader", 4); ChunkMonitor.fireChunkProviderInitialized(this); } public void setBlockEntityRegistry(BlockEntityRegistry value) { this.registry = value; } @Override public ChunkViewCore getLocalView(Vector3i centerChunkPos) { Region3i region = Region3i.createFromCenterExtents(centerChunkPos, ChunkConstants.LOCAL_REGION_EXTENTS); if (getChunk(centerChunkPos) != null) { return createWorldView(region, Vector3i.one()); } return null; } @Override public ChunkViewCore getSubviewAroundBlock(Vector3i blockPos, int extent) { Region3i region = ChunkMath.getChunkRegionAroundWorldPos(blockPos, extent); return createWorldView(region, new Vector3i(-region.min().x, -region.min().y, -region.min().z)); } @Override public ChunkViewCore getSubviewAroundChunk(Vector3i chunkPos) { Region3i region = Region3i.createFromCenterExtents(chunkPos, ChunkConstants.LOCAL_REGION_EXTENTS); if (getChunk(chunkPos) != null) { return createWorldView(region, new Vector3i(-region.min().x, -region.min().y, -region.min().z)); } return null; } private ChunkViewCore createWorldView(Region3i region, Vector3i offset) { Chunk[] chunks = new Chunk[region.sizeX() * region.sizeY() * region.sizeZ()]; for (Vector3i chunkPos : region) { Chunk chunk = nearCache.get(chunkPos); if (chunk == null || !chunk.isReady()) { return null; } chunkPos.sub(region.minX(), region.minY(), region.minZ()); int index = TeraMath.calculate3DArrayIndex(chunkPos, region.size()); chunks[index] = chunk; } return new ChunkViewCoreImpl(chunks, region, offset, blockManager.getBlock(BlockManager.AIR_ID)); } @Override public void setWorldEntity(EntityRef worldEntity) { this.worldEntity = worldEntity; } @Override public void addRelevanceEntity(EntityRef entity, Vector3i distance) { addRelevanceEntity(entity, distance, null); } @Override public void addRelevanceEntity(EntityRef entity, Vector3i distance, ChunkRegionListener listener) { if (!entity.exists()) { return; } regionLock.readLock().lock(); try { ChunkRelevanceRegion region = regions.get(entity); if (region != null) { region.setRelevanceDistance(distance); return; } } finally { regionLock.readLock().unlock(); } ChunkRelevanceRegion region = new ChunkRelevanceRegion(entity, distance); if (listener != null) { region.setListener(listener); } regionLock.writeLock().lock(); try { regions.put(entity, region); } finally { regionLock.writeLock().unlock(); } for (Vector3i pos : region.getCurrentRegion()) { Chunk chunk = getChunk(pos); if (chunk != null) { region.checkIfChunkIsRelevant(chunk); } else { createOrLoadChunk(pos); } } } @Override public void updateRelevanceEntity(EntityRef entity, Vector3i distance) { regionLock.readLock().lock(); try { ChunkRelevanceRegion region = regions.get(entity); if (region != null) { region.setRelevanceDistance(distance); } } finally { regionLock.readLock().unlock(); } } @Override public void removeRelevanceEntity(EntityRef entity) { regionLock.writeLock().lock(); try { regions.remove(entity); } finally { regionLock.writeLock().unlock(); } } @Override public void completeUpdate() { ReadyChunkInfo readyChunkInfo = lightMerger.completeMerge(); if (readyChunkInfo != null) { Chunk chunk = readyChunkInfo.getChunk(); chunk.markReady(); updateAdjacentChunksReadyFieldOf(chunk); updateAdjacentChunksReadyFieldOfAdjChunks(chunk); if (readyChunkInfo.isNewChunk()) { PerformanceMonitor.startActivity("Generating queued Entities"); readyChunkInfo.getEntities().forEach(this::generateQueuedEntities); PerformanceMonitor.endActivity(); } if (readyChunkInfo.getChunkStore() != null) { readyChunkInfo.getChunkStore().restoreEntities(); } if (!readyChunkInfo.isNewChunk()) { PerformanceMonitor.startActivity("Sending OnAddedBlocks"); readyChunkInfo.getBlockPositionMapppings().forEachEntry((id, positions) -> { if (positions.size() > 0) { blockManager.getBlock(id).getEntity().send(new OnAddedBlocks(positions, registry)); } return true; }); PerformanceMonitor.endActivity(); } PerformanceMonitor.startActivity("Sending OnActivateBlocks"); readyChunkInfo.getBlockPositionMapppings().forEachEntry((id, positions) -> { if (positions.size() > 0) { blockManager.getBlock(id).getEntity().send(new OnActivatedBlocks(positions, registry)); } return true; }); PerformanceMonitor.endActivity(); if (readyChunkInfo.isNewChunk()) { worldEntity.send(new OnChunkGenerated(readyChunkInfo.getPos())); } worldEntity.send(new OnChunkLoaded(readyChunkInfo.getPos())); } } private void generateQueuedEntities(EntityStore store) { Prefab prefab = store.getPrefab(); EntityRef entity; if (prefab != null) { entity = entityManager.create(prefab); } else { entity = entityManager.create(); } for (Component component : store.iterateComponents()) { entity.addComponent(component); } } @Override public void beginUpdate() { regionLock.readLock().lock(); try { updateRelevance(); deactivateBlocks(); checkForUnload(); makeChunksAvailable(); } finally { regionLock.readLock().unlock(); } } private void makeChunksAvailable() { List<ReadyChunkInfo> newReadyChunks = Lists.newArrayListWithExpectedSize(readyChunks.size()); readyChunks.drainTo(newReadyChunks); for (ReadyChunkInfo readyChunkInfo : newReadyChunks) { nearCache.put(readyChunkInfo.getPos(), readyChunkInfo.getChunk()); preparingChunks.remove(readyChunkInfo.getPos()); } updateRelevanceRegionsWithNewChunks(newReadyChunks); if (!newReadyChunks.isEmpty()) { sortedReadyChunks.addAll(newReadyChunks); Collections.sort(sortedReadyChunks, new ReadyChunkRelevanceComparator()); } if (!sortedReadyChunks.isEmpty()) { boolean loaded = false; for (int i = sortedReadyChunks.size() - 1; i >= 0 && !loaded; i--) { ReadyChunkInfo chunkInfo = sortedReadyChunks.get(i); PerformanceMonitor.startActivity("Make Chunk Available"); if (makeChunkAvailable(chunkInfo)) { sortedReadyChunks.remove(i); loaded = true; } PerformanceMonitor.endActivity(); } } } private void updateRelevanceRegionsWithNewChunks(List<ReadyChunkInfo> newReadyChunks) { for (ReadyChunkInfo readyChunkInfo : newReadyChunks) { for (ChunkRelevanceRegion region : regions.values()) { region.checkIfChunkIsRelevant(readyChunkInfo.getChunk()); } } } private void deactivateBlocks() { List<TShortObjectMap<TIntList>> deactivatedBlockSets = Lists.newArrayListWithExpectedSize(deactivateBlocksQueue.size()); deactivateBlocksQueue.drainTo(deactivatedBlockSets); for (TShortObjectMap<TIntList> deactivatedBlockSet : deactivatedBlockSets) { deactivatedBlockSet.forEachEntry((id, positions) -> { if (positions.size() > 0) { blockManager.getBlock(id).getEntity().send(new BeforeDeactivateBlocks(positions, registry)); } return true; }); } } private void checkForUnload() { PerformanceMonitor.startActivity("Unloading irrelevant chunks"); int unloaded = 0; logger.debug("Compacting cache"); Iterator<Vector3i> iterator = nearCache.keySet().iterator(); while (iterator.hasNext()) { Vector3i pos = iterator.next(); boolean keep = false; for (ChunkRelevanceRegion region : regions.values()) { if (region.getCurrentRegion().expand(UNLOAD_LEEWAY).encompasses(pos)) { keep = true; break; } } if (!keep) { // TODO: need some way to not dispose chunks being edited or processed (or do so safely) // Note: Above won't matter if all changes are on the main thread if (unloadChunkInternal(pos)) { iterator.remove(); if (++unloaded >= UNLOAD_PER_FRAME) { break; } } } } PerformanceMonitor.endActivity(); } private boolean unloadChunkInternal(Vector3i pos) { Chunk chunk = nearCache.get(pos); if (!chunk.isReady()) { // Chunk hasn't been finished or changed, so just drop it. Iterator<ReadyChunkInfo> infoIterator = sortedReadyChunks.iterator(); while (infoIterator.hasNext()) { ReadyChunkInfo next = infoIterator.next(); if (next.getPos().equals(chunk.getPosition())) { infoIterator.remove(); break; } } return true; } worldEntity.send(new BeforeChunkUnload(pos)); for (ChunkRelevanceRegion region : regions.values()) { region.chunkUnloaded(pos); } storageManager.deactivateChunk(chunk); chunk.dispose(); updateAdjacentChunksReadyFieldOfAdjChunks(chunk); try { unloadRequestTaskMaster.put(new ChunkUnloadRequest(chunk, this)); } catch (InterruptedException e) { logger.error("Failed to enqueue unload request for {}", chunk.getPosition(), e); } return true; } private boolean areAdjacentChunksReady(Chunk chunk) { Vector3i centerChunkPos = chunk.getPosition(); for (Side side : Side.values()) { Vector3i adjChunkPos = side.getAdjacentPos(centerChunkPos); Chunk adjChunk = nearCache.get(adjChunkPos); boolean adjChunkReady = (adjChunk != null && adjChunk.isReady()); if (!adjChunkReady) { return false; } } return true; } private void updateAdjacentChunksReadyFieldOf(Chunk chunk) { chunk.setAdjacentChunksReady(areAdjacentChunksReady(chunk)); } private void updateAdjacentChunksReadyFieldOfAdjChunks(Chunk chunkInCenter) { Vector3i centerChunkPos = chunkInCenter.getPosition(); for (Side side : Side.values()) { Vector3i adjChunkPos = side.getAdjacentPos(centerChunkPos); Chunk adjChunk = nearCache.get(adjChunkPos); if (adjChunk != null) { updateAdjacentChunksReadyFieldOf(adjChunk); } } } private void updateRelevance() { for (ChunkRelevanceRegion chunkRelevanceRegion : regions.values()) { chunkRelevanceRegion.update(); if (chunkRelevanceRegion.isDirty()) { for (Vector3i pos : chunkRelevanceRegion.getNeededChunks()) { Chunk chunk = nearCache.get(pos); if (chunk != null) { chunkRelevanceRegion.checkIfChunkIsRelevant(chunk); } else { createOrLoadChunk(pos); } } chunkRelevanceRegion.setUpToDate(); } } } private boolean makeChunkAvailable(final ReadyChunkInfo readyChunkInfo) { final Chunk chunk = nearCache.get(readyChunkInfo.getPos()); if (chunk == null) { return false; } for (Vector3i pos : Region3i.createFromCenterExtents(readyChunkInfo.getPos(), 1)) { if (nearCache.get(pos) == null) { return false; } } lightMerger.beginMerge(chunk, readyChunkInfo); return true; } void gatherBlockPositionsForDeactivate(Chunk chunk) { try { deactivateBlocksQueue.put(createBatchBlockEventMappings(chunk)); } catch (InterruptedException e) { logger.error("Failed to queue deactivation of blocks for {}", chunk.getPosition()); } } private TShortObjectMap<TIntList> createBatchBlockEventMappings(Chunk chunk) { TShortObjectMap<TIntList> batchBlockMap = new TShortObjectHashMap<>(); blockManager.listRegisteredBlocks().stream().filter(Block::isLifecycleEventsRequired).forEach(block -> batchBlockMap.put(block.getId(), new TIntArrayList())); ChunkBlockIterator i = chunk.getBlockIterator(); while (i.next()) { if (i.getBlock().isLifecycleEventsRequired()) { TIntList positionList = batchBlockMap.get(i.getBlock().getId()); positionList.add(i.getBlockPos().x); positionList.add(i.getBlockPos().y); positionList.add(i.getBlockPos().z); } } return batchBlockMap; } @Override public Chunk getChunk(int x, int y, int z) { return getChunk(new Vector3i(x, y, z)); } @Override public Chunk getChunk(Vector3i pos) { Chunk chunk = nearCache.get(pos); if (isChunkReady(chunk)) { return chunk; } return null; } @Override public Collection<Chunk> getAllChunks() { return nearCache.values(); } @Override public void restart() { pipeline.restart(); unloadRequestTaskMaster.restart(); lightMerger.restart(); } @Override public void shutdown() { pipeline.shutdown(); unloadRequestTaskMaster.shutdown(new ChunkUnloadRequest(), true); lightMerger.shutdown(); } @Override public void dispose() { shutdown(); for (Chunk chunk : nearCache.values()) { unloadChunkInternal(chunk.getPosition()); chunk.dispose(); } nearCache.clear(); /* * The chunk monitor needs to clear chunk references, so it's important * that no new chunk get created */ ChunkMonitor.fireChunkProviderDisposed(this); } @Override public boolean reloadChunk(Vector3i coords) { if (!nearCache.containsKey(coords)) { return false; } if (unloadChunkInternal(coords)) { nearCache.remove(coords); createOrLoadChunk(coords); return true; } return false; } @Override public void purgeWorld() { ChunkMonitor.fireChunkProviderDisposed(this); pipeline.shutdown(); unloadRequestTaskMaster.shutdown(new ChunkUnloadRequest(), true); lightMerger.shutdown(); nearCache.values().stream().filter(ManagedChunk::isReady).forEach(chunk -> { worldEntity.send(new BeforeChunkUnload(chunk.getPosition())); storageManager.deactivateChunk(chunk); chunk.dispose(); }); nearCache.clear(); readyChunks.clear(); sortedReadyChunks.clear(); storageManager.deleteWorld(); preparingChunks.clear(); worldEntity.send(new PurgeWorldEvent()); pipeline = new ChunkGenerationPipeline(new ChunkTaskRelevanceComparator()); unloadRequestTaskMaster = TaskMaster.createFIFOTaskMaster("Chunk-Unloader", 8); lightMerger = new LightMerger<>(this); lightMerger.restart(); ChunkMonitor.fireChunkProviderInitialized(this); for (ChunkRelevanceRegion chunkRelevanceRegion : regions.values()) { for (Vector3i pos : chunkRelevanceRegion.getCurrentRegion()) { createOrLoadChunk(pos); } chunkRelevanceRegion.setUpToDate(); } } private void createOrLoadChunk(Vector3i chunkPos) { Chunk chunk = nearCache.get(chunkPos); if (chunk == null && !preparingChunks.contains(chunkPos)) { preparingChunks.add(chunkPos); pipeline.doTask(new AbstractChunkTask(chunkPos) { @Override public String getName() { return "Create or Load Chunk"; } @Override public void run() { ChunkStore chunkStore = storageManager.loadChunkStore(getPosition()); Chunk chunk; EntityBufferImpl buffer = new EntityBufferImpl(); if (chunkStore == null) { chunk = new ChunkImpl(getPosition(), blockManager, biomeManager); generator.createChunk(chunk, buffer); } else { chunk = chunkStore.getChunk(); } InternalLightProcessor.generateInternalLighting(chunk); chunk.deflate(); TShortObjectMap<TIntList> mappings = createBatchBlockEventMappings(chunk); readyChunks.offer(new ReadyChunkInfo(chunk, mappings, chunkStore, buffer.getAll())); } }); } } @Override public void onChunkIsReady(Chunk chunk) { readyChunks.offer(new ReadyChunkInfo(chunk, createBatchBlockEventMappings(chunk), Collections.emptyList())); } @Override public Chunk getChunkUnready(Vector3i pos) { return nearCache.get(pos); } @Override public boolean isChunkReady(Vector3i pos) { return isChunkReady(nearCache.get(pos)); } private boolean isChunkReady(Chunk chunk) { return chunk != null && chunk.isReady(); } private class ChunkTaskRelevanceComparator implements Comparator<ChunkTask> { @Override public int compare(ChunkTask o1, ChunkTask o2) { return score(o1) - score(o2); } private int score(ChunkTask task) { if (task.isTerminateSignal()) { return -1; } return score(task.getPosition()); } private int score(Vector3i chunk) { int score = Integer.MAX_VALUE; regionLock.readLock().lock(); try { for (ChunkRelevanceRegion region : regions.values()) { int dist = distFromRegion(chunk, region.getCenter()); if (dist < score) { score = dist; } } return score; } finally { regionLock.readLock().unlock(); } } private int distFromRegion(Vector3i pos, Vector3i regionCenter) { return pos.gridDistance(regionCenter); } } private class ReadyChunkRelevanceComparator implements Comparator<ReadyChunkInfo> { @Override public int compare(ReadyChunkInfo o1, ReadyChunkInfo o2) { return score(o2.getPos()) - score(o1.getPos()); } private int score(Vector3i chunk) { int score = Integer.MAX_VALUE; regionLock.readLock().lock(); try { for (ChunkRelevanceRegion region : regions.values()) { int dist = distFromRegion(chunk, region.getCenter()); if (dist < score) { score = dist; } } return score; } finally { regionLock.readLock().unlock(); } } private int distFromRegion(Vector3i pos, Vector3i regionCenter) { return pos.gridDistance(regionCenter); } } }