package net.minecraft.server; import com.destroystokyo.paper.PaperConfig; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIterator; import it.unimi.dsi.fastutil.longs.LongArraySet; import it.unimi.dsi.fastutil.longs.LongIterator; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import com.destroystokyo.paper.exception.ServerInternalException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; // CraftBukkit start import org.bukkit.craftbukkit.chunkio.ChunkIOExecutor; import org.bukkit.event.world.ChunkUnloadEvent; // CraftBukkit end public class ChunkProviderServer implements IChunkProvider { private static final Logger a = LogManager.getLogger(); public final LongArraySet unloadQueue = new LongArraySet(512); // Dionysus public final ChunkGenerator chunkGenerator; private final IChunkLoader chunkLoader; // Paper start - chunk save stats private long lastQueuedSaves = 0L; // Paper private long lastProcessedSaves = 0L; // Paper private long lastSaveStatPrinted = System.currentTimeMillis(); // Paper end // Paper start protected Chunk lastChunkByPos = null; public Long2ObjectOpenHashMap chunks = new Long2ObjectOpenHashMap(8192) { @Override public Chunk get(long key) { if (lastChunkByPos != null && key == lastChunkByPos.chunkKey) { return lastChunkByPos; } return lastChunkByPos = super.get(key); } @Override public Chunk remove(long key) { if (lastChunkByPos != null && key == lastChunkByPos.chunkKey) { lastChunkByPos = null; } return super.remove(key); } }; // CraftBukkit // Paper end public final WorldServer world; public ChunkProviderServer(WorldServer worldserver, IChunkLoader ichunkloader, ChunkGenerator chunkgenerator) { this.world = worldserver; this.chunkLoader = ichunkloader; this.chunkGenerator = chunkgenerator; } public Collection a() { return this.chunks.values(); } public void unload(Chunk chunk) { if (this.world.worldProvider.c(chunk.locX, chunk.locZ)) { this.unloadQueue.add(Long.valueOf(ChunkCoordIntPair.a(chunk.locX, chunk.locZ))); chunk.d = true; } } public void b() { ObjectIterator objectiterator = this.chunks.values().iterator(); while (objectiterator.hasNext()) { Chunk chunk = (Chunk) objectiterator.next(); this.unload(chunk); } } // Paper start public boolean isChunkGenerated(int x, int z) { return this.chunks.containsKey(ChunkCoordIntPair.asLong(x, z)) || this.chunkLoader.chunkExists(x, z); } // Paper end @Nullable public Chunk getLoadedChunkAt(int i, int j) { long k = ChunkCoordIntPair.a(i, j); Chunk chunk = (Chunk) this.chunks.get(k); if (chunk != null) { chunk.d = false; } return chunk; } @Nullable public Chunk getOrLoadChunkAt(int i, int j) { Chunk chunk = this.getLoadedChunkAt(i, j); if (chunk == null) { // CraftBukkit start ChunkRegionLoader loader = null; if (this.chunkLoader instanceof ChunkRegionLoader) { loader = (ChunkRegionLoader) this.chunkLoader; } if (loader != null && loader.chunkExists(i, j)) { chunk = ChunkIOExecutor.syncChunkLoad(world, loader, this, i, j); } } return chunk; } @Nullable public Chunk originalGetOrLoadChunkAt(int i, int j) { // CraftBukkit end Chunk chunk = this.getLoadedChunkAt(i, j); if (chunk == null) { chunk = this.loadChunk(i, j); if (chunk != null) { this.chunks.put(ChunkCoordIntPair.a(i, j), chunk); chunk.addEntities(); chunk.loadNearby(this, this.chunkGenerator, false); // CraftBukkit } } return chunk; } // CraftBukkit start public Chunk getChunkIfLoaded(int x, int z) { return chunks.get(ChunkCoordIntPair.a(x, z)); } // CraftBukkit end public Chunk getChunkAt(int i, int j) { return getChunkAt(i, j, null); } public Chunk getChunkAt(int i, int j, Runnable runnable) { return getChunkAt(i, j, runnable, true); } public Chunk getChunkAt(int i, int j, Runnable runnable, boolean generate) { Chunk chunk = world.paperConfig.allowPermaChunkLoaders ? getLoadedChunkAt(i, j) : getChunkIfLoaded(i, j); // Paper - Configurable perma chunk loaders ChunkRegionLoader loader = null; if (this.chunkLoader instanceof ChunkRegionLoader) { loader = (ChunkRegionLoader) this.chunkLoader; } // We can only use the queue for already generated chunks if (chunk == null && loader != null && loader.chunkExists(i, j)) { if (runnable != null) { ChunkIOExecutor.queueChunkLoad(world, loader, this, i, j, runnable); return null; } else { chunk = ChunkIOExecutor.syncChunkLoad(world, loader, this, i, j); // Paper start - If there was an issue loading the chunk from region, stage1 will fail and stage2 will load it sync // all we need to do is fetch an instance if (chunk == null) { chunk = getChunkIfLoaded(i, j); } // Paper end } } else if (chunk == null && generate) { chunk = originalGetChunkAt(i, j); } // If we didn't load the chunk async and have a callback run it now if (runnable != null) { runnable.run(); } return chunk; } public Chunk originalGetChunkAt(int i, int j) { Chunk chunk = this.originalGetOrLoadChunkAt(i, j); // CraftBukkit end if (chunk == null) { world.timings.syncChunkLoadTimer.startTiming(); // Spigot long k = ChunkCoordIntPair.a(i, j); try { chunk = this.chunkGenerator.getOrCreateChunk(i, j); } catch (Throwable throwable) { CrashReport crashreport = CrashReport.a(throwable, "Exception generating new chunk"); CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Chunk to be generated"); crashreportsystemdetails.a("Location", (Object) String.format("%d,%d", new Object[] { Integer.valueOf(i), Integer.valueOf(j)})); crashreportsystemdetails.a("Position hash", (Object) Long.valueOf(k)); crashreportsystemdetails.a("Generator", (Object) this.chunkGenerator); throw new ReportedException(crashreport); } this.chunks.put(k, chunk); chunk.addEntities(); chunk.loadNearby(this, this.chunkGenerator, true); // CraftBukkit world.timings.syncChunkLoadTimer.stopTiming(); // Spigot } return chunk; } @Nullable public Chunk loadChunk(int i, int j) { try { Chunk chunk = this.chunkLoader.a(this.world, i, j); if (chunk != null) { chunk.setLastSaved(this.world.getTime()); this.chunkGenerator.recreateStructures(chunk, i, j); } return chunk; } catch (Exception exception) { // Paper start String msg = "Couldn\'t load chunk"; ChunkProviderServer.a.error(msg, exception); ServerInternalException.reportInternalException(exception); // Paper end return null; } } public void saveChunkNOP(Chunk chunk) { try { // this.chunkLoader.b(this.world, chunk); // Spigot } catch (Exception exception) { // Paper start String msg = "Couldn\'t save entities"; ChunkProviderServer.a.error(msg, exception); ServerInternalException.reportInternalException(exception); // Paper end } } public void saveChunk(Chunk chunk, boolean unloaded) { // Spigot try (co.aikar.timings.Timing timed = world.timings.chunkSaveData.startTiming()) { chunk.setLastSaved(this.world.getTime()); this.chunkLoader.saveChunk(this.world, chunk, unloaded); // Spigot } catch (IOException ioexception) { // Paper start String msg = "Couldn\'t save chunk"; ChunkProviderServer.a.error(msg, ioexception); ServerInternalException.reportInternalException(ioexception); } catch (ExceptionWorldConflict exceptionworldconflict) { String msg = "Couldn\'t save chunk; already in use by another instance of Minecraft?"; ChunkProviderServer.a.error(msg, exceptionworldconflict); ServerInternalException.reportInternalException(exceptionworldconflict); } } public boolean a(boolean flag) { int i = 0; // CraftBukkit start // Paper start final ChunkRegionLoader chunkLoader = (ChunkRegionLoader) world.getChunkProviderServer().chunkLoader; final int queueSize = chunkLoader.getQueueSize(); final long now = System.currentTimeMillis(); final long timeSince = (now - lastSaveStatPrinted) / 1000; final Integer printRateSecs = Integer.getInteger("printSaveStats"); if (printRateSecs != null && timeSince >= printRateSecs) { final String timeStr = "/" + timeSince +"s"; final long queuedSaves = chunkLoader.getQueuedSaves(); long queuedDiff = queuedSaves - lastQueuedSaves; lastQueuedSaves = queuedSaves; final long processedSaves = chunkLoader.getProcessedSaves(); long processedDiff = processedSaves - lastProcessedSaves; lastProcessedSaves = processedSaves; lastSaveStatPrinted = now; if (processedDiff > 0 || queueSize > 0 || queuedDiff > 0) { System.out.println("[Chunk Save Stats] " + world.worldData.getName() + " - Current: " + queueSize + " - Queued: " + queuedDiff + timeStr + " - Processed: " +processedDiff + timeStr ); } } if (queueSize > world.paperConfig.queueSizeAutoSaveThreshold){ return false; } final int autoSaveLimit = world.paperConfig.maxAutoSaveChunksPerTick; // Paper end Iterator iterator = this.chunks.values().iterator(); while (iterator.hasNext()) { Chunk chunk = (Chunk) iterator.next(); // CraftBukkit end if (flag) { this.saveChunkNOP(chunk); } if (chunk.a(flag)) { this.saveChunk(chunk, false); // Spigot chunk.f(false); ++i; if (!flag && i >= autoSaveLimit) { // Spigot - // Paper - Incremental Auto Save - cap max per tick return false; } } } return true; } public void c() { this.chunkLoader.c(); } private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.96; public boolean unloadChunks() { if (!this.world.savingDisabled) { if (!this.unloadQueue.isEmpty()) { // Spigot start org.spigotmc.SlackActivityAccountant activityAccountant = this.world.getMinecraftServer().slackActivityAccountant; activityAccountant.startActivity(0.5); int targetSize = Math.min(this.unloadQueue.size() - 100, (int) (this.unloadQueue.size() * UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive // Spigot end LongIterator iterator = this.unloadQueue.iterator(); while (iterator.hasNext()) { // Spigot Long chunkKey = iterator.nextLong(); iterator.remove(); // Spigot Chunk chunk = (Chunk) this.chunks.get(chunkKey); if (chunk != null && chunk.d) { // CraftBukkit start - move unload logic to own method chunk.setShouldUnload(false); // Paper if (!unloadChunk(chunk, true)) { continue; } // CraftBukkit end // Spigot start if (this.unloadQueue.size() <= targetSize && activityAccountant.activityTimeIsExhausted()) { break; } // Spigot end } } activityAccountant.endActivity(); // Spigot } // Paper start - delayed chunk unloads long now = System.currentTimeMillis(); long unloadAfter = world.paperConfig.delayChunkUnloadsBy; if (unloadAfter > 0) { //noinspection Convert2streamapi for (Chunk chunk : chunks.values()) { if (chunk.scheduledForUnload != null && now - chunk.scheduledForUnload > unloadAfter) { chunk.scheduledForUnload = null; unload(chunk); } } } // Paper end this.chunkLoader.b(); } return false; } // CraftBukkit start public boolean unloadChunk(Chunk chunk, boolean save) { ChunkUnloadEvent event = new ChunkUnloadEvent(chunk.bukkitChunk, save); this.world.getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { return false; } save = event.isSaveChunk(); chunk.lightingQueue.processUnload(); // Paper // Update neighbor counts for (int x = -2; x < 3; x++) { for (int z = -2; z < 3; z++) { if (x == 0 && z == 0) { continue; } Chunk neighbor = this.getChunkIfLoaded(chunk.locX + x, chunk.locZ + z); if (neighbor != null) { neighbor.setNeighborUnloaded(-x, -z); chunk.setNeighborUnloaded(x, z); } } } // Moved from unloadChunks above chunk.removeEntities(); if (save) { this.saveChunk(chunk, true); // Spigot this.saveChunkNOP(chunk); } this.chunks.remove(chunk.chunkKey); return true; } // CraftBukkit end public boolean e() { return !this.world.savingDisabled; } public String getName() { return "ServerChunkCache: " + this.chunks.size() + " Drop: " + this.unloadQueue.size(); } public List a(EnumCreatureType enumcreaturetype, BlockPosition blockposition) { return this.chunkGenerator.getMobsFor(enumcreaturetype, blockposition); } @Nullable public BlockPosition a(World world, String s, BlockPosition blockposition, boolean flag) { return this.chunkGenerator.findNearestMapFeature(world, s, blockposition, flag); } public boolean a(World world, String s, BlockPosition blockposition) { return this.chunkGenerator.a(world, s, blockposition); } public int g() { return this.chunks.size(); } public boolean isLoaded(int i, int j) { return this.chunks.containsKey(ChunkCoordIntPair.a(i, j)); } public boolean e(int i, int j) { return this.chunks.containsKey(ChunkCoordIntPair.a(i, j)) || this.chunkLoader.chunkExists(i, j); } }