package org.torch.server;
import lombok.Getter;
import net.minecraft.server.BiomeBase;
import net.minecraft.server.BlockPosition;
import net.minecraft.server.Chunk;
import net.minecraft.server.ChunkCoordIntPair;
import net.minecraft.server.ChunkGenerator;
import net.minecraft.server.ChunkProviderServer;
import net.minecraft.server.ChunkRegionLoader;
import net.minecraft.server.CrashReport;
import net.minecraft.server.CrashReportSystemDetails;
import net.minecraft.server.EnumCreatureType;
import net.minecraft.server.ExceptionWorldConflict;
import net.minecraft.server.IChunkLoader;
import net.minecraft.server.ReportedException;
import net.minecraft.server.World;
import net.minecraft.server.WorldServer;
import org.bukkit.craftbukkit.chunkio.ChunkIOExecutor;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.spigotmc.SlackActivityAccountant;
import org.torch.api.TorchReactor;
import com.destroystokyo.paper.exception.ServerInternalException;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import static org.torch.server.TorchServer.logger;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
@Getter
public final class TorchChunkProvider implements net.minecraft.server.IChunkProvider, org.torch.api.IChunkProvider, TorchReactor {
private final ChunkProviderServer servant;
public static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.96;
public final LongSet unloadQueue = new LongArraySet();
public final ChunkGenerator chunkGenerator;
private final IChunkLoader chunkLoader;
// Paper - Chunk save stats
private long lastQueuedSaves = 0L;
private long lastProcessedSaves = 0L;
private long lastSaveStatPrinted = System.currentTimeMillis();
protected Chunk lastChunkByPos = null;
public final WorldServer world;
/** Map of chunk Id's to Chunk instances */
public Long2ObjectOpenHashMap<Chunk> chunks = new Long2ObjectOpenHashMap<Chunk>(8192) {
private static final long serialVersionUID = -1L;
@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);
}
};
public TorchChunkProvider(WorldServer world, IChunkLoader loader, ChunkGenerator generator, ChunkProviderServer servant) {
this.servant = servant;
this.world = world;
this.chunkLoader = loader;
this.chunkGenerator = generator;
}
public Collection<Chunk> getLoadedChunks() {
return this.chunks.values();
}
/**
* Post a chunk to unload queue
*/
public void postChunkToUnload(Chunk chunk) {
if (this.world.worldProvider.c(chunk.locX, chunk.locZ)) {
this.unloadQueue.add(Long.valueOf(ChunkCoordIntPair.chunkXZ2Int(chunk.locX, chunk.locZ)));
chunk.d = true; // PAIL: unloaded
}
}
/**
* Marks all chunks for unload, ignoring those near the spawn
*/
public void unloadAllChunks() {
ObjectIterator<Chunk> it = this.chunks.values().iterator();
while (it.hasNext()) this.postChunkToUnload(it.next());
}
@Nullable
public Chunk getChunkIfLoaded(int chunkX, int chunkZ, boolean markUnloaded) {
return markUnloaded ? getLoadedChunkAt(chunkX, chunkZ) : chunks.get(ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ));
}
@Override @Nullable
public Chunk getLoadedChunkAt(int chunkX, int chunkZ) {
Chunk chunk = this.chunks.get(ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ));
if (chunk != null) chunk.d = false; // PAIL: unloaded
return chunk;
}
@Nullable
public Chunk getOrLoadChunkAt(int x, int z) {
Chunk chunk = this.getLoadedChunkAt(x, z);
if (chunk == null) {
ChunkRegionLoader loader = null;
if (this.chunkLoader instanceof ChunkRegionLoader) {
loader = (ChunkRegionLoader) this.chunkLoader;
}
if (loader != null && loader.chunkExists(x, z)) {
chunk = ChunkIOExecutor.syncChunkLoad(world, loader, servant, x, z);
}
}
return chunk;
}
@Nullable
public Chunk originalGetOrLoadChunkAt(int chunkX, int chunkZ) {
Chunk chunk = this.getLoadedChunkAt(chunkX, chunkZ);
if (chunk == null) {
chunk = this.loadChunkFromFile(chunkX, chunkZ);
if (chunk != null) {
this.chunks.put(ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ), chunk);
chunk.addEntities();
chunk.loadNearby(servant, this.chunkGenerator, false);
}
}
return chunk;
}
@Override
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 = getChunkIfLoaded(i, j, false);
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, servant, i, j, runnable);
return null;
} else {
chunk = ChunkIOExecutor.syncChunkLoad(world, loader, servant, i, j);
}
} 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 chunkX, int chunkZ) {
Chunk chunk = this.originalGetOrLoadChunkAt(chunkX, chunkZ);
if (chunk == null) {
world.timings.syncChunkLoadTimer.startTiming();
long chunkPos = ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ);
try {
chunk = this.chunkGenerator.getOrCreateChunk(chunkX, chunkZ);
} catch (Throwable t) {
CrashReport crashReport = CrashReport.a(t, "Exception generating new chunk");
CrashReportSystemDetails systemDetails = crashReport.a("Chunk to be generated");
systemDetails.a("Location", String.format("%d,%d", new Object[] { Integer.valueOf(chunkX), Integer.valueOf(chunkZ)}));
systemDetails.a("Position hash", Long.valueOf(chunkPos));
systemDetails.a("Generator", this.chunkGenerator);
throw new ReportedException(crashReport);
}
this.chunks.put(chunkPos, chunk);
chunk.addEntities();
chunk.loadNearby(servant, this.chunkGenerator, true);
world.timings.syncChunkLoadTimer.stopTiming();
}
return chunk;
}
@Nullable
public Chunk loadChunkFromFile(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 (Throwable t) {
logger.error("Couldn\'t load chunk", t);
ServerInternalException.reportInternalException(t);
return null;
}
}
public void saveChunkExtraData(Chunk chunk) {
try (co.aikar.timings.Timing timed = world.timings.chunkSaveNop.startTiming()) {
this.chunkLoader.b(this.world, chunk); // saveChunkExtraData
} catch (Throwable t) {
logger.error("Couldn\'t save entities", t);
ServerInternalException.reportInternalException(t);
}
}
public void saveChunkData(Chunk chunk) {
try (co.aikar.timings.Timing timed = world.timings.chunkSaveData.startTiming()) {
chunk.setLastSaved(this.world.getTime());
this.chunkLoader.a(this.world, chunk); // saveChunkData
} catch (IOException io) {
logger.error("Couldn\'t save chunk", io);
ServerInternalException.reportInternalException(io);
} catch (ExceptionWorldConflict conflict) {
logger.error("Couldn\'t save chunk; already in use by another instance of Minecraft?", conflict);
ServerInternalException.reportInternalException(conflict);
}
}
public boolean saveChunks(boolean saveExtraData) {
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) {
logger.info("[Chunk Save Stats] " + world.worldData.getName() +
" - Current: " + queueSize +
" - Queued: " + queuedDiff + timeStr +
" - Processed: " +processedDiff + timeStr
);
}
}
if (queueSize > world.paperConfig.queueSizeAutoSaveThreshold) return false;
int savedChunkCount = 0;
for (Chunk chunk : this.chunks.values()) {
if (saveExtraData) this.saveChunkExtraData(chunk);
if (chunk.a(saveExtraData)) { // If the chunk needs saving
this.saveChunkData(chunk);
chunk.f(false); // PAIL: setModified(false)
savedChunkCount++;
// Paper - Incremental Auto Save - cap max per tick
if (!saveExtraData && savedChunkCount >= world.paperConfig.maxAutoSaveChunksPerTick) return false;
}
}
return true;
}
/**
* Save extra data not associated with any Chunk.
* Not saved during autosave, only during world unload. Currently unimplemented.
*/
public void saveExtraData() {
this.chunkLoader.b(); // PAIL: saveExtraData()
}
/**
* Unloads chunks that are marked to be unloaded. This is not guaranteed to unload every such chunk.
*/
@SuppressWarnings("deprecation")
@Override
public boolean unloadChunks() {
if (this.world.savingDisabled) return false;
if (!this.unloadQueue.isEmpty()) {
SlackActivityAccountant activityAccountant = this.world.getMinecraftServer().slackActivityAccountant;
activityAccountant.startActivity(0.5);
// Paper - Make more aggressive
int targetSize = Math.min(this.unloadQueue.size() - 100, (int) (this.unloadQueue.size() * UNLOAD_QUEUE_RESIZE_FACTOR));
for (Long chunkPos : this.unloadQueue) {
this.unloadQueue.remove(chunkPos);
Chunk chunk = this.chunks.get(chunkPos);
if (chunk != null && chunk.d) { // PAIL: chunk.unloaded
chunk.setShouldUnload(false);
if (!unloadChunk(chunk, true)) continue;
if (this.unloadQueue.size() <= targetSize && activityAccountant.activityTimeIsExhausted()) break;
}
}
activityAccountant.endActivity(); // Spigot
}
// Paper - delayed chunk unloads
long now = System.currentTimeMillis();
long unloadAfter = world.paperConfig.delayChunkUnloadsBy;
if (unloadAfter > 0) {
for (Chunk chunk : chunks.values()) { // TODO: Convert2streamapi
if (chunk.scheduledForUnload != null && now - chunk.scheduledForUnload > unloadAfter) {
chunk.scheduledForUnload = null;
this.postChunkToUnload(chunk);
}
}
}
this.chunkLoader.a(); // PAIL: chunkTick()
return false;
}
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();
// 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, false);
if (neighbor != null) {
neighbor.setNeighborUnloaded(-x, -z);
chunk.setNeighborUnloaded(x, z);
}
}
}
chunk.removeEntities();
if (save) {
this.saveChunkData(chunk);
this.saveChunkExtraData(chunk);
}
this.chunks.remove(chunk.chunkKey);
return true;
}
/**
* Returns if the world supports saving
*/
public boolean canSave() {
return !this.world.savingDisabled;
}
/**
* Converts the instance data to a readable string
*/
@Override
public String getName() {
return "ServerChunkCache: " + this.chunks.size() + " Drop: " + this.unloadQueue.size();
}
public List<BiomeBase.BiomeMeta> getPossibleCreatures(EnumCreatureType creatureType, BlockPosition position) {
return this.chunkGenerator.getMobsFor(creatureType, position);
}
@Nullable
public BlockPosition findNearestMapFeature(World world, String structureName, BlockPosition position, boolean flag) {
return this.chunkGenerator.findNearestMapFeature(world, structureName, position, flag);
}
public int getLoadedChunkCount() {
return this.chunks.size();
}
/**
* Checks to see if a chunk exists at x, z
*/
public boolean isLoaded(int chunkX, int chunkZ) {
return this.chunks.containsKey(ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ));
}
@Override @Deprecated public boolean e(int x, int z) { return this.isChunkGeneratedAt(x, z); } // Implement from net.minecraft.IChunkProvider
@Override public boolean isChunkGeneratedAt(int chunkX, int chunkZ) {
return this.chunks.containsKey(ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ)) || this.chunkLoader.a(chunkX, chunkZ); // PAIL: a -> isChunkGeneratedAt(x, z)
}
}