From d7a653379c6a6446a42907cd03e9d1348844f893 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 8 Jul 2019 03:24:59 -0700 Subject: [PATCH] Asynchronous chunk loading api --- .../minecraft/server/ChunkProviderServer.java | 134 ++++++++++++++++++ .../net/minecraft/server/ChunkStatus.java | 1 + .../java/net/minecraft/server/MCUtil.java | 5 + .../java/net/minecraft/server/RegionFile.java | 2 +- .../java/net/minecraft/server/TicketType.java | 1 + .../org/bukkit/craftbukkit/CraftWorld.java | 19 +-- 6 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index db9113994e..b46285ecdc 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -147,6 +147,140 @@ public class ChunkProviderServer extends IChunkProvider { return playerChunk.getAvailableChunkNow(); } + + private long asyncLoadSeqCounter; + + public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) { + if (Thread.currentThread() != this.serverThread) { + this.serverThreadQueue.execute(() -> { + this.getChunkAtAsynchronously(x, z, gen, onComplete); + }); + return; + } + + long k = ChunkCoordIntPair.pair(x, z); + ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); + + IChunkAccess ichunkaccess; + + // try cache + for (int l = 0; l < 4; ++l) { + if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) { + ichunkaccess = this.cacheChunk[l]; + if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime + + // move to first in cache + + for (int i1 = 3; i1 > 0; --i1) { + this.cachePos[i1] = this.cachePos[i1 - 1]; + this.cacheStatus[i1] = this.cacheStatus[i1 - 1]; + this.cacheChunk[i1] = this.cacheChunk[i1 - 1]; + } + + this.cachePos[0] = k; + this.cacheStatus[0] = ChunkStatus.FULL; + this.cacheChunk[0] = ichunkaccess; + + onComplete.accept((Chunk)ichunkaccess); + + return; + } + } + } + + if (gen) { + this.bringToFullStatusAsync(x, z, chunkPos, onComplete); + return; + } + + IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions + if (current != null) { + if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) { + onComplete.accept(null); // the chunk is not gen'd + return; + } + // we know the chunk is at full status here (either in read-only mode or the real thing) + this.bringToFullStatusAsync(x, z, chunkPos, onComplete); + return; + } else { + RegionFile file; + + try { + file = this.world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + ChunkStatus status; + if (!file.chunkExists(chunkPos) || ((status = file.getStatusIfCached(x, z)) != null && status != ChunkStatus.FULL)) { + onComplete.accept(null); // cached status says not generated, or data does not exist on disk + return; + } + + + if (status == ChunkStatus.FULL) { + // at this stage we know it is fully generated, but is on disk + this.bringToFullStatusAsync(x, z, chunkPos, onComplete); + return; + } + + // at this stage we don't know what status the chunk is in + } + + // here we don't know what status it is and we're not supposed to generate + // so we asynchronously load empty status + + this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> { + if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { + // the chunk on disk was not a full status chunk + onComplete.accept(null); + return; + } + this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required + }); + } + + private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) { + this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete); + } + + + private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) { + CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true); + long identifier = this.asyncLoadSeqCounter++; + int ticketLevel = MCUtil.getTicketLevelFor(status); + this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); + + future.whenCompleteAsync((Either either, Throwable throwable) -> { + // either left -> success + // either right -> failure + + if (throwable != null) { + throw new RuntimeException(throwable); + } + + this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); + this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading + + Optional failure = either.right(); + + if (failure.isPresent()) { + // failure + throw new IllegalStateException("Chunk failed to load: " + failure.get().toString()); + } + + onComplete.accept(either.left().get()); + + }, this.serverThreadQueue); + } + + public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { + this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); + } + + public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { + this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); + } // Paper end @Nullable diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java index e324989b46..abb0d69d2f 100644 --- a/src/main/java/net/minecraft/server/ChunkStatus.java +++ b/src/main/java/net/minecraft/server/ChunkStatus.java @@ -153,6 +153,7 @@ public class ChunkStatus { return ChunkStatus.q.size(); } + public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER public static int a(ChunkStatus chunkstatus) { return ChunkStatus.r.getInt(chunkstatus.c()); } diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java index 23d1935dd5..14f8b61042 100644 --- a/src/main/java/net/minecraft/server/MCUtil.java +++ b/src/main/java/net/minecraft/server/MCUtil.java @@ -530,4 +530,9 @@ public final class MCUtil { out.print(fileData); } } + + public static int getTicketLevelFor(ChunkStatus status) { + // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean) + return 33 + ChunkStatus.getTicketLevelOffset(status); + } } diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java index 66c8b0307f..3e80f6c53e 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -310,7 +310,7 @@ public class RegionFile implements AutoCloseable { return this.c[this.f(chunkcoordintpair)]; } - public final boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER + public boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER public boolean d(ChunkCoordIntPair chunkcoordintpair) { return this.getOffset(chunkcoordintpair) != 0; } diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java index 5acb0732c3..0ed2d2fbf9 100644 --- a/src/main/java/net/minecraft/server/TicketType.java +++ b/src/main/java/net/minecraft/server/TicketType.java @@ -22,6 +22,7 @@ public class TicketType { public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray + public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper public static TicketType a(String s, Comparator comparator) { return new TicketType<>(s, comparator, 0L); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index c5321c5076..7691f23316 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -2323,16 +2323,17 @@ public class CraftWorld implements World { @Override public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) { - // TODO placeholder - if (Bukkit.isPrimaryThread()) { - return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen)); - } else { - CompletableFuture ret = new CompletableFuture<>(); - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { - ret.complete(getChunkAtGen(x, z, gen)); - }); - return ret; + net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z); + if (immediate != null) { + return CompletableFuture.completedFuture(immediate.bukkitChunk); } + + CompletableFuture ret = new CompletableFuture<>(); + this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> { + ret.complete(chunk == null ? null : chunk.bukkitChunk); + }); + + return ret; } // Paper end -- 2.20.1