diff --git a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java index f25ef3a8d..2431bba94 100644 --- a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java @@ -214,6 +214,8 @@ public abstract class MixinMinecraftServer { WorldServer world = worlds.get(i < worlds.size() ? i : 0); synchronized (((IMixinLockProvider) world).lock()) { tickEntities(world); + world.getTracker().updatePlayers(); + world.explosionDensityCache.clear(); // Paper - Optimize explosions } } }, null); @@ -222,7 +224,6 @@ public abstract class MixinMinecraftServer { WorldServer world = worlds.get(i); synchronized (((IMixinLockProvider) world).lock()) { tickWorld(world); - world.explosionDensityCache.clear(); // Paper - Optimize explosions } } @@ -243,11 +244,6 @@ public abstract class MixinMinecraftServer { while ((runnable = Akari.callbackQueue.poll()) != null) runnable.run(); Akari.callbackTiming.stopTiming(); - for (int i = 0; i < worlds.size(); ++i) { - WorldServer world = worlds.get(i); - tickUnsafeSync(world); - } - MinecraftTimings.connectionTimer.startTiming(); serverConnection().c(); MinecraftTimings.connectionTimer.stopTiming(); @@ -266,12 +262,4 @@ public abstract class MixinMinecraftServer { } MinecraftTimings.tickablesTimer.stopTiming(); } - - public void tickUnsafeSync(WorldServer world) { - world.timings.doChunkMap.startTiming(); - world.manager.flush(); - world.timings.doChunkMap.stopTiming(); - - world.getTracker().updatePlayers(); - } } diff --git a/sources/src/main/java/io/akarin/server/mixin/core/MixinWorldServer.java b/sources/src/main/java/io/akarin/server/mixin/core/MixinWorldServer.java index 28fcffe46..ef9f68241 100644 --- a/sources/src/main/java/io/akarin/server/mixin/core/MixinWorldServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/core/MixinWorldServer.java @@ -1,20 +1,11 @@ package io.akarin.server.mixin.core; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - import io.akarin.api.internal.mixin.IMixinLockProvider; import net.minecraft.server.WorldServer; @Mixin(value = WorldServer.class, remap = false) public abstract class MixinWorldServer implements IMixinLockProvider { - @Redirect(method = "doTick()V", at = @At( - value = "INVOKE", - target = "net/minecraft/server/PlayerChunkMap.flush()V" - )) - public void onFlush() {} // Migrated to main thread - private final Object tickLock = new Object(); @Override diff --git a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunk.java b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunk.java index a69215412..aa8e8ac79 100644 --- a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunk.java +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunk.java @@ -130,7 +130,7 @@ public abstract class MixinChunk implements IMixinChunk { this.ticked = true; - if ((true || !this.isLightPopulated) && this.isTerrainPopulated && !neighbors.isEmpty()) { + if (!this.isLightPopulated && this.isTerrainPopulated && !neighbors.isEmpty()) { lightExecutorService.execute(() -> { this.checkLightAsync(neighbors); }); @@ -170,47 +170,12 @@ public abstract class MixinChunk implements IMixinChunk { return this.checkWorldLightFor(enumSkyBlock, pos); } - @Inject(method = "h(Z)V", at = @At("HEAD"), cancellable = true) - private void onRecheckGaps(CallbackInfo ci) { - if (this.world.getMinecraftServer().isStopped() || lightExecutorService.isShutdown()) { - return; - } - - if (this.isUnloading()) { - return; - } - final List neighborChunks = this.getSurroundingChunks(); - if (neighborChunks.isEmpty()) { - this.isGapLightingUpdated = true; - return; - } - - if (Akari.isPrimaryThread()) { - try { - lightExecutorService.execute(() -> { - this.recheckGapsAsync(neighborChunks); - }); - } catch (RejectedExecutionException ex) { - // This could happen if ServerHangWatchdog kills the server - // between the start of the method and the execute() call. - if (!this.world.getMinecraftServer().isStopped() && !lightExecutorService.isShutdown()) { - throw ex; - } - } - } else { - this.recheckGapsAsync(neighborChunks); - } - ci.cancel(); - } - /** * Rechecks chunk gaps async. * * @param neighbors A thread-safe list of surrounding neighbor chunks */ private void recheckGapsAsync(List neighbors) { - this.isLightPopulated = false; - for (int i = 0; i < 16; ++i) { for (int j = 0; j < 16; ++j) { if (this.updateSkylightColumns[i + j * 16]) { @@ -236,7 +201,7 @@ public abstract class MixinChunk implements IMixinChunk { } } - this.isGapLightingUpdated = false; + // this.isGapLightingUpdated = false; } } @@ -328,7 +293,11 @@ public abstract class MixinChunk implements IMixinChunk { chunk.a(enumfacing.opposite()); // OBFHELPER: checkLightSide } - this.setSkylightUpdated(); + for (int i = 0; i < this.updateSkylightColumns.length; ++i) { + this.updateSkylightColumns[i] = true; + } + + this.recheckGapsAsync(neighbors); } } } diff --git a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java index 0157daa83..8461839a3 100644 --- a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java @@ -203,6 +203,7 @@ public abstract class MixinWorldServer extends MixinWorld implements IMixinWorld } if (currentChunk == null) { + if (!Akari.isPrimaryThread()) return false; currentChunk = MCUtil.getLoadedChunkWithoutMarkingActive(chunkProvider, pos.getX() >> 4, pos.getZ() >> 4); } diff --git a/sources/src/main/java/net/minecraft/server/PlayerChunkMap.java b/sources/src/main/java/net/minecraft/server/PlayerChunkMap.java new file mode 100644 index 000000000..782109301 --- /dev/null +++ b/sources/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -0,0 +1,605 @@ +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.atomic.AtomicLong; +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.newArrayList(); + private AtomicInteger j = new AtomicInteger(); public int getViewDistance() { return j.get(); } // Paper OBFHELPER // Akarin - atmoic + private AtomicLong k = new AtomicLong(); // Akarin - atmoic + 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 synchronized Iterator b() { // Akarin - synchronized + 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 Object computeNext() { + return this.a(); + } + }; + } + + public synchronized void flush() { // Akarin - synchronized + long i = this.world.getTime(); + int j; + PlayerChunk playerchunk; + + if (i - this.k.get() > 8000L) { // Akarin - atmoic + try (Timing ignored = world.timings.doChunkMapUpdate.startTiming()) { // Paper + this.k.getAndSet(i); // Akarin - atmoic + + 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 + } + + boolean unlockRequired = true; // Akarin + managedPlayersLock.readLock().lock(); // Akarin + if (this.managedPlayers.isEmpty()) { + managedPlayersLock.readLock().unlock(); // Akarin + unlockRequired = false; // Akarin + 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 + } + if (unlockRequired) 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 // Akarin + 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 +}