package net.minecraft.server; import co.aikar.timings.Timing; import com.google.common.base.Predicate; import com.google.common.collect.AbstractIterator; import com.google.common.collect.ComparisonChain; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; // CraftBukkit start import java.util.LinkedList; // CraftBukkit end /** * Akarin Changes Note * 1) Make whole class thread-safe (safety issue) */ @ThreadSafe // Akarin - idk why we need do so!! public class PlayerChunkMap { private static final Predicate a = new Predicate() { public boolean a(@Nullable EntityPlayer entityplayer) { return entityplayer != null && !entityplayer.isSpectator(); } public boolean apply(@Nullable Object object) { return this.a((EntityPlayer) object); } }; private static final Predicate b = new Predicate() { public boolean a(@Nullable EntityPlayer entityplayer) { return entityplayer != null && (!entityplayer.isSpectator() || entityplayer.x().getGameRules().getBoolean("spectatorsGenerateChunks")); } public boolean apply(@Nullable Object object) { return this.a((EntityPlayer) object); } }; private final WorldServer world; private final List managedPlayers = Lists.newArrayList(); private final ReentrantReadWriteLock managedPlayersLock = new ReentrantReadWriteLock(); // Akarin - add lock private final Long2ObjectMap e = new Long2ObjectOpenHashMap(4096); private final Set f = Sets.newHashSet(); private final List g = Lists.newLinkedList(); private final List h = Lists.newLinkedList(); private final List i = Lists.newCopyOnWriteArrayList(); // Akarin - bad plugin will access this private AtomicInteger j = new AtomicInteger(); public int getViewDistance() { return j.get(); } // Paper OBFHELPER // Akarin - atmoic private long k; private AtomicBoolean l = new AtomicBoolean(true); // Akarin - atmoic private AtomicBoolean m = new AtomicBoolean(true); // Akarin - atmoic private boolean wasNotEmpty; // CraftBukkit - add field public PlayerChunkMap(WorldServer worldserver) { this.world = worldserver; this.a(worldserver.spigotConfig.viewDistance); // Spigot } public WorldServer getWorld() { return this.world; } public Iterator b() { final Iterator iterator = this.i.iterator(); return new AbstractIterator() { protected Chunk a() { while (true) { if (iterator.hasNext()) { PlayerChunk playerchunk = (PlayerChunk) iterator.next(); Chunk chunk = playerchunk.f(); if (chunk == null) { continue; } if (!chunk.v() && chunk.isDone()) { return chunk; } if (!chunk.j()) { return chunk; } if (!playerchunk.a(128.0D, PlayerChunkMap.a)) { continue; } return chunk; } return (Chunk) this.endOfData(); } } protected Chunk computeNext() { return this.a(); } }; } public synchronized void flush() { // Akarin - synchronized long i = this.world.getTime(); int j; PlayerChunk playerchunk; if (i - this.k > 8000L) { try (Timing ignored = world.timings.doChunkMapUpdate.startTiming()) { // Paper this.k = i; for (j = 0; j < this.i.size(); ++j) { playerchunk = (PlayerChunk) this.i.get(j); playerchunk.d(); playerchunk.c(); } } // Paper timing } if (!this.f.isEmpty()) { try (Timing ignored = world.timings.doChunkMapToUpdate.startTiming()) { // Paper Iterator iterator = this.f.iterator(); while (iterator.hasNext()) { playerchunk = (PlayerChunk) iterator.next(); playerchunk.d(); } this.f.clear(); } // Paper timing } if (this.l.get() && i % 4L == 0L) { this.l.getAndSet(false); try (Timing ignored = world.timings.doChunkMapSortMissing.startTiming()) { // Paper Collections.sort(this.h, new Comparator() { public int a(PlayerChunk playerchunk, PlayerChunk playerchunk1) { return ComparisonChain.start().compare(playerchunk.g(), playerchunk1.g()).result(); } public int compare(Object object, Object object1) { return this.a((PlayerChunk) object, (PlayerChunk) object1); } }); } // Paper timing } if (this.m.get() && i % 4L == 2L) { this.m.getAndSet(false); try (Timing ignored = world.timings.doChunkMapSortSendToPlayers.startTiming()) { // Paper Collections.sort(this.g, new Comparator() { public int a(PlayerChunk playerchunk, PlayerChunk playerchunk1) { return ComparisonChain.start().compare(playerchunk.g(), playerchunk1.g()).result(); } public int compare(Object object, Object object1) { return this.a((PlayerChunk) object, (PlayerChunk) object1); } }); } // Paper timing } if (!this.h.isEmpty()) { try (Timing ignored = world.timings.doChunkMapPlayersNeedingChunks.startTiming()) { // Paper // Spigot start org.spigotmc.SlackActivityAccountant activityAccountant = this.world.getMinecraftServer().slackActivityAccountant; activityAccountant.startActivity(0.5); int chunkGensAllowed = world.paperConfig.maxChunkGensPerTick; // Paper // Spigot end Iterator iterator1 = this.h.iterator(); while (iterator1.hasNext()) { PlayerChunk playerchunk1 = (PlayerChunk) iterator1.next(); if (playerchunk1.f() == null) { boolean flag = playerchunk1.a(PlayerChunkMap.b); // Paper start if (flag && !playerchunk1.chunkExists && chunkGensAllowed-- <= 0) { continue; } // Paper end if (playerchunk1.a(flag)) { iterator1.remove(); if (playerchunk1.b()) { this.g.remove(playerchunk1); } if (activityAccountant.activityTimeIsExhausted()) { // Spigot break; } } // CraftBukkit start - SPIGOT-2891: remove once chunk has been provided } else { iterator1.remove(); } // CraftBukkit end } activityAccountant.endActivity(); // Spigot } // Paper timing } if (!this.g.isEmpty()) { j = world.paperConfig.maxChunkSendsPerTick; // Paper try (Timing ignored = world.timings.doChunkMapPendingSendToPlayers.startTiming()) { // Paper Iterator iterator2 = this.g.iterator(); while (iterator2.hasNext()) { PlayerChunk playerchunk2 = (PlayerChunk) iterator2.next(); if (playerchunk2.b()) { iterator2.remove(); --j; if (j < 0) { break; } } } } // Paper timing } managedPlayersLock.readLock().lock(); // Akarin if (this.managedPlayers.isEmpty()) { try (Timing ignored = world.timings.doChunkMapUnloadChunks.startTiming()) { // Paper WorldProvider worldprovider = this.world.worldProvider; if (!worldprovider.e() && !this.world.savingDisabled) { // Paper - respect saving disabled setting this.world.getChunkProviderServer().b(); } } // Paper timing } managedPlayersLock.readLock().unlock(); // Akarin } public synchronized boolean a(int i, int j) { // Akarin - synchronized long k = d(i, j); return this.e.get(k) != null; } @Nullable public synchronized PlayerChunk getChunk(int i, int j) { // Akarin - synchronized return (PlayerChunk) this.e.get(d(i, j)); } private PlayerChunk c(int i, int j) { long k = d(i, j); PlayerChunk playerchunk = (PlayerChunk) this.e.get(k); if (playerchunk == null) { playerchunk = new PlayerChunk(this, i, j); this.e.put(k, playerchunk); this.i.add(playerchunk); if (playerchunk.f() == null) { this.h.add(playerchunk); } if (!playerchunk.b()) { this.g.add(playerchunk); } } return playerchunk; } // CraftBukkit start - add method public final boolean isChunkInUse(int x, int z) { PlayerChunk pi = getChunk(x, z); if (pi != null) { return (pi.c.size() > 0); } return false; } // CraftBukkit end public void flagDirty(BlockPosition blockposition) { int i = blockposition.getX() >> 4; int j = blockposition.getZ() >> 4; PlayerChunk playerchunk = this.getChunk(i, j); if (playerchunk != null) { playerchunk.a(blockposition.getX() & 15, blockposition.getY(), blockposition.getZ() & 15); } } public void addPlayer(EntityPlayer entityplayer) { int i = (int) entityplayer.locX >> 4; int j = (int) entityplayer.locZ >> 4; entityplayer.d = entityplayer.locX; entityplayer.e = entityplayer.locZ; // CraftBukkit start - Load nearby chunks first List chunkList = new LinkedList(); // Paper start - Player view distance API int viewDistance = entityplayer.getViewDistance(); for (int k = i - viewDistance; k <= i + viewDistance; ++k) { for (int l = j - viewDistance; l <= j + viewDistance; ++l) { // Paper end chunkList.add(new ChunkCoordIntPair(k, l)); } } Collections.sort(chunkList, new ChunkCoordComparator(entityplayer)); synchronized (this) { // Akarin - synchronized for (ChunkCoordIntPair pair : chunkList) { this.c(pair.x, pair.z).a(entityplayer); } } // Akarin // CraftBukkit end managedPlayersLock.writeLock().lock(); // Akarin this.managedPlayers.add(entityplayer); managedPlayersLock.writeLock().unlock(); // Akarin this.e(); } public void removePlayer(EntityPlayer entityplayer) { int i = (int) entityplayer.d >> 4; int j = (int) entityplayer.e >> 4; // Paper start - Player view distance API int viewDistance = entityplayer.getViewDistance(); for (int k = i - viewDistance; k <= i + viewDistance; ++k) { for (int l = j - viewDistance; l <= j + viewDistance; ++l) { // Paper end PlayerChunk playerchunk = this.getChunk(k, l); if (playerchunk != null) { playerchunk.b(entityplayer); } } } managedPlayersLock.writeLock().lock(); // Akarin this.managedPlayers.remove(entityplayer); managedPlayersLock.writeLock().unlock(); // Akarin this.e(); } private boolean a(int i, int j, int k, int l, int i1) { int j1 = i - k; int k1 = j - l; return j1 >= -i1 && j1 <= i1 ? k1 >= -i1 && k1 <= i1 : false; } public void movePlayer(EntityPlayer entityplayer) { int i = (int) entityplayer.locX >> 4; int j = (int) entityplayer.locZ >> 4; double d0 = entityplayer.d - entityplayer.locX; double d1 = entityplayer.e - entityplayer.locZ; double d2 = d0 * d0 + d1 * d1; if (d2 >= 64.0D) { int k = (int) entityplayer.d >> 4; int l = (int) entityplayer.e >> 4; final int viewDistance = entityplayer.getViewDistance(); // Paper - Player view distance API int i1 = Math.max(getViewDistance(), viewDistance); // Paper - Player view distance API 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, viewDistance)) { // Paper - Player view distance API // this.c(l1, i2).a(entityplayer); chunksToLoad.add(new ChunkCoordIntPair(l1, i2)); // CraftBukkit } if (!this.a(l1 - j1, i2 - k1, i, j, i1)) { PlayerChunk playerchunk = this.getChunk(l1 - j1, i2 - k1); if (playerchunk != null) { playerchunk.b(entityplayer); } } } } entityplayer.d = entityplayer.locX; entityplayer.e = entityplayer.locZ; this.e(); // CraftBukkit start - send nearest chunks first Collections.sort(chunksToLoad, new ChunkCoordComparator(entityplayer)); synchronized (this) { // Akarin - synchronized for (ChunkCoordIntPair pair : chunksToLoad) { this.c(pair.x, pair.z).a(entityplayer); } } // Akarin // CraftBukkit end } } } public boolean a(EntityPlayer entityplayer, int i, int j) { PlayerChunk playerchunk = this.getChunk(i, j); return playerchunk != null && playerchunk.d(entityplayer) && playerchunk.e(); } public final void setViewDistanceForAll(int viewDistance) { this.a(viewDistance); } // Paper - OBFHELPER // Paper start - Separate into two methods public void a(int i) { i = MathHelper.clamp(i, 3, 32); if (i != this.j.get()) { // Akarin - atmoic int j = i - this.j.get(); // Akarin - atmoic managedPlayersLock.readLock().lock(); // Akarin ArrayList arraylist = Lists.newArrayList(this.managedPlayers); managedPlayersLock.readLock().unlock(); // Akarin Iterator iterator = arraylist.iterator(); while (iterator.hasNext()) { EntityPlayer entityplayer = (EntityPlayer) iterator.next(); this.setViewDistance(entityplayer, i, false); // Paper - Split, don't mark sort pending, we'll handle it after } this.j.getAndSet(i); // Akarin - atmoic this.e(); } } public void setViewDistance(EntityPlayer entityplayer, int i) { this.setViewDistance(entityplayer, i, true); // Mark sort pending by default so we don't have to remember to do so all the time } // Copied from above with minor changes public void setViewDistance(EntityPlayer entityplayer, int i, boolean markSort) { i = MathHelper.clamp(i, 3, 32); int oldViewDistance = entityplayer.getViewDistance(); if (i != oldViewDistance) { int j = i - oldViewDistance; int k = (int) entityplayer.locX >> 4; int l = (int) entityplayer.locZ >> 4; int i1; int j1; if (j > 0) { synchronized (this) { // Akarin - synchronized for (i1 = k - i; i1 <= k + i; ++i1) { for (j1 = l - i; j1 <= l + i; ++j1) { PlayerChunk playerchunk = this.c(i1, j1); if (!playerchunk.d(entityplayer)) { playerchunk.a(entityplayer); } } } } // Akarin } else { synchronized (this) { // Akarin - synchronized for (i1 = k - oldViewDistance; i1 <= k + oldViewDistance; ++i1) { for (j1 = l - oldViewDistance; j1 <= l + oldViewDistance; ++j1) { if (!this.a(i1, j1, k, l, i)) { this.c(i1, j1).b(entityplayer); } } } } // Akarin if (markSort) { this.e(); } } } } // Paper end private void e() { this.l.getAndSet(true); // Akarin - atmoic this.m.getAndSet(true); // Akarin - atmoic } public static int getFurthestViewableBlock(int i) { return i * 16 - 16; } private static long d(int i, int j) { return (long) i + 2147483647L | (long) j + 2147483647L << 32; } public synchronized void a(PlayerChunk playerchunk) { // Akarin - synchronized // org.spigotmc.AsyncCatcher.catchOp("Async Player Chunk Add"); // Paper // Akarin this.f.add(playerchunk); } public synchronized void b(PlayerChunk playerchunk) { // Akarin - synchronized org.spigotmc.AsyncCatcher.catchOp("Async Player Chunk Remove"); // Paper ChunkCoordIntPair chunkcoordintpair = playerchunk.a(); long i = d(chunkcoordintpair.x, chunkcoordintpair.z); playerchunk.c(); this.e.remove(i); this.i.remove(playerchunk); this.f.remove(playerchunk); this.g.remove(playerchunk); this.h.remove(playerchunk); Chunk chunk = playerchunk.f(); if (chunk != null) { // Paper start - delay chunk unloads if (world.paperConfig.delayChunkUnloadsBy <= 0) { this.getWorld().getChunkProviderServer().unload(chunk); } else { chunk.scheduledForUnload = System.currentTimeMillis(); } // Paper end } } // 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 // Paper start - Player view distance API public void updateViewDistance(EntityPlayer player, int distanceIn) { final int oldViewDistance = player.getViewDistance(); // This represents the view distance that we will set on the player // It can exist as a negative value int playerViewDistance = MathHelper.clamp(distanceIn, 3, 32); // This value is the one we actually use to update the chunk map // We don't ever want this to be a negative int toSet = playerViewDistance; if (distanceIn < 0) { playerViewDistance = -1; toSet = world.getPlayerChunkMap().getViewDistance(); } if (toSet != oldViewDistance) { // Order matters this.setViewDistance(player, toSet); player.setViewDistance(playerViewDistance); } } // Paper end }