more patches updated

This commit is contained in:
Jake 2021-11-23 20:25:34 -08:00 committed by MiniDigger | Martin
parent 5d0d11e4b6
commit 2b29fe37a2
8 changed files with 95 additions and 130 deletions

View file

@ -0,0 +1,252 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Sat, 13 Sep 2014 23:14:43 -0400
Subject: [PATCH] Configurable Keep Spawn Loaded range per world
This lets you disable it for some worlds and lower it for others.
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index a5da82e0dea8a1ac5699179639fbd9ebe7b6e220..522c2a86c916f3558a8d5b34e456f8b5fb7d9d3b 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -55,6 +55,12 @@ public class PaperWorldConfig {
}
}
+ public short keepLoadedRange;
+ private void keepLoadedRange() {
+ keepLoadedRange = (short) (getInt("keep-spawn-loaded-range", Math.min(spigotConfig.viewDistance, 10)) * 16);
+ log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16));
+ }
+
private boolean getBoolean(String path, boolean def) {
config.addDefault("world-settings.default." + path, def);
return config.getBoolean("world-settings." + worldName + "." + path, config.getBoolean("world-settings.default." + path));
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index beefed4d29fee9c20ee69bef88a7af2cc3770707..977dc521cff5b448665e00b953f4a02ecf04ee75 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -759,35 +759,36 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// CraftBukkit start
public void prepareLevels(ChunkProgressListener worldloadlistener, ServerLevel worldserver) {
- if (!worldserver.getWorld().getKeepSpawnInMemory()) {
- return;
- }
+ ServerChunkCache chunkproviderserver = worldserver.getChunkSource(); // Paper
// WorldServer worldserver = this.overworld();
this.forceTicks = true;
// CraftBukkit end
+ if (worldserver.getWorld().getKeepSpawnInMemory()) { // Paper
MinecraftServer.LOGGER.info("Preparing start region for dimension {}", worldserver.dimension().location());
BlockPos blockposition = worldserver.getSharedSpawnPos();
worldloadlistener.updateSpawnPos(new ChunkPos(blockposition));
- ServerChunkCache chunkproviderserver = worldserver.getChunkSource();
+ //ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider(); // Paper - move up
chunkproviderserver.getLightEngine().setTaskPerBatch(500);
this.nextTickTime = Util.getMillis();
- chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(blockposition), 11, Unit.INSTANCE);
-
- while (chunkproviderserver.getTickingGenerated() != 441) {
- // CraftBukkit start
- // this.nextTickTime = SystemUtils.getMillis() + 10L;
- this.executeModerately();
- // CraftBukkit end
- }
+ // Paper start - configurable spawn reason
+ int radiusBlocks = worldserver.paperConfig.keepLoadedRange;
+ int radiusChunks = radiusBlocks / 16 + ((radiusBlocks & 15) != 0 ? 1 : 0);
+ int totalChunks = ((radiusChunks) * 2 + 1);
+ totalChunks *= totalChunks;
+ worldloadlistener.setChunkRadius(radiusBlocks / 16);
+
+ worldserver.addTicketsForSpawn(radiusBlocks, blockposition);
+ // Paper end
// CraftBukkit start
// this.nextTickTime = SystemUtils.getMillis() + 10L;
this.executeModerately();
// Iterator iterator = this.levels.values().iterator();
+ }
if (true) {
ServerLevel worldserver1 = worldserver;
@@ -810,7 +811,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// this.nextTickTime = SystemUtils.getMillis() + 10L;
this.executeModerately();
// CraftBukkit end
- worldloadlistener.stop();
+ if (worldserver.getWorld().getKeepSpawnInMemory()) worldloadlistener.stop(); // Paper
chunkproviderserver.getLightEngine().setTaskPerBatch(5);
// CraftBukkit start
// this.updateMobSpawningFlags();
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index d5d72c70d4e414f618d7a6478843deec82f2386a..53067edd56a4b7571f693c593c0f103567d589ec 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -59,6 +59,7 @@ import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.network.protocol.game.DebugPackets;
import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.MCUtil;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.ServerScoreboard;
import net.minecraft.server.level.progress.ChunkProgressListener;
@@ -1583,12 +1584,84 @@ public class ServerLevel extends Level implements WorldGenLevel {
return ((MapIndex) this.getServer().overworld().getDataStorage().computeIfAbsent(MapIndex::load, MapIndex::new, "idcounts")).getFreeAuxValueForMap();
}
+ // Paper start - helper function for configurable spawn radius
+ public void addTicketsForSpawn(int radiusInBlocks, BlockPos spawn) {
+ // In order to respect vanilla behavior, which is ensuring everything but the spawn border can tick, we add tickets
+ // with level 31 for the non-border spawn chunks
+ ServerChunkCache chunkproviderserver = this.getChunkSource();
+ int tickRadius = radiusInBlocks - 16;
+
+ // add ticking chunks
+ for (int x = -tickRadius; x <= tickRadius; x += 16) {
+ for (int z = -tickRadius; z <= tickRadius; z += 16) {
+ // radius of 2 will have the current chunk be level 31
+ chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, z)), 2, Unit.INSTANCE);
+ }
+ }
+
+ // add border chunks
+
+ // add border along x axis (including corner chunks)
+ for (int x = -radiusInBlocks; x <= radiusInBlocks; x += 16) {
+ // top
+ chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+ // bottom
+ chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, -radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+ }
+
+ // add border along z axis (excluding corner chunks)
+ for (int z = -radiusInBlocks + 16; z < radiusInBlocks; z += 16) {
+ // right
+ chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+ // left
+ chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+ }
+ }
+ public void removeTicketsForSpawn(int radiusInBlocks, BlockPos spawn) {
+ // In order to respect vanilla behavior, which is ensuring everything but the spawn border can tick, we added tickets
+ // with level 31 for the non-border spawn chunks
+ ServerChunkCache chunkproviderserver = this.getChunkSource();
+ int tickRadius = radiusInBlocks - 16;
+
+ // remove ticking chunks
+ for (int x = -tickRadius; x <= tickRadius; x += 16) {
+ for (int z = -tickRadius; z <= tickRadius; z += 16) {
+ // radius of 2 will have the current chunk be level 31
+ chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, z)), 2, Unit.INSTANCE);
+ }
+ }
+
+ // remove border chunks
+
+ // remove border along x axis (including corner chunks)
+ for (int x = -radiusInBlocks; x <= radiusInBlocks; x += 16) {
+ // top
+ chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+ // bottom
+ chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, -radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+ }
+
+ // remove border along z axis (excluding corner chunks)
+ for (int z = -radiusInBlocks + 16; z < radiusInBlocks; z += 16) {
+ // right
+ chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+ // left
+ chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+ }
+ }
+ // Paper end
+
public void setDefaultSpawnPos(BlockPos pos, float angle) {
- ChunkPos chunkcoordintpair = new ChunkPos(new BlockPos(this.levelData.getXSpawn(), 0, this.levelData.getZSpawn()));
+ // Paper - configurable spawn radius
+ BlockPos prevSpawn = this.getSharedSpawnPos();
+ //ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(new BlockPosition(this.worldData.a(), 0, this.worldData.c()));
this.levelData.setSpawn(pos, angle);
- this.getChunkSource().removeRegionTicket(TicketType.START, chunkcoordintpair, 11, Unit.INSTANCE);
- this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), 11, Unit.INSTANCE);
+ if (this.keepSpawnInMemory) {
+ // if this keepSpawnInMemory is false a plugin has already removed our tickets, do not re-add
+ this.removeTicketsForSpawn(this.paperConfig.keepLoadedRange, prevSpawn);
+ this.addTicketsForSpawn(this.paperConfig.keepLoadedRange, pos);
+ }
this.getServer().getPlayerList().broadcastAll(new ClientboundSetDefaultSpawnPositionPacket(pos, angle));
}
diff --git a/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java b/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
index 1b565b2809c2d367e21971c5154f35c9763995e6..b0f899835ded29aff108d1674bf4a1a6c89693db 100644
--- a/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
+++ b/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
@@ -12,4 +12,6 @@ public interface ChunkProgressListener {
void start();
void stop();
+
+ void setChunkRadius(int radius); // Paper - allow changing chunk radius
}
diff --git a/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java b/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
index 4185e6bcf9b2bb65b2a0fa5fcbeb5684615169a7..dbc29442f2b2ad3ea451910f4944e90114a317c7 100644
--- a/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
+++ b/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
@@ -11,12 +11,19 @@ import org.apache.logging.log4j.Logger;
public class LoggerChunkProgressListener implements ChunkProgressListener {
private static final Logger LOGGER = LogManager.getLogger();
- private final int maxCount;
+ private int maxCount; // Paper - remove final
private int count;
private long startTime;
private long nextTickTime = Long.MAX_VALUE;
public LoggerChunkProgressListener(int radius) {
+ // Paper start - Allow changing radius later for configurable spawn patch
+ this.setChunkRadius(radius); // Move to method
+ }
+
+ @Override
+ public void setChunkRadius(int radius) {
+ // Paper end
int i = radius * 2 + 1;
this.maxCount = i * i;
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index f68fa8f971c39e3d150860542341a2564730b089..bb95fec09594f7e2d38619ae4055ec9964635622 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1337,15 +1337,21 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void setKeepSpawnInMemory(boolean keepLoaded) {
- world.keepSpawnInMemory = keepLoaded;
+ // Paper start - Configurable spawn radius
+ if (keepLoaded == world.keepSpawnInMemory) {
+ // do nothing, nothing has changed
+ return;
+ }
+ this.world.keepSpawnInMemory = keepLoaded;
// Grab the worlds spawn chunk
BlockPos chunkcoordinates = this.world.getSharedSpawnPos();
if (keepLoaded) {
- this.world.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(chunkcoordinates), 11, Unit.INSTANCE);
+ this.world.addTicketsForSpawn(this.world.paperConfig.keepLoadedRange, chunkcoordinates);
} else {
- // TODO: doesn't work well if spawn changed....
- this.world.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(chunkcoordinates), 11, Unit.INSTANCE);
+ // TODO: doesn't work well if spawn changed.... // Paper - resolved
+ this.world.removeTicketsForSpawn(this.world.paperConfig.keepLoadedRange, chunkcoordinates);
}
+ // Paper end
}
@Override

View file

@ -0,0 +1,84 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Shane Freeder <theboyetronic@gmail.com>
Date: Wed, 29 May 2019 04:01:22 +0100
Subject: [PATCH] ChunkMapDistance CME
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index 5b999da8c5ad430f9157276857165596d1208f64..26a787864058dccc0365680b0259ddf0dad22b58 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -70,6 +70,7 @@ public class ChunkHolder {
private boolean resendLight;
private CompletableFuture<Void> pendingFullStateConfirmation;
+ boolean isUpdateQueued = false; // Paper
private final ChunkMap chunkMap; // Paper
public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index ef87c37633cee4ab438f1991144188ade1c4e65f..975f1aab48f04a9f3cae04141b3fb65247f0ffaf 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -51,7 +51,16 @@ public abstract class DistanceManager {
private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
private final TickingTracker tickingTicketsTracker = new TickingTracker();
private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33);
- final Set<ChunkHolder> chunksToUpdateFutures = Sets.newHashSet();
+ // Paper start use a queue, but still keep unique requirement
+ public final java.util.Queue<ChunkHolder> pendingChunkUpdates = new java.util.ArrayDeque<ChunkHolder>() {
+ @Override
+ public boolean add(ChunkHolder o) {
+ if (o.isUpdateQueued) return true;
+ o.isUpdateQueued = true;
+ return super.add(o);
+ }
+ };
+ // Paper end
final ChunkTaskPriorityQueueSorter ticketThrottler;
final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> ticketThrottlerInput;
final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
@@ -124,26 +133,14 @@ public abstract class DistanceManager {
;
}
- if (!this.chunksToUpdateFutures.isEmpty()) {
- // CraftBukkit start
- // Iterate pending chunk updates with protection against concurrent modification exceptions
- java.util.Iterator<ChunkHolder> iter = this.chunksToUpdateFutures.iterator();
- int expectedSize = this.chunksToUpdateFutures.size();
- do {
- ChunkHolder playerchunk = iter.next();
- iter.remove();
- expectedSize--;
-
- playerchunk.updateFutures(chunkStorage, this.mainThreadExecutor);
-
- // Reset iterator if set was modified using add()
- if (this.chunksToUpdateFutures.size() != expectedSize) {
- expectedSize = this.chunksToUpdateFutures.size();
- iter = this.chunksToUpdateFutures.iterator();
- }
- } while (iter.hasNext());
- // CraftBukkit end
-
+ // Paper start
+ if (!this.pendingChunkUpdates.isEmpty()) {
+ while(!this.pendingChunkUpdates.isEmpty()) {
+ ChunkHolder remove = this.pendingChunkUpdates.remove();
+ remove.isUpdateQueued = false;
+ remove.updateFutures(chunkStorage, this.mainThreadExecutor);
+ }
+ // Paper end
return true;
} else {
if (!this.ticketsToRelease.isEmpty()) {
@@ -432,7 +429,7 @@ public abstract class DistanceManager {
if (k != level) {
playerchunk = DistanceManager.this.updateChunkScheduling(id, level, playerchunk, k);
if (playerchunk != null) {
- DistanceManager.this.chunksToUpdateFutures.add(playerchunk);
+ DistanceManager.this.pendingChunkUpdates.add(playerchunk);
}
}

View file

@ -0,0 +1,430 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sat, 1 Jun 2019 13:00:55 -0700
Subject: [PATCH] Chunk debug command
Prints all chunk information to a text file into the debug
folder in the root server folder. The format is in JSON, and
the data format is described in MCUtil#dumpChunks(File)
The command will output server version and all online players to the
file as well. We do not log anything but the location, world and
username of the player.
Also logs the value of these config values (note not all are paper's):
- keep spawn loaded value
- spawn radius
- view distance
Each chunk has the following logged:
- Coordinate
- Ticket level & its corresponding state
- Whether it is queued for unload
- Chunk status (may be unloaded)
- All tickets on the chunk
Example log:
https://gist.githubusercontent.com/Spottedleaf/0131e7710ffd5d531e5fd246c3367380/raw/169ae1b2e240485f99bc7a6bd8e78d90e3af7397/chunks-2019-06-01_19.57.05.txt
For references on certain keywords (ticket, status, etc), please see:
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528273&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528273
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528577&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528577
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index 1eb45df9dca5d0c31ac46709e706136a246cb8ea..005361c38b02713fb823d0be40954400d59f0c4d 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -6,13 +6,15 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.ChunkPos;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MCUtil;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.bukkit.Bukkit;
@@ -41,7 +43,7 @@ import java.util.stream.Collectors;
public class PaperCommand extends Command {
private static final String BASE_PERM = "bukkit.command.paper.";
- private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version").build();
+ private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo").build();
public PaperCommand(String name) {
super(name);
@@ -69,6 +71,21 @@ public class PaperCommand extends Command {
if (args.length == 3)
return getListMatchingLast(sender, args, EntityType.getEntityNameList().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new));
break;
+ case "debug":
+ if (args.length == 2) {
+ return getListMatchingLast(sender, args, "help", "chunks");
+ }
+ break;
+ case "chunkinfo":
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 2) {
+ return getListMatchingLast(sender, args, worldNames);
+ }
+ break;
}
return Collections.emptyList();
}
@@ -135,6 +152,12 @@ public class PaperCommand extends Command {
case "reload":
doReload(sender);
break;
+ case "debug":
+ doDebug(sender, args);
+ break;
+ case "chunkinfo":
+ doChunkInfo(sender, args);
+ break;
case "ver":
if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set)
case "version":
@@ -152,6 +175,114 @@ public class PaperCommand extends Command {
return true;
}
+ private void doChunkInfo(CommandSender sender, String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 2 || args[1].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length - 1);
+ for (int i = 1; i < args.length; ++i) {
+ org.bukkit.World world = Bukkit.getWorld(args[i]);
+ if (world == null) {
+ sender.sendMessage(ChatColor.RED + "World '" + args[i] + "' is invalid");
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedInactive = 0;
+ int accumulatedBorder = 0;
+ int accumulatedTicking = 0;
+ int accumulatedEntityTicking = 0;
+
+ for (org.bukkit.World bukkitWorld : worlds) {
+ ServerLevel world = ((CraftWorld)bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int border = 0;
+ int ticking = 0;
+ int entityTicking = 0;
+
+ for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
+ if (chunk.getFullChunkUnchecked() == null) {
+ continue;
+ }
+
+ ++total;
+
+ ChunkHolder.FullChunkStatus state = ChunkHolder.getFullChunkStatus(chunk.getTicketLevel());
+
+ switch (state) {
+ case INACCESSIBLE:
+ ++inactive;
+ continue;
+ case BORDER:
+ ++border;
+ continue;
+ case TICKING:
+ ++ticking;
+ continue;
+ case ENTITY_TICKING:
+ ++entityTicking;
+ continue;
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedInactive += inactive;
+ accumulatedBorder += border;
+ accumulatedTicking += ticking;
+ accumulatedEntityTicking += entityTicking;
+
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + bukkitWorld.getName() + ChatColor.DARK_AQUA + ":");
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + total + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
+ + inactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + border + ChatColor.BLUE + " Ticking: "
+ + ChatColor.DARK_AQUA + ticking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + entityTicking);
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + "all listed worlds" + ChatColor.DARK_AQUA + ":");
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + accumulatedTotal + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
+ + accumulatedInactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + accumulatedBorder + ChatColor.BLUE + " Ticking: "
+ + ChatColor.DARK_AQUA + accumulatedTicking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + accumulatedEntityTicking);
+ }
+ }
+
+ private void doDebug(CommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+
+ String debugType = args[1].toLowerCase(Locale.ENGLISH);
+ switch (debugType) {
+ case "chunks":
+ if (args.length >= 3 && args[2].toLowerCase(Locale.ENGLISH).equals("help")) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug chunks to dump loaded chunk information to a file");
+ break;
+ }
+ File file = new File(new File(new File("."), "debug"),
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
+ sender.sendMessage(ChatColor.GREEN + "Writing chunk information dump to " + file.toString());
+ try {
+ MCUtil.dumpChunks(file);
+ sender.sendMessage(ChatColor.GREEN + "Successfully written chunk information!");
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(ChatColor.RED + "Failed to dump chunk information, see console");
+ }
+
+ break;
+ case "help":
+ // fall through to default
+ default:
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+ }
+
/*
* Ported from MinecraftForge - author: LexManos <LexManos@gmail.com> - License: LGPLv2.1
*/
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index 3b10ef3801ffd47707836b3ed3482e99ddd0050b..2fe519d4059fac06781c30e140895b604e13104f 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -9,13 +9,27 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.DistanceManager;
+import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.Ticket;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ChunkStatus;
import org.apache.commons.lang.exception.ExceptionUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
import com.mojang.authlib.GameProfile;
+import com.mojang.datafixers.util.Either;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import org.bukkit.Location;
import org.bukkit.block.BlockFace;
import org.bukkit.craftbukkit.CraftWorld;
@@ -24,8 +38,11 @@ import org.spigotmc.AsyncCatcher;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import java.io.*;
+import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
@@ -535,6 +552,172 @@ public final class MCUtil {
return null;
}
+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
+ List<ChunkStatus> statuses = net.minecraft.server.level.ServerChunkCache.CHUNK_STATUSES;
+ for (int i = statuses.size() - 1; i >= 0; --i) {
+ ChunkStatus curr = statuses.get(i);
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = chunk.getFutureIfPresentUnchecked(curr);
+ if (future != ChunkHolder.UNLOADED_CHUNK_FUTURE) {
+ return curr;
+ }
+ }
+ return null; // unloaded
+ }
+
+ public static void dumpChunks(File file) throws IOException {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ /*
+ * Json format:
+ *
+ * Main data format:
+ * -server-version:<string>
+ * -data-version:<int>
+ * -worlds:
+ * -name:<world name>
+ * -view-distance:<int>
+ * -keep-spawn-loaded:<boolean>
+ * -keep-spawn-loaded-range:<int>
+ * -visible-chunk-count:<int>
+ * -loaded-chunk-count:<int>
+ * -verified-fully-loaded-chunks:<int>
+ * -players:<array of player>
+ * -chunk-data:<array of chunks>
+ *
+ * Player format:
+ * -name:<string>
+ * -x:<double>
+ * -y:<double>
+ * -z:<double>
+ *
+ * Chunk Format:
+ * -x:<integer>
+ * -z:<integer>
+ * -ticket-level:<integer>
+ * -state:<string>
+ * -queued-for-unload:<boolean>
+ * -status:<string>
+ * -tickets:<array of tickets>
+ *
+ *
+ * Ticket format:
+ * -ticket-type:<string>
+ * -ticket-level:<int>
+ * -add-tick:<long>
+ * -object-reason:<string> // This depends on the type of ticket. ie POST_TELEPORT -> entity id
+ */
+ List<org.bukkit.World> worlds = org.bukkit.Bukkit.getWorlds();
+ JsonObject data = new JsonObject();
+
+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion());
+ data.addProperty("data-version", 0);
+
+ JsonArray worldsData = new JsonArray();
+
+ for (org.bukkit.World bukkitWorld : worlds) {
+ JsonObject worldData = new JsonObject();
+
+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
+ ChunkMap chunkMap = world.getChunkSource().chunkMap;
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
+ DistanceManager chunkMapDistance = chunkMap.distanceManager;
+ List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
+ List<ServerPlayer> players = world.players;
+
+ int fullLoadedChunks = 0;
+
+ for (ChunkHolder chunk : allChunks) {
+ if (chunk.getFullChunkUnchecked() != null) {
+ ++fullLoadedChunks;
+ }
+ }
+
+ // sorting by coordinate makes the log easier to read
+ allChunks.sort((ChunkHolder v1, ChunkHolder v2) -> {
+ if (v1.pos.x != v2.pos.x) {
+ return Integer.compare(v1.pos.x, v2.pos.x);
+ }
+ return Integer.compare(v1.pos.z, v2.pos.z);
+ });
+
+ worldData.addProperty("name", world.getWorld().getName());
+ worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
+ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
+ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange);
+ worldData.addProperty("visible-chunk-count", visibleChunks.size());
+ worldData.addProperty("loaded-chunk-count", chunkMap.entitiesInLevel.size());
+ worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks);
+
+ JsonArray playersData = new JsonArray();
+
+ for (ServerPlayer player : players) {
+ JsonObject playerData = new JsonObject();
+
+ playerData.addProperty("name", player.getScoreboardName());
+ playerData.addProperty("x", player.getX());
+ playerData.addProperty("y", player.getY());
+ playerData.addProperty("z", player.getZ());
+
+ playersData.add(playerData);
+
+ }
+
+ worldData.add("players", playersData);
+
+ JsonArray chunksData = new JsonArray();
+
+ for (ChunkHolder playerChunk : allChunks) {
+ JsonObject chunkData = new JsonObject();
+
+ Set<Ticket<?>> tickets = chunkMapDistance.tickets.get(playerChunk.pos.longKey);
+ ChunkStatus status = getChunkStatus(playerChunk);
+
+ chunkData.addProperty("x", playerChunk.pos.x);
+ chunkData.addProperty("z", playerChunk.pos.z);
+ chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
+ chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString());
+ chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey));
+ chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
+
+ JsonArray ticketsData = new JsonArray();
+
+ if (tickets != null) {
+ for (Ticket<?> ticket : tickets) {
+ JsonObject ticketData = new JsonObject();
+
+ ticketData.addProperty("ticket-type", ticket.getType().toString());
+ ticketData.addProperty("ticket-level", ticket.getTicketLevel());
+ ticketData.addProperty("object-reason", String.valueOf(ticket.key));
+ ticketData.addProperty("add-tick", ticket.createdTick);
+
+ ticketsData.add(ticketData);
+ }
+ }
+
+ chunkData.add("tickets", ticketsData);
+ chunksData.add(chunkData);
+ }
+
+
+ worldData.add("chunk-data", chunksData);
+ worldsData.add(worldData);
+ }
+
+ data.add("worlds", worldsData);
+
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+ jsonWriter.setLenient(false);
+ Streams.write(data, jsonWriter);
+
+ String fileData = stringWriter.toString();
+
+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")) {
+ out.print(fileData);
+ }
+ }
+
public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
}

View file

@ -0,0 +1,254 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Fri, 15 Feb 2019 01:08:19 -0500
Subject: [PATCH] Allow Saving of Oversized Chunks
Note 1.17 update: With 1.17, Entities are no longer stored in chunk slices, so this needs updating!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
The Minecraft World Region File format has a hard cap of 1MB per chunk.
This is due to the fact that the header of the file format only allocates
a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector.
This limit can be reached fairly easily with books, resulting in the chunk being unable
to save to the world. Worse off, is that nothing printed when this occured, and silently
performed a chunk rollback on next load.
This leads to security risk with duplication and is being actively exploited.
This patch catches the too large scenario, falls back and moves any large Entity
or Tile Entity into a new compound, and this compound is saved into a different file.
On Chunk Load, we check for oversized status, and if so, we load the extra file and
merge the Entities and Tile Entities from the oversized chunk back into the level to
then be loaded as normal.
Once a chunk is returned back to normal size, the oversized flag will clear, and no
extra data file will exist.
This fix maintains compatability with all existing Anvil Region Format tools as it
does not alter the save format. They will just not know about the extra entities.
This fix also maintains compatability if someone switches server jars to one without
this fix, as the data will remain in the oversized file. Once the server returns
to a jar with this fix, the data will be restored.
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
index 6f7bcf74b0ff42841e37f36561d6c54e1ee8c989..d511db5453b722f747cc24cf2b4d97a0be75ce6a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -17,8 +17,12 @@ import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
+import java.util.zip.InflaterInputStream; // Paper
+
import javax.annotation.Nullable;
import net.minecraft.Util;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -45,6 +49,7 @@ public class RegionFile implements AutoCloseable {
@VisibleForTesting
protected final RegionBitmap usedSectors;
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
+ public final Path regionFile; // Paper
public RegionFile(Path path, Path path1, boolean dsync) throws IOException {
this(path, path1, RegionFileVersion.VERSION_DEFLATE, dsync);
@@ -52,6 +57,8 @@ public class RegionFile implements AutoCloseable {
public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException {
this.header = ByteBuffer.allocateDirect(8192);
+ this.regionFile = file; // Paper
+ initOversizedState(); // Paper
this.usedSectors = new RegionBitmap();
this.version = outputChunkStreamVersion;
if (!Files.isDirectory(directory, new LinkOption[0])) {
@@ -430,6 +437,74 @@ public class RegionFile implements AutoCloseable {
}
+ // Paper start
+ private final byte[] oversized = new byte[1024];
+ private int oversizedCount = 0;
+
+ private synchronized void initOversizedState() throws IOException {
+ Path metaFile = getOversizedMetaFile();
+ if (Files.exists(metaFile)) {
+ final byte[] read = java.nio.file.Files.readAllBytes(metaFile);
+ System.arraycopy(read, 0, oversized, 0, oversized.length);
+ for (byte temp : oversized) {
+ oversizedCount += temp;
+ }
+ }
+ }
+
+ private static int getChunkIndex(int x, int z) {
+ return (x & 31) + (z & 31) * 32;
+ }
+ synchronized boolean isOversized(int x, int z) {
+ return this.oversized[getChunkIndex(x, z)] == 1;
+ }
+ synchronized void setOversized(int x, int z, boolean oversized) throws IOException {
+ final int offset = getChunkIndex(x, z);
+ boolean previous = this.oversized[offset] == 1;
+ this.oversized[offset] = (byte) (oversized ? 1 : 0);
+ if (!previous && oversized) {
+ oversizedCount++;
+ } else if (!oversized && previous) {
+ oversizedCount--;
+ }
+ if (previous && !oversized) {
+ Path oversizedFile = getOversizedFile(x, z);
+ if (Files.exists(oversizedFile)) {
+ Files.delete(oversizedFile);
+ }
+ }
+ if (oversizedCount > 0) {
+ if (previous != oversized) {
+ writeOversizedMeta();
+ }
+ } else if (previous) {
+ Path oversizedMetaFile = getOversizedMetaFile();
+ if (Files.exists(oversizedMetaFile)) {
+ Files.delete(oversizedMetaFile);
+ }
+ }
+ }
+
+ private void writeOversizedMeta() throws IOException {
+ java.nio.file.Files.write(getOversizedMetaFile(), oversized);
+ }
+
+ private Path getOversizedMetaFile() {
+ return this.regionFile.getParent().resolve(this.regionFile.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt");
+ }
+
+ private Path getOversizedFile(int x, int z) {
+ return this.regionFile.getParent().resolve(this.regionFile.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt");
+ }
+
+ synchronized CompoundTag getOversizedData(int x, int z) throws IOException {
+ Path file = getOversizedFile(x, z);
+ try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) {
+ return NbtIo.read((java.io.DataInput) out);
+ }
+
+ }
+ // Paper end
private class ChunkBuffer extends ByteArrayOutputStream {
private final ChunkPos pos;
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 40830a2b231df9bbf676d8325e76c8252a6c1d6c..164db987210aae06432a6abd9b22796c2168e212 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -11,8 +11,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.StreamTagVisitor;
+import net.minecraft.nbt.Tag;
import net.minecraft.util.ExceptionCollector;
import net.minecraft.world.level.ChunkPos;
@@ -79,6 +81,74 @@ public class RegionFileStorage implements AutoCloseable {
}
}
+ // Paper start
+ private static void printOversizedLog(String msg, Path file, int x, int z) {
+ org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed.");
+ }
+
+ private static final int DEFAULT_SIZE_THRESHOLD = 1024 * 8;
+ private static final int OVERZEALOUS_TOTAL_THRESHOLD = 1024 * 64;
+ private static final int OVERZEALOUS_THRESHOLD = 1024;
+ private static int SIZE_THRESHOLD = DEFAULT_SIZE_THRESHOLD;
+ private static void resetFilterThresholds() {
+ SIZE_THRESHOLD = Math.max(1024 * 4, Integer.getInteger("Paper.FilterThreshhold", DEFAULT_SIZE_THRESHOLD));
+ }
+ static {
+ resetFilterThresholds();
+ }
+
+ static boolean isOverzealous() {
+ return SIZE_THRESHOLD == OVERZEALOUS_THRESHOLD;
+ }
+
+
+ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException {
+ synchronized (regionfile) {
+ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) {
+ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z);
+ CompoundTag chunk = NbtIo.read((DataInput) datainputstream);
+ if (oversizedData == null) {
+ return chunk;
+ }
+ CompoundTag oversizedLevel = oversizedData.getCompound("Level");
+ CompoundTag level = chunk.getCompound("Level");
+
+ mergeChunkList(level, oversizedLevel, "Entities");
+ mergeChunkList(level, oversizedLevel, "TileEntities");
+
+ chunk.put("Level", level);
+
+ return chunk;
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw throwable;
+ }
+ }
+ }
+
+ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key) {
+ ListTag levelList = level.getList(key, 10);
+ ListTag oversizedList = oversizedLevel.getList(key, 10);
+
+ if (!oversizedList.isEmpty()) {
+ levelList.addAll(oversizedList);
+ level.put(key, levelList);
+ }
+ }
+
+ private static int getNBTSize(Tag nbtBase) {
+ DataOutputStream test = new DataOutputStream(new org.apache.commons.io.output.NullOutputStream());
+ try {
+ nbtBase.write(test);
+ return test.size();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return 0;
+ }
+ }
+
+ // Paper End
+
@Nullable
public CompoundTag read(ChunkPos pos) throws IOException {
// CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
@@ -90,6 +160,12 @@ public class RegionFileStorage implements AutoCloseable {
try { // Paper
DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
+ // Paper start
+ if (regionfile.isOversized(pos.x, pos.z)) {
+ printOversizedLog("Loading Oversized Chunk!", regionfile.regionFile, pos.x, pos.z);
+ return readOversizedChunk(regionfile, pos);
+ }
+ // Paper end
CompoundTag nbttagcompound;
label43:
{
@@ -172,6 +248,7 @@ public class RegionFileStorage implements AutoCloseable {
try {
NbtIo.write(nbt, (DataOutput) dataoutputstream);
+ regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
} catch (Throwable throwable) {
if (dataoutputstream != null) {
try {

View file

@ -0,0 +1,21 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <blake.galbreath@gmail.com>
Date: Sat, 20 Apr 2019 19:47:34 -0500
Subject: [PATCH] Expose the internal current tick
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 634c50378d860808081868b2d20145d3b01259a7..8d1cbefc82f64a120eeea8c6bdd6361ffa513099 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -2588,5 +2588,10 @@ public final class CraftServer implements Server {
}
return new com.destroystokyo.paper.profile.CraftPlayerProfile(uuid, name);
}
+
+ @Override
+ public int getCurrentTick() {
+ return net.minecraft.server.MinecraftServer.currentTick;
+ }
// Paper end
}

View file

@ -0,0 +1,292 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sat, 15 Jun 2019 08:54:33 -0700
Subject: [PATCH] Fix World#isChunkGenerated calls
Optimize World#loadChunk() too
This patch also adds a chunk status cache on region files (note that
its only purpose is to cache the status on DISK)
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index a605b0868b8214408e20419e336ad52659b12e4f..18d442feb77f7763d8f6c411b0e5cb0aeca1f725 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -86,6 +86,7 @@ import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.storage.ChunkSerializer;
import net.minecraft.world.level.chunk.storage.ChunkStorage;
+import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.entity.ChunkStatusUpdateListener;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import net.minecraft.world.level.levelgen.structure.StructureStart;
@@ -1182,10 +1183,59 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Nullable
public CompoundTag readChunk(ChunkPos pos) throws IOException {
CompoundTag nbttagcompound = this.read(pos);
+ // Paper start - Cache chunk status on disk
+ if (nbttagcompound == null) {
+ return null;
+ }
+
+ nbttagcompound = this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator.getTypeNameForDataFixer(), pos, level); // CraftBukkit
+ if (nbttagcompound == null) {
+ return null;
+ }
+
+ this.updateChunkStatusOnDisk(pos, nbttagcompound);
+
+ return nbttagcompound;
+ // Paper end
+ }
+
+ // Paper start - chunk status cache "api"
+ public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) {
+ RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos);
- return nbttagcompound == null ? null : this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator.getTypeNameForDataFixer(), pos, level); // CraftBukkit
+ return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
}
+ public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException {
+ RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true);
+
+ if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) {
+ return null;
+ }
+
+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+
+ if (status != null) {
+ return status;
+ }
+
+ this.readChunk(chunkPos);
+
+ return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+ }
+
+ public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException {
+ RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false);
+
+ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound));
+ }
+
+ public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) {
+ ChunkHolder chunkHolder = this.pendingUnloads.get(ChunkPos.asLong(chunkX, chunkZ));
+ return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow();
+ }
+ // Paper end
+
boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) {
// Spigot start
return this.anyPlayerCloseEnoughForSpawning(pos, false);
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
index 328f482a0bae8d2f8013ae9a90f0500ef889ffb5..6c72854aa975800bd6160d104936a5ba978f4d67 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
@@ -290,6 +290,17 @@ public class ChunkStatus {
return this.chunkType;
}
+ // Paper start
+ public static ChunkStatus getStatus(String name) {
+ try {
+ // We need this otherwise we return EMPTY for invalid names
+ ResourceLocation key = new ResourceLocation(name);
+ return Registry.CHUNK_STATUS.getOptional(key).orElse(null);
+ } catch (Exception ex) {
+ return null; // invalid name
+ }
+ }
+ // Paper end
public static ChunkStatus byName(String id) {
return (ChunkStatus) Registry.CHUNK_STATUS.get(ResourceLocation.tryParse(id));
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
index ff53c9238325e025c502886cc5f4bc02b25f8fda..992a7d6c4372942711a1381ac08ee13910a62cb0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -595,6 +595,17 @@ public class ChunkSerializer {
}));
}
+ // Paper start
+ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) {
+ if (compound == null) {
+ return null;
+ }
+
+ // Note: Copied from below
+ return ChunkStatus.getStatus(compound.getCompound("Level").getString("Status"));
+ }
+ // Paper end
+
public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) {
return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkStatus.ChunkType.PROTOCHUNK;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
index d511db5453b722f747cc24cf2b4d97a0be75ce6a..988bc12ce011839dddbcb2f3ff96ed3829b3538e 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -24,6 +24,7 @@ import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -51,6 +52,30 @@ public class RegionFile implements AutoCloseable {
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
public final Path regionFile; // Paper
+ // Paper start - Cache chunk status
+ private final ChunkStatus[] statuses = new ChunkStatus[32 * 32];
+
+ private boolean closed;
+
+ // invoked on write/read
+ public void setStatus(int x, int z, ChunkStatus status) {
+ if (this.closed) {
+ // We've used an invalid region file.
+ throw new IllegalStateException("RegionFile is closed");
+ }
+ this.statuses[getChunkLocation(x, z)] = status;
+ }
+
+ public ChunkStatus getStatusIfCached(int x, int z) {
+ if (this.closed) {
+ // We've used an invalid region file.
+ throw new IllegalStateException("RegionFile is closed");
+ }
+ final int location = getChunkLocation(x, z);
+ return this.statuses[location];
+ }
+ // Paper end
+
public RegionFile(Path path, Path path1, boolean dsync) throws IOException {
this(path, path1, RegionFileVersion.VERSION_DEFLATE, dsync);
}
@@ -398,6 +423,7 @@ public class RegionFile implements AutoCloseable {
return this.getOffset(pos) != 0;
}
+ private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - OBFHELPER - sort of, mirror of logic below
private static int getOffsetIndex(ChunkPos pos) {
return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32;
}
@@ -408,6 +434,7 @@ public class RegionFile implements AutoCloseable {
synchronized (this) {
try {
// Paper end
+ this.closed = true; // Paper
try {
this.padToFullSector();
} finally {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 164db987210aae06432a6abd9b22796c2168e212..91cf5878dc13e1abca0c301d1346248bda251cbf 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -248,6 +248,7 @@ public class RegionFileStorage implements AutoCloseable {
try {
NbtIo.write(nbt, (DataOutput) dataoutputstream);
+ regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - cache status on disk
regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
} catch (Throwable throwable) {
if (dataoutputstream != null) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index bb95fec09594f7e2d38619ae4055ec9964635622..c4d5d78064cbef9386c00dc962e0a7889ed73d9d 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -19,6 +19,7 @@ import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.core.BlockPos;
@@ -280,8 +281,22 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public boolean isChunkGenerated(int x, int z) {
+ // Paper start - Fix this method
+ if (!Bukkit.isPrimaryThread()) {
+ return CompletableFuture.supplyAsync(() -> {
+ return CraftWorld.this.isChunkGenerated(x, z);
+ }, world.getChunkSource().mainThreadProcessor).join();
+ }
+ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z);
+ if (chunk == null) {
+ chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z);
+ }
+ if (chunk != null) {
+ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk;
+ }
try {
- return this.world.getChunkSource().getChunkAtIfCachedImmediately(x, z) != null || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)) != null; // Paper (TODO check if the first part can be removed)
+ return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL;
+ // Paper end
} catch (IOException ex) {
throw new RuntimeException(ex);
}
@@ -392,20 +407,48 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public boolean loadChunk(int x, int z, boolean generate) {
org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot
- ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper
+ // Paper start - Optimize this method
+ ChunkPos chunkPos = new ChunkPos(x, z);
- // If generate = false, but the chunk already exists, we will get this back.
- if (chunk instanceof ImposterProtoChunk) {
- // We then cycle through again to get the full chunk immediately, rather than after the ticket addition
- chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
- }
+ if (!generate) {
+ ChunkAccess immediate = world.getChunkSource().getChunkAtImmediately(x, z);
+ if (immediate == null) {
+ immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z);
+ }
+ if (immediate != null) {
+ if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) {
+ return false; // not full status
+ }
+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE);
+ world.getChunk(x, z); // make sure we're at ticket level 32 or lower
+ return true;
+ }
- if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) {
- this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 1, Unit.INSTANCE);
- return true;
+ net.minecraft.world.level.chunk.storage.RegionFile file;
+ try {
+ file = world.getChunkSource().chunkMap.regionFileCache.getRegionFile(chunkPos, false);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ ChunkStatus status = file.getStatusIfCached(x, z);
+ if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) {
+ return false;
+ }
+
+ ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true);
+ if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) {
+ return false;
+ }
+
+ // fall through to load
+ // we do this so we do not re-read the chunk data on disk
}
- return false;
+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE);
+ world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
+ return true;
+ // Paper end
}
@Override