diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index f0f4e1e4f6..c0bab0fa3f 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.Random; import org.bukkit.Server; +import org.bukkit.craftbukkit.chunkio.ChunkIOExecutor; import org.bukkit.craftbukkit.util.LongHash; import org.bukkit.craftbukkit.util.LongHashSet; import org.bukkit.craftbukkit.util.LongObjectHashMap; @@ -79,11 +80,26 @@ public class ChunkProviderServer implements IChunkProvider { } } + // CraftBukkit start - add async variant, provide compatibility public Chunk getChunkAt(int i, int j) { - // CraftBukkit start + return getChunkAt(i, j, null); + } + + public Chunk getChunkAt(int i, int j, Runnable runnable) { this.unloadQueue.remove(i, j); Chunk chunk = (Chunk) this.chunks.get(LongHash.toLong(i, j)); boolean newChunk = false; + ChunkRegionLoader loader = null; + + if (this.e instanceof ChunkRegionLoader) { + loader = (ChunkRegionLoader) this.e; + } + + // If the chunk exists but isn't loaded do it async + if (chunk == null && runnable != null && loader != null && loader.chunkExists(this.world, i, j)) { + ChunkIOExecutor.queueChunkLoad(this.world, loader, this, i, j, runnable); + return null; + } // CraftBukkit end if (chunk == null) { @@ -127,6 +143,12 @@ public class ChunkProviderServer implements IChunkProvider { chunk.a(this, this, i, j); } + // CraftBukkit start - If we didn't need to load the chunk run the callback now + if (runnable != null) { + runnable.run(); + } + // CraftBukkit end + return chunk; } diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java index 21ade1751c..7926c1dce6 100644 --- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java +++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java @@ -22,7 +22,39 @@ public class ChunkRegionLoader implements IAsyncChunkSaver, IChunkLoader { this.d = file1; } + // CraftBukkit start + public boolean chunkExists(World world, int i, int j) { + ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(i, j); + + synchronized (this.c) { + if (this.b.contains(chunkcoordintpair)) { + for (int k = 0; k < this.a.size(); ++k) { + if (((PendingChunkToSave) this.a.get(k)).a.equals(chunkcoordintpair)) { + return true; + } + } + } + } + + return RegionFileCache.a(this.d, i, j).chunkExists(i & 31, j & 31); + } + // CraftBukkit end + + // CraftBukkit start - add async variant, provide compatibility public Chunk a(World world, int i, int j) { + Object[] data = this.loadChunk(world, i, j); + if (data != null) { + Chunk chunk = (Chunk) data[0]; + NBTTagCompound nbttagcompound = (NBTTagCompound) data[1]; + this.loadEntities(chunk, nbttagcompound.getCompound("Level"), world); + return chunk; + } + + return null; + } + + public Object[] loadChunk(World world, int i, int j) { + // CraftBukkit end NBTTagCompound nbttagcompound = null; ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(i, j); Object object = this.c; @@ -51,7 +83,7 @@ public class ChunkRegionLoader implements IAsyncChunkSaver, IChunkLoader { return this.a(world, i, j, nbttagcompound); } - protected Chunk a(World world, int i, int j, NBTTagCompound nbttagcompound) { + protected Object[] a(World world, int i, int j, NBTTagCompound nbttagcompound) { // CraftBukkit - return Chunk -> Object[] if (!nbttagcompound.hasKey("Level")) { System.out.println("Chunk file at " + i + "," + j + " is missing level data, skipping"); return null; @@ -68,7 +100,12 @@ public class ChunkRegionLoader implements IAsyncChunkSaver, IChunkLoader { chunk = this.a(world, nbttagcompound.getCompound("Level")); } - return chunk; + // CraftBukkit start + Object[] data = new Object[2]; + data[0] = chunk; + data[1] = nbttagcompound; + return data; + // CraftBukkit end } } @@ -271,6 +308,13 @@ public class ChunkRegionLoader implements IAsyncChunkSaver, IChunkLoader { chunk.a(nbttagcompound.getByteArray("Biomes")); } + // CraftBukkit start - end this method here and split off entity loading to another method + return chunk; + } + + public void loadEntities(Chunk chunk, NBTTagCompound nbttagcompound, World world) { + // CraftBukkit end + NBTTagList nbttaglist1 = nbttagcompound.getList("Entities"); if (nbttaglist1 != null) { @@ -310,6 +354,6 @@ public class ChunkRegionLoader implements IAsyncChunkSaver, IChunkLoader { } } - return chunk; + // return chunk; // CraftBukkit } } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index f0e6cf7338..6b10bfcba7 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -533,6 +533,8 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo processQueue.remove().run(); } + org.bukkit.craftbukkit.chunkio.ChunkIOExecutor.tick(); + // Send timeupdates to everyone, it will get the right time from the world the player is in. if (this.ticks % 20 == 0) { for (int i = 0; i < this.getServerConfigurationManager().players.size(); ++i) { diff --git a/src/main/java/net/minecraft/server/PlayerInstance.java b/src/main/java/net/minecraft/server/PlayerInstance.java index ba0797bfc4..a4d9047516 100644 --- a/src/main/java/net/minecraft/server/PlayerInstance.java +++ b/src/main/java/net/minecraft/server/PlayerInstance.java @@ -10,6 +10,7 @@ class PlayerInstance { private short[] dirtyBlocks; private int dirtyCount; private int f; + private boolean loaded = false; // CraftBukkit final PlayerManager playerManager; @@ -19,15 +20,33 @@ class PlayerInstance { this.dirtyBlocks = new short[64]; this.dirtyCount = 0; this.location = new ChunkCoordIntPair(i, j); - playermanager.a().chunkProviderServer.getChunkAt(i, j); + // CraftBukkit start + playermanager.a().chunkProviderServer.getChunkAt(i, j, new Runnable() { + public void run() { + PlayerInstance.this.loaded = true; + } + }); + // CraftBukkit end } - public void a(EntityPlayer entityplayer) { + public void a(final EntityPlayer entityplayer) { // CraftBukkit - added final to argument if (this.b.contains(entityplayer)) { throw new IllegalStateException("Failed to add player. " + entityplayer + " already is in chunk " + this.location.x + ", " + this.location.z); } else { this.b.add(entityplayer); - entityplayer.chunkCoordIntPairQueue.add(this.location); + + // CraftBukkit start + if (this.loaded) { + entityplayer.chunkCoordIntPairQueue.add(this.location); + } else { + // Abuse getChunkAt to add another callback + this.playerManager.a().chunkProviderServer.getChunkAt(this.location.x, this.location.z, new Runnable() { + public void run() { + entityplayer.chunkCoordIntPairQueue.add(PlayerInstance.this.location); + } + }); + } + // CraftBukkit end } } diff --git a/src/main/java/net/minecraft/server/PlayerManager.java b/src/main/java/net/minecraft/server/PlayerManager.java index 2b4f88498b..3b776fad7e 100644 --- a/src/main/java/net/minecraft/server/PlayerManager.java +++ b/src/main/java/net/minecraft/server/PlayerManager.java @@ -4,8 +4,10 @@ import java.util.ArrayList; import java.util.List; // CraftBukkit start +import java.util.Collections; import java.util.Queue; import java.util.Iterator; +import java.util.LinkedList; // CraftBukkit end public class PlayerManager { @@ -98,12 +100,20 @@ public class PlayerManager { entityplayer.d = entityplayer.locX; entityplayer.e = entityplayer.locZ; + // CraftBukkit start - load nearby chunks first + List chunkList = new LinkedList(); for (int k = i - this.e; k <= i + this.e; ++k) { for (int l = j - this.e; l <= j + this.e; ++l) { - this.a(k, l, true).a(entityplayer); + chunkList.add(new ChunkCoordIntPair(k, l)); } } + Collections.sort(chunkList, new ChunkCoordComparator(entityplayer)); + for (ChunkCoordIntPair pair : chunkList) { + this.a(pair.x, pair.z, true).a(entityplayer); + } + // CraftBukkit end + this.managedPlayers.add(entityplayer); this.b(entityplayer); } @@ -189,12 +199,13 @@ public class PlayerManager { int i1 = this.e; int j1 = i - k; int k1 = j - l; + List chunksToLoad = new LinkedList(); // CraftBukkit if (j1 != 0 || k1 != 0) { for (int l1 = i - i1; l1 <= i + i1; ++l1) { for (int i2 = j - i1; i2 <= j + i1; ++i2) { if (!this.a(l1, i2, k, l, i1)) { - this.a(l1, i2, true).a(entityplayer); + chunksToLoad.add(new ChunkCoordIntPair(l1, i2)); // CraftBukkit } if (!this.a(l1 - j1, i2 - k1, i, j, i1)) { @@ -212,16 +223,13 @@ public class PlayerManager { entityplayer.e = entityplayer.locZ; // CraftBukkit start - send nearest chunks first - if (i1 > 1 || i1 < -1 || j1 > 1 || j1 < -1) { - final int x = i; - final int z = j; - List chunksToSend = entityplayer.chunkCoordIntPairQueue; + Collections.sort(chunksToLoad, new ChunkCoordComparator(entityplayer)); + for (ChunkCoordIntPair pair : chunksToLoad) { + this.a(pair.x, pair.z, true).a(entityplayer); + } - java.util.Collections.sort(chunksToSend, new java.util.Comparator() { - public int compare(ChunkCoordIntPair a, ChunkCoordIntPair b) { - return Math.max(Math.abs(a.x - x), Math.abs(a.z - z)) - Math.max(Math.abs(b.x - x), Math.abs(b.z - z)); - } - }); + if (i1 > 1 || i1 < -1 || j1 > 1 || j1 < -1) { + Collections.sort(entityplayer.chunkCoordIntPairQueue, new ChunkCoordComparator(entityplayer)); } // CraftBukkit end } @@ -249,4 +257,47 @@ public class PlayerManager { static Queue c(PlayerManager playermanager) { // CraftBukkit List -> Queue return playermanager.d; } + + // CraftBukkit start - sorter to load nearby chunks first + private static class ChunkCoordComparator implements java.util.Comparator { + private int x; + private int z; + + public ChunkCoordComparator (EntityPlayer entityplayer) { + x = (int) entityplayer.locX >> 4; + z = (int) entityplayer.locZ >> 4; + } + + public int compare(ChunkCoordIntPair a, ChunkCoordIntPair b) { + if (a.equals(b)) { + return 0; + } + + // Subtract current position to set center point + int ax = a.x - this.x; + int az = a.z - this.z; + int bx = b.x - this.x; + int bz = b.z - this.z; + + int result = ((ax - bx) * (ax + bx)) + ((az - bz) * (az + bz)); + if (result != 0) { + return result; + } + + if (ax < 0) { + if (bx < 0) { + return bz - az; + } else { + return -1; + } + } else { + if (bx < 0) { + return 1; + } else { + return az - bz; + } + } + } + } + // CraftBukkit end } diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java index 016397fcd1..07a8888f6c 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -87,6 +87,45 @@ public class RegionFile { } } + // CraftBukkit start - this is a copy (sort of) of the method below it, make sure they stay in sync + public synchronized boolean chunkExists(int i, int j) { + if (this.d(i, j)) { + return false; + } else { + try { + int k = this.e(i, j); + + if (k == 0) { + return false; + } else { + int l = k >> 8; + int i1 = k & 255; + + if (l + i1 > this.f.size()) { + return false; + } + + this.c.seek((long) (l * 4096)); + int j1 = this.c.readInt(); + + if (j1 > 4096 * i1 || j1 <= 0) { + return false; + } + + byte b0 = this.c.readByte(); + if (b0 == 1 || b0 == 2) { + return true; + } + } + } catch (IOException ioexception) { + return false; + } + } + + return false; + } + // CraftBukkit end + public synchronized DataInputStream a(int i, int j) { if (this.d(i, j)) { return null; @@ -211,7 +250,7 @@ public class RegionFile { } } - private void a(int i, byte[] abyte, int j) { + private void a(int i, byte[] abyte, int j) throws IOException { // CraftBukkit - added throws this.c.seek((long) (i * 4096)); this.c.writeInt(j + 1); this.c.writeByte(2); @@ -230,19 +269,19 @@ public class RegionFile { return this.e(i, j) != 0; } - private void a(int i, int j, int k) { + private void a(int i, int j, int k) throws IOException { // CraftBukkit - added throws this.d[i + j * 32] = k; this.c.seek((long) ((i + j * 32) * 4)); this.c.writeInt(k); } - private void b(int i, int j, int k) { + private void b(int i, int j, int k) throws IOException { // CraftBukkit - added throws this.e[i + j * 32] = k; this.c.seek((long) (4096 + (i + j * 32) * 4)); this.c.writeInt(k); } - public void c() { + public void c() throws IOException { // CraftBukkit - added throws if (this.c != null) { this.c.close(); } diff --git a/src/main/java/net/minecraft/server/ServerConfigurationManagerAbstract.java b/src/main/java/net/minecraft/server/ServerConfigurationManagerAbstract.java index 12f8dfc36e..367cada1f3 100644 --- a/src/main/java/net/minecraft/server/ServerConfigurationManagerAbstract.java +++ b/src/main/java/net/minecraft/server/ServerConfigurationManagerAbstract.java @@ -15,6 +15,7 @@ import java.util.logging.Logger; import org.bukkit.Location; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.chunkio.ChunkIOExecutor; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerPortalEvent; @@ -149,6 +150,7 @@ public abstract class ServerConfigurationManagerAbstract { this.players.add(entityplayer); WorldServer worldserver = this.server.getWorldServer(entityplayer.dimension); + // CraftBukkit start PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(this.cserver.getPlayer(entityplayer), "\u00A7e" + entityplayer.name + " joined the game."); this.cserver.getPluginManager().callEvent(playerJoinEvent); @@ -158,6 +160,8 @@ public abstract class ServerConfigurationManagerAbstract { this.server.getServerConfigurationManager().sendAll(new Packet3Chat(joinMessage)); } this.cserver.onPlayerJoin(playerJoinEvent.getPlayer()); + + ChunkIOExecutor.adjustPoolSize(this.getPlayerCount()); // CraftBukkit end // CraftBukkit start - only add if the player wasn't moved in the event @@ -207,6 +211,7 @@ public abstract class ServerConfigurationManagerAbstract { worldserver.kill(entityplayer); worldserver.getPlayerManager().removePlayer(entityplayer); this.players.remove(entityplayer); + ChunkIOExecutor.adjustPoolSize(this.getPlayerCount()); // CraftBukkit // CraftBukkit start - .name -> .listName, replace sendAll with loop Packet201PlayerInfo packet = new Packet201PlayerInfo(entityplayer.listName, false, 9999); diff --git a/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOExecutor.java b/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOExecutor.java new file mode 100644 index 0000000000..92fbc4f95d --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOExecutor.java @@ -0,0 +1,32 @@ +package org.bukkit.craftbukkit.chunkio; + +import net.minecraft.server.Chunk; +import net.minecraft.server.ChunkProviderServer; +import net.minecraft.server.ChunkRegionLoader; +import net.minecraft.server.World; +import org.bukkit.craftbukkit.util.AsynchronousExecutor; +import org.bukkit.craftbukkit.util.LongHash; + +public class ChunkIOExecutor { + static final int BASE_THREADS = 1; + static final int PLAYERS_PER_THREAD = 50; + + private static final AsynchronousExecutor instance = new AsynchronousExecutor(new ChunkIOProvider(), BASE_THREADS); + + public static void waitForChunkLoad(World world, int x, int z) { + instance.get(new QueuedChunk(LongHash.toLong(x, z), null, world, null)); + } + + public static void queueChunkLoad(World world, ChunkRegionLoader loader, ChunkProviderServer provider, int x, int z, Runnable runnable) { + instance.add(new QueuedChunk(LongHash.toLong(x, z), loader, world, provider), runnable); + } + + public static void adjustPoolSize(int players) { + int size = Math.max(BASE_THREADS, (int) Math.ceil(players / PLAYERS_PER_THREAD)); + instance.setActiveThreads(size); + } + + public static void tick() { + instance.finishActive(); + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOProvider.java b/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOProvider.java new file mode 100644 index 0000000000..48cf5bac09 --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/chunkio/ChunkIOProvider.java @@ -0,0 +1,73 @@ +package org.bukkit.craftbukkit.chunkio; + +import net.minecraft.server.Chunk; +import net.minecraft.server.ChunkRegionLoader; +import net.minecraft.server.NBTTagCompound; + +import org.bukkit.Server; +import org.bukkit.craftbukkit.util.AsynchronousExecutor; +import org.bukkit.craftbukkit.util.LongHash; + +import java.util.concurrent.atomic.AtomicInteger; + +class ChunkIOProvider implements AsynchronousExecutor.CallBackProvider { + private final AtomicInteger threadNumber = new AtomicInteger(1); + + // async stuff + public Chunk callStage1(QueuedChunk queuedChunk) throws RuntimeException { + ChunkRegionLoader loader = queuedChunk.loader; + Object[] data = loader.loadChunk(queuedChunk.world, LongHash.msw(queuedChunk.coords), LongHash.lsw(queuedChunk.coords)); + + if (data != null) { + queuedChunk.compound = (NBTTagCompound) data[1]; + return (Chunk) data[0]; + } + + return null; + } + + // sync stuff + public void callStage2(QueuedChunk queuedChunk, Chunk chunk) throws RuntimeException { + if(chunk == null) { + // If the chunk loading failed just do it synchronously (may generate) + queuedChunk.provider.getChunkAt(LongHash.msw(queuedChunk.coords), LongHash.lsw(queuedChunk.coords)); + return; + } + + int x = LongHash.msw(queuedChunk.coords); + int z = LongHash.lsw(queuedChunk.coords); + + // See if someone already loaded this chunk while we were working on it (API, etc) + if (queuedChunk.provider.chunks.containsKey(queuedChunk.coords)) { + // Make sure it isn't queued for unload, we need it + queuedChunk.provider.unloadQueue.remove(queuedChunk.coords); + return; + } + + queuedChunk.loader.loadEntities(chunk, queuedChunk.compound.getCompound("Level"), queuedChunk.world); + chunk.n = queuedChunk.provider.world.getTime(); + queuedChunk.provider.chunks.put(queuedChunk.coords, chunk); + chunk.addEntities(); + + if (queuedChunk.provider.chunkProvider != null) { + queuedChunk.provider.chunkProvider.recreateStructures(x, z); + } + + Server server = queuedChunk.provider.world.getServer(); + if (server != null) { + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(chunk.bukkitChunk, false)); + } + + chunk.a(queuedChunk.provider, queuedChunk.provider, x, z); + } + + public void callStage3(QueuedChunk queuedChunk, Chunk chunk, Runnable runnable) throws RuntimeException { + runnable.run(); + } + + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "Chunk I/O Executor Thread-" + threadNumber.getAndIncrement()); + thread.setDaemon(true); + return thread; + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/chunkio/QueuedChunk.java b/src/main/java/org/bukkit/craftbukkit/chunkio/QueuedChunk.java new file mode 100644 index 0000000000..299b1d8acd --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/chunkio/QueuedChunk.java @@ -0,0 +1,36 @@ +package org.bukkit.craftbukkit.chunkio; + +import net.minecraft.server.ChunkProviderServer; +import net.minecraft.server.ChunkRegionLoader; +import net.minecraft.server.NBTTagCompound; +import net.minecraft.server.World; + +class QueuedChunk { + long coords; + ChunkRegionLoader loader; + World world; + ChunkProviderServer provider; + NBTTagCompound compound; + + public QueuedChunk(long coords, ChunkRegionLoader loader, World world, ChunkProviderServer provider) { + this.coords = coords; + this.loader = loader; + this.world = world; + this.provider = provider; + } + + @Override + public int hashCode() { + return (int) coords ^ world.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof QueuedChunk) { + QueuedChunk other = (QueuedChunk) object; + return coords == other.coords && world == other.world; + } + + return false; + } +}