/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.world.chunk; import static com.google.common.base.Preconditions.checkNotNull; import static org.lanternpowered.server.util.Conditions.checkPlugin; import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_AREA; import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_HEIGHT; import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_SECTIONS; import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_SECTION_SIZE; import static org.lanternpowered.server.world.chunk.LanternChunk.CHUNK_SECTION_VOLUME; import static org.lanternpowered.server.world.chunk.LanternChunkLayout.CHUNK_BIOME_VOLUME; import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector3i; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.MapMaker; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.lanternpowered.server.config.world.WorldConfig; import org.lanternpowered.server.data.io.ChunkIOService; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.game.LanternGame; import org.lanternpowered.server.game.registry.type.block.BlockRegistryModule; import org.lanternpowered.server.util.FastSoftThreadLocal; import org.lanternpowered.server.util.ThreadHelper; import org.lanternpowered.server.util.gen.biome.ObjectArrayImmutableBiomeBuffer; import org.lanternpowered.server.util.gen.biome.ShortArrayMutableBiomeBuffer; import org.lanternpowered.server.util.gen.block.AbstractMutableBlockBuffer; import org.lanternpowered.server.util.gen.block.AtomicShortArrayMutableBlockBuffer; import org.lanternpowered.server.util.gen.block.ShortArrayImmutableBlockBuffer; import org.lanternpowered.server.util.gen.block.ShortArrayMutableBlockBuffer; import org.lanternpowered.server.world.LanternWorld; import org.lanternpowered.server.world.chunk.LanternChunk.ChunkSection; import org.lanternpowered.server.world.extent.ExtentBufferHelper; import org.lanternpowered.server.world.extent.SoftBufferExtentViewDownsize; import org.spongepowered.api.Sponge; import org.spongepowered.api.block.BlockState; import org.spongepowered.api.block.BlockTypes; import org.spongepowered.api.entity.Entity; import org.spongepowered.api.event.EventManager; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.util.GuavaCollectors; import org.spongepowered.api.world.Chunk; import org.spongepowered.api.world.ChunkTicketManager; import org.spongepowered.api.world.ChunkTicketManager.EntityLoadingTicket; import org.spongepowered.api.world.ChunkTicketManager.LoadingTicket; import org.spongepowered.api.world.ChunkTicketManager.PlayerEntityLoadingTicket; import org.spongepowered.api.world.ChunkTicketManager.PlayerLoadingTicket; import org.spongepowered.api.world.biome.BiomeGenerationSettings; import org.spongepowered.api.world.biome.BiomeType; import org.spongepowered.api.world.biome.BiomeTypes; import org.spongepowered.api.world.biome.VirtualBiomeType; import org.spongepowered.api.world.extent.Extent; import org.spongepowered.api.world.extent.ImmutableBiomeVolume; import org.spongepowered.api.world.extent.ImmutableBlockVolume; import org.spongepowered.api.world.extent.MutableBlockVolume; import org.spongepowered.api.world.extent.StorageType; import org.spongepowered.api.world.gen.BiomeGenerator; import org.spongepowered.api.world.gen.GenerationPopulator; import org.spongepowered.api.world.gen.Populator; import org.spongepowered.api.world.gen.WorldGenerator; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; public final class LanternChunkManager { // The maximum amount of threads that can load chunks asynchronously private static final int CHUNK_LOADING_MAX_POOL_SIZE = 10; // The core amount of threads that can load chunks asynchronously private static final int CHUNK_LOADING_CORE_POOL_SIZE = 4; // The delay to unload chunks that are not forced, // loaded through loadChunk methods private static final long UNLOAD_DELAY = TimeUnit.SECONDS.toMillis(1); // All the attached tickets mapped by the forced chunk coordinates private final Map<Vector2i, Set<ChunkLoadingTicket>> ticketsByPos = new ConcurrentHashMap<>(); // All the loading tickets that are still usable private final Set<LanternLoadingTicket> tickets = Sets.newConcurrentHashSet(); // All the chunks that are loaded into the server private final Map<Vector2i, LanternChunk> loadedChunks = new ConcurrentHashMap<>(); // A cache that can be used to get chunks that weren't unloaded // so much after all, because of active references to the chunk private final Map<Vector2i, LanternChunk> reusableChunks = new MapMaker().weakValues().makeMap(); // A set which contains chunks that are pending for removal, // chunks loaded by loadChunk may not have been locked in the process, // and using a queue for removal should prevent the chunks from unloading too early private final Queue<UnloadingChunkEntry> pendingForUnload = new ConcurrentLinkedQueue<>(); private class UnloadingChunkEntry { final Vector2i coords; final long time; private UnloadingChunkEntry(Vector2i coords) { this.time = System.currentTimeMillis(); this.coords = coords; } @Override public boolean equals(Object obj) { return obj instanceof UnloadingChunkEntry && ((UnloadingChunkEntry) obj).coords.equals(this.coords); } @Override public int hashCode() { return this.coords.hashCode(); } } // All the futures that will cause chunk loading/unloading, they are stored // here to allow them to be cancelled private final Map<Vector2i, LanternChunkQueueTask> chunkQueueTasks = new ConcurrentHashMap<>(); // The chunk load executor private final ThreadPoolExecutor chunkTaskExecutor = new ThreadPoolExecutor( CHUNK_LOADING_CORE_POOL_SIZE, CHUNK_LOADING_MAX_POOL_SIZE, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), ThreadHelper.newFastThreadLocalThreadFactory()); // Some objects that can be used in {@link Chunk} population. private class PopulationData { private final Random random = new Random(); private final ChunkLoadingTicket lockTicket = new InternalLoadingTicket(); } private LanternChunkQueueTask queueTask(Vector2i coords, Runnable runnable) { final LanternChunkQueueTask task = new LanternChunkQueueTask(coords, runnable); task.setFuture(this.chunkTaskExecutor.submit(task)); return task; } private class LanternChunkQueueTask implements Callable<Void> { private final Vector2i coords; // The runnable that should be executed private final Runnable runnable; // The future attached to this callable @Nullable private Future<Void> future; public LanternChunkQueueTask(Vector2i coords, Runnable runnable) { this.runnable = runnable; this.coords = coords; } public void setFuture(Future<Void> future) { this.future = future; synchronized (this) { notifyAll(); } } @Override public Void call() throws Exception { // Wait for the future to be set, in case it's getting directly executed synchronized (this) { while (this.future == null) { try { wait(); } catch (InterruptedException ignored) { } } } this.runnable.run(); return null; } boolean cancel() { // We have to wait for the future to be set before we // can cancel it, shouldn't be long synchronized (this) { while (this.future == null) { try { wait(); } catch (InterruptedException ignored) { } } } return this.future.cancel(false); } @Override public boolean equals(Object other) { return other instanceof LanternChunkQueueTask && ((LanternChunkQueueTask) other).coords.equals(this.coords); } @Override public int hashCode() { return this.coords.hashCode(); } } private class LanternChunkUnloadTask implements Runnable { // The callable that is bound to the unloading task private final LanternChunkQueueTask callable; private LanternChunkUnloadTask(LanternChunkQueueTask callable) { this.callable = callable; } @Override public void run() { unload0(this.callable.coords, () -> Cause.source(world).build(), false); } } private class LanternChunkLoadTask implements Runnable { // The coordinates of the chunk private final Vector2i coords; private LanternChunkLoadTask(Vector2i coords) { this.coords = coords; } @Override public void run() { doChunkLoad(this.coords); } } private void doChunkLoad(Vector2i coords) { final Set<ChunkLoadingTicket> tickets = ticketsByPos.get(coords); if (tickets == null) { return; } // Chunk may be null if's already being loaded by a different thread. getOrCreateChunk(coords, () -> { // Build the cause only if the chunk isn't already loaded return Cause.source(world).named("tickets", tickets.toArray(new Object[tickets.size()])).build(); }, true, false); } // The game instance private final LanternGame game; // The world private final LanternWorld world; // The world configuration private final WorldConfig worldConfig; // The chunk I/O service private final ChunkIOService chunkIOService; // The chunk load (ticket) service private final LanternChunkTicketManager chunkLoadService; // The world folder private final Path worldFolder; private class GenerationBuffers { final ChunkBiomeBuffer chunkBiomeBuffer = new ChunkBiomeBuffer(); final ChunkBlockBuffer chunkBlockBuffer = new ChunkBlockBuffer(); } // The world generation buffers that will be reused private final FastSoftThreadLocal<GenerationBuffers> genBuffers = FastSoftThreadLocal.withInitial(GenerationBuffers::new); // The randoms that will be shared for population private final FastSoftThreadLocal<PopulationData> populationData = FastSoftThreadLocal.withInitial(PopulationData::new); // The world generator private volatile WorldGenerator worldGenerator; /** * Creates a new chunk manager. * * @param game the game instance * @param world the world this chunk manage is attached to * @param worldConfig the configuration file of the world * @param chunkLoadService the chunk load (ticket) service * @param chunkIOService the chunk i/o service * @param worldGenerator the world generator * @param worldFolder the world data folder */ public LanternChunkManager(LanternGame game, LanternWorld world, WorldConfig worldConfig, LanternChunkTicketManager chunkLoadService, ChunkIOService chunkIOService, WorldGenerator worldGenerator, Path worldFolder) { this.chunkLoadService = chunkLoadService; this.chunkIOService = chunkIOService; this.worldGenerator = worldGenerator; this.worldFolder = worldFolder; this.worldConfig = worldConfig; this.world = world; this.game = game; } public ChunkIOService getChunkIOService() { return this.chunkIOService; } /** * Sets the generator of the world (chunk manager). * * @param worldGenerator the world generator */ public void setWorldGenerator(WorldGenerator worldGenerator) { this.worldGenerator = checkNotNull(worldGenerator, "worldGenerator"); } /** * Gets the generator of the world (chunk manager). * * @return the world generator */ public WorldGenerator getWorldGenerator() { return this.worldGenerator; } /** * Gets whether a loading ticket exists for the chunk * at the specified coordinates. * * @param coords the coordinates * @return has ticket */ public boolean hasTicket(Vector2i coords) { return this.ticketsByPos.containsKey(checkNotNull(coords, "coords")); } /** * Gets whether a loading ticket exists for the chunk * at the specified coordinates. * * @param x the x coordinate * @param z the z coordinate * @return has ticket */ public boolean hasTicket(int x, int z) { return this.ticketsByPos.containsKey(new Vector2i(x, z)); } /** * Gets a map with all the forced chunk and the assigned tickets. * * @return the tickets */ public ImmutableSetMultimap<Vector3i, LoadingTicket> getForced() { final ImmutableSetMultimap.Builder<Vector3i, LoadingTicket> builder = ImmutableSetMultimap.builder(); for (Entry<Vector2i, Set<ChunkLoadingTicket>> en : this.ticketsByPos.entrySet()) { final Vector2i pos0 = en.getKey(); final Vector3i pos = new Vector3i(pos0.getX(), 0, pos0.getY()); for (ChunkLoadingTicket ticket : en.getValue()) { builder.put(pos, ticket); } } return builder.build(); } /** * Gets the amount of tickets that are attached to the player. * * @param player the player uuid * @return the tickets */ public int getTicketsForPlayer(UUID player) { checkNotNull(player, "player"); return (int) this.tickets.stream().filter(ticket -> ticket instanceof PlayerLoadingTicket && player.equals(((PlayerLoadingTicket) ticket).getPlayerUniqueId())).count(); } /** * Gets the amount of tickets that are attached to the plugin. * * @param plugin the plugin * @return the tickets */ public int getTicketsForPlugin(Object plugin) { return getTicketsForPlugin(checkPlugin(plugin, "plugin").getId()); } /** * Gets the amount of tickets that are attached to the plugin. * * @param pluginId the plugin id * @return the tickets */ public int getTicketsForPlugin(String pluginId) { checkNotNull(pluginId, "pluginId"); return (int) this.tickets.stream().filter(ticket -> ticket.getPlugin().equals(pluginId)).count(); } /** * Gets the maximum amount of tickets for the plugin per world. * * @param plugin the plugin * @return the maximum amount of tickets */ public int getMaxTicketsForPlugin(Object plugin) { return getMaxTicketsForPlugin(checkPlugin(plugin, "plugin").getId()); } /** * Gets the maximum amount of tickets for the plugin per world. * * @param plugin the plugin * @return the maximum amount of tickets */ public int getMaxTicketsForPlugin(String plugin) { return this.worldConfig.getChunkLoadingTickets(plugin).getMaximumTicketCount(); } /** * Gets the maximum amount of forced chunks each ticket of the plugin can contain. * * @param plugin the plugin * @return the maximum amount of forced chunks */ public int getMaxChunksForPluginTicket(Object plugin) { return getMaxChunksForPluginTicket(checkPlugin(plugin, "plugin").getId()); } /** * Gets the maximum amount of forced chunks each ticket of the plugin can contain. * * @param plugin the plugin * @return the maximum amount of forced chunks */ public int getMaxChunksForPluginTicket(String plugin) { return this.worldConfig.getChunkLoadingTickets(plugin).getMaximumChunksPerTicket(); } /** * Attempts to create a new loading ticket for the specified plugin. * * @param plugin the plugin * @return the loading ticket if available */ public Optional<LoadingTicket> createTicket(Object plugin) { final String pluginId = checkPlugin(plugin, "plugin").getId(); if (getTicketsForPlugin(pluginId) >= getMaxTicketsForPlugin(pluginId)) { return Optional.empty(); } final int maxChunks = getMaxChunksForPluginTicket(pluginId); final LanternLoadingTicket ticket = new LanternLoadingTicket(pluginId, this, maxChunks); this.tickets.add(ticket); return Optional.of(ticket); } /** * Attempts to create a new entity loading ticket for the specified plugin. * * @param plugin the plugin * @return the loading ticket if available */ public Optional<EntityLoadingTicket> createEntityTicket(Object plugin) { final String pluginId = checkPlugin(plugin, "plugin").getId(); if (getTicketsForPlugin(pluginId) >= getMaxTicketsForPlugin(pluginId)) { return Optional.empty(); } final int maxChunks = getMaxChunksForPluginTicket(pluginId); final LanternEntityLoadingTicket ticket = new LanternEntityLoadingTicket( pluginId, this, maxChunks); this.tickets.add(ticket); return Optional.of(ticket); } /** * Attempts to create a new player loading ticket for the specified plugin. * * @param plugin the plugin * @param player the unique id of the player * @return the loading ticket if available */ public Optional<PlayerLoadingTicket> createPlayerTicket(Object plugin, UUID player) { checkNotNull(player, "player"); final String pluginId = checkPlugin(plugin, "plugin").getId(); if (getTicketsForPlugin(pluginId) >= getMaxTicketsForPlugin(pluginId) || this.chunkLoadService.getAvailableTickets(player) <= 0) { return Optional.empty(); } final int maxChunks = getMaxChunksForPluginTicket(pluginId); final LanternPlayerLoadingTicket ticket = new LanternPlayerLoadingTicket( pluginId, this, player, maxChunks); this.tickets.add(ticket); return Optional.of(ticket); } /** * Attempts to create a new player loading ticket for the specified plugin. * * @param plugin the plugin * @param player the unique id of the player * @return the loading ticket if available */ public Optional<PlayerEntityLoadingTicket> createPlayerEntityTicket(Object plugin, UUID player) { checkNotNull(player, "player"); final String pluginId = checkPlugin(plugin, "plugin").getId(); if (getTicketsForPlugin(pluginId) >= getMaxTicketsForPlugin(pluginId) || this.chunkLoadService.getAvailableTickets(player) <= 0) { return Optional.empty(); } final int maxChunks = getMaxChunksForPluginTicket(pluginId); final LanternPlayerEntityLoadingTicket ticket = new LanternPlayerEntityLoadingTicket( pluginId, this, player, maxChunks); this.tickets.add(ticket); return Optional.of(ticket); } /** * Gets a immutable set with all the loaded chunks. * * @return the loaded chunks */ public ImmutableSet<Chunk> getLoadedChunks() { return this.loadedChunks.values().stream().filter(Chunk::isLoaded).collect(GuavaCollectors.toImmutableSet()); } /** * Gets a chunk for the coordinates, * may not be loaded yet. * * @param coords the coordinates * @return the chunk if loaded, otherwise null */ @Nullable public LanternChunk getChunk(Vector2i coords) { return getChunk(coords, true); } @Nullable public LanternChunk getChunkIfLoaded(Vector2i coords) { final LanternChunk chunk = this.loadedChunks.get(checkNotNull(coords, "coords")); if (chunk != null && !chunk.loaded) { return null; } return chunk; } @Nullable public LanternChunk getChunkIfLoaded(int x, int z) { return getChunkIfLoaded(new Vector2i(x, z)); } @Nullable private LanternChunk getChunk(Vector2i coords, boolean wait) { final LanternChunk chunk = this.loadedChunks.get(checkNotNull(coords, "coords")); if (wait && chunk != null && !chunk.loaded && chunk.lockState == LanternChunk.LockState.LOADING) { // Wait for the chunk to finish loading chunk.lockCondition.awaitUninterruptibly(); } return chunk; } /** * Gets a chunk for the coordinates, may not be loaded yet. * * @param x the x coordinate * @param z the z coordinate * @return the chunk if loaded, otherwise null */ @Nullable public LanternChunk getChunk(int x, int z) { return getChunk(new Vector2i(x, z)); } /** * Gets a chunk safely (new one will be created) for the coordinates, may * not be loaded yet. * * @param x the x coordinate * @param z the z coordinate * @return the chunk */ public LanternChunk getOrLoadChunk(int x, int z) { return getOrCreateChunk(x, z, false); } /** * Gets a chunk safely (new one will be created) for the coordinates, may * not be loaded yet. * * @param x the x coordinate * @param z the z coordinate * @param generate whether the new chunk should be generated * if not done before * @return the chunk */ public LanternChunk getOrCreateChunk(int x, int z, boolean generate) { return getOrCreateChunk(x, z, () -> Cause.source(this.world).build(), generate); } /** * Gets a chunk safely (new one will be created) for the coordinates, may * not be loaded yet. * * @param x the x coordinate * @param z the z coordinate * @return the chunk */ public LanternChunk getOrCreateChunk(int x, int z, Supplier<Cause> cause, boolean generate) { return getOrCreateChunk(new Vector2i(x, z), cause, generate); } /** * Gets a chunk safely (new one will be created) for the coordinates, may * not be loaded yet. * * @param coords the coordinates * @param cause the cause * @param generate whether the chunk should be generated if missing * @return the chunk */ public LanternChunk getOrCreateChunk(Vector2i coords, Supplier<Cause> cause, boolean generate) { return getOrCreateChunk(coords, cause, generate, true); } /** * * @param coords the coordinates of the chunk to load * @param cause a supplier of the cause that triggered the chunk load * @param generate whether the chunk should be generated if not found * @param wait whether the current thread should wait for the loading to finish, this should only * be internally used inside the chunk manager * @return the chunk */ private LanternChunk getOrCreateChunk(Vector2i coords, Supplier<Cause> cause, boolean generate, boolean wait) { checkNotNull(cause, "cause"); LanternChunk chunk = this.loadedChunks.get(checkNotNull(coords, "coords")); // Chunk is already loaded if (chunk != null) { if (!this.ticketsByPos.containsKey(coords)) { this.pendingForUnload.add(new UnloadingChunkEntry(coords)); } return chunk; } // Lets try to visit the graveyard, try to retrieve chunks that where // not gc yet, allowing us to reuse them to avoid loading a new chunk chunk = this.reusableChunks.get(coords); if (chunk != null) { this.loadedChunks.put(coords, chunk); this.reusableChunks.remove(coords); if (!this.ticketsByPos.containsKey(coords)) { this.pendingForUnload.add(new UnloadingChunkEntry(coords)); } this.game.getEventManager().post(SpongeEventFactory.createLoadChunkEvent(cause.get(), chunk)); this.world.getEventListener().onLoadChunk(chunk); // Resurrect all the entities in the chunk chunk.resurrectEntities(); this.world.addEntities(chunk.getEntities()); return chunk; } boolean[] newChunk = new boolean[1]; // Finally, create a new chunk if needed chunk = this.loadedChunks.computeIfAbsent(coords, coords0 -> { newChunk[0] = true; return new LanternChunk(this.world, coords0.getX(), coords0.getY()); }); // This method call was too late if (!newChunk[0]) { // If the chunk is already loaded, just return it if (chunk.loaded) { return chunk; } if (chunk.lockState == LanternChunk.LockState.LOADING) { // Don't bother waiting for the chunk to finish if (!wait) { return chunk; } // Wait for the chunk to finish loading chunk.lockCondition.awaitUninterruptibly(); } // Loading is not triggered? return chunk; } // Try to load the chunk load(chunk, cause, generate); this.world.addEntities(chunk.getEntities()); if (!this.ticketsByPos.containsKey(coords)) { this.pendingForUnload.add(new UnloadingChunkEntry(coords)); } return chunk; } private static final int UP = 0; private static final int DOWN = 1; private static final int RIGHT = 2; private static final int RIGHT_UP = 3; private static final int RIGHT_DOWN = 4; private static final int LEFT = 5; private static final int LEFT_UP = 6; private static final int LEFT_DOWN = 7; private static Vector2i[] getSides(Vector2i center) { final Vector2i[] sides = new Vector2i[8]; sides[UP] = center.add(0, 1); sides[DOWN] = center.add(0, -1); sides[RIGHT] = center.add(1, 0); sides[RIGHT_UP] = center.add(1, 1); sides[RIGHT_DOWN] = center.add(1, -1); sides[LEFT] = center.add(-1, 0); sides[LEFT_UP] = center.add(-1, 1); sides[LEFT_DOWN] = center.add(-1, -1); return sides; } /** * This is taken from the {@link Populator} class to give a bit more info * about what we are trying here to achieve. * * +----------+----------+ . . The chunk provided as a parameter * | | | . . to this method. * | | | * | #####|##### | ### The volume you (the populator) should populate. * | #####|##### | ### * +----------+----------+ * | . . #####|##### | * | . . #####|##### | * | . . . . .| | * | . . . . .| | * +----------+----------+ * * @param chunk the chunk */ private void tryPopulateSurroundingChunks(LanternChunk chunk, Cause cause) { final Vector2i pos = chunk.chunkPos; final Vector2i[] sides = getSides(pos); final PopulationData populationData = this.populationData.get(); final Random random = populationData.random; // TODO: Populating must be done in the sync thread??? for (Vector2i side : sides) { lockInternally(side, populationData.lockTicket); } LanternChunk up = isChunkLoaded(sides[UP]); LanternChunk right = isChunkLoaded(sides[RIGHT]); LanternChunk rightUp = isChunkLoaded(sides[RIGHT_UP]); if (up != null && right != null && rightUp != null) { if (!chunk.populating && !chunk.populated) { populateChunk(chunk, cause, random); } } LanternChunk left = isChunkLoaded(sides[LEFT]); LanternChunk leftDown = isChunkLoaded(sides[LEFT_DOWN]); LanternChunk down = isChunkLoaded(sides[DOWN]); if (leftDown != null && left != null && down != null) { if (!leftDown.populating && !leftDown.populated) { populateChunk(leftDown, cause, random); } } if (up == null) { // Maybe it is loaded by now? up = isChunkLoaded(sides[UP]); } if (left == null) { // Maybe it is loaded by now? left = isChunkLoaded(sides[LEFT]); } LanternChunk leftUp = isChunkLoaded(sides[LEFT_UP]); if (left != null && leftUp != null && up != null) { if (!left.populating && !left.populated) { populateChunk(left, cause, random); } } if (right == null) { // Maybe it is loaded by now? right = isChunkLoaded(sides[RIGHT]); } if (down == null) { // Maybe it is loaded by now? down = isChunkLoaded(sides[DOWN]); } LanternChunk rightDown = isChunkLoaded(sides[RIGHT_DOWN]); if (down != null && rightDown != null && right != null) { if (!down.populating && !down.populated) { populateChunk(down, cause, random); } } for (Vector2i side : sides) { unlockInternally(side, populationData.lockTicket); } } private void populateChunk(LanternChunk chunk, Cause cause, Random random) { chunk.populating = true; // Populate int chunkX = chunk.getX() * 16; int chunkZ = chunk.getZ() * 16; long worldSeed = this.world.getProperties().getSeed(); random.setSeed(worldSeed); long xSeed = random.nextLong() / 2 * 2 + 1; long zSeed = random.nextLong() / 2 * 2 + 1; long chunkSeed = xSeed * chunkX + zSeed * chunkZ ^ worldSeed; random.setSeed(chunkSeed); //noinspection ConstantConditions final ChunkBiomeBuffer biomeBuffer = this.genBuffers.get().chunkBiomeBuffer; biomeBuffer.reuse(new Vector3i(chunkX + 8, 0, chunkZ + 8)); // We ave to regenerate the biomes so that any // virtual biomes can be passed to the populator. final BiomeGenerator biomeGenerator = this.worldGenerator.getBiomeGenerator(); biomeGenerator.generateBiomes(biomeBuffer); // Initialize the biomes into the chunk final ImmutableBiomeVolume immutableBiomeVolume = biomeBuffer.getImmutableBiomeCopy(); chunk.initializeBiomes(biomeBuffer.detach().clone()); // Using the biome at an arbitrary point within the chunk // ({16, 0, 16} in the vanilla game) final BiomeType biomeType = immutableBiomeVolume.getBiome(chunkX + 16, 0, chunkZ + 16); // Get the generation settings final BiomeGenerationSettings biomeGenSettings = this.worldGenerator.getBiomeSettings(biomeType); final List<Populator> populators = new LinkedList<>(biomeGenSettings.getPopulators()); populators.addAll(this.worldGenerator.getPopulators()); final EventManager eventManager = Sponge.getEventManager(); Vector3i min = new Vector3i(chunkX + 8, 0, chunkZ + 8); Extent volume = new SoftBufferExtentViewDownsize(chunk.getWorld(), min, min.add(15, 0, 15), min.sub(8, 0, 8), min.add(23, 0, 23)); // Call the pre populate event, this allows // modifications to the populators list // Called before a chunk begins populating. (javadoc) eventManager.post(SpongeEventFactory.createPopulateChunkEventPre(cause, populators, chunk)); // First populate the chunk with the biome populators for (Populator populator : populators) { // Called when a populator is about to run against a chunk. (javadoc) eventManager.post(SpongeEventFactory.createPopulateChunkEventPopulate(cause, populator, chunk)); populator.populate(this.world, volume, random); } // Called when a chunk finishes populating. (javadoc) eventManager.post(SpongeEventFactory.createPopulateChunkEventPost(cause, ImmutableList.copyOf(populators), chunk)); this.world.getEventListener().onPopulateChunk(chunk); // We are done chunk.populated = true; chunk.populating = false; } @Nullable private LanternChunk isChunkLoaded(Vector2i pos) { final LanternChunk chunk = this.getChunk(pos, false); return chunk != null && chunk.loaded ? chunk : null; } /** * Loads the specified chunk and attempts to generate it if not done before * if {@code generate} is set to {@code true}. * * @param chunk the chunk to load * @param cause the cause * @param generate whether the chunk should be generated if missing * @return true if it was successful */ public boolean load(LanternChunk chunk, Supplier<Cause> cause, boolean generate) { return this.load0(chunk, cause, generate, true); } private boolean load0(LanternChunk chunk, Supplier<Cause> cause, boolean generate, boolean wait) { checkNotNull(chunk, "chunk"); checkNotNull(cause, "cause"); if (chunk.loaded) { return chunk.loadingSuccess; } // The chunk is already getting lock by a different thread, // wait for the task to finish if (!chunk.lock.tryLock()) { if (chunk.lockState == LanternChunk.LockState.LOADING) { if (wait) { // Wait for the chunk to finish loading chunk.lockCondition.awaitUninterruptibly(); // Consider it was a success? return chunk.loadingSuccess; } else { return false; } } else if (chunk.lockState == LanternChunk.LockState.UNLOADING) { // Acquire the lock and wait for the chunk to get unloaded chunk.lock.lock(); // The chunk can only be saved if it was once loaded, so return true } else if (chunk.lockState == LanternChunk.LockState.SAVING) { return true; } } boolean success = true; try { chunk.lockState = LanternChunk.LockState.LOADING; final LanternChunkQueueTask task = this.chunkQueueTasks.remove(chunk.getCoords()); // Try to cancel the task, the task will probably be ignored // because we are already locked if (task != null) { task.cancel(); } try { // Try to load the chunk if (this.chunkIOService.read(chunk)) { this.game.getEventManager().post(SpongeEventFactory.createLoadChunkEvent(cause.get(), chunk)); return true; } } catch (Exception e) { this.game.getLogger().error("Error while loading chunk ({};{})", chunk.getX(), chunk.getZ(), e); // An error in chunk reading may have left the chunk in an invalid state // (i.e. double initialization errors), so it's forcibly unloaded here // chunk.unloadChunk(); TODO } // Stop here if we can't generate if (!generate) { chunk.initializeEmpty(); return success = false; } Cause cause0 = cause.get(); // Generate chunk try { this.generate(chunk, cause0); } catch (Throwable e) { this.game.getLogger().error("Error while generating chunk ({};{})", chunk.getX(), chunk.getZ(), e); return success = false; } // Try to populate the chunk tryPopulateSurroundingChunks(chunk, cause0); this.game.getEventManager().post(SpongeEventFactory.createLoadChunkEvent(cause0, chunk)); this.world.getEventListener().onLoadChunk(chunk); return true; } finally { chunk.lockState = LanternChunk.LockState.NONE; chunk.loaded = true; chunk.loadingSuccess = success; chunk.lockCondition.signalAll(); chunk.lock.unlock(); } } /** * Attempts to generate the chunk. * * @param chunk the chunk */ private void generate(LanternChunk chunk, Cause cause) { final EventManager eventManager = Sponge.getEventManager(); eventManager.post(SpongeEventFactory.createGenerateChunkEventPre(cause, chunk)); final GenerationBuffers buffers = this.genBuffers.get(); //noinspection ConstantConditions final ChunkBiomeBuffer biomeBuffer = buffers.chunkBiomeBuffer; biomeBuffer.reuse(new Vector3i(chunk.getX() << 4, 0, chunk.getZ() << 4)); // Generate the biomes final BiomeGenerator biomeGenerator = this.worldGenerator.getBiomeGenerator(); biomeGenerator.generateBiomes(biomeBuffer); // Initialize the biomes into the chunk final ImmutableBiomeVolume immutableBiomeVolume = biomeBuffer.getImmutableBiomeCopy(); chunk.initializeBiomes(biomeBuffer.detach().clone()); final ChunkBlockBuffer blockBuffer = buffers.chunkBlockBuffer; blockBuffer.reuse(new Vector3i(chunk.getX() << 4, 0, chunk.getZ() << 4)); // Apply the main world generator final GenerationPopulator baseGenerator = this.worldGenerator.getBaseGenerationPopulator(); baseGenerator.populate(this.world, blockBuffer, immutableBiomeVolume); // Get all the used biome types final Set<BiomeType> biomeTypes = ImmutableSet.copyOf(biomeBuffer.biomeTypes); for (BiomeType biomeType : biomeTypes) { final BiomeGenerationSettings settings = this.worldGenerator.getBiomeSettings(biomeType); for (GenerationPopulator generator : settings.getGenerationPopulators()) { generator.populate(this.world, blockBuffer, immutableBiomeVolume); } } // Apply the generator populators to complete the block buffer for (GenerationPopulator generator : this.worldGenerator.getGenerationPopulators()) { generator.populate(this.world, blockBuffer, immutableBiomeVolume); } // Create the chunk sections final ChunkSection[] sections = new ChunkSection[CHUNK_SECTIONS]; for (int sy = 0; sy < CHUNK_SECTIONS; sy++) { final int nonAirCount = blockBuffer.nonAirCount[sy]; if (nonAirCount > 0) { sections[sy] = new ChunkSection(blockBuffer.types[sy], nonAirCount); } } // Initialize the chunk chunk.initializeSections(sections); chunk.initializeHeightMap(null); chunk.initializeLight(); eventManager.post(SpongeEventFactory.createGenerateChunkEventPost(cause, chunk)); } private static final Vector3i CHUNK_SIZE = new Vector3i( CHUNK_SECTION_SIZE, CHUNK_HEIGHT, CHUNK_SECTION_SIZE); /** * A biome buffer that also holds a backing array with all the biome * type objects to allow faster access to all the used biome types. */ private final class ChunkBiomeBuffer extends ShortArrayMutableBiomeBuffer { private final BiomeType[] biomeTypes; ChunkBiomeBuffer() { super(Vector3i.ZERO, CHUNK_BIOME_VOLUME); this.biomeTypes = new BiomeType[CHUNK_AREA]; Arrays.fill(this.biomeTypes, BiomeTypes.OCEAN); detach(); } @Override public void setBiome(int x, int y, int z, BiomeType biome) { super.setBiome(x, y, z, biome instanceof VirtualBiomeType ? ((VirtualBiomeType) biome).getPersistedType() : biome); this.biomeTypes[index(x, y, z)] = biome; } @Override public BiomeType getBiome(int x, int y, int z) { checkOpen(); checkRange(x, y, z); return this.biomeTypes[index(x, y, z)]; } @Override protected int index(int x, int y, int z) { return (z & 0xf) << 4 | x & 0xf; } @Override public void reuse(Vector3i start) { super.reuse(start); Arrays.fill(this.biomeTypes, BiomeTypes.OCEAN); } @Override public ImmutableBiomeVolume getImmutableBiomeCopy() { checkOpen(); return new ObjectArrayImmutableBiomeBuffer(this.biomeTypes, this.start, this.size); } } /** * A custom block buffer that will be used to generate the chunks * in the world generation to increase the performance. */ private final class ChunkBlockBuffer extends AbstractMutableBlockBuffer { private final short[][] types = new short[CHUNK_SECTIONS][CHUNK_SECTION_VOLUME]; private final int[] nonAirCount = new int[CHUNK_SECTIONS]; ChunkBlockBuffer() { super(Vector3i.ZERO, CHUNK_SIZE); } void reuse(Vector3i start) { this.start = checkNotNull(start, "start"); this.end = this.start.add(this.size).sub(Vector3i.ONE); for (int i = 0; i < CHUNK_SECTIONS; i++) { Arrays.fill(this.types[i], (short) 0); } Arrays.fill(this.nonAirCount, 0); } @Override public boolean setBlock(int x, int y, int z, BlockState block, Cause cause) { checkNotNull(block, "blockState"); checkRange(x, y, z); final int sy = y >> 4; final int index = ((y & 0xf) << 8) | ((z & 0xf) << 4) | x & 0xf; final short[] types = this.types[sy]; final short type = BlockRegistryModule.get().getStateInternalIdAndData(block); if (type == 0 && types[index] != 0) { this.nonAirCount[sy]--; } else if (type != 0 && types[index] == 0) { this.nonAirCount[sy]++; } types[index] = type; return true; } @Override public BlockState getBlock(int x, int y, int z) { checkRange(x, y, z); return BlockRegistryModule.get().getStateByInternalIdAndData(this.types[y >> 4][((y & 0xf) << 8) | ((z & 0xf) << 4) | x & 0xf]) .orElse(BlockTypes.AIR.getDefaultState()); } @Override public MutableBlockVolume getBlockCopy(StorageType type) { checkNotNull(type, "storageType"); switch (type) { case STANDARD: return new ShortArrayMutableBlockBuffer(ExtentBufferHelper.copyToBlockArray( this, this.start, this.end, this.size), this.start, this.size); case THREAD_SAFE: return new AtomicShortArrayMutableBlockBuffer(ExtentBufferHelper.copyToBlockArray( this, this.start, this.end, this.size), this.start, this.size); default: throw new UnsupportedOperationException(type.name()); } } @Override public ImmutableBlockVolume getImmutableBlockCopy() { return ShortArrayImmutableBlockBuffer.newWithoutArrayClone(ExtentBufferHelper.copyToBlockArray( this, this.start, this.end, this.size), this.start, this.size); } } /** * Attempts to save the specified chunk. * * @param chunk the chunk * @return true if it was successful */ public boolean save(LanternChunk chunk) { checkNotNull(chunk, "chunk"); chunk.lock.lock(); try { chunk.lockState = LanternChunk.LockState.SAVING; return save0(chunk); } finally { chunk.lockState = LanternChunk.LockState.NONE; chunk.lockCondition.signalAll(); chunk.lock.unlock(); } } private boolean save0(LanternChunk chunk) { try { this.chunkIOService.write(chunk); return true; } catch (IOException e) { this.game.getLogger().error("Error while saving " + chunk, e); } return false; } /** * Attempts to unload the chunk at the specified coordinates. * * @param x the x coordinate * @param z the z coordinate * @param cause the cause * @return true if it was successful */ public boolean unload(int x, int z, Supplier<Cause> cause) { return unload(new Vector2i(x, z), cause); } /** * Attempts to unload the chunk at the specified coordinates. * * @param coords the coordinates * @param cause the cause * @return true if it was successful */ public boolean unload(Vector2i coords, Supplier<Cause> cause) { return unload0(coords, cause, true); } private boolean unload0(Vector2i coords, Supplier<Cause> cause, boolean wait) { checkNotNull(cause, "cause"); final LanternChunk chunk = this.getChunk(checkNotNull(coords, "coords")); if (chunk != null) { return unload0(chunk, cause, wait); } return true; } /** * Attempts to unload the specified chunk. * * @param chunk the chunk to unload * @param cause the cause * @return true if it was successful */ public boolean unload(LanternChunk chunk, Supplier<Cause> cause) { checkNotNull(chunk, "chunk"); checkNotNull(cause, "cause"); return unload0(chunk, cause, true); } private boolean unload0(LanternChunk chunk, Supplier<Cause> cause, boolean wait) { final Vector2i coords = chunk.getCoords(); // Forced chunks cannot be unloaded if (this.ticketsByPos.containsKey(coords)) { chunk.unloadingSuccess = false; return false; } if (!chunk.lock.tryLock()) { // The chunk is already unloading, so wait for it to complete if (chunk.lockState == LanternChunk.LockState.UNLOADING) { if (wait) { chunk.lockCondition.awaitUninterruptibly(); return chunk.unloadingSuccess; } else { return false; } // The chunk is currently loading or saving, wait for it to complete // and then unload it again } else { chunk.lock.lock(); } } try { chunk.lockState = LanternChunk.LockState.UNLOADING; // The chunk isn't loaded yet, fast fail if (!chunk.loaded) { return true; } final LanternChunkQueueTask task = this.chunkQueueTasks.remove(coords); // Try to cancel all the current tasks if (task != null) { task.cancel(); } // Post the chunk unload event this.game.getEventManager().post(SpongeEventFactory.createUnloadChunkEvent(cause.get(), chunk)); this.world.getEventListener().onUnloadChunk(chunk); // Remove from the loaded chunks this.loadedChunks.remove(coords); // Move the chunk to the graveyard this.reusableChunks.put(coords, chunk); // Bury the entities chunk.buryEntities(); save0(chunk); return true; } finally { chunk.lockState = LanternChunk.LockState.NONE; chunk.unloadingSuccess = true; chunk.lockCondition.signalAll(); chunk.lock.unlock(); } } /** * Locks the chunk of the coordinates with a internal loading ticket. This * method does not trigger the loading of a chunk but locks the chunk from * unloading. * * @param coords the coordinates * @return whether it was previously empty */ private boolean lockInternally(Vector2i coords, ChunkLoadingTicket ticket) { final boolean[] empty = new boolean[1]; this.ticketsByPos.computeIfAbsent(coords, coords0 -> { empty[0] = true; return Sets.newConcurrentHashSet(); }).add(ticket); return empty[0]; } private boolean unlockInternally(Vector2i coords, ChunkLoadingTicket ticket) { final Set<ChunkLoadingTicket> set = this.ticketsByPos.get(coords); if (set != null && set.remove(ticket)) { if (set.isEmpty()) { this.ticketsByPos.remove(coords); } return true; } return false; } /** * Forces the specified chunk coordinates for a specific ticket. * * @param ticket the ticket * @param coords the coordinates */ void force(LanternLoadingTicket ticket, Vector2i coords) { this.force(ticket, coords, true); } /** * Forces the specified chunk coordinates for a specific ticket. * * @param ticket the ticket * @param coords the coordinates * @param callEvents whether the force chunk events should be called */ void force(LanternLoadingTicket ticket, Vector2i coords, boolean callEvents) { final LanternChunk chunk = this.getChunk(coords, false); // The chunk at this coords is already loaded, // wa can call the event directly lockInternally(coords, ticket); // Remove from unload through loadChunk this.pendingForUnload.removeIf(e -> e.coords.equals(coords)); // Whether the chunk should be queued for loading boolean queueLoad = false; if (chunk != null) { if (chunk.lock.isLocked() && chunk.lockState == LanternChunk.LockState.UNLOADING) { queueLoad = true; } // Queue the chunk to load } else { queueLoad = true; } if (queueLoad) { LanternChunkQueueTask task = this.chunkQueueTasks.get(coords); if (task == null || !(task.runnable instanceof LanternChunkLoadTask)) { this.chunkQueueTasks.computeIfAbsent(coords, coords1 -> queueTask(coords1, new LanternChunkLoadTask(coords1))); } } if (callEvents) { final Vector3i coords0 = new Vector3i(coords.getX(), 0, coords.getY()); this.game.getEventManager().post(SpongeEventFactory.createForcedChunkEvent( Cause.source(ticket).owner(this.world).build(), coords0, ticket)); } } /** * Unforces the specified chunk coordinates for a specific ticket. * * @param ticket the ticket * @param coords the coordinates */ void unforce(LanternLoadingTicket ticket, Vector2i coords, boolean callEvents) { if (unlockInternally(coords, ticket)) { final LanternChunk chunk = getChunk(coords, false); // Try to cancel any queued chunk loadings if (chunk != null && chunk.lock.isLocked() && chunk.lockState == LanternChunk.LockState.LOADING) { final LanternChunkQueueTask task = this.chunkQueueTasks.get(coords); if (task != null && task.runnable instanceof LanternChunkLoadTask) { task.cancel(); } // Queue the chunk for unload, will be some ticks later } else { final UnloadingChunkEntry entry = new UnloadingChunkEntry(coords); if (!this.pendingForUnload.contains(entry)) { this.pendingForUnload.offer(entry); } } } if (callEvents) { final Vector3i coords0 = new Vector3i(coords.getX(), 0, coords.getY()); this.game.getEventManager().post(SpongeEventFactory.createUnforcedChunkEvent( Cause.source(ticket).owner(this.world).build(), coords0, ticket)); } } /** * Releases the ticket. * * @param ticket the ticket */ void release(LanternLoadingTicket ticket) { this.tickets.remove(ticket); } void attach(LanternLoadingTicket ticket) { this.tickets.add(ticket); } public void save() { try { LanternLoadingTicketIO.save(this.worldFolder, this.tickets); } catch (IOException e) { this.game.getLogger().warn("An error occurred while saving the chunk loading tickets", e); } for (Entry<Vector2i, LanternChunk> entry : this.loadedChunks.entrySet()) { // Save the chunk save(entry.getValue()); } } /** * Shuts the chunk manager down, all the chunks will * be saved in the process. */ public void shutdown() { try { LanternLoadingTicketIO.save(this.worldFolder, this.tickets); } catch (IOException e) { this.game.getLogger().warn("An error occurred while saving the chunk loading tickets", e); } for (Entry<Vector2i, LanternChunk> entry : this.loadedChunks.entrySet()) { final LanternChunk chunk = entry.getValue(); // Post the chunk unload event this.game.getEventManager().post(SpongeEventFactory.createUnloadChunkEvent( Cause.source(this.game.getMinecraftPlugin()).owner(this.world).build(), chunk)); // Save the chunk save(chunk); } // Cleanup this.loadedChunks.clear(); this.reusableChunks.clear(); this.chunkTaskExecutor.shutdown(); } /** * Pulses the chunk manager. */ public void pulse() { UnloadingChunkEntry entry; while ((entry = this.pendingForUnload.peek()) != null && (System.currentTimeMillis() - entry.time) > UNLOAD_DELAY) { this.pendingForUnload.poll(); if (!this.ticketsByPos.containsKey(entry.coords)) { // TODO: Create unload tasks unload(entry.coords, () -> Cause.source(this.world).build()); } } } public void loadTickets() throws IOException { final Multimap<String, LanternLoadingTicket> tickets = LanternLoadingTicketIO.load(this.worldFolder, this, this.chunkLoadService); final Iterator<Entry<String, LanternLoadingTicket>> it = tickets.entries().iterator(); while (it.hasNext()) { final LanternLoadingTicket ticket = it.next().getValue(); if (ticket instanceof LanternEntityLoadingTicket) { final LanternEntityLoadingTicket ticket0 = (LanternEntityLoadingTicket) ticket; final EntityReference ref = ticket0.getEntityReference().orElse(null); if (ref != null) { final LanternChunk chunk = getOrCreateChunk(ref.getChunkCoords(), () -> Cause.source(ticket0).owner(this.world).build(), true, true); final Entity entity = chunk.getEntity(ref.getUniqueId()).orElse(null); if (entity != null) { ticket0.bindToEntity(entity); } else { // The entity is gone? it.remove(); } } else { // The entity is gone? it.remove(); } } } for (Entry<String, Collection<LanternLoadingTicket>> entry : tickets.asMap().entrySet()) { final Collection<ChunkTicketManager.Callback> callbacks = this.chunkLoadService.getCallbacks().get(entry.getKey()); // These maps will be loaded lazily ImmutableListMultimap<UUID, LoadingTicket> playerLoadedTickets = null; ImmutableList<LoadingTicket> nonPlayerLoadedTickets = null; final Set<LoadingTicket> resultPlayerLoadedTickets = entry.getValue().stream() .filter(ticket -> ticket instanceof PlayerLoadingTicket) .collect(Collectors.toSet()); final Set<LoadingTicket> resultNonPlayerLoadedTickets = entry.getValue().stream() .filter(ticket -> !(ticket instanceof PlayerLoadingTicket)) .collect(Collectors.toSet()); final int maxTickets = this.chunkLoadService.getMaxTicketsById(entry.getKey()); for (ChunkTicketManager.Callback callback : callbacks) { if (callback instanceof ChunkTicketManager.OrderedCallback) { if (nonPlayerLoadedTickets == null) { nonPlayerLoadedTickets = ImmutableList.copyOf(resultNonPlayerLoadedTickets); resultNonPlayerLoadedTickets.clear(); } final List<LoadingTicket> result = ((ChunkTicketManager.OrderedCallback) callback).onLoaded( nonPlayerLoadedTickets, this.world, maxTickets); checkNotNull(result, "The OrderedCallback#onLoaded method may not return null, " + "error caused by (plugin=%s, clazz=%s)", entry.getKey(), callback.getClass().getName()); resultNonPlayerLoadedTickets.addAll(result); } if (callback instanceof ChunkTicketManager.PlayerOrderedCallback) { if (playerLoadedTickets == null) { final ImmutableListMultimap.Builder<UUID, LoadingTicket> mapBuilder = ImmutableListMultimap.builder(); resultPlayerLoadedTickets.forEach(ticket -> mapBuilder.put(((PlayerLoadingTicket) ticket).getPlayerUniqueId(), ticket)); resultPlayerLoadedTickets.clear(); playerLoadedTickets = mapBuilder.build(); } final ListMultimap<UUID, LoadingTicket> result = ((ChunkTicketManager.PlayerOrderedCallback) callback) .onPlayerLoaded(playerLoadedTickets, this.world); checkNotNull(result, "The PlayerOrderedCallback#onPlayerLoaded method may not return null, " + "error caused by (plugin=%s, clazz=%s)", entry.getKey(), callback.getClass().getName()); resultPlayerLoadedTickets.addAll(result.values()); } } final List<LoadingTicket> resultLoadedTickets = new ArrayList<>(); resultLoadedTickets.addAll(resultPlayerLoadedTickets); resultLoadedTickets.addAll(resultNonPlayerLoadedTickets); // Lets see how many plugins attempted to add loading tickets final int sizeA = resultLoadedTickets.size(); resultLoadedTickets.retainAll(entry.getValue()); final int sizeB = resultLoadedTickets.size(); if (sizeA != sizeB) { Lantern.getLogger().warn("The plugin {} attempted to add LoadingTicket's that were previously not present.", entry.getKey()); } // Remove all the tickets that are already released resultLoadedTickets.removeIf(ticket -> ((ChunkLoadingTicket) ticket).isReleased()); if (resultLoadedTickets.size() > maxTickets) { Lantern.getLogger().warn("The plugin {} has too many open chunk loading tickets {}. " + "Excess will be dropped", entry.getKey(), resultLoadedTickets.size()); resultLoadedTickets.subList(maxTickets, resultLoadedTickets.size()).clear(); } // Release all the tickets that were no longer usable final List<LoadingTicket> removedTickets = new ArrayList<>(entry.getValue()); removedTickets.removeAll(resultLoadedTickets); removedTickets.forEach(LoadingTicket::release); final ImmutableList<LoadingTicket> loadedTickets = ImmutableList.copyOf(resultLoadedTickets); for (ChunkTicketManager.Callback callback : callbacks) { callback.onLoaded(loadedTickets, this.world); } } } }