diff --git a/sources/src/main/java/io/akarin/api/IMixinChunk.java b/sources/src/main/java/io/akarin/api/IMixinChunk.java deleted file mode 100644 index c33b1579c..000000000 --- a/sources/src/main/java/io/akarin/api/IMixinChunk.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.akarin.api; - -import java.util.concurrent.atomic.AtomicInteger; - -public interface IMixinChunk { - AtomicInteger getPendingLightUpdates(); - - long getLightUpdateTime(); -} \ No newline at end of file diff --git a/sources/src/main/java/io/akarin/api/mixin/IMixinChunk.java b/sources/src/main/java/io/akarin/api/mixin/IMixinChunk.java new file mode 100644 index 000000000..c1ab14c12 --- /dev/null +++ b/sources/src/main/java/io/akarin/api/mixin/IMixinChunk.java @@ -0,0 +1,28 @@ +package io.akarin.api.mixin; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nullable; + +import net.minecraft.server.Chunk; +import net.minecraft.server.EnumSkyBlock; + +public interface IMixinChunk { + AtomicInteger getPendingLightUpdates(); + + long getLightUpdateTime(); + + boolean areNeighborsLoaded(); + + @Nullable Chunk getNeighborChunk(int index); + + CopyOnWriteArrayList getQueuedLightingUpdates(EnumSkyBlock type); + + List getNeighbors(); + + void setNeighborChunk(int index, @Nullable Chunk chunk); + + void setLightUpdateTime(long time); +} \ No newline at end of file diff --git a/sources/src/main/java/io/akarin/api/mixin/IMixinWorldServer.java b/sources/src/main/java/io/akarin/api/mixin/IMixinWorldServer.java new file mode 100644 index 000000000..0e8fbcdab --- /dev/null +++ b/sources/src/main/java/io/akarin/api/mixin/IMixinWorldServer.java @@ -0,0 +1,13 @@ +package io.akarin.api.mixin; + +import java.util.concurrent.ExecutorService; + +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Chunk; +import net.minecraft.server.EnumSkyBlock; + +public interface IMixinWorldServer { + boolean updateLightAsync(EnumSkyBlock lightType, BlockPosition pos, Chunk chunk); + + ExecutorService getLightingExecutor(); +} \ No newline at end of file diff --git a/sources/src/main/java/io/akarin/server/mixin/cps/MixinChunk.java b/sources/src/main/java/io/akarin/server/mixin/cps/MixinChunk.java new file mode 100644 index 000000000..599fff391 --- /dev/null +++ b/sources/src/main/java/io/akarin/server/mixin/cps/MixinChunk.java @@ -0,0 +1,127 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.akarin.server.mixin.cps; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nullable; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.google.common.collect.Lists; + +import io.akarin.api.mixin.IMixinChunk; +import net.minecraft.server.Chunk; +import net.minecraft.server.EnumDirection; +import net.minecraft.server.MCUtil; +import net.minecraft.server.World; + +@Mixin(value = Chunk.class, remap = false) +public abstract class MixinChunk implements IMixinChunk { + private Chunk[] neighborChunks = new Chunk[4]; + private static final EnumDirection[] CARDINAL_DIRECTIONS = new EnumDirection[] {EnumDirection.NORTH, EnumDirection.SOUTH, EnumDirection.EAST, EnumDirection.WEST}; + + @Shadow @Final public World world; + @Shadow @Final public int locX; + @Shadow @Final public int locZ; + + @Override + public Chunk getNeighborChunk(int index) { + return this.neighborChunks[index]; + } + + @Override + public void setNeighborChunk(int index, @Nullable Chunk chunk) { + this.neighborChunks[index] = chunk; + } + + @Override + public List getNeighbors() { + List neighborList = Lists.newArrayList(); + for (Chunk neighbor : this.neighborChunks) { + if (neighbor != null) { + neighborList.add(neighbor); + } + } + return neighborList; + } + + @Override + public boolean areNeighborsLoaded() { + for (int i = 0; i < 4; i++) { + if (this.neighborChunks[i] == null) { + return false; + } + } + return true; + } + + private static int directionToIndex(EnumDirection direction) { + switch (direction) { + case NORTH: + return 0; + case SOUTH: + return 1; + case EAST: + return 2; + case WEST: + return 3; + default: + throw new IllegalArgumentException("Unexpected direction"); + } + } + + @Inject(method = "addEntities", at = @At("RETURN")) + public void onLoadReturn(CallbackInfo ci) { + for (EnumDirection direction : CARDINAL_DIRECTIONS) { + Chunk neighbor = MCUtil.getLoadedChunkWithoutMarkingActive(world.getChunkProvider(), locX, locZ); + if (neighbor != null) { + int neighborIndex = directionToIndex(direction); + int oppositeNeighborIndex = directionToIndex(direction.opposite()); + this.setNeighborChunk(neighborIndex, neighbor); + ((IMixinChunk) neighbor).setNeighborChunk(oppositeNeighborIndex, (Chunk) (Object) this); + } + } + } + + @Inject(method = "removeEntities", at = @At("RETURN")) + public void onUnload(CallbackInfo ci) { + for (EnumDirection direction : CARDINAL_DIRECTIONS) { + Chunk neighbor = MCUtil.getLoadedChunkWithoutMarkingActive(world.getChunkProvider(), locX, locZ); + if (neighbor != null) { + int neighborIndex = directionToIndex(direction); + int oppositeNeighborIndex = directionToIndex(direction.opposite()); + this.setNeighborChunk(neighborIndex, null); + ((IMixinChunk) neighbor).setNeighborChunk(oppositeNeighborIndex, null); + } + } + } +} 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 new file mode 100644 index 000000000..d55ca443c --- /dev/null +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunk.java @@ -0,0 +1,640 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.akarin.server.mixin.lighting; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nullable; + +import org.bukkit.Bukkit; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.google.common.collect.Lists; + +import io.akarin.api.mixin.IMixinChunk; +import io.akarin.api.mixin.IMixinWorldServer; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Blocks; +import net.minecraft.server.Chunk; +import net.minecraft.server.ChunkSection; +import net.minecraft.server.EnumDirection; +import net.minecraft.server.EnumSkyBlock; +import net.minecraft.server.IBlockData; +import net.minecraft.server.MCUtil; +import net.minecraft.server.TileEntity; +import net.minecraft.server.World; +import net.minecraft.server.BlockPosition.MutableBlockPosition; + +@Mixin(value = Chunk.class, remap = false, priority = 1001) +public abstract class MixinChunk implements IMixinChunk { + + // Keeps track of block positions in this chunk currently queued for sky light update + private CopyOnWriteArrayList queuedSkyLightingUpdates = new CopyOnWriteArrayList<>(); + // Keeps track of block positions in this chunk currently queued for block light update + private CopyOnWriteArrayList queuedBlockLightingUpdates = new CopyOnWriteArrayList<>(); + private AtomicInteger pendingLightUpdates = new AtomicInteger(); + private long lightUpdateTime; + private ExecutorService lightExecutorService; + + @Shadow private boolean m; // PAIL: isGapLightingUpdated + @Shadow private boolean r; // PAIL: ticked + @Shadow @Final private ChunkSection[] sections; + @Shadow @Final public int locX; + @Shadow @Final public int locZ; + @Shadow @Final public World world; + @Shadow @Final public int[] heightMap; + /** Which columns need their skylightMaps updated. */ + @Shadow @Final private boolean[] i; // PAIL: updateSkylightColumns + /** Queue containing the BlockPosition of tile entities queued for creation */ + @Shadow @Final private ConcurrentLinkedQueue y; // PAIL: tileEntityPosQueue + /** Boolean value indicating if the terrain is populated. */ + @Shadow private boolean done; // isTerrainPopulated + @Shadow(aliases = "lit") private boolean isLightPopulated; + /** Lowest value in the heightmap. */ + @Shadow private int v; // PAIL: heightMapMinimum + + @Shadow public abstract int b(int x, int z); // PAIL: getHeightValue + @Shadow @Nullable public abstract TileEntity g(BlockPosition pos); // PAIL: createNewTileEntity + @Shadow @Nullable public abstract TileEntity a(BlockPosition pos, Chunk.EnumTileEntityState state); // PAIL: getTileEntity + @Shadow @Final public abstract IBlockData getBlockData(BlockPosition pos); + @Shadow @Final public abstract IBlockData getBlockData(int x, int y, int z); + @Shadow public abstract boolean isUnloading(); + /** Checks the height of a block next to a sky-visible block and schedules a lighting update as necessary */ + @Shadow public abstract void b(int x, int z, int maxValue); // PAIL: checkSkylightNeighborHeight + @Shadow public abstract void a(int x, int z, int startY, int endY); // PAIL: updateSkylightNeighborHeight + @Shadow public abstract void z(); // PAIL: setSkylightUpdated + @Shadow public abstract int g(); // PAIL: getTopFilledSegment + @Shadow public abstract void markDirty(); + + @Inject(method = "", at = @At("RETURN")) + public void onConstruct(World worldIn, int x, int z, CallbackInfo ci) { + this.lightExecutorService = ((IMixinWorldServer) worldIn).getLightingExecutor(); + } + + @Override + public AtomicInteger getPendingLightUpdates() { + return this.pendingLightUpdates; + } + + @Override + public long getLightUpdateTime() { + return this.lightUpdateTime; + } + + @Override + public void setLightUpdateTime(long time) { + this.lightUpdateTime = time; + } + + @Inject(method = "b(Z)V", at = @At("HEAD"), cancellable = true) + private void onTickHead(boolean skipRecheckGaps, CallbackInfo ci) { + final List neighbors = this.getSurroundingChunks(); + if (this.m && this.world.worldProvider.m() && !skipRecheckGaps && !neighbors.isEmpty()) { // PAIL: isGapLightingUpdated - hasSkyLight + this.lightExecutorService.execute(() -> { + this.recheckGapsAsync(neighbors); + }); + this.m = false; // PAIL: isGapLightingUpdated + } + + this.r = true; // PAIL: ticked + + if (!this.isLightPopulated && this.done && !neighbors.isEmpty()) { + this.lightExecutorService.execute(() -> { + this.checkLightAsync(neighbors); + }); + // set to true to avoid requeuing the same task when not finished + this.isLightPopulated = true; + } + + while (!this.y.isEmpty()) { // PAIL: tileEntityPosQueue + BlockPosition blockpos = this.y.poll(); // PAIL: tileEntityPosQueue + + if (this.a(blockpos, Chunk.EnumTileEntityState.CHECK) == null && this.getBlockData(blockpos).getBlock().isTileEntity()) { // PAIL: getTileEntity + TileEntity tileentity = this.g(blockpos); // PAIL: createNewTileEntity + this.world.setTileEntity(blockpos, tileentity); + this.world.b(blockpos, blockpos); // PAIL: markBlockRangeForRenderUpdate + } + } + ci.cancel(); + } + + @Redirect(method = "b(III)V", at = @At(value = "INVOKE", target = "net/minecraft/server/World.getHighestBlockYAt(Lnet/minecraft/server/BlockPosition;)Lnet/minecraft/server/BlockPosition;")) + private BlockPosition onCheckSkylightGetHeight(World world, BlockPosition pos) { + final Chunk chunk = this.getLightChunk(pos.getX() >> 4, pos.getZ() >> 4, null); + if (chunk == null) { + return BlockPosition.ZERO; + } + + return new BlockPosition(pos.getX(), chunk.b(pos.getX() & 15, pos.getZ() & 15), pos.getZ()); // PAIL: getHeightValue + } + + @Redirect(method = "a(IIII)V", at = @At(value = "INVOKE", target = "net/minecraft/server/World.areChunksLoaded(Lnet/minecraft/server/BlockPosition;I)Z")) + private boolean onAreaLoadedSkyLightNeighbor(World world, BlockPosition pos, int radius) { + return this.isAreaLoaded(); + } + + @Redirect(method = "a(IIII)V", at = @At(value = "INVOKE", target = "net/minecraft/server/World.c(Lnet/minecraft/server/EnumSkyBlock;Lnet/minecraft/server/BlockPosition;)Z")) + private boolean onCheckLightForSkylightNeighbor(World world, EnumSkyBlock enumSkyBlock, BlockPosition pos) { + return this.checkWorldLightFor(enumSkyBlock, pos); + } + + /** + * Rechecks chunk gaps async. + * + * @param neighbors A thread-safe list of surrounding neighbor chunks + */ + private void recheckGapsAsync(List neighbors) { + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 16; ++j) { + if (this.i[i + j * 16]) { // PAIL: updateSkylightColumns + this.i[i + j * 16] = false; // PAIL: updateSkylightColumns + int k = this.b(i, j); // PAIL: getHeightValue + int l = this.locX * 16 + i; + int i1 = this.locZ * 16 + j; + int j1 = Integer.MAX_VALUE; + + for (EnumDirection enumfacing : EnumDirection.EnumDirectionLimit.HORIZONTAL) { + final Chunk chunk = this.getLightChunk((l + enumfacing.getAdjacentX()) >> 4, (i1 + enumfacing.getAdjacentZ()) >> 4, neighbors); + if (chunk == null || chunk.isUnloading()) { + continue; + } + j1 = Math.min(j1, chunk.w()); // PAIL: getLowestHeight + } + + this.b(l, i1, j1); // PAIL: checkSkylightNeighborHeight + + for (EnumDirection enumfacing1 : EnumDirection.EnumDirectionLimit.HORIZONTAL) { + this.b(l + enumfacing1.getAdjacentX(), i1 + enumfacing1.getAdjacentZ(), k); // PAIL: checkSkylightNeighborHeight + } + } + } + + // this.m = false; // PAIL: isGapLightingUpdated + } + } + + @Redirect(method = "n()V", at = @At(value = "INVOKE", target = "net/minecraft/server/World.getType(Lnet/minecraft/server/BlockPosition;)Lnet/minecraft/server/IBlockData;")) + private IBlockData onRelightChecksGetBlockData(World world, BlockPosition pos) { + Chunk chunk = MCUtil.getLoadedChunkWithoutMarkingActive(world.getChunkProvider(), pos.getX() >> 4, pos.getZ() >> 4); + + final IMixinChunk spongeChunk = (IMixinChunk) chunk; + if (chunk == null || chunk.isUnloading() || !spongeChunk.areNeighborsLoaded()) { + return Blocks.AIR.getBlockData(); + } + + return chunk.getBlockData(pos); + } + + @Redirect(method = "n()V", at = @At(value = "INVOKE", target = "net/minecraft/server/World.w(Lnet/minecraft/server/BlockPosition;)Z")) + private boolean onRelightChecksCheckLight(World world, BlockPosition pos) { + return this.checkWorldLight(pos); + } + + // Avoids grabbing chunk async during light check + @Redirect(method = "e(II)Z", at = @At(value = "INVOKE", target = "net/minecraft/server/World.w(Lnet/minecraft/server/BlockPosition;)Z")) + private boolean onCheckLightWorld(World world, BlockPosition pos) { + return this.checkWorldLight(pos); + } + + @Inject(method = "o()V", at = @At("HEAD"), cancellable = true) + private void checkLightHead(CallbackInfo ci) { + if (this.world.getMinecraftServer().isStopped() || this.lightExecutorService.isShutdown()) { + return; + } + + if (this.isUnloading()) { + return; + } + final List neighborChunks = this.getSurroundingChunks(); + if (neighborChunks.isEmpty()) { + this.isLightPopulated = false; + return; + } + + if (Bukkit.isPrimaryThread()) { + try { + this.lightExecutorService.execute(() -> { + this.checkLightAsync(neighborChunks); + }); + } catch (RejectedExecutionException e) { + // This could happen if ServerHangWatchdog kills the server + // between the start of the method and the execute() call. + if (!this.world.getMinecraftServer().isStopped() && !this.lightExecutorService.isShutdown()) { + throw e; + } + } + } else { + this.checkLightAsync(neighborChunks); + } + ci.cancel(); + } + + /** + * Checks light async. + * + * @param neighbors A thread-safe list of surrounding neighbor chunks + */ + private void checkLightAsync(List neighbors) { + this.done = true; + this.isLightPopulated = true; + BlockPosition blockpos = new BlockPosition(this.locX << 4, 0, this.locZ << 4); + + if (this.world.worldProvider.m()) { // PAIL: hasSkyLight + label44: + + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 16; ++j) { + if (!this.checkLightAsync(i, j, neighbors)) { + this.isLightPopulated = false; + break label44; + } + } + } + + if (this.isLightPopulated) { + for (EnumDirection enumfacing : EnumDirection.EnumDirectionLimit.HORIZONTAL) { + int k = enumfacing.c() == EnumDirection.EnumAxisDirection.POSITIVE ? 16 : 1; // PAIL: getAxisDirection + final BlockPosition pos = blockpos.shift(enumfacing, k); + final Chunk chunk = this.getLightChunk(pos.getX() >> 4, pos.getZ() >> 4, neighbors); + if (chunk == null) { + continue; + } + chunk.a(enumfacing.opposite()); // PAIL: checkLightSide + } + + this.z(); // PAIL: setSkylightUpdated + } + } + } + + /** + * Checks light async. + * + * @param x The x position of chunk + * @param z The z position of chunk + * @param neighbors A thread-safe list of surrounding neighbor chunks + * @return True if light update was successful, false if not + */ + private boolean checkLightAsync(int x, int z, List neighbors) { + int i = this.g(); // PAIL: getTopFilledSegment + boolean flag = false; + boolean flag1 = false; + MutableBlockPosition blockpos$mutableblockpos = new MutableBlockPosition((this.locX << 4) + x, 0, (this.locZ << 4) + z); + + for (int j = i + 16 - 1; j > this.world.getSeaLevel() || j > 0 && !flag1; --j) { + blockpos$mutableblockpos.setValues(blockpos$mutableblockpos.getX(), j, blockpos$mutableblockpos.getZ()); + int k = this.getBlockData(blockpos$mutableblockpos).c(); // PAIL: getLightOpacity + + if (k == 255 && blockpos$mutableblockpos.getY() < this.world.getSeaLevel()) { + flag1 = true; + } + + if (!flag && k > 0) { + flag = true; + } else if (flag && k == 0 && !this.checkWorldLight(blockpos$mutableblockpos, neighbors)) { + return false; + } + } + + for (int l = blockpos$mutableblockpos.getY(); l > 0; --l) { + blockpos$mutableblockpos.setValues(blockpos$mutableblockpos.getX(), l, blockpos$mutableblockpos.getZ()); + + if (this.getBlockData(blockpos$mutableblockpos).d() > 0) { // getLightValue + this.checkWorldLight(blockpos$mutableblockpos, neighbors); + } + } + + return true; + } + + /** + * Thread-safe method to retrieve a chunk during async light updates. + * + * @param chunkX The x position of chunk. + * @param chunkZ The z position of chunk. + * @param neighbors A thread-safe list of surrounding neighbor chunks + * @return The chunk if available, null if not + */ + private Chunk getLightChunk(int chunkX, int chunkZ, List neighbors) { + final Chunk currentChunk = (Chunk) (Object) this; + if (currentChunk.a(chunkX, chunkZ)) { // PAIL: isAtLocation + if (currentChunk.isUnloading()) { + return null; + } + return currentChunk; + } + if (neighbors == null) { + neighbors = this.getSurroundingChunks(); + if (neighbors.isEmpty()) { + return null; + } + } + for (Chunk neighbor : neighbors) { + if (neighbor.a(chunkX, chunkZ)) { // PAIL: isAtLocation + if (neighbor.isUnloading()) { + return null; + } + return neighbor; + } + } + + return null; + } + + /** + * Checks if surrounding chunks are loaded thread-safe. + * + * @return True if surrounded chunks are loaded, false if not + */ + private boolean isAreaLoaded() { + if (!this.areNeighborsLoaded()) { + return false; + } + + // add diagonal chunks + final Chunk southEastChunk = ((IMixinChunk) this.getNeighborChunk(0)).getNeighborChunk(2); + if (southEastChunk == null) { + return false; + } + + final Chunk southWestChunk = ((IMixinChunk) this.getNeighborChunk(0)).getNeighborChunk(3); + if (southWestChunk == null) { + return false; + } + + final Chunk northEastChunk = ((IMixinChunk) this.getNeighborChunk(1)).getNeighborChunk(2); + if (northEastChunk == null) { + return false; + } + + final Chunk northWestChunk = ((IMixinChunk) this.getNeighborChunk(1)).getNeighborChunk(3); + if (northWestChunk == null) { + return false; + } + + return true; + } + + /** + * Gets surrounding chunks thread-safe. + * + * @return The list of surrounding chunks, empty list if not loaded + */ + private List getSurroundingChunks() { + if (!this.areNeighborsLoaded()) { + return Collections.emptyList(); + } + + // add diagonal chunks + final Chunk southEastChunk = ((IMixinChunk) this.getNeighborChunk(0)).getNeighborChunk(2); + if (southEastChunk == null) { + return Collections.emptyList(); + } + + final Chunk southWestChunk = ((IMixinChunk) this.getNeighborChunk(0)).getNeighborChunk(3); + if (southWestChunk == null) { + return Collections.emptyList(); + } + + final Chunk northEastChunk = ((IMixinChunk) this.getNeighborChunk(1)).getNeighborChunk(2); + if (northEastChunk == null) { + return Collections.emptyList(); + } + + final Chunk northWestChunk = ((IMixinChunk) this.getNeighborChunk(1)).getNeighborChunk(3); + if (northWestChunk == null) { + return Collections.emptyList(); + } + + List chunkList = Lists.newArrayList(); + chunkList = this.getNeighbors(); + chunkList.add(southEastChunk); + chunkList.add(southWestChunk); + chunkList.add(northEastChunk); + chunkList.add(northWestChunk); + return chunkList; + } + + @Inject(method = "c(III)V", at = @At("HEAD"), cancellable = true) + private void onRelightBlock(int x, int y, int z, CallbackInfo ci) { + this.lightExecutorService.execute(() -> { + this.relightBlockAsync(x, y, z); + }); + ci.cancel(); + } + + /** + * Relight's a block async. + * + * @param x The x position + * @param y The y position + * @param z The z position + */ + private void relightBlockAsync(int x, int y, int z) { + int i = this.heightMap[z << 4 | x] & 255; + int j = i; + + if (y > i) { + j = y; + } + + while (j > 0 && this.getBlockData(x, j - 1, z).c() == 0) { + --j; + } + + if (j != i) { + this.markBlocksDirtyVerticalAsync(x + this.locX * 16, z + this.locZ * 16, j, i); + this.heightMap[z << 4 | x] = j; + int k = this.locX * 16 + x; + int l = this.locZ * 16 + z; + + if (this.world.worldProvider.m()) { // PAIL: hasSkyLight + if (j < i) { + for (int j1 = j; j1 < i; ++j1) { + ChunkSection extendedblockstorage2 = this.sections[j1 >> 4]; + + if (extendedblockstorage2 != Chunk.EMPTY_CHUNK_SECTION) { + extendedblockstorage2.a(x, j1 & 15, z, 15); // PAIL: setSkyLight + this.world.m(new BlockPosition((this.locX << 4) + x, j1, (this.locZ << 4) + z)); // PAIL: notifyLightSet + } + } + } else { + for (int i1 = i; i1 < j; ++i1) { + ChunkSection extendedblockstorage = this.sections[i1 >> 4]; + + if (extendedblockstorage != Chunk.EMPTY_CHUNK_SECTION) { + extendedblockstorage.a(x, i1 & 15, z, 0); // PAIL: setSkyLight + this.world.m(new BlockPosition((this.locX << 4) + x, i1, (this.locZ << 4) + z)); // PAIL: notifyLightSet + } + } + } + + int k1 = 15; + + while (j > 0 && k1 > 0) { + --j; + int i2 = this.getBlockData(x, j, z).c(); + + if (i2 == 0) { + i2 = 1; + } + + k1 -= i2; + + if (k1 < 0) { + k1 = 0; + } + + ChunkSection extendedblockstorage1 = this.sections[j >> 4]; + + if (extendedblockstorage1 != Chunk.EMPTY_CHUNK_SECTION) { + extendedblockstorage1.a(x, j & 15, z, k1); // PAIL: setSkyLight + } + } + } + + int l1 = this.heightMap[z << 4 | x]; + int j2 = i; + int k2 = l1; + + if (l1 < i) { + j2 = l1; + k2 = i; + } + + if (l1 < this.v) { // PAIL: heightMapMinimum + this.v = l1; // PAIL: heightMapMinimum + } + + if (this.world.worldProvider.m()) { // PAIL: hasSkyLight + for (EnumDirection enumfacing : EnumDirection.EnumDirectionLimit.HORIZONTAL) { + this.a(k + enumfacing.getAdjacentX(), l + enumfacing.getAdjacentZ(), j2, k2); // PAIL: updateSkylightNeighborHeight + } + + this.a(k, l, j2, k2); // PAIL: updateSkylightNeighborHeight + } + + this.markDirty(); + } + } + + /** + * Marks a vertical line of blocks as dirty async. + * Instead of calling world directly, we pass chunk safely for async light method. + * + * @param x1 + * @param z1 + * @param x2 + * @param z2 + */ + private void markBlocksDirtyVerticalAsync(int x1, int z1, int x2, int z2) { + if (x2 > z2) { + int i = z2; + z2 = x2; + x2 = i; + } + + if (this.world.worldProvider.m()) { // PAIL: hasSkyLight + for (int j = x2; j <= z2; ++j) { + final BlockPosition pos = new BlockPosition(x1, j, z1); + final Chunk chunk = this.getLightChunk(pos.getX() >> 4, pos.getZ() >> 4, null); + if (chunk == null) { + continue; + } + ((IMixinWorldServer) this.world).updateLightAsync(EnumSkyBlock.SKY, new BlockPosition(x1, j, z1), chunk); + } + } + + this.world.b(x1, x2, z1, x1, z2, z1); // PAIL: markBlockRangeForRenderUpdate + } + + /** + * Checks world light thread-safe. + * + * @param lightType The type of light to check + * @param pos The block position + * @return True if light update was successful, false if not + */ + private boolean checkWorldLightFor(EnumSkyBlock lightType, BlockPosition pos) { + final Chunk chunk = this.getLightChunk(pos.getX() >> 4, pos.getZ() >> 4, null); + if (chunk == null) { + return false; + } + + return ((IMixinWorldServer) this.world).updateLightAsync(lightType, pos, chunk); + } + + private boolean checkWorldLight(BlockPosition pos) { + return this.checkWorldLight(pos, null); + } + + /** + * Checks world light async. + * + * @param pos The block position + * @param neighbors A thread-safe list of surrounding neighbor chunks + * @return True if light update was successful, false if not + */ + private boolean checkWorldLight(BlockPosition pos, List neighbors) { + boolean flag = false; + final Chunk chunk = this.getLightChunk(pos.getX() >> 4, pos.getZ() >> 4, neighbors); + if (chunk == null) { + return false; + } + + if (this.world.worldProvider.m()) { // PAIL: hasSkyLight + flag |= ((IMixinWorldServer) this.world).updateLightAsync(EnumSkyBlock.SKY, pos, chunk); + } + + flag = flag | ((IMixinWorldServer) this.world).updateLightAsync(EnumSkyBlock.BLOCK, pos, chunk); + return flag; + } + + /** + * Gets the list of block positions currently queued for lighting updates. + * + * @param type The light type + * @return The list of queued block positions, empty if none + */ + @Override + public CopyOnWriteArrayList getQueuedLightingUpdates(EnumSkyBlock type) { + if (type == EnumSkyBlock.SKY) { + return this.queuedSkyLightingUpdates; + } + return this.queuedBlockLightingUpdates; + } +} diff --git a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunkProviderServer.java b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunkProviderServer.java index 31a6ffa2d..97ff1d842 100644 --- a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunkProviderServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinChunkProviderServer.java @@ -1,3 +1,27 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package io.akarin.server.mixin.lighting; import org.spongepowered.asm.mixin.Final; @@ -6,7 +30,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.Redirect; -import io.akarin.api.IMixinChunk; +import io.akarin.api.mixin.IMixinChunk; import net.minecraft.server.ChunkProviderServer; import net.minecraft.server.WorldServer; diff --git a/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorld.java b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorld.java new file mode 100644 index 000000000..66aa26d78 --- /dev/null +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorld.java @@ -0,0 +1,45 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.akarin.server.mixin.lighting; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.server.BlockPosition; +import net.minecraft.server.EnumSkyBlock; +import net.minecraft.server.IChunkProvider; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.World; + +@Mixin(value = World.class, remap = false) +public abstract class MixinWorld { + @Shadow protected IChunkProvider chunkProvider; + @Shadow int[] J; // PAIL: lightUpdateBlockList + + @Shadow public abstract boolean c(EnumSkyBlock lightType, BlockPosition pos); // PAIL: checkLightFor + @Shadow public abstract MinecraftServer getMinecraftServer(); + @Shadow public abstract boolean areChunksLoaded(BlockPosition center, int radius, boolean allowEmpty); + @Shadow public abstract void m(BlockPosition pos); // PAIL: notifyLightSet +} 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 new file mode 100644 index 000000000..ffdb214b1 --- /dev/null +++ b/sources/src/main/java/io/akarin/server/mixin/lighting/MixinWorldServer.java @@ -0,0 +1,368 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.akarin.server.mixin.lighting; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.annotation.Nullable; + +import org.bukkit.Bukkit; +import org.spongepowered.asm.mixin.Mixin; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import io.akarin.api.mixin.IMixinChunk; +import io.akarin.api.mixin.IMixinWorldServer; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Chunk; +import net.minecraft.server.EnumDirection; +import net.minecraft.server.EnumSkyBlock; +import net.minecraft.server.IBlockData; +import net.minecraft.server.MCUtil; +import net.minecraft.server.WorldServer; +import net.minecraft.server.BlockPosition.PooledBlockPosition; + +@Mixin(value = WorldServer.class, remap = false) +public abstract class MixinWorldServer extends MixinWorld implements IMixinWorldServer { + + private static final int NUM_XZ_BITS = 4; + private static final int NUM_SHORT_Y_BITS = 8; + private static final short XZ_MASK = 0xF; + private static final short Y_SHORT_MASK = 0xFF; + + private final ExecutorService lightExecutorService = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setNameFormat("Akarin Async Light Thread").build()); + + @Override + public boolean c(EnumSkyBlock lightType, BlockPosition pos) { // PAIL: checkLightFor + return updateLightAsync(lightType, pos, null); + } + + public boolean checkLightAsync(EnumSkyBlock lightType, BlockPosition pos, Chunk currentChunk, List neighbors) { + // Sponge - This check is not needed as neighbors are checked in updateLightAsync + if (false && !this.areChunksLoaded(pos, 17, false)) { + return false; + } else { + final IMixinChunk spongeChunk = (IMixinChunk) currentChunk; + int i = 0; + int j = 0; + int k = this.getLightForAsync(lightType, pos, currentChunk, neighbors); // Sponge - use thread safe method + int l = this.getRawBlockLightAsync(lightType, pos, currentChunk, neighbors); // Sponge - use thread safe method + int i1 = pos.getX(); + int j1 = pos.getY(); + int k1 = pos.getZ(); + + if (l > k) { + this.J[j++] = 133152; + } else if (l < k) { + this.J[j++] = 133152 | k << 18; + + while (i < j) { + int l1 = this.J[i++]; + int i2 = (l1 & 63) - 32 + i1; + int j2 = (l1 >> 6 & 63) - 32 + j1; + int k2 = (l1 >> 12 & 63) - 32 + k1; + int l2 = l1 >> 18 & 15; + BlockPosition blockpos = new BlockPosition(i2, j2, k2); + int i3 = this.getLightForAsync(lightType, blockpos, currentChunk, neighbors); // Sponge - use thread safe method + + if (i3 == l2) { + this.setLightForAsync(lightType, blockpos, 0, currentChunk, neighbors); // Sponge - use thread safe method + + if (l2 > 0) { + int j3 = Math.abs(i2 - i1); // TODO MathHelper + int k3 = Math.abs(j2 - j1); + int l3 = Math.abs(k2 - k1); + + if (j3 + k3 + l3 < 17) { + PooledBlockPosition blockpos$pooledmutableblockpos = PooledBlockPosition.aquire(); + + for (EnumDirection enumfacing : EnumDirection.values()) { + int i4 = i2 + enumfacing.getAdjacentX(); + int j4 = j2 + enumfacing.getAdjacentX(); + int k4 = k2 + enumfacing.getAdjacentX(); + blockpos$pooledmutableblockpos.setValues(i4, j4, k4); + // Sponge start - get chunk safely + final Chunk pooledChunk = this.getLightChunk(blockpos$pooledmutableblockpos, currentChunk, neighbors); + if (pooledChunk == null) { + continue; + } + int l4 = Math.max(1, pooledChunk.getBlockData(blockpos$pooledmutableblockpos).c()); // PAIL: getLightOpacity + i3 = this.getLightForAsync(lightType, blockpos$pooledmutableblockpos, currentChunk, neighbors); + // Sponge end + + if (i3 == l2 - l4 && j < this.J.length) { + this.J[j++] = i4 - i1 + 32 | j4 - j1 + 32 << 6 | k4 - k1 + 32 << 12 | l2 - l4 << 18; + } + } + + blockpos$pooledmutableblockpos.free(); + } + } + } + } + + i = 0; + } + + while (i < j) { + int i5 = this.J[i++]; + int j5 = (i5 & 63) - 32 + i1; + int k5 = (i5 >> 6 & 63) - 32 + j1; + int l5 = (i5 >> 12 & 63) - 32 + k1; + BlockPosition blockpos1 = new BlockPosition(j5, k5, l5); + int i6 = this.getLightForAsync(lightType, blockpos1, currentChunk, neighbors); // Sponge - use thread safe method + int j6 = this.getRawBlockLightAsync(lightType, blockpos1, currentChunk, neighbors); // Sponge - use thread safe method + + if (j6 != i6) { + this.setLightForAsync(lightType, blockpos1, j6, currentChunk, neighbors); // Sponge - use thread safe method + + if (j6 > i6) { + int k6 = Math.abs(j5 - i1); + int l6 = Math.abs(k5 - j1); + int i7 = Math.abs(l5 - k1); + boolean flag = j < this.J.length - 6; + + if (k6 + l6 + i7 < 17 && flag) { + // Sponge start - use thread safe method getLightForAsync + if (this.getLightForAsync(lightType, blockpos1.west(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 - 1 - i1 + 32 + (k5 - j1 + 32 << 6) + (l5 - k1 + 32 << 12); + } + + if (this.getLightForAsync(lightType, blockpos1.east(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 + 1 - i1 + 32 + (k5 - j1 + 32 << 6) + (l5 - k1 + 32 << 12); + } + + if (this.getLightForAsync(lightType, blockpos1.down(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 - i1 + 32 + (k5 - 1 - j1 + 32 << 6) + (l5 - k1 + 32 << 12); + } + + if (this.getLightForAsync(lightType, blockpos1.up(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 - i1 + 32 + (k5 + 1 - j1 + 32 << 6) + (l5 - k1 + 32 << 12); + } + + if (this.getLightForAsync(lightType, blockpos1.north(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 - i1 + 32 + (k5 - j1 + 32 << 6) + (l5 - 1 - k1 + 32 << 12); + } + + if (this.getLightForAsync(lightType, blockpos1.south(), currentChunk, neighbors) < j6) { + this.J[j++] = j5 - i1 + 32 + (k5 - j1 + 32 << 6) + (l5 + 1 - k1 + 32 << 12); + } + // Sponge end + } + } + } + } + + // Sponge start - Asynchronous light updates + spongeChunk.getQueuedLightingUpdates(lightType).remove((Short) this.blockPosToShort(pos)); + spongeChunk.getPendingLightUpdates().decrementAndGet(); + for (Chunk neighborChunk : neighbors) { + final IMixinChunk neighbor = (IMixinChunk) neighborChunk; + neighbor.getPendingLightUpdates().decrementAndGet(); + } + + // Sponge end + return true; + } + } + + @Override + public boolean updateLightAsync(EnumSkyBlock lightType, BlockPosition pos, @Nullable Chunk currentChunk) { + if (this.getMinecraftServer().isStopped() || this.lightExecutorService.isShutdown()) { + return false; + } + + if (currentChunk == null) { + currentChunk = MCUtil.getLoadedChunkWithoutMarkingActive(chunkProvider, pos.getX() >> 4, pos.getZ() >> 4); + } + + final IMixinChunk spongeChunk = (IMixinChunk) currentChunk; + if (currentChunk == null || currentChunk.isUnloading() || !spongeChunk.areNeighborsLoaded()) { + return false; + } + + final short shortPos = this.blockPosToShort(pos); + if (spongeChunk.getQueuedLightingUpdates(lightType).contains(shortPos)) { + return false; + } + + final Chunk chunk = currentChunk; + spongeChunk.getQueuedLightingUpdates(lightType).add(shortPos); + spongeChunk.getPendingLightUpdates().incrementAndGet(); + spongeChunk.setLightUpdateTime(chunk.getWorld().getTime()); + + List neighbors = spongeChunk.getNeighbors(); + // add diagonal chunks + Chunk southEastChunk = ((IMixinChunk) spongeChunk.getNeighborChunk(0)).getNeighborChunk(2); + Chunk southWestChunk = ((IMixinChunk) spongeChunk.getNeighborChunk(0)).getNeighborChunk(3); + Chunk northEastChunk = ((IMixinChunk) spongeChunk.getNeighborChunk(1)).getNeighborChunk(2); + Chunk northWestChunk = ((IMixinChunk) spongeChunk.getNeighborChunk(1)).getNeighborChunk(3); + if (southEastChunk != null) { + neighbors.add(southEastChunk); + } + if (southWestChunk != null) { + neighbors.add(southWestChunk); + } + if (northEastChunk != null) { + neighbors.add(northEastChunk); + } + if (northWestChunk != null) { + neighbors.add(northWestChunk); + } + + for (Chunk neighborChunk : neighbors) { + final IMixinChunk neighbor = (IMixinChunk) neighborChunk; + neighbor.getPendingLightUpdates().incrementAndGet(); + neighbor.setLightUpdateTime(chunk.getWorld().getTime()); + } + + //System.out.println("size = " + ((ThreadPoolExecutor) this.lightExecutorService).getQueue().size()); + if (Bukkit.isPrimaryThread()) { + this.lightExecutorService.execute(() -> { + this.checkLightAsync(lightType, pos, chunk, neighbors); + }); + } else { + this.checkLightAsync(lightType, pos, chunk, neighbors); + } + + return true; + } + + public ExecutorService getLightingExecutor() { + return this.lightExecutorService; + } + + // Thread safe methods to retrieve a chunk during async light updates + // Each method avoids calling getLoadedChunk and instead accesses the passed neighbor chunk list to avoid concurrency issues + public Chunk getLightChunk(BlockPosition pos, Chunk currentChunk, List neighbors) { + if (currentChunk.a(pos.getX() >> 4, pos.getZ() >> 4)) { // PAIL: isAtLocation + if (currentChunk.isUnloading()) { + return null; + } + return currentChunk; + } + for (Chunk neighbor : neighbors) { + if (neighbor.a(pos.getX() >> 4, pos.getZ() >> 4)) { // PAIL: isAtLocation + if (neighbor.isUnloading()) { + return null; + } + return neighbor; + } + } + + return null; + } + + private int getLightForAsync(EnumSkyBlock lightType, BlockPosition pos, Chunk currentChunk, List neighbors) { + if (pos.getY() < 0) { + pos = new BlockPosition(pos.getX(), 0, pos.getZ()); + } + if (!(pos.isValidLocation())) { + return lightType.c; // PAIL: defaultLightValue + } + + final Chunk chunk = this.getLightChunk(pos, currentChunk, neighbors); + if (chunk == null || chunk.isUnloading()) { + return lightType.c; // PAIL: defaultLightValue + } + + return chunk.getBrightness(lightType, pos); + } + + private int getRawBlockLightAsync(EnumSkyBlock lightType, BlockPosition pos, Chunk currentChunk, List neighbors) { + final Chunk chunk = getLightChunk(pos, currentChunk, neighbors); + if (chunk == null || chunk.isUnloading()) { + return lightType.c; // PAIL: defaultLightValue + } + if (lightType == EnumSkyBlock.SKY && chunk.c(pos)) { // PAIL: canSeeSky + return 15; + } else { + IBlockData blockState = chunk.getBlockData(pos); + int blockLight = blockState.d(); // getLightValue + int i = lightType == EnumSkyBlock.SKY ? 0 : blockLight; + int j = blockState.c(); // PAIL: getLightOpacity + + if (j >= 15 && blockLight > 0) { + j = 1; + } + + if (j < 1) { + j = 1; + } + + if (j >= 15) { + return 0; + } else if (i >= 14) { + return i; + } else { + for (EnumDirection facing : EnumDirection.values()) { + BlockPosition blockpos = pos.shift(facing); + int k = this.getLightForAsync(lightType, blockpos, currentChunk, neighbors) - j; + + if (k > i) { + i = k; + } + + if (i >= 14) { + return i; + } + } + + return i; + } + } + } + + public void setLightForAsync(EnumSkyBlock type, BlockPosition pos, int lightValue, Chunk currentChunk, List neighbors) { + if (pos.isValidLocation()) { + final Chunk chunk = this.getLightChunk(pos, currentChunk, neighbors); + if (chunk != null && !chunk.isUnloading()) { + chunk.a(type, pos, lightValue); // PAIL: setBrightness + this.m(pos); // PAIL: notifyLightSet + } + } + } + + private short blockPosToShort(BlockPosition pos) { + short serialized = (short) setNibble(0, pos.getX() & XZ_MASK, 0, NUM_XZ_BITS); + serialized = (short) setNibble(serialized, pos.getY() & Y_SHORT_MASK, 1, NUM_SHORT_Y_BITS); + serialized = (short) setNibble(serialized, pos.getZ() & XZ_MASK, 3, NUM_XZ_BITS); + return serialized; + } + + /** + * Modifies bits in an integer. + * + * @param num Integer to modify + * @param data Bits of data to add + * @param which Index of nibble to start at + * @param bitsToReplace The number of bits to replace starting from nibble index + * @return The modified integer + */ + private int setNibble(int num, int data, int which, int bitsToReplace) { + return (num & ~(bitsToReplace << (which * 4)) | (data << (which * 4))); + } +} diff --git a/sources/src/main/java/net/minecraft/server/Chunk.java b/sources/src/main/java/net/minecraft/server/Chunk.java new file mode 100644 index 000000000..15d654976 --- /dev/null +++ b/sources/src/main/java/net/minecraft/server/Chunk.java @@ -0,0 +1,1444 @@ +package net.minecraft.server; + +import com.destroystokyo.paper.exception.ServerInternalException; +import com.google.common.base.Predicate; +import com.google.common.collect.Maps; +import com.google.common.collect.Queues; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.annotation.Nullable; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.collect.Lists; // CraftBukkit +import org.bukkit.Server; // CraftBukkit +import org.bukkit.craftbukkit.util.CraftMagicNumbers; // Paper + +public class Chunk { + + private static final Logger e = LogManager.getLogger(); + public static final ChunkSection a = null; public static final ChunkSection EMPTY_CHUNK_SECTION = Chunk.a; // Paper - OBFHELPER + private final ChunkSection[] sections; + private final byte[] g; + private final int[] h; + private final boolean[] i; + private boolean j; + public final World world; + public final int[] heightMap; + public Long scheduledForUnload; // Paper - delay chunk unloads + public final int locX; + public final int locZ; + private boolean m; + public final Map tileEntities; + public final List[] entitySlices; // Spigot + final PaperLightingQueue.LightingQueue lightingQueue = new PaperLightingQueue.LightingQueue(this); // Paper + private boolean done; + private boolean lit; + private boolean r; private boolean isTicked() { return r; }; // Paper - OBFHELPER + private boolean s; + private boolean t; + private long lastSaved; + private int v; + private long w; + private int x; + private final ConcurrentLinkedQueue y; + public boolean d; public void setShouldUnload(boolean unload) { this.d = unload; } public boolean isUnloading() { return d; } // Paper - OBFHELPER + protected gnu.trove.map.hash.TObjectIntHashMap entityCount = new gnu.trove.map.hash.TObjectIntHashMap(); // Spigot + + // Paper start + // Track the number of minecarts and items + // Keep this synced with entitySlices.add() and entitySlices.remove() + private final int[] itemCounts = new int[16]; + private final int[] inventoryEntityCounts = new int[16]; + // Paper end + + // CraftBukkit start - Neighbor loaded cache for chunk lighting and entity ticking + private int neighbors = 0x1 << 12; + public long chunkKey; + + public boolean areNeighborsLoaded(final int radius) { + switch (radius) { + case 2: + return this.neighbors == Integer.MAX_VALUE >> 6; + case 1: + final int mask = + // x z offset x z offset x z offset + (0x1 << (1 * 5 + 1 + 12)) | (0x1 << (0 * 5 + 1 + 12)) | (0x1 << (-1 * 5 + 1 + 12)) | + (0x1 << (1 * 5 + 0 + 12)) | (0x1 << (0 * 5 + 0 + 12)) | (0x1 << (-1 * 5 + 0 + 12)) | + (0x1 << (1 * 5 + -1 + 12)) | (0x1 << (0 * 5 + -1 + 12)) | (0x1 << (-1 * 5 + -1 + 12)); + return (this.neighbors & mask) == mask; + default: + throw new UnsupportedOperationException(String.valueOf(radius)); + } + } + + public void setNeighborLoaded(final int x, final int z) { + this.neighbors |= 0x1 << (x * 5 + 12 + z); + } + + public void setNeighborUnloaded(final int x, final int z) { + this.neighbors &= ~(0x1 << (x * 5 + 12 + z)); + } + // CraftBukkit end + + public Chunk(World world, int i, int j) { + this.sections = new ChunkSection[16]; + this.g = new byte[256]; + this.h = new int[256]; + this.i = new boolean[256]; + this.tileEntities = Maps.newHashMap(); + this.x = 4096; + this.y = Queues.newConcurrentLinkedQueue(); + this.entitySlices = (new List[16]); // Spigot + this.world = world; + this.locX = i; + this.locZ = j; + this.heightMap = new int[256]; + + for (int k = 0; k < this.entitySlices.length; ++k) { + this.entitySlices[k] = new org.bukkit.craftbukkit.util.UnsafeList(); // Spigot + } + + Arrays.fill(this.h, -999); + Arrays.fill(this.g, (byte) -1); + // CraftBukkit start + this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); + this.chunkKey = ChunkCoordIntPair.a(this.locX, this.locZ); + } + + public org.bukkit.Chunk bukkitChunk; + public boolean mustSave; + // CraftBukkit end + + public Chunk(World world, ChunkSnapshot chunksnapshot, int i, int j) { + this(world, i, j); + boolean flag = true; + boolean flag1 = world.worldProvider.m(); + + for (int k = 0; k < 16; ++k) { + for (int l = 0; l < 16; ++l) { + for (int i1 = 0; i1 < 256; ++i1) { + IBlockData iblockdata = chunksnapshot.a(k, i1, l); + + if (iblockdata.getMaterial() != Material.AIR) { + int j1 = i1 >> 4; + + if (this.sections[j1] == Chunk.a) { + this.sections[j1] = new ChunkSection(j1 << 4, flag1, world.chunkPacketBlockController.getPredefinedBlockData(this, j1)); // Paper - Anti-Xray - Add predefined block data + } + + this.sections[j1].setType(k, i1 & 15, l, iblockdata); + } + } + } + } + + } + + public boolean a(int i, int j) { + return i == this.locX && j == this.locZ; + } + + public int e(BlockPosition blockposition) { + return this.b(blockposition.getX() & 15, blockposition.getZ() & 15); + } + + public int b(int i, int j) { + return this.heightMap[j << 4 | i]; + } + + @Nullable + private ChunkSection y() { + for (int i = this.sections.length - 1; i >= 0; --i) { + if (this.sections[i] != Chunk.a) { + return this.sections[i]; + } + } + + return null; + } + + public int g() { + ChunkSection chunksection = this.y(); + + return chunksection == null ? 0 : chunksection.getYPosition(); + } + + public ChunkSection[] getSections() { + return this.sections; + } + + public void initLighting() { + int i = this.g(); + + this.v = Integer.MAX_VALUE; + + for (int j = 0; j < 16; ++j) { + int k = 0; + + while (k < 16) { + this.h[j + (k << 4)] = -999; + int l = i + 16; + + while (true) { + if (l > 0) { + if (this.d(j, l - 1, k) == 0) { + --l; + continue; + } + + this.heightMap[k << 4 | j] = l; + if (l < this.v) { + this.v = l; + } + } + + if (this.world.worldProvider.m()) { + l = 15; + int i1 = i + 16 - 1; + + do { + int j1 = this.d(j, i1, k); + + if (j1 == 0 && l != 15) { + j1 = 1; + } + + l -= j1; + if (l > 0) { + ChunkSection chunksection = this.sections[i1 >> 4]; + + if (chunksection != Chunk.a) { + chunksection.a(j, i1 & 15, k, l); + this.world.m(new BlockPosition((this.locX << 4) + j, i1, (this.locZ << 4) + k)); + } + } + + --i1; + } while (i1 > 0 && l > 0); + } + + ++k; + break; + } + } + } + + this.s = true; + } + + private void d(int i, int j) { + this.i[i + j * 16] = true; + this.m = true; + } + + private void h(boolean flag) { + this.world.methodProfiler.a("recheckGaps"); + if (this.world.areChunksLoaded(new BlockPosition(this.locX * 16 + 8, 0, this.locZ * 16 + 8), 16)) { + this.runOrQueueLightUpdate(() -> recheckGaps(flag)); // Paper - Queue light update + } + } + + private void recheckGaps(boolean flag) { + if (true) { + // Paper end + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 16; ++j) { + if (this.i[i + j * 16]) { + this.i[i + j * 16] = false; + int k = this.b(i, j); + int l = this.locX * 16 + i; + int i1 = this.locZ * 16 + j; + int j1 = Integer.MAX_VALUE; + + Iterator iterator; + EnumDirection enumdirection; + + for (iterator = EnumDirection.EnumDirectionLimit.HORIZONTAL.iterator(); iterator.hasNext(); j1 = Math.min(j1, this.world.d(l + enumdirection.getAdjacentX(), i1 + enumdirection.getAdjacentZ()))) { + enumdirection = (EnumDirection) iterator.next(); + } + + this.b(l, i1, j1); + iterator = EnumDirection.EnumDirectionLimit.HORIZONTAL.iterator(); + + while (iterator.hasNext()) { + enumdirection = (EnumDirection) iterator.next(); + this.b(l + enumdirection.getAdjacentX(), i1 + enumdirection.getAdjacentZ(), k); + } + + if (flag) { + this.world.methodProfiler.b(); + return; + } + } + } + } + + this.m = false; + } + + this.world.methodProfiler.b(); + } + + private void b(int i, int j, int k) { + int l = this.world.getHighestBlockYAt(new BlockPosition(i, 0, j)).getY(); + + if (l > k) { + this.a(i, j, k, l + 1); + } else if (l < k) { + this.a(i, j, l, k + 1); + } + + } + + private void a(int i, int j, int k, int l) { + if (l > k && this.world.areChunksLoaded(new BlockPosition(i, 0, j), 16)) { + for (int i1 = k; i1 < l; ++i1) { + this.world.c(EnumSkyBlock.SKY, new BlockPosition(i, i1, j)); + } + + this.s = true; + } + + } + + private void c(int i, int j, int k) { + int l = this.heightMap[k << 4 | i] & 255; + int i1 = l; + + if (j > l) { + i1 = j; + } + + while (i1 > 0 && this.d(i, i1 - 1, k) == 0) { + --i1; + } + + if (i1 != l) { + this.world.a(i + this.locX * 16, k + this.locZ * 16, i1, l); + this.heightMap[k << 4 | i] = i1; + int j1 = this.locX * 16 + i; + int k1 = this.locZ * 16 + k; + int l1; + int i2; + + if (this.world.worldProvider.m()) { + ChunkSection chunksection; + + if (i1 < l) { + for (l1 = i1; l1 < l; ++l1) { + chunksection = this.sections[l1 >> 4]; + if (chunksection != Chunk.a) { + chunksection.a(i, l1 & 15, k, 15); + this.world.m(new BlockPosition((this.locX << 4) + i, l1, (this.locZ << 4) + k)); + } + } + } else { + for (l1 = l; l1 < i1; ++l1) { + chunksection = this.sections[l1 >> 4]; + if (chunksection != Chunk.a) { + chunksection.a(i, l1 & 15, k, 0); + this.world.m(new BlockPosition((this.locX << 4) + i, l1, (this.locZ << 4) + k)); + } + } + } + + l1 = 15; + + while (i1 > 0 && l1 > 0) { + --i1; + i2 = this.d(i, i1, k); + if (i2 == 0) { + i2 = 1; + } + + l1 -= i2; + if (l1 < 0) { + l1 = 0; + } + + ChunkSection chunksection1 = this.sections[i1 >> 4]; + + if (chunksection1 != Chunk.a) { + chunksection1.a(i, i1 & 15, k, l1); + } + } + } + + l1 = this.heightMap[k << 4 | i]; + i2 = l; + int j2 = l1; + + if (l1 < l) { + i2 = l1; + j2 = l; + } + + if (l1 < this.v) { + this.v = l1; + } + + if (this.world.worldProvider.m()) { + Iterator iterator = EnumDirection.EnumDirectionLimit.HORIZONTAL.iterator(); + + while (iterator.hasNext()) { + EnumDirection enumdirection = (EnumDirection) iterator.next(); + + this.a(j1 + enumdirection.getAdjacentX(), k1 + enumdirection.getAdjacentZ(), i2, j2); + } + + this.a(j1, k1, i2, j2); + } + + this.s = true; + } + } + + public int b(BlockPosition blockposition) { + return this.getBlockData(blockposition).c(); + } + + private int d(int i, int j, int k) { + return this.a(i, j, k).c(); + } + + // Paper start - Optimize getBlockData to reduce instructions + public final IBlockData getBlockData(final BlockPosition pos) { + return getBlockData(pos.getX(), pos.getY(), pos.getZ()); + } + + public final IBlockData getBlockData(final int x, final int y, final int z) { + // Method body / logic copied from below + final int i = y >> 4; + if (y >= 0 && i < this.sections.length && this.sections[i] != null) { + // Inlined ChunkSection.getType() and DataPaletteBlock.a(int,int,int) + return this.sections[i].blockIds.a((y & 15) << 8 | (z & 15) << 4 | x & 15); + } + return Blocks.AIR.getBlockData(); + } + + public IBlockData a(final int i, final int j, final int k) { + return getBlockData(i, j, k); + } + + public IBlockData unused(final int i, final int j, final int k) { + // Paper end + if (this.world.N() == WorldType.DEBUG_ALL_BLOCK_STATES) { + IBlockData iblockdata = null; + + if (j == 60) { + iblockdata = Blocks.BARRIER.getBlockData(); + } + + if (j == 70) { + iblockdata = ChunkProviderDebug.c(i, k); + } + + return iblockdata == null ? Blocks.AIR.getBlockData() : iblockdata; + } else { + try { + if (j >= 0 && j >> 4 < this.sections.length) { + ChunkSection chunksection = this.sections[j >> 4]; + + if (chunksection != Chunk.a) { + return chunksection.getType(i & 15, j & 15, k & 15); + } + } + + return Blocks.AIR.getBlockData(); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.a(throwable, "Getting block state"); + CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Block being got"); + + crashreportsystemdetails.a("Location", new CrashReportCallable() { + public String a() throws Exception { + return CrashReportSystemDetails.a(i, j, k); + } + + @Override + public Object call() throws Exception { + return this.a(); + } + }); + throw new ReportedException(crashreport); + } + } + } + + @Nullable + public IBlockData a(BlockPosition blockposition, IBlockData iblockdata) { + int i = blockposition.getX() & 15; + int j = blockposition.getY(); + int k = blockposition.getZ() & 15; + int l = k << 4 | i; + + if (j >= this.h[l] - 1) { + this.h[l] = -999; + } + + int i1 = this.heightMap[l]; + IBlockData iblockdata1 = this.getBlockData(blockposition); + + if (iblockdata1 == iblockdata) { + return null; + } else { + Block block = iblockdata.getBlock(); + Block block1 = iblockdata1.getBlock(); + ChunkSection chunksection = this.sections[j >> 4]; + boolean flag = false; + + if (chunksection == Chunk.a) { + if (block == Blocks.AIR) { + return null; + } + + chunksection = new ChunkSection(j >> 4 << 4, this.world.worldProvider.m(), this.world.chunkPacketBlockController.getPredefinedBlockData(this, j >> 4)); // Paper - Anti-Xray - Add predefined block data + this.sections[j >> 4] = chunksection; + flag = j >= i1; + } + + chunksection.setType(i, j & 15, k, iblockdata); + if (block1 != block) { + if (!this.world.isClientSide) { + block1.remove(this.world, blockposition, iblockdata1); + } else if (block1 instanceof ITileEntity) { + this.world.s(blockposition); + } + } + + if (chunksection.getType(i, j & 15, k).getBlock() != block) { + return null; + } else { + if (flag) { + this.initLighting(); + } else { + this.runOrQueueLightUpdate(() -> { // Paper - Queue light update + int j1 = iblockdata.c(); + int k1 = iblockdata1.c(); + + if (j1 > 0) { + if (j >= i1) { + this.c(i, j + 1, k); + } + } else if (j == i1 - 1) { + this.c(i, j, k); + } + + if (j1 != k1 && (j1 < k1 || this.getBrightness(EnumSkyBlock.SKY, blockposition) > 0 || this.getBrightness(EnumSkyBlock.BLOCK, blockposition) > 0)) { + this.d(i, k); + } + }); // Paper + } + + TileEntity tileentity; + + if (block1 instanceof ITileEntity) { + tileentity = this.a(blockposition, Chunk.EnumTileEntityState.CHECK); + if (tileentity != null) { + tileentity.invalidateBlockCache(); + } + } + + // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. + if (!this.world.isClientSide && block1 != block && (!this.world.captureBlockStates || block instanceof BlockTileEntity)) { + block.onPlace(this.world, blockposition, iblockdata); + } + + if (block instanceof ITileEntity) { + tileentity = this.a(blockposition, Chunk.EnumTileEntityState.CHECK); + if (tileentity == null) { + tileentity = ((ITileEntity) block).a(this.world, block.toLegacyData(iblockdata)); + this.world.setTileEntity(blockposition, tileentity); + } + + if (tileentity != null) { + tileentity.invalidateBlockCache(); + } + } + + this.s = true; + return iblockdata1; + } + } + } + + public int getBrightness(EnumSkyBlock enumskyblock, BlockPosition blockposition) { + int i = blockposition.getX() & 15; + int j = blockposition.getY(); + int k = blockposition.getZ() & 15; + ChunkSection chunksection = this.sections[j >> 4]; + + return chunksection == Chunk.a ? (this.c(blockposition) ? enumskyblock.c : 0) : (enumskyblock == EnumSkyBlock.SKY ? (!this.world.worldProvider.m() ? 0 : chunksection.b(i, j & 15, k)) : (enumskyblock == EnumSkyBlock.BLOCK ? chunksection.c(i, j & 15, k) : enumskyblock.c)); + } + + public void a(EnumSkyBlock enumskyblock, BlockPosition blockposition, int i) { + int j = blockposition.getX() & 15; + int k = blockposition.getY(); + int l = blockposition.getZ() & 15; + ChunkSection chunksection = this.sections[k >> 4]; + + if (chunksection == Chunk.a) { + chunksection = new ChunkSection(k >> 4 << 4, this.world.worldProvider.m(), this.world.chunkPacketBlockController.getPredefinedBlockData(this, k >> 4)); // Paper - Anti-Xray - Add predefined block data + this.sections[k >> 4] = chunksection; + this.initLighting(); + } + + this.s = true; + if (enumskyblock == EnumSkyBlock.SKY) { + if (this.world.worldProvider.m()) { + chunksection.a(j, k & 15, l, i); + } + } else if (enumskyblock == EnumSkyBlock.BLOCK) { + chunksection.b(j, k & 15, l, i); + } + + } + + public final int getLightSubtracted(BlockPosition blockposition, int i) { return this.a(blockposition, i); } // Paper - OBFHELPER + public int a(BlockPosition blockposition, int i) { + int j = blockposition.getX() & 15; + int k = blockposition.getY(); + int l = blockposition.getZ() & 15; + ChunkSection chunksection = this.sections[k >> 4]; + + if (chunksection == Chunk.a) { + return this.world.worldProvider.m() && i < EnumSkyBlock.SKY.c ? EnumSkyBlock.SKY.c - i : 0; + } else { + int i1 = !this.world.worldProvider.m() ? 0 : chunksection.b(j, k & 15, l); + + i1 -= i; + int j1 = chunksection.c(j, k & 15, l); + + if (j1 > i1) { + i1 = j1; + } + + return i1; + } + } + + public void a(Entity entity) { + this.t = true; + int i = MathHelper.floor(entity.locX / 16.0D); + int j = MathHelper.floor(entity.locZ / 16.0D); + + if (i != this.locX || j != this.locZ) { + Chunk.e.warn("Wrong location! ({}, {}) should be ({}, {}), {}", Integer.valueOf(i), Integer.valueOf(j), Integer.valueOf(this.locX), Integer.valueOf(this.locZ), entity); + entity.die(); + } + + int k = MathHelper.floor(entity.locY / 16.0D); + + if (k < 0) { + k = 0; + } + + if (k >= this.entitySlices.length) { + k = this.entitySlices.length - 1; + } + + entity.aa = true; + entity.ab = this.locX; + entity.ac = k; + entity.ad = this.locZ; + this.entitySlices[k].add(entity); + // Paper start - update count + if (entity instanceof EntityItem) { + itemCounts[k]++; + } else if (entity instanceof IInventory) { + inventoryEntityCounts[k]++; + } + // Paper end + // Spigot start - increment creature type count + // Keep this synced up with World.a(Class) + if (entity instanceof EntityInsentient) { + EntityInsentient entityinsentient = (EntityInsentient) entity; + if (entityinsentient.isTypeNotPersistent() && entityinsentient.isPersistent()) { + return; + } + } + for ( EnumCreatureType creatureType : EnumCreatureType.values() ) + { + if ( creatureType.a().isAssignableFrom( entity.getClass() ) ) + { + this.entityCount.adjustOrPutValue( creatureType.a(), 1, 1 ); + } + } + // Spigot end + } + + public void b(Entity entity) { + this.a(entity, entity.ac); + } + + public void a(Entity entity, int i) { + if (i < 0) { + i = 0; + } + + if (i >= this.entitySlices.length) { + i = this.entitySlices.length - 1; + } + + if (!this.entitySlices[i].remove(entity)) { return; } // Paper + // Paper start - update counts + if (entity instanceof EntityItem) { + itemCounts[i]--; + } else if (entity instanceof IInventory) { + inventoryEntityCounts[i]--; + } + // Paper end + // Spigot start - decrement creature type count + // Keep this synced up with World.a(Class) + if (entity instanceof EntityInsentient) { + EntityInsentient entityinsentient = (EntityInsentient) entity; + if (entityinsentient.isTypeNotPersistent() && entityinsentient.isPersistent()) { + return; + } + } + for ( EnumCreatureType creatureType : EnumCreatureType.values() ) + { + if ( creatureType.a().isAssignableFrom( entity.getClass() ) ) + { + this.entityCount.adjustValue( creatureType.a(), -1 ); + } + } + // Spigot end + } + + public boolean c(BlockPosition blockposition) { + int i = blockposition.getX() & 15; + int j = blockposition.getY(); + int k = blockposition.getZ() & 15; + + return j >= this.heightMap[k << 4 | i]; + } + + @Nullable + private TileEntity g(BlockPosition blockposition) { + IBlockData iblockdata = this.getBlockData(blockposition); + Block block = iblockdata.getBlock(); + + return !block.isTileEntity() ? null : ((ITileEntity) block).a(this.world, iblockdata.getBlock().toLegacyData(iblockdata)); + } + + @Nullable public final TileEntity getTileEntityImmediately(BlockPosition pos) { return this.a(pos, EnumTileEntityState.IMMEDIATE); } // Paper - OBFHELPER + @Nullable + public TileEntity a(BlockPosition blockposition, Chunk.EnumTileEntityState chunk_enumtileentitystate) { + // CraftBukkit start + TileEntity tileentity = null; + if (world.captureBlockStates) { + tileentity = world.capturedTileEntities.get(blockposition); + } + if (tileentity == null) { + tileentity = this.tileEntities.get(blockposition); + } + // CraftBukkit end + + if (tileentity == null) { + if (chunk_enumtileentitystate == Chunk.EnumTileEntityState.IMMEDIATE) { + tileentity = this.g(blockposition); + this.world.setTileEntity(blockposition, tileentity); + } else if (chunk_enumtileentitystate == Chunk.EnumTileEntityState.QUEUED) { + this.y.add(blockposition); + } + } else if (tileentity.y()) { + this.tileEntities.remove(blockposition); + return null; + } + + return tileentity; + } + + public void a(TileEntity tileentity) { + this.a(tileentity.getPosition(), tileentity); + if (this.j) { + this.world.a(tileentity); + } + + } + + public void a(BlockPosition blockposition, TileEntity tileentity) { + tileentity.a(this.world); + tileentity.setPosition(blockposition); + if (this.getBlockData(blockposition).getBlock() instanceof ITileEntity) { + if (this.tileEntities.containsKey(blockposition)) { + this.tileEntities.get(blockposition).z(); + } + + tileentity.A(); + this.tileEntities.put(blockposition, tileentity); + // CraftBukkit start + // Paper start - Remove invalid mob spawner tile entities + } else if (tileentity instanceof TileEntityMobSpawner && org.bukkit.craftbukkit.util.CraftMagicNumbers.getMaterial(getBlockData(blockposition).getBlock()) != org.bukkit.Material.MOB_SPAWNER) { + this.tileEntities.remove(blockposition); + // Paper end + } else { + // Paper start + ServerInternalException e = new ServerInternalException( + "Attempted to place a tile entity (" + tileentity + ") at " + tileentity.position.getX() + "," + + tileentity.position.getY() + "," + tileentity.position.getZ() + + " (" + CraftMagicNumbers.getMaterial(getBlockData(blockposition).getBlock()) + ") where there was no entity tile!\n" + + "Chunk coordinates: " + (this.locX * 16) + "," + (this.locZ * 16)); + e.printStackTrace(); + ServerInternalException.reportInternalException(e); + + if (this.world.paperConfig.removeCorruptTEs) { + this.removeTileEntity(tileentity.getPosition()); + org.bukkit.Bukkit.getLogger().info("Removing corrupt tile entity"); + } + // Paper end + // CraftBukkit end + } + } + + public void removeTileEntity(BlockPosition blockposition) { this.d(blockposition); } // Paper - OBFHELPER + public void d(BlockPosition blockposition) { + if (this.j) { + TileEntity tileentity = this.tileEntities.remove(blockposition); + + if (tileentity != null) { + tileentity.z(); + } + } + + } + + public void addEntities() { + this.j = true; + this.world.b(this.tileEntities.values()); + List[] aentityslice = this.entitySlices; // Spigot + int i = aentityslice.length; + + for (int j = 0; j < i; ++j) { + List entityslice = aentityslice[j]; // Spigot + + this.world.a(entityslice); + } + + } + + public void removeEntities() { + this.j = false; + Iterator iterator = this.tileEntities.values().iterator(); + + while (iterator.hasNext()) { + TileEntity tileentity = (TileEntity) iterator.next(); + // Spigot Start + if ( tileentity instanceof IInventory ) + { + for ( org.bukkit.entity.HumanEntity h : Lists.newArrayList(( (IInventory) tileentity ).getViewers() ) ) + { + if ( h instanceof org.bukkit.craftbukkit.entity.CraftHumanEntity ) + { + ( (org.bukkit.craftbukkit.entity.CraftHumanEntity) h).getHandle().closeInventory(); + } + } + } + // Spigot End + + this.world.b(tileentity); + } + + List[] aentityslice = this.entitySlices; // Spigot + int i = aentityslice.length; + + for (int j = 0; j < i; ++j) { + // CraftBukkit start + List newList = Lists.newArrayList(aentityslice[j]); + java.util.Iterator iter = newList.iterator(); + while (iter.hasNext()) { + Entity entity = iter.next(); + // Spigot Start + if ( entity instanceof IInventory ) + { + for ( org.bukkit.entity.HumanEntity h : Lists.newArrayList( ( (IInventory) entity ).getViewers() ) ) + { + if ( h instanceof org.bukkit.craftbukkit.entity.CraftHumanEntity ) + { + ( (org.bukkit.craftbukkit.entity.CraftHumanEntity) h).getHandle().closeInventory(); + } + } + } + // Spigot End + + // Do not pass along players, as doing so can get them stuck outside of time. + // (which for example disables inventory icon updates and prevents block breaking) + if (entity instanceof EntityPlayer) { + iter.remove(); + } + } + + this.world.c(newList); + // CraftBukkit end + } + + } + + public void markDirty() { + this.s = true; + } + + public void a(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List list, Predicate predicate) { + int i = MathHelper.floor((axisalignedbb.b - 2.0D) / 16.0D); + int j = MathHelper.floor((axisalignedbb.e + 2.0D) / 16.0D); + + i = MathHelper.clamp(i, 0, this.entitySlices.length - 1); + j = MathHelper.clamp(j, 0, this.entitySlices.length - 1); + + for (int k = i; k <= j; ++k) { + if (!this.entitySlices[k].isEmpty()) { + Iterator iterator = this.entitySlices[k].iterator(); + + // Paper start - Don't search for inventories if we have none, and that is all we want + /* + * We check if they want inventories by seeing if it is the static `IEntitySelector.c` + * + * Make sure the inventory selector stays in sync. + * It should be the one that checks `var1 instanceof IInventory && var1.isAlive()` + */ + if (predicate == IEntitySelector.c && inventoryEntityCounts[k] <= 0) continue; + // Paper end + while (iterator.hasNext()) { + Entity entity1 = (Entity) iterator.next(); + + if (entity1.getBoundingBox().c(axisalignedbb) && entity1 != entity) { + if (predicate == null || predicate.apply(entity1)) { + list.add(entity1); + } + + Entity[] aentity = entity1.bb(); + + if (aentity != null) { + Entity[] aentity1 = aentity; + int l = aentity.length; + + for (int i1 = 0; i1 < l; ++i1) { + Entity entity2 = aentity1[i1]; + + if (entity2 != entity && entity2.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.apply(entity2))) { + list.add(entity2); + } + } + } + } + } + } + } + + } + + public void a(Class oclass, AxisAlignedBB axisalignedbb, List list, Predicate predicate) { + int i = MathHelper.floor((axisalignedbb.b - 2.0D) / 16.0D); + int j = MathHelper.floor((axisalignedbb.e + 2.0D) / 16.0D); + + i = MathHelper.clamp(i, 0, this.entitySlices.length - 1); + j = MathHelper.clamp(j, 0, this.entitySlices.length - 1); + + // Paper start + int[] counts; + if (EntityItem.class.isAssignableFrom(oclass)) { + counts = itemCounts; + } else if (IInventory.class.isAssignableFrom(oclass)) { + counts = inventoryEntityCounts; + } else { + counts = null; + } + // Paper end + for (int k = i; k <= j; ++k) { + if (counts != null && counts[k] <= 0) continue; // Paper - Don't check a chunk if it doesn't have the type we are looking for + Iterator iterator = this.entitySlices[k].iterator(); // Spigot + + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + + if (oclass.isInstance(entity) && entity.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.apply((T) entity))) { // CraftBukkit - fix decompile error // Spigot - instance check + list.add((T) entity); // Fix decompile error + } + } + } + + } + + public boolean a(boolean flag) { + if (flag) { + if (this.t && this.world.getTime() != this.lastSaved || this.s) { + return true; + } + } + // This !flag section should say if s(isModified) or t(hasEntities), then check auto save + return ((this.s || this.t) && this.world.getTime() >= this.lastSaved + world.paperConfig.autoSavePeriod); // Paper - Make world configurable and incremental + } + + public Random a(long i) { + return new Random(this.world.getSeed() + this.locX * this.locX * 4987142 + this.locX * 5947611 + this.locZ * this.locZ * 4392871L + this.locZ * 389711 ^ i); + } + + public boolean isEmpty() { + return false; + } + + // CraftBukkit start + public void loadNearby(IChunkProvider ichunkprovider, ChunkGenerator chunkgenerator, boolean newChunk) { + world.timings.syncChunkLoadPostTimer.startTiming(); // Paper + Server server = world.getServer(); + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside + * the World constructor. We can't reliably alter that, so we have + * no way of creating a CraftWorld/CraftServer at that point. + */ + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, newChunk)); + } + + // Update neighbor counts + for (int x = -2; x < 3; x++) { + for (int z = -2; z < 3; z++) { + if (x == 0 && z == 0) { + continue; + } + + Chunk neighbor = getWorld().getChunkIfLoaded(locX + x, locZ + z); + if (neighbor != null) { + neighbor.setNeighborLoaded(-x, -z); + setNeighborLoaded(x, z); + } + } + } + // CraftBukkit end + world.timings.syncChunkLoadPostTimer.stopTiming(); // Paper + world.timings.syncChunkLoadPopulateNeighbors.startTiming(); // Paper + Chunk chunk = MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX, this.locZ - 1); // Paper + Chunk chunk1 = MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX + 1, this.locZ); // Paper + Chunk chunk2 = MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX, this.locZ + 1); // Paper + Chunk chunk3 = MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX - 1, this.locZ); // Paper + + if (chunk1 != null && chunk2 != null && MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX + 1, this.locZ + 1) != null) { // Paper + this.a(chunkgenerator); + } + + if (chunk3 != null && chunk2 != null && MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX - 1, this.locZ + 1) != null) { // Paper + chunk3.a(chunkgenerator); + } + + if (chunk != null && chunk1 != null && MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX + 1, this.locZ - 1) != null) { // Paper + chunk.a(chunkgenerator); + } + + if (chunk != null && chunk3 != null) { + Chunk chunk4 = MCUtil.getLoadedChunkWithoutMarkingActive(ichunkprovider,this.locX - 1, this.locZ - 1); // Paper + + if (chunk4 != null) { + chunk4.a(chunkgenerator); + } + } + world.timings.syncChunkLoadPopulateNeighbors.stopTiming(); // Paper + + } + + protected void a(ChunkGenerator chunkgenerator) { + if (this.isDone()) { + if (chunkgenerator.a(this, this.locX, this.locZ)) { + this.markDirty(); + } + } else { + this.o(); + chunkgenerator.recreateStructures(this.locX, this.locZ); + + // CraftBukkit start + BlockSand.instaFall = true; + Random random = new Random(); + random.setSeed(world.getSeed()); + long xRand = random.nextLong() / 2L * 2L + 1L; + long zRand = random.nextLong() / 2L * 2L + 1L; + random.setSeed(locX * xRand + locZ * zRand ^ world.getSeed()); + + org.bukkit.World world = this.world.getWorld(); + if (world != null) { + this.world.populating = true; + try { + for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { + populator.populate(world, random, bukkitChunk); + } + } finally { + this.world.populating = false; + } + } + BlockSand.instaFall = false; + this.world.getServer().getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); + // CraftBukkit end + this.markDirty(); + } + + } + + public BlockPosition f(BlockPosition blockposition) { + int i = blockposition.getX() & 15; + int j = blockposition.getZ() & 15; + int k = i | j << 4; + BlockPosition blockposition1 = new BlockPosition(blockposition.getX(), this.h[k], blockposition.getZ()); + + if (blockposition1.getY() == -999) { + int l = this.g() + 15; + + blockposition1 = new BlockPosition(blockposition.getX(), l, blockposition.getZ()); + int i1 = -1; + + while (blockposition1.getY() > 0 && i1 == -1) { + IBlockData iblockdata = this.getBlockData(blockposition1); + Material material = iblockdata.getMaterial(); + + if (!material.isSolid() && !material.isLiquid()) { + blockposition1 = blockposition1.down(); + } else { + i1 = blockposition1.getY() + 1; + } + } + + this.h[k] = i1; + } + + return new BlockPosition(blockposition.getX(), this.h[k], blockposition.getZ()); + } + + public void b(boolean flag) { + if (this.m && this.world.worldProvider.m() && !flag) { + this.h(this.world.isClientSide); + } + + this.r = true; + if (!this.lit && this.done && this.world.spigotConfig.randomLightUpdates) { // Spigot - also use random light updates setting to determine if we should relight + this.o(); + } + + while (!this.y.isEmpty()) { + BlockPosition blockposition = this.y.poll(); + + if (this.a(blockposition, Chunk.EnumTileEntityState.CHECK) == null && this.getBlockData(blockposition).getBlock().isTileEntity()) { + TileEntity tileentity = this.g(blockposition); + + this.world.setTileEntity(blockposition, tileentity); + this.world.b(blockposition, blockposition); + } + } + + } + + public boolean isReady() { + // Spigot Start + /* + * As of 1.7, Mojang added a check to make sure that only chunks which have been lit are sent to the client. + * Unfortunately this interferes with our modified chunk ticking algorithm, which will only tick chunks distant from the player on a very infrequent basis. + * We cannot unfortunately do this lighting stage during chunk gen as it appears to put a lot more noticeable load on the server, than when it is done at play time. + * For now at least we will simply send all chunks, in accordance with pre 1.7 behaviour. + */ + // Paper Start + // if randomLightUpdates are enabled, we should always return true, otherwise chunks may never send + // to the client due to not being lit, otherwise retain standard behavior and only send properly lit chunks. + return !this.world.spigotConfig.randomLightUpdates || (this.isTicked() && this.done && this.lit); + // Paper End + // Spigot End + } + + public boolean j() { + return this.r; + } + + public ChunkCoordIntPair k() { + return new ChunkCoordIntPair(this.locX, this.locZ); + } + + public boolean c(int i, int j) { + if (i < 0) { + i = 0; + } + + if (j >= 256) { + j = 255; + } + + for (int k = i; k <= j; k += 16) { + ChunkSection chunksection = this.sections[k >> 4]; + + if (chunksection != Chunk.a && !chunksection.a()) { + return false; + } + } + + return true; + } + + public void a(ChunkSection[] achunksection) { + if (this.sections.length != achunksection.length) { + Chunk.e.warn("Could not set level chunk sections, array length is {} instead of {}", Integer.valueOf(achunksection.length), Integer.valueOf(this.sections.length)); + } else { + System.arraycopy(achunksection, 0, this.sections, 0, this.sections.length); + } + } + + public BiomeBase getBiome(BlockPosition blockposition, WorldChunkManager worldchunkmanager) { + int i = blockposition.getX() & 15; + int j = blockposition.getZ() & 15; + int k = this.g[j << 4 | i] & 255; + BiomeBase biomebase; + + if (k == 255) { + biomebase = worldchunkmanager.getBiome(blockposition, Biomes.c); + k = BiomeBase.a(biomebase); + this.g[j << 4 | i] = (byte) (k & 255); + } + + biomebase = BiomeBase.getBiome(k); + return biomebase == null ? Biomes.c : biomebase; + } + + public byte[] getBiomeIndex() { + return this.g; + } + + public void a(byte[] abyte) { + if (this.g.length != abyte.length) { + Chunk.e.warn("Could not set level chunk biomes, array length is {} instead of {}", Integer.valueOf(abyte.length), Integer.valueOf(this.g.length)); + } else { + System.arraycopy(abyte, 0, this.g, 0, this.g.length); + } + } + + public void m() { + this.x = 0; + } + + public void n() { + if (this.x < 4096) { + BlockPosition blockposition = new BlockPosition(this.locX << 4, 0, this.locZ << 4); + + for (int i = 0; i < 8; ++i) { + if (this.x >= 4096) { + return; + } + + int j = this.x % 16; + int k = this.x / 16 % 16; + int l = this.x / 256; + + ++this.x; + + for (int i1 = 0; i1 < 16; ++i1) { + BlockPosition blockposition1 = blockposition.a(k, (j << 4) + i1, l); + boolean flag = i1 == 0 || i1 == 15 || k == 0 || k == 15 || l == 0 || l == 15; + + if (this.sections[j] == Chunk.a && flag || this.sections[j] != Chunk.a && this.sections[j].getType(k, i1, l).getMaterial() == Material.AIR) { + EnumDirection[] aenumdirection = EnumDirection.values(); + int j1 = aenumdirection.length; + + for (int k1 = 0; k1 < j1; ++k1) { + EnumDirection enumdirection = aenumdirection[k1]; + BlockPosition blockposition2 = blockposition1.shift(enumdirection); + + if (this.world.getType(blockposition2).d() > 0) { + this.world.w(blockposition2); + } + } + + this.world.w(blockposition1); + } + } + } + + } + } + + public void o() { + world.timings.lightChunk.startTiming(); // Paper + this.done = true; + this.lit = true; + BlockPosition blockposition = new BlockPosition(this.locX << 4, 0, this.locZ << 4); + + if (this.world.worldProvider.m()) { + if (this.world.areChunksLoadedBetween(blockposition.a(-1, 0, -1), blockposition.a(16, this.world.getSeaLevel(), 16))) { + label42: + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 16; ++j) { + if (!this.e(i, j)) { + this.lit = false; + break label42; + } + } + } + + if (this.lit) { + Iterator iterator = EnumDirection.EnumDirectionLimit.HORIZONTAL.iterator(); + + while (iterator.hasNext()) { + EnumDirection enumdirection = (EnumDirection) iterator.next(); + int k = enumdirection.c() == EnumDirection.EnumAxisDirection.POSITIVE ? 16 : 1; + + this.world.getChunkAtWorldCoords(blockposition.shift(enumdirection, k)).a(enumdirection.opposite()); + } + + this.z(); + } + } else { + this.lit = false; + } + } + + world.timings.lightChunk.stopTiming(); // Paper + } + + private void z() { + for (int i = 0; i < this.i.length; ++i) { + this.i[i] = true; + } + + this.h(false); + } + + public void a(EnumDirection enumdirection) { // Akarin - private -> public - PAIL: checkLightSide + if (this.done) { + int i; + + if (enumdirection == EnumDirection.EAST) { + for (i = 0; i < 16; ++i) { + this.e(15, i); + } + } else if (enumdirection == EnumDirection.WEST) { + for (i = 0; i < 16; ++i) { + this.e(0, i); + } + } else if (enumdirection == EnumDirection.SOUTH) { + for (i = 0; i < 16; ++i) { + this.e(i, 15); + } + } else if (enumdirection == EnumDirection.NORTH) { + for (i = 0; i < 16; ++i) { + this.e(i, 0); + } + } + + } + } + + private boolean e(int i, int j) { + int k = this.g(); + boolean flag = false; + boolean flag1 = false; + BlockPosition.MutableBlockPosition blockposition_mutableblockposition = new BlockPosition.MutableBlockPosition((this.locX << 4) + i, 0, (this.locZ << 4) + j); + + int l; + + for (l = k + 16 - 1; l > this.world.getSeaLevel() || l > 0 && !flag1; --l) { + blockposition_mutableblockposition.c(blockposition_mutableblockposition.getX(), l, blockposition_mutableblockposition.getZ()); + int i1 = this.b(blockposition_mutableblockposition); + + if (i1 == 255 && blockposition_mutableblockposition.getY() < this.world.getSeaLevel()) { + flag1 = true; + } + + if (!flag && i1 > 0) { + flag = true; + } else if (flag && i1 == 0 && !this.world.w(blockposition_mutableblockposition)) { + return false; + } + } + + for (l = blockposition_mutableblockposition.getY(); l > 0; --l) { + blockposition_mutableblockposition.c(blockposition_mutableblockposition.getX(), l, blockposition_mutableblockposition.getZ()); + if (this.getBlockData(blockposition_mutableblockposition).d() > 0) { + this.world.w(blockposition_mutableblockposition); + } + } + + return true; + } + + public boolean p() { + return this.j; + } + + public World getWorld() { + return this.world; + } + + public int[] r() { + return this.heightMap; + } + + public void a(int[] aint) { + if (this.heightMap.length != aint.length) { + Chunk.e.warn("Could not set level chunk heightmap, array length is {} instead of {}", Integer.valueOf(aint.length), Integer.valueOf(this.heightMap.length)); + } else { + System.arraycopy(aint, 0, this.heightMap, 0, this.heightMap.length); + } + } + + public Map getTileEntities() { + return this.tileEntities; + } + + public List[] getEntitySlices() { + return this.entitySlices; + } + + public boolean isDone() { + return this.done; + } + + public void d(boolean flag) { + this.done = flag; + } + + public boolean v() { + return this.lit; + } + + public void e(boolean flag) { + this.lit = flag; + } + + public void f(boolean flag) { + this.s = flag; + } + + public void g(boolean flag) { + this.t = flag; + } + + public void setLastSaved(long i) { + this.lastSaved = i; + } + + public int w() { + return this.v; + } + + public long x() { + return world.paperConfig.useInhabitedTime ? this.w : 0; // Paper + } + + public void c(long i) { + this.w = i; + } + + // Paper start + public void runOrQueueLightUpdate(Runnable runnable) { + if (this.world.paperConfig.queueLightUpdates) { + lightingQueue.add(runnable); + } else { + runnable.run(); + } + } + // Paper end + + public static enum EnumTileEntityState { + + IMMEDIATE, QUEUED, CHECK; + + private EnumTileEntityState() {} + } +} diff --git a/sources/src/main/resources/mixins.akarin.core.json b/sources/src/main/resources/mixins.akarin.core.json index 773846062..6aba30a0d 100644 --- a/sources/src/main/resources/mixins.akarin.core.json +++ b/sources/src/main/resources/mixins.akarin.core.json @@ -1,6 +1,6 @@ { "required": true, - "minVersion": "1", + "minVersion": "0.7.8", "package": "io.akarin.server.mixin", "target": "@env(DEFAULT)", "compatibilityLevel": "JAVA_8", @@ -21,6 +21,7 @@ "core.MixinMinecraftServer", "core.MixinChunkIOExecutor", + "cps.MixinChunk", "cps.MixinCraftWorld", "cps.MixinChunkProviderServer", @@ -28,6 +29,11 @@ "nsc.OptimisticNetworkManager", "nsc.NonblockingServerConnection", + "lighting.MixinChunk", + "lighting.MixinWorld", + "lighting.MixinWorldServer", + "lighting.MixinChunkProviderServer", + "optimization.WeakBigTree", "optimization.WeakEnchantmentManager", "optimization.MixinEntityHorseAbstract", diff --git a/work/Paper b/work/Paper index 78469842d..88d8c2594 160000 --- a/work/Paper +++ b/work/Paper @@ -1 +1 @@ -Subproject commit 78469842d7a7b7ef17ea3ffd2d577c1a14fb3ab8 +Subproject commit 88d8c2594e87335dae69f19df45f2edba4e76a21