From ea776989adcecaeda1cef54b34a65f1c580b4c51 Mon Sep 17 00:00:00 2001 From: Nassim Jahnke Date: Sat, 5 Feb 2022 20:47:11 +0100 Subject: [PATCH] Implement World#regenerateChunk (#7425) Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com> --- patches/api/Implement-regenerateChunk.patch | 21 ++++ .../server/Implement-regenerateChunk.patch | 97 +++++++++++++++++++ ...-data-to-disk-if-it-serializes-witho.patch | 49 ++++++++++ 3 files changed, 167 insertions(+) create mode 100644 patches/api/Implement-regenerateChunk.patch create mode 100644 patches/server/Implement-regenerateChunk.patch diff --git a/patches/api/Implement-regenerateChunk.patch b/patches/api/Implement-regenerateChunk.patch new file mode 100644 index 0000000000..bd83bf80d4 --- /dev/null +++ b/patches/api/Implement-regenerateChunk.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Sat, 5 Feb 2022 20:25:28 +0100 +Subject: [PATCH] Implement regenerateChunk + + +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -0,0 +0,0 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient + * @return Whether the chunk was actually regenerated + * + * @deprecated regenerating a single chunk is not likely to produce the same +- * chunk as before as terrain decoration may be spread across chunks. Use of +- * this method should be avoided as it is known to produce buggy results. ++ * chunk as before as terrain decoration may be spread across chunks. It may ++ * or may not change blocks in the adjacent chunks as well. + */ + @Deprecated + public boolean regenerateChunk(int x, int z); diff --git a/patches/server/Implement-regenerateChunk.patch b/patches/server/Implement-regenerateChunk.patch new file mode 100644 index 0000000000..2313a91952 --- /dev/null +++ b/patches/server/Implement-regenerateChunk.patch @@ -0,0 +1,97 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Mon, 31 Jan 2022 11:21:50 +0100 +Subject: [PATCH] Implement regenerateChunk + +Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com> + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -0,0 +0,0 @@ import org.bukkit.util.Vector; + + public class CraftWorld extends CraftRegionAccessor implements World { + public static final int CUSTOM_DIMENSION_OFFSET = 10; ++ private static final ChunkStatus[] REGEN_CHUNK_STATUSES = {ChunkStatus.BIOMES, ChunkStatus.NOISE, ChunkStatus.SURFACE, ChunkStatus.CARVERS, ChunkStatus.LIQUID_CARVERS, ChunkStatus.FEATURES}; // Paper - implement regenerate chunk method + + private final ServerLevel world; + private WorldBorder worldBorder; +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public boolean regenerateChunk(int x, int z) { + org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot +- throw new UnsupportedOperationException("Not supported in this Minecraft version! Unless you can fix it, this is not a bug :)"); +- /* +- if (!unloadChunk0(x, z, false)) { +- return false; +- } +- +- final long chunkKey = ChunkCoordIntPair.pair(x, z); +- world.getChunkProvider().unloadQueue.remove(chunkKey); ++ // Paper start - implement regenerateChunk method ++ final ServerLevel serverLevel = this.world; ++ final net.minecraft.server.level.ServerChunkCache serverChunkCache = serverLevel.getChunkSource(); ++ final ChunkPos chunkPos = new ChunkPos(x, z); ++ final net.minecraft.world.level.chunk.LevelChunk levelChunk = serverChunkCache.getChunk(chunkPos.x, chunkPos.z, true); ++ for (final BlockPos blockPos : BlockPos.betweenClosed(chunkPos.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPos.getMinBlockZ(), chunkPos.getMaxBlockX(), serverLevel.getMaxBuildHeight() - 1, chunkPos.getMaxBlockZ())) { ++ levelChunk.removeBlockEntity(blockPos); ++ serverLevel.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 16); ++ } ++ ++ for (final ChunkStatus chunkStatus : REGEN_CHUNK_STATUSES) { ++ final List list = new ArrayList<>(); ++ final int range = Math.max(1, chunkStatus.getRange()); ++ for (int chunkX = chunkPos.z - range; chunkX <= chunkPos.z + range; chunkX++) { ++ for (int chunkZ = chunkPos.x - range; chunkZ <= chunkPos.x + range; chunkZ++) { ++ ChunkAccess chunkAccess = serverChunkCache.getChunk(chunkZ, chunkX, chunkStatus.getParent(), true); ++ if (chunkAccess instanceof ImposterProtoChunk accessProtoChunk) { ++ chunkAccess = new ImposterProtoChunk(accessProtoChunk.getWrapped(), true); ++ } else if (chunkAccess instanceof net.minecraft.world.level.chunk.LevelChunk accessLevelChunk) { ++ chunkAccess = new ImposterProtoChunk(accessLevelChunk, true); ++ } ++ list.add(chunkAccess); ++ } ++ } + +- net.minecraft.server.Chunk chunk = world.getChunkProvider().generateChunk(x, z); +- PlayerChunk playerChunk = world.getPlayerChunkMap().getChunk(x, z); +- if (playerChunk != null) { +- playerChunk.chunk = chunk; ++ final com.mojang.datafixers.util.Either either = chunkStatus.generate( ++ Runnable::run, ++ serverLevel, ++ serverChunkCache.getGenerator(), ++ serverLevel.getStructureManager(), ++ serverChunkCache.getLightEngine(), ++ chunk -> { ++ throw new UnsupportedOperationException("Not creating full chunks here"); ++ }, ++ list, ++ true ++ ).join(); ++ if (chunkStatus == ChunkStatus.NOISE) { ++ either.left().ifPresent(chunk -> net.minecraft.world.level.levelgen.Heightmap.primeHeightmaps(chunk, ChunkStatus.POST_FEATURES)); ++ } + } + +- if (chunk != null) { +- refreshChunk(x, z); ++ for (final BlockPos blockPos : BlockPos.betweenClosed(chunkPos.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPos.getMinBlockZ(), chunkPos.getMaxBlockX(), serverLevel.getMaxBuildHeight() - 1, chunkPos.getMaxBlockZ())) { ++ serverChunkCache.blockChanged(blockPos); + } + +- return chunk != null; +- */ ++ final Set chunksToRelight = new HashSet<>(9); ++ for (int chunkX = chunkPos.x - 1; chunkX <= chunkPos.x + 1 ; chunkX++) { ++ for (int chunkZ = chunkPos.z - 1; chunkZ <= chunkPos.z + 1 ; chunkZ++) { ++ chunksToRelight.add(new ChunkPos(chunkX, chunkZ)); ++ } ++ } ++ serverChunkCache.getLightEngine().relight(chunksToRelight, pos -> {}, relit -> {}); ++ return true; ++ // Paper end + } + + @Override diff --git a/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch b/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch index ffdc576182..8b12bec56d 100644 --- a/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch +++ b/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch @@ -7,6 +7,35 @@ Subject: [PATCH] Only write chunk data to disk if it serializes without This ensures at least a valid version of the chunk exists on disk, even if outdated +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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 +@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable { + this.pos = chunkcoordintpair; + } + ++ // Paper start - don't write garbage data to disk if writing serialization fails ++ @Override ++ public void write(final int b) { ++ if (this.count > 500_000_000) { ++ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + this.count); ++ } ++ super.write(b); ++ } ++ ++ @Override ++ public void write(final byte[] b, final int off, final int len) { ++ if (this.count + len > 500_000_000) { ++ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + (this.count + len)); ++ } ++ super.write(b, off, len); ++ } ++ // Paper end ++ + public void close() throws IOException { + ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); + 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -16,6 +45,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 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 + dataoutputstream.close(); // Paper - only write if successful ++ // Paper start - don't write garbage data to disk if writing serialization fails ++ } catch (RegionFileSizeException e) { ++ attempts = 5; // Don't retry ++ regionfile.clear(pos); ++ throw e; ++ // Paper end - don't write garbage data to disk if writing serialization fails } catch (Throwable throwable) { if (dataoutputstream != null) { try { @@ -36,3 +71,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } // Paper start +@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable { + } + + } ++ ++ // Paper start ++ public static final class RegionFileSizeException extends RuntimeException { ++ ++ public RegionFileSizeException(String message) { ++ super(message); ++ } ++ } ++ // Paper end + }