From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 12 Jun 2021 16:40:34 +0200 Subject: [PATCH] Tuinity Server Changes This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/build.gradle.kts b/build.gradle.kts index 5540da58e66f83b283863d3158a9b4ab5ba636db..ab8de6c4e3c0bea2b9f498da00adf88e987d2364 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,16 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import io.papermc.paperweight.tasks.BaseTask +import io.papermc.paperweight.tasks.GenerateReobfMappings +import io.papermc.paperweight.tasks.PatchMappings +import io.papermc.paperweight.util.Constants import io.papermc.paperweight.util.Git +import io.papermc.paperweight.util.cache import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.openZip import io.papermc.paperweight.util.path +import io.papermc.paperweight.util.registering +import io.papermc.paperweight.util.set import shadow.org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE import java.nio.file.Files import java.text.SimpleDateFormat @@ -30,8 +36,8 @@ repositories { } dependencies { - implementation(project(":Paper-API")) - implementation(project(":Paper-MojangAPI")) + implementation(project(":Tuinity-API")) // Tuinity + implementation("com.destroystokyo.paper:paper-mojangapi:1.16.5-R0.1-SNAPSHOT") // Tuinity // Paper start implementation("org.jline:jline-terminal-jansi:3.12.1") implementation("net.minecrell:terminalconsoleappender:1.2.0") @@ -82,7 +88,7 @@ tasks.jar { attributes( "Main-Class" to "org.bukkit.craftbukkit.Main", "Implementation-Title" to "CraftBukkit", - "Implementation-Version" to "git-Paper-$implementationVersion", + "Implementation-Version" to "git-Tuinity-$implementationVersion", // Tuinity "Implementation-Vendor" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date()), // Paper "Specification-Title" to "Bukkit", "Specification-Version" to project.version, @@ -107,6 +113,22 @@ publishing { } } +val generateReobfMappings = rootProject.tasks.named("generateReobfMappings") + +val patchReobfMappings by tasks.registering { + inputMappings.set(generateReobfMappings.flatMap { it.reobfMappings }) + patch.set(rootProject.layout.cache.resolve("paperweight/upstreams/paper/build-data/reobf-mappings-patch.tiny")) + + fromNamespace.set(Constants.DEOBF_NAMESPACE) + toNamespace.set(Constants.SPIGOT_NAMESPACE) + + outputMappings.set(layout.cache.resolve("paperweight/mappings/reobf-patched.tiny")) +} + +tasks.reobfJar { + mappingsFile.set(patchReobfMappings.flatMap { it.outputMappings }) +} + val generatePom = tasks.named("generatePomFileForMavenPublication") tasks.shadowJar { @@ -178,7 +200,7 @@ tasks.test { fun TaskContainer.registerRunTask( name: String, block: JavaExec.() -> Unit ): TaskProvider = register(name) { - group = "paper" + group = "paperweight" standardInput = System.`in` workingDir = rootProject.layout.projectDirectory.dir( providers.gradleProperty("runWorkDir").forUseAtConfigurationTime().orElse("run") diff --git a/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..9efbdba758aebcad3454a9a52c8a7eae4b7fc7eb --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java @@ -0,0 +1,283 @@ +package ca.spottedleaf.starlight.light; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final Level world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return chunk.getBlockEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + chunk.setBlockEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return chunk.getBlockNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically + // because a block was removed - which can decrease light. with sky data, block breaking can only result + // in increases, and thus the existing sky block check will actually correctly propagate light through + // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove + // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running + // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence + // of vanilla data management we "hide" them. + nibble.setHidden(); + } + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); + } + } else { + nibble.setNonNull(); + } + } + + @Override + protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change emitted light + // blocks can change direction of propagation + + final int encodeOffset = this.coordinateOffset; + final int emittedMask = this.emittedLightMask; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = blockState.getLightEmission() & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase + if (emittedLevel != 0) { + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + } + // this also accounts for a change in emitted light that would cause a decrease + // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) + // as it checks all neighbours (even if current level is 0) + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + // always keep sided transparent false here, new block might be conditionally transparent which would + // prevent us from decreasing sources in the directions where the new block is opaque + // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always + // catch that and fix it. + ); + // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int level = centerState.getLightEmission() & 0xF; + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState conditionallyOpaqueState; + int opacity = centerState.getOpacityIfCached(); + + if (opacity == -1) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else if (opacity >= 15) { + return level; + } else { + conditionallyOpaqueState = null; + } + opacity = Math.max(1, opacity); + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected Iterator getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { + if (chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) { + // implementation on Chunk is pretty awful, so write our own here. The big optimisation is + // skipping empty sections, and the far more optimised reading of types. + List sources = new ArrayList<>(); + + int offX = chunk.getPos().x << 4; + int offZ = chunk.getPos().z << 4; + + final LevelChunkSection[] sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final LevelChunkSection section = sections[sectionY - this.minSection]; + if (section == null || section.isEmpty()) { + // no sources in empty sections + continue; + } + final PalettedContainer states = section.states; + final int offY = sectionY << 4; + + for (int index = 0; index < (16 * 16 * 16); ++index) { + final BlockState state = states.get(index); + if (state.getLightEmission() <= 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) + sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); + } + } + + return sources.iterator(); + } else { + // world gen and lighting run in parallel, and if lighting keeps up it can be lighting chunks that are + // being generated. In the nether, lava will add a lot of sources. This resulted in quite a few CME crashes. + // So all we do spinloop until we can collect a list of sources, and even if it is out of date we will pick up + // the missing sources from checkBlock. + for (;;) { + try { + return chunk.getLights().collect(Collectors.toList()).iterator(); + } catch (final Exception cme) { + continue; + } + } + } + } + + @Override + public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources + final int emittedMask = this.emittedLightMask; + for (final Iterator positions = this.getSources(lightAccess, chunk); positions.hasNext();) { + final BlockPos pos = positions.next(); + final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + final int emittedLight = blockState.getLightEmission() & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); + } + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + // verify neighbour edges + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + } else { + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); + + this.performLightIncrease(lightAccess); + } + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java new file mode 100644 index 0000000000000000000000000000000000000000..174dc7ffa66258da0b867fba5c54880e81daa6ce --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java @@ -0,0 +1,439 @@ +package ca.spottedleaf.starlight.light; + +import net.minecraft.world.level.chunk.DataLayer; + +import java.util.ArrayDeque; +import java.util.Arrays; + +// SWMR -> Single Writer Multi Reader Nibble Array +public final class SWMRNibbleArray { + + /* + * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null + * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised + * nibbles can be written to. + * + * Uninitialised nibble - They are all 0, but the backing array isn't initialised. + * + * Initialised nibble - Has light data. + */ + + protected static final int INIT_STATE_NULL = 0; // null + protected static final int INIT_STATE_UNINIT = 1; // uninitialised + protected static final int INIT_STATE_INIT = 2; // initialised + protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL + + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + // this allows us to maintain only 1 byte array when we're not updating + static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); + + private static byte[] allocateBytes() { + final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); + if (inPool != null) { + return inPool; + } + + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { + WORKING_BYTES_POOL.get().addFirst(bytes); + } + + public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { + if (nibble == null) { + return new SWMRNibbleArray(null, true); + } else if (nibble.isEmpty()) { + return new SWMRNibbleArray(); + } else { + return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later + } + } + + protected int stateUpdating; + protected volatile int stateVisible; + + protected byte[] storageUpdating; + protected boolean updatingDirty; // only returns whether storageUpdating is dirty + protected byte[] storageVisible; + + public SWMRNibbleArray() { + this(null, false); // lazy init + } + + public SWMRNibbleArray(final byte[] bytes) { + this(bytes, false); + } + + public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; + this.storageUpdating = this.storageVisible = bytes; + } + + public SWMRNibbleArray(final byte[] bytes, final int state) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { + throw new IllegalArgumentException("Data cannot be null and have state be initialised"); + } + this.stateUpdating = this.stateVisible = state; + this.storageUpdating = this.storageVisible = bytes; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("State: "); + switch (this.stateVisible) { + case INIT_STATE_NULL: + stringBuilder.append("null"); + break; + case INIT_STATE_UNINIT: + stringBuilder.append("uninitialised"); + break; + case INIT_STATE_INIT: + stringBuilder.append("initialised"); + break; + case INIT_STATE_HIDDEN: + stringBuilder.append("hidden"); + break; + default: + stringBuilder.append("unknown"); + break; + } + stringBuilder.append("\nData:\n"); + + final byte[] data = this.storageVisible; + if (data != null) { + for (int i = 0; i < 4096; ++i) { + // Copied from NibbleArray#toString + final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); + + stringBuilder.append(Integer.toHexString(level)); + if ((i & 15) == 15) { + stringBuilder.append("\n"); + } + + if ((i & 255) == 255) { + stringBuilder.append("\n"); + } + } + } else { + stringBuilder.append("null"); + } + + return stringBuilder.toString(); + } + + public SaveState getSaveState() { + synchronized (this) { + final int state = this.stateVisible; + final byte[] data = this.storageVisible; + if (state == INIT_STATE_NULL) { + return null; + } + if (state == INIT_STATE_UNINIT) { + return new SaveState(null, state); + } + final boolean zero = isAllZero(data); + if (zero) { + return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; + } else { + return new SaveState(data.clone(), state); + } + } + } + + protected static boolean isAllZero(final byte[] data) { + for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { + byte whole = data[i << 4]; + + for (int k = 1; k < (1 << 4); ++k) { + whole |= data[(i << 4) | k]; + } + + if (whole != 0) { + return false; + } + } + + return true; + } + + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { + throw new IllegalArgumentException(); + } + + if (other.storageUpdating == null) { + this.setUninitialised(); + return; + } + + final byte[] src = other.storageUpdating; + final byte[] into; + + if (this.storageUpdating != null) { + into = this.storageUpdating; + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + return; + } + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setHidden() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + return; + } + if (this.stateUpdating != INIT_STATE_INIT) { + this.setNull(); + } else { + this.stateUpdating = INIT_STATE_HIDDEN; + } + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + public boolean isHiddenUpdating() { + return this.stateUpdating == INIT_STATE_HIDDEN; + } + + // operation type: updating + public boolean isHiddenVisible() { + return this.stateVisible == INIT_STATE_HIDDEN; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + if (this.storageUpdating != this.storageVisible) { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + } + + if (this.storageUpdating != this.storageVisible) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public DataLayer toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_HIDDEN: + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new DataLayer(); + case INIT_STATE_INIT: + return new DataLayer(this.storageVisible.clone()); + default: + throw new IllegalStateException(); + } + } + } + + /* x | (z << 4) | (y << 8) */ + + // operation type: updating + public int getUpdating(final int x, final int y, final int z) { + return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: updating + public int getUpdating(final int index) { + // indices range from 0 -> 4096 + final byte[] bytes = this.storageUpdating; + if (bytes == null) { + return 0; + } + final byte value = bytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: visible + public int getVisible(final int x, final int y, final int z) { + return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: visible + public int getVisible(final int index) { + synchronized (this) { + // indices range from 0 -> 4096 + final byte[] visibleBytes = this.storageVisible; + if (visibleBytes == null) { + return 0; + } + final byte value = visibleBytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + } + + // operation type: updating + public void set(final int x, final int y, final int z, final int value) { + this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); + } + + // operation type: updating + public void set(final int index, final int value) { + if (!this.updatingDirty) { + this.swapUpdatingAndMarkDirty(); + } + final int shift = (index & 1) << 2; + final int i = index >>> 1; + + this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); + } + + public static final class SaveState { + + public final byte[] data; + public final int state; + + public SaveState(final byte[] data, final int state) { + this.data = data; + this.state = state; + } + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..143810dc53782a9a6d870089d7bd5c3006a565b3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java @@ -0,0 +1,715 @@ +package ca.spottedleaf.starlight.light; + +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.Set; + +public final class SkyStarLightEngine extends StarLightEngine { + + /* + Specification for managing the initialisation and de-initialisation of skylight nibble arrays: + + Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. + + This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. + However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees + that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise + our own) - we need a radius of 2 to de-initialise neighbour nibbles. + How do we solve this? + + Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. + If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the + chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last + known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data + to see if any of its nibbles need to be de-initialised. + + The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, + and if it doesn't have data then we know it will correctly de-initialise once it fills up. + + Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking + around those. + */ + + protected final int[] heightMapBlockChange = new int[16 * 16]; + { + Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap + } + + protected final boolean[] nullPropagationCheckCache; + + public SkyStarLightEngine(final Level world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); + } + } + this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + nibble.setNull(); + } + } + + protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { + if (!currNibble.isNullNibbleUpdating()) { + // already initialised + return; + } + + final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); + + // are we above this chunk's lowest empty section? + int lowestY = this.minLightSection - 1; + for (int currY = this.maxSection; currY >= this.minSection; --currY) { + if (emptinessMap == null) { + // cannot delay nibble init for lit chunks, as we need to init to propagate into them. + final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); + if (current == null || current == EMPTY_CHUNK_SECTION) { + continue; + } + } else { + if (emptinessMap[currY - this.minSection]) { + continue; + } + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (chunkY > lowestY) { + // we need to set this one to full + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + nibble.setNonNull(); + nibble.setFull(); + return; + } + + if (extrude) { + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + currNibble.setNonNull(); + currNibble.extrudeLower(nibble); + break; + } + } + } else { + currNibble.setNonNull(); + } + } + + protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble != null && nibble.isNullNibbleUpdating()) { + // stop propagation in these areas + this.nibbleCache[index] = null; + nibble.updateVisible(); + } + } + } + + // rets whether neighbours were init'd + + protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, + final boolean extrudeInitialised) { + // null chunk sections may have nibble neighbours in the horizontal 1 radius that are + // non-null. Propagation to these neighbours is necessary. + // What makes this easy is we know none of these neighbours are non-empty (otherwise + // this nibble would be initialised). So, we don't have to initialise + // the neighbours in the full 1 radius, because there's no worry that any "paths" + // to the neighbours on this horizontal plane are blocked. + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { + return false; + } + this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; + + // check horizontal neighbours + boolean needInitNeighbours = false; + neighbour_search: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + needInitNeighbours = true; + break neighbour_search; + } + } + } + + if (needInitNeighbours) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); + } + } + } + + return needInitNeighbours; + } + + protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { + final int chunkX = worldX >> 4; + int chunkY = worldY >> 4; + final int chunkZ = worldZ >> 4; + + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + return nibble.getUpdating(worldX, worldY, worldZ); + } + + for (;;) { + if (++chunkY > this.maxLightSection) { + return 15; + } + + nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + + if (nibble != null) { + return nibble.getUpdating(worldX, 0, worldZ); + } + } + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return chunk.getSkyEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + chunk.setSkyEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return chunk.getSkyNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = (int)iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + @Override + protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change direction of propagation + + // same logic applies from BlockStarLightEngine#checkBlock + + final int encodeOffset = this.coordinateOffset; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + + if (currentLevel == 15) { + // must re-propagate clobbered source + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent + ); + } else { + this.setLightLevel(worldX, worldY, worldZ, 0); + } + + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + ); + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int opacity = centerState.getOpacityIfCached(); + + BlockState conditionallyOpaqueState; + if (opacity < 0) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else { + conditionallyOpaqueState = null; + opacity = Math.max(1, opacity); + } + + int level = 0; + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final int chunkX = atChunk.getPos().x; + final int chunkZ = atChunk.getPos().z; + final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); + + // setup heightmap for changes + for (final BlockPos pos : positions) { + final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; + final int curr = this.heightMapBlockChange[index]; + if (pos.getY() > curr) { + this.heightMapBlockChange[index] = pos.getY(); + } + } + + // note: light sets are delayed while processing skylight source changes due to how + // nibbles are initialised, as we want to avoid clobbering nibble values so what when + // below nibbles are initialised they aren't reading from partially modified nibbles + + // now we can recalculate the sources for the changed columns + for (int index = 0; index < (16 * 16); ++index) { + final int maxY = this.heightMapBlockChange[index]; + if (maxY == Integer.MIN_VALUE) { + // not changed + continue; + } + this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller + + final int columnX = (index & 15) | (chunkX << 4); + final int columnZ = (index >>> 4) | (chunkZ << 4); + + // try and propagate from the above y + // delay light set until after processing all sources to setup + final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); + + // maxPropagationY is now the highest block that could not be propagated to + + // remove all sources below that are 15 + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; + final int encodeOffset = this.coordinateOffset; + + if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); + + for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { + if ((currY & 15) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); + } + + // ensure section below is always checked + final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); + if (nibble == null) { + // advance currY to the the top of the section below + currY = (currY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + continue; + } + + if (nibble.getUpdating(columnX, currY, columnZ) != 15) { + break; + } + + // delay light set until after processing all sources to setup + this.appendToDecreaseQueue( + ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + // do not set transparent blocks for the same reason we don't in the checkBlock method + ); + } + } + } + + // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads + // immediate light value + this.processDelayedIncreases(); + this.processDelayedDecreases(); + + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected final int[] heightMapGen = new int[32 * 32]; + + @Override + protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + final LevelChunkSection[] sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isEmpty()) { + this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); + // try propagate FULL to neighbours + + // check neighbours to see if we need to propagate into them + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourX = chunkX + direction.x; + final int neighbourZ = chunkZ + direction.z; + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); + if (neighbourNibble == null) { + // unloaded neighbour + // most of the time we fall here + continue; + } + + // it looks like we need to propagate into the neighbour + + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (direction.x != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (direction.z < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction + + for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) + ); + } + } + } + + if (highestNonEmptySection-- == (this.minSection - 1)) { + break; + } + } + + if (highestNonEmptySection >= this.minSection) { + // fill out our other sources + final int minX = chunkPos.x << 4; + final int maxX = chunkPos.x << 4 | 15; + final int minZ = chunkPos.z << 4; + final int maxZ = chunkPos.z << 4 | 15; + final int startY = highestNonEmptySection << 4 | 15; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); + } + } + } // else: apparently the chunk is empty + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(lightAccess); + } + } + + protected final void processDelayedIncreases() { + // copied from performLightIncrease + final long[] queue = this.increaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + } + + protected final void processDelayedDecreases() { + // copied from performLightDecrease + final long[] queue = this.decreaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + + this.setLightLevel(posX, posY, posZ, 0); + } + } + + // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays + // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { + final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + + if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { + return startY; + } + + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + + BlockState above = this.getBlockState(worldX, startY + 1, worldZ); + if (above == null) { + above = AIR_BLOCK_STATE; + } + + for (;startY >= (this.minLightSection << 4); --startY) { + if ((startY & 15) == 15) { + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + } + BlockState current = this.getBlockState(worldX, startY, worldZ); + if (current == null) { + current = AIR_BLOCK_STATE; + } + + final VoxelShape fromShape; + if (above.isConditionallyFullOpaque()) { + this.mutablePos2.set(worldX, startY + 1, worldZ); + fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); + if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + // above wont let us propagate + break; + } + } else { + fromShape = Shapes.empty(); + } + + final int opacityIfCached = current.getOpacityIfCached(); + // does light propagate from the top down? + if (opacityIfCached != -1) { + if (opacityIfCached != 0) { + // we cannot propagate 15 through this + break; + } + // most of the time it falls here. + // add to propagate + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + ); + } else { + mutablePos.set(worldX, startY, worldZ); + long flags = 0L; + if (current.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + // can't propagate here, we're done on this column. + break; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = current.getLightBlock(world, mutablePos); + if (opacity > 0) { + // let the queued value (if any) handle it from here. + break; + } + + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + | flags + ); + } + + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { + // we skip empty sections here, as this is just an easy way of making sure the above block + // can propagate through air. + + // nothing can propagate in null sections, remove the queue entry for it + --this.increaseQueueInitialLength; + + // advance currY to the the top of the section below + startY = (startY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + + // make sure this is marked as AIR + above = AIR_BLOCK_STATE; + } else if (!delayLightSet) { + this.setLightLevel(worldX, startY, worldZ, 15); + } + } + + return startY; + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..ef930395407533a2c669499c02989bbbe0ac6101 --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java @@ -0,0 +1,1573 @@ +package ca.spottedleaf.starlight.light; + +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.IntegerUtil; +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.*; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + + protected static final LevelChunkSection EMPTY_CHUNK_SECTION = new LevelChunkSection(0); + + protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); + protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; + protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { + AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, + AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z + }; + + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. + POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), + POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), + POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; + POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; + POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; + } + + protected AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + public final Direction nms; + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + + AxisDirection(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + this.nms = Direction.fromNormal(x, y, z); + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); + } + + public AxisDirection getOpposite() { + return this.opposite; + } + } + + // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 + // for explaining how light propagates via breadth-first search + + // While the above is a good start to understanding the general idea of what the general principles are, it's not + // exactly how the vanilla light engine should behave for minecraft. + + // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + // null index indicates the chunk section doesn't exist (empty or out of bounds) + protected final LevelChunkSection[] sectionCache; + + // the exact same as above, except for storing fast access to SWMRNibbleArray + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final SWMRNibbleArray[] nibbleCache; + + // the exact same as above, except for storing fast access to nibbles to call change callbacks for + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final boolean[] notifyUpdateCache; + + // always initialsed during start of lighting. + // index = x + (z * 5) + protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; + + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + + protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); + + protected int encodeOffsetX; + protected int encodeOffsetY; + protected int encodeOffsetZ; + + protected int coordinateOffset; + + protected int chunkOffsetX; + protected int chunkOffsetY; + protected int chunkOffsetZ; + + protected int chunkIndexOffset; + protected int chunkSectionIndexOffset; + + protected final boolean skylightPropagator; + protected final int emittedLightMask; + protected final boolean isClientSide; + + protected final Level world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final Level world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + this.isClientSide = world.isClientSide; + this.world = world; + this.minLightSection = WorldUtil.getMinLightSection(world); + this.maxLightSection = WorldUtil.getMaxLightSection(world); + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + + this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + } + + protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { + // 31 = center + encodeOffset + this.encodeOffsetX = 31 - centerX; + this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value + this.encodeOffsetZ = 31 - centerZ; + + // coordinateIndex = x | (z << 6) | (y << 12) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); + + // 2 = (centerX >> 4) + chunkOffset + this.chunkOffsetX = 2 - (centerX >> 4); + this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 + this.chunkOffsetZ = 2 - (centerZ >> 4); + + // chunk index = x + (5 * z) + this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); + + // chunk section index = x + (5 * z) + ((5*5) * y) + this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); + } + + protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkY = centerY >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); + + final int radius = tryToLoadChunksFor2Radius ? 2 : 1; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final int cx = centerChunkX + dx; + final int cz = centerChunkZ + dz; + final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; + final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); + + if (chunk == null) { + if (relaxed | isTwoRadius) { + continue; + } + throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); + } + + if (!this.canUseChunk(chunk)) { + continue; + } + + this.setChunkInCache(cx, cz, chunk); + this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); + if (!isTwoRadius) { + this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); + this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); + } + } + } + } + + protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { + return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { + return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setChunkSectionInCache(chunkX, cy, chunkZ, + sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? (sections[cy - this.minSection] == null || sections[cy - this.minSection].isEmpty() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION)); + } + } + + protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { + return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; + + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; + } + + return ret; + } + + protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { + this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; + } + + protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); + } + } + + protected final void updateVisible(final LightChunkGetter lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { + continue; + } + + final int chunkX = (index % 5) - this.chunkOffsetX; + final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; + final int chunkY = ((index / (5*5)) % (16 + 2 + 2)) - this.chunkOffsetY; + if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ)); + } + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + if (this.isClientSide) { + Arrays.fill(this.notifyUpdateCache, false); + } + } + + protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { + final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); + } + + return null; + } + + protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { + final LevelChunkSection section = this.sectionCache[sectionIndex]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.states.get(localIndex); + } + + return null; + } + + protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { + final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); + } + + protected final int getLightLevel(final int sectionIndex, final int localIndex) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + return nibble == null ? 0 : nibble.getUpdating(localIndex); + } + + protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { + final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + + protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set(localIndex, level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { + return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { + this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; + } + + protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) { + throw new UnsupportedOperationException(); // :( + } + + // warn: localIndex = y | (x << 4) | (z << 8) + protected final long getKnownTransparency(final int sectionIndex, final int localIndex) { + throw new UnsupportedOperationException(); // :( + } + + public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) { + return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); + } + + private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; + + for (int i = 0, len = ret.length; i < len; ++i) { + ret[i] = new SWMRNibbleArray(null, true); + } + + return ret; + } + + protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk); + + protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); + + protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final ChunkAccess chunk); + + public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Set positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + if (changedSections != null) { + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + } + if (!positions.isEmpty()) { + this.propagateBlockChanges(lightAccess, chunk, positions); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); + + protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); + + // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) + // if ret == expect, then expect is the correct light value for pos + // if ret < expect, then ret is the real light value + protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect); + + protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; + protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; + + protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (currNibble == null) { + return; + } + + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + chunkY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null) { + continue; + } + + if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { + // both are zero, nothing to check. + continue; + } + + // this chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + int centerDelayedChecks = 0; + int neighbourDelayedChecks = 0; + for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int neighbourX = currX + neighbourOffX; + final int neighbourZ = currZ + neighbourOffZ; + + final int currentIndex = (currX & 15) | + ((currZ & 15)) << 4 | + ((currY & 15) << 8); + final int currentLevel = currNibble.getUpdating(currentIndex); + + final int neighbourIndex = + (neighbourX & 15) | + ((neighbourZ & 15)) << 4 | + ((currY & 15) << 8); + final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); + + // the checks are delayed because the checkBlock method clobbers light values - which then + // affect later calculate light value operations. While they don't affect it in a behaviourly significant + // way, they do have a negative performance impact due to simply queueing more values + + if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { + this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; + } + + if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { + this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; + } + } + } + + final int currentChunkOffX = chunkX << 4; + final int currentChunkOffZ = chunkZ << 4; + final int neighbourChunkOffX = (chunkX + direction.x) << 4; + final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; + final int chunkOffY = chunkY << 4; + for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { + // try to queue neighbouring data together + // index = x | (z << 4) | (y << 8) + if (i < centerDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesCenter[i]; + this.checkBlock(lightAccess, currentChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + currentChunkOffZ | ((value >>> 4) & 0xF)); + } + if (i < neighbourDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; + this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + neighbourChunkOffZ | ((value >>> 4) & 0xF)); + } + } + } + } + + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // verifies that light levels on this chunks edges are consistent with this chunk's neighbours + // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). + // This does not resolve skylight source problems. + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. + protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); + if (currNibble == null) { + continue; + } + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + currSectionY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { + // can't pull from 0 + continue; + } + + // neighbour chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = (chunkX << 4) - 1; + } else { + startX = (chunkX << 4) + 16; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = (chunkZ << 4) - 1; + } else { + startZ = (chunkZ << 4) + 16; + } + startX = chunkX << 4; + } + + final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk + final int encodeOffset = this.coordinateOffset; + + for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int level = neighbourNibble.getUpdating( + (currX & 15) + | ((currZ & 15) << 4) + | ((currY & 15) << 8) + ); + + if (level <= 1) { + // nothing to propagate + continue; + } + + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((level & 0xFL) << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. + ); + } + } + } + } + } + + public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { + final LevelChunkSection[] sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.length]; + + for (int i = 0; i < sections.length; ++i) { + if (sections[i] == null || sections[i].isEmpty()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); + + protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // subclasses are guaranteed that this is always called before a changed block set + // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks + // rets non-null when the emptiness map changed and needs to be updated + protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final Level world = (Level)lightAccess.getLevel(); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); + boolean[] ret = null; + final boolean needsInit = unlit || chunkEmptinessMap == null; + if (needsInit) { + this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); + } + + // update emptiness map + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + if (valueBoxed == null) { + if (needsInit) { + throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?"); + } + continue; + } + chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); + } + + // now init neighbour nibbles + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + final int sectionY = sectionIndex + this.minSection; + if (valueBoxed == null) { + continue; + } + + final boolean empty = valueBoxed.booleanValue(); + + if (empty) { + continue; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // if we're not empty, we also need to initialise nibbles + // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up + final boolean extrude = (dx | dz) != 0 || !unlit; + for (int dy = 1; dy >= -1; --dy) { + this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + // check for de-init and lazy-init + // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running + // init checks. + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // does this neighbour have 1 radius loaded? + boolean neighboursLoaded = true; + neighbour_loaded_search: + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { + neighboursLoaded = false; + break neighbour_loaded_search; + } + } + } + + for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { + // check neighbours to see if we need to de-init this one + boolean allEmpty = true; + neighbour_search: + for (int dy2 = -1; dy2 <= 1; ++dy2) { + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); + if (emptinessMap != null) { + if (!emptinessMap[y - this.minSection]) { + allEmpty = false; + break neighbour_search; + } + } else { + final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); + if (section != null && section != EMPTY_CHUNK_SECTION) { + allEmpty = false; + break neighbour_search; + } + } + } + } + } + + if (allEmpty & neighboursLoaded) { + // can only de-init when neighbours are loaded + // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting + // to be correct + + // all were empty, so de-init + this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); + } else if (!allEmpty) { + // must init + final boolean extrude = (dx | dz) != 0 || !unlit; + this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + return ret; + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, sections); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current + // chunks light values with respect to neighbours + // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function + // does not need to detect empty chunks itself (and it should do no handling for them either!) + protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); + + public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + + try { + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.lightChunk(lightAccess, chunk, true); + this.setNibbles(chunk, nibbles); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void relightChunks(final LightChunkGetter lightAccess, final Set chunks, + final Consumer chunkLightCallback, final IntConsumer onComplete) { + // it's recommended for maximum performance that the set is ordered according to a BFS from the center of + // the region of chunks to relight + // it's required that tickets are added for each chunk to keep them loaded + final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); + + final int[] neighbourLightOrder = new int[] { + // d = 0 + 0, 0, + // d = 1 + -1, 0, + 0, -1, + 1, 0, + 0, 1, + // d = 2 + -1, 1, + 1, 1, + -1, -1, + 1, -1, + }; + + int lightCalls = 0; + + for (final ChunkPos chunkPos : chunks) { + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); + if (chunk == null || !this.canUseChunk(chunk)) { + throw new IllegalStateException(); + } + + for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { + final int dx = neighbourLightOrder[i]; + final int dz = neighbourLightOrder[i + 1]; + final int neighbourX = dx + chunkX; + final int neighbourZ = dz + chunkZ; + + final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); + try { + // insert all neighbouring chunks for this neighbour that we have data for + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int neighbourX2 = neighbourX + dx2; + final int neighbourZ2 = neighbourZ + dz2; + final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); + final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); + if (neighbour2 == null || !this.canUseChunk(neighbour2)) { + continue; + } + + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); + if (nibbles == null) { + // we haven't lit this chunk + continue; + } + + this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); + this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); + this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); + this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); + } + } + + final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + // now insert the neighbour chunk and light it + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); + nibblesByChunk.put(key, nibbles); + + this.setChunkInCache(neighbourX, neighbourZ, neighbour); + this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); + this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); + + final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); + emptinessMapByChunk.put(key, neighbourEmptiness); + if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { + this.setEmptinessMap(neighbour, neighbourEmptiness); + } + + this.lightChunk(lightAccess, neighbour, false); + } finally { + this.destroyCaches(); + } + } + + // done lighting all neighbours, so the chunk is now fully lit + + // make sure nibbles are fully updated before calling back + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); + } + + // now do callback + if (chunkLightCallback != null) { + chunkLightCallback.accept(chunkPos); + } + ++lightCalls; + } + + if (onComplete != null) { + onComplete.accept(lightCalls); + } + } + + // old algorithm for propagating + // this is also the basic algorithm, the optimised algorithm is always going to be tested against this one + // and this one is always tested against vanilla + // contains: + // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) + // next 4 bits: propagated light level (0, 15] + // next 6 bits: propagation direction bitset + // next 24 bits: unused + // last 4 bits: state flags + // state flags: + // whether the propagation must set the current position's light value (0 if decrease, propagated light level if increase) + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to consider if its block is conditionally transparent + protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; + + protected long[] increaseQueue = new long[16 * 16 * 16]; + protected int increaseQueueInitialLength; + protected long[] decreaseQueue = new long[16 * 16 * 16]; + protected int decreaseQueueInitialLength; + + protected final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + protected final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + protected final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; + protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; + static { + for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { + final List directions = new ArrayList<>(); + for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { + directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); + } + OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); + } + } + + protected final void performLightIncrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { + // not at the level we expect, so something changed. + continue; + } + } + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want or unloaded + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } + } + } + + protected final void performLightDecrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L); + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | flags; + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L); + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | flags; + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(lightAccess); + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..300364f693583be802a71d94cda5d96c77c7b67c --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java @@ -0,0 +1,635 @@ +package ca.spottedleaf.starlight.light; + +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public final class StarLightInterface { + + public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); + + /** + * Can be {@code null}, indicating the light is all empty. + */ + protected final Level world; + protected final LightChunkGetter lightAccess; + + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + + protected final LightQueue lightQueue = new LightQueue(this); + + protected final LayerLightEventListener skyReader; + protected final LayerLightEventListener blockReader; + protected final boolean isClientSide; + + protected final int minSection; + protected final int maxSection; + protected final int minLightSection; + protected final int maxLightSection; + + public final LevelLightEngine lightEngine; + + public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { + this.lightAccess = lightAccess; + this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); + this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; + this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; + this.isClientSide = !(this.world instanceof ServerLevel); + if (this.world == null) { + this.minSection = 0; + this.maxSection = 15; + this.minLightSection = -1; + this.maxLightSection = 16; + } else { + this.minSection = WorldUtil.getMinSection(this.world); + this.maxSection = WorldUtil.getMaxSection(this.world); + this.minLightSection = WorldUtil.getMinLightSection(this.world); + this.maxLightSection = WorldUtil.getMaxLightSection(this.world); + } + this.lightEngine = lightEngine; + this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) { + // skylight doesn't care + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runUpdates(final int i, final boolean bl, final boolean bl2) { + throw new UnsupportedOperationException(); + } + + @Override + public void enableLightSources(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return null; + } + + final int sectionY = pos.getY(); + + if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { + return null; + } + + if (chunk.getSkyEmptinessMap() == null) { + return null; + } + + return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + final int x = blockPos.getX(); + int y = blockPos.getY(); + final int z = blockPos.getZ(); + + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return 15; + } + + int sectionY = y >> 4; + + if (sectionY > StarLightInterface.this.maxLightSection) { + return 15; + } + + if (sectionY < StarLightInterface.this.minLightSection) { + sectionY = StarLightInterface.this.minLightSection; + y = sectionY << 4; + } + + final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); + final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection]; + + if (StarLightInterface.this.isClientSide) { + if (!immediate.isNullNibbleUpdating()) { + return immediate.getUpdating(x, y, z); + } + } else { + if (!immediate.isNullNibbleVisible()) { + return immediate.getVisible(x, y, z); + } + } + + final boolean[] emptinessMap = chunk.getSkyEmptinessMap(); + + if (emptinessMap == null) { + return 15; + } + + // are we above this chunk's lowest empty section? + int lowestY = StarLightInterface.this.minLightSection - 1; + for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) { + if (emptinessMap[currY - StarLightInterface.this.minSection]) { + continue; + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (sectionY > lowestY) { + return 15; + } + + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = sectionY + 1; currY <= StarLightInterface.this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + if (!nibble.isNullNibbleUpdating()) { + return nibble.getUpdating(x, 0, z); + } + } else { + if (!nibble.isNullNibbleVisible()) { + return nibble.getVisible(x, 0, z); + } + } + } + + // should never reach here + return 15; + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) { + this.checkBlock(blockPos); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runUpdates(final int i, final boolean bl, final boolean bl2) { + throw new UnsupportedOperationException(); + } + + @Override + public void enableLightSources(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + + if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { + return null; + } + + return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + final int cx = blockPos.getX() >> 4; + final int cy = blockPos.getY() >> 4; + final int cz = blockPos.getZ() >> 4; + + if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) { + return 0; + } + + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz); + + if (chunk == null) { + return 0; + } + + final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } else { + return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + } + + public LayerLightEventListener getSkyReader() { + return this.skyReader; + } + + public LayerLightEventListener getBlockReader() { + return this.blockReader; + } + + public boolean isClientSide() { + return this.isClientSide; + } + + public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { + if (this.world == null) { + // empty world + return null; + } + return ((ServerLevel)this.world).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); + } + + public boolean hasUpdates() { + return !this.lightQueue.isEmpty(); + } + + public Level getWorld() { + return this.world; + } + + public LightChunkGetter getLightAccess() { + return this.lightAccess; + } + + protected final SkyStarLightEngine getSkyLightEngine() { + if (this.cachedSkyPropagators == null) { + return null; + } + final SkyStarLightEngine ret; + synchronized (this.cachedSkyPropagators) { + ret = this.cachedSkyPropagators.pollFirst(); + } + + if (ret == null) { + return new SkyStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { + if (this.cachedSkyPropagators == null) { + return; + } + synchronized (this.cachedSkyPropagators) { + this.cachedSkyPropagators.addFirst(engine); + } + } + + protected final BlockStarLightEngine getBlockLightEngine() { + if (this.cachedBlockPropagators == null) { + return null; + } + final BlockStarLightEngine ret; + synchronized (this.cachedBlockPropagators) { + ret = this.cachedBlockPropagators.pollFirst(); + } + + if (ret == null) { + return new BlockStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { + if (this.cachedBlockPropagators == null) { + return; + } + synchronized (this.cachedBlockPropagators) { + this.cachedBlockPropagators.addFirst(engine); + } + } + + public CompletableFuture blockChange(final BlockPos pos) { + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return null; + } + + return this.lightQueue.queueBlockChange(pos); + } + + public CompletableFuture sectionChange(final SectionPos pos, final boolean newEmptyValue) { + if (this.world == null) { // empty world + return null; + } + + return this.lightQueue.queueSectionChange(pos, newEmptyValue); + } + + public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + if (blockEngine != null) { + blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.light(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void relightChunks(final Set chunks, final Consumer chunkLightCallback, + final IntConsumer onComplete) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, + blockEngine == null ? onComplete : null); + } + if (blockEngine != null) { + blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkChunkEdges(final int chunkX, final int chunkZ) { + this.checkSkyEdges(chunkX, chunkZ); + this.checkBlockEdges(chunkX, chunkZ); + } + + public void checkSkyEdges(final int chunkX, final int chunkZ) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { + this.lightQueue.queueChunkLighting(pos, run); + } + + public void removeChunkTasks(final ChunkPos pos) { + this.lightQueue.removeChunk(pos); + } + + public void propagateChanges() { + if (this.lightQueue.isEmpty()) { + return; + } + + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + LightQueue.ChunkTasks task; + while ((task = this.lightQueue.removeFirstTask()) != null) { + if (task.lightTasks != null) { + for (final Runnable run : task.lightTasks) { + run.run(); + } + } + + final long coordinate = task.chunkCoordinate; + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + final Set positions = task.changedPositions; + final Boolean[] sectionChanges = task.changedSectionSet; + + if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + + if (skyEngine != null && task.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); + } + if (blockEngine != null && task.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); + } + + task.onComplete.complete(null); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + protected static final class LightQueue { + + protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); + protected final StarLightInterface manager; + + public LightQueue(final StarLightInterface manager) { + this.manager = manager; + } + + public synchronized boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + public synchronized CompletableFuture queueBlockChange(final BlockPos pos) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + tasks.changedPositions.add(pos.immutable()); + return tasks.onComplete; + } + + public synchronized CompletableFuture queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + if (tasks.changedSectionSet == null) { + tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; + } + tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + if (tasks.lightTasks == null) { + tasks.lightTasks = new ArrayList<>(); + } + tasks.lightTasks.add(lightTask); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks.onComplete; + } + + public void removeChunk(final ChunkPos pos) { + final ChunkTasks tasks; + synchronized (this) { + tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); + } + if (tasks != null) { + tasks.onComplete.complete(null); + } + } + + public synchronized ChunkTasks removeFirstTask() { + if (this.chunkTasks.isEmpty()) { + return null; + } + return this.chunkTasks.removeFirst(); + } + + protected static final class ChunkTasks { + + public final Set changedPositions = new HashSet<>(); + public Boolean[] changedSectionSet; + public ShortOpenHashSet queuedEdgeChecksSky; + public ShortOpenHashSet queuedEdgeChecksBlock; + public List lightTasks; + + public final CompletableFuture onComplete = new CompletableFuture<>(); + + public final long chunkCoordinate; + + public ChunkTasks(final long chunkCoordinate) { + this.chunkCoordinate = chunkCoordinate; + } + } + } +} diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java index b9cdbf8acccfd6b207a0116f068168f3b8c8e17d..7404989c37ee1b7aa4e6999a063180d099532f7e 100644 --- a/src/main/java/co/aikar/timings/MinecraftTimings.java +++ b/src/main/java/co/aikar/timings/MinecraftTimings.java @@ -45,6 +45,8 @@ public final class MinecraftTimings { public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update"); public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); + public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager + public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap(); diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..9d920565ff65a84b1b9a2a4777fd8bc8f07e0153 100644 --- a/src/main/java/co/aikar/timings/TimingsExport.java +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -153,7 +153,7 @@ public class TimingsExport extends Thread { return pair(rule, world.getWorld().getGameRuleValue(rule)); })), pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()), - pair("notick-viewdistance", world.getChunkSource().chunkMap.getEffectiveNoTickViewDistance()) + pair("notick-viewdistance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()) // Tuinity - replace old player chunk management )); })); @@ -228,7 +228,8 @@ public class TimingsExport extends Thread { parent.put("config", createObject( pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), - pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)) + pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Tuinity - add config to timings report + pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)) // Tuinity - add config to timings report )); new TimingsExport(listeners, parent, history).start(); diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..3918b24c98faa5232c7ffd733ba8000562132785 100644 --- a/src/main/java/com/destroystokyo/paper/Metrics.java +++ b/src/main/java/com/destroystokyo/paper/Metrics.java @@ -593,7 +593,7 @@ public class Metrics { boolean logFailedRequests = config.getBoolean("logFailedRequests", false); // Only start Metrics, if it's enabled in the config if (config.getBoolean("enabled", true)) { - Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); + Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); @@ -603,7 +603,7 @@ public class Metrics { metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); - metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); + metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java index ba8395435482fc4d6e02fc86794cdb0d35d4399c..af66c6d863a57b2c34006b06852e9811a74d7dfa 100644 --- a/src/main/java/com/destroystokyo/paper/PaperCommand.java +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -272,7 +272,7 @@ public class PaperCommand extends Command { int ticking = 0; int entityTicking = 0; - for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) { + for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunks.getUpdatingMap().values()) { // Tuinity - change updating chunks map if (chunk.getFullChunkUnchecked() == null) { continue; } @@ -496,6 +496,46 @@ public class PaperCommand extends Command { } } + // Tuinity start - rewrite light engine + private void starlightFixLight(ServerPlayer sender, ServerLevel world, ThreadedLevelLightEngine lightengine, int radius) { + long start = System.nanoTime(); + java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + + int[] pending = new int[1]; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkPos chunkPos = iterator.next(); + + final net.minecraft.world.level.chunk.ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + ++pending[0]; + } + + int[] relitChunks = new int[1]; + lightengine.relight(chunks, + (ChunkPos chunkPos) -> { + ++relitChunks[0]; + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE + + ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%" + ); + }, + (int totalRelit) -> { + final long end = System.nanoTime(); + final long diff = Math.round(1.0e-6*(end - start)); + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " + + ChatColor.DARK_AQUA + diff + "ms" + ); + }); + sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks"); + } + // Tuinity end - rewrite light engine + private void doFixLight(CommandSender sender, String[] args) { if (!(sender instanceof Player)) { sender.sendMessage("Only players can use this command"); @@ -504,7 +544,7 @@ public class PaperCommand extends Command { int radius = 2; if (args.length > 1) { try { - radius = Math.min(5, Integer.parseInt(args[1])); + radius = Math.min(32, Integer.parseInt(args[1])); // Tuinity - MOOOOOORE } catch (Exception e) { sender.sendMessage("Not a number"); return; @@ -517,6 +557,13 @@ public class PaperCommand extends Command { ServerLevel world = (ServerLevel) handle.level; ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); + // Tuinity start - rewrite light engine + if (true) { + this.starlightFixLight(handle, world, lightengine, radius); + return; + } + // Tuinity end - rewrite light engine + net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation()); Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); updateLight(sender, world, lightengine, queue); diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java index 580bae0d414d371a07a6bfeefc41fdd989dc0083..d50b61876f15d95b836b3dd81d9c3492c91a8448 100644 --- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java @@ -29,8 +29,8 @@ public class PaperVersionFetcher implements VersionFetcher { @Nonnull @Override public Component getVersionMessage(@Nonnull String serverVersion) { - String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]"); - final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]); + String[] parts = serverVersion.substring("git-Tuinity-".length()).split("[-\\s]"); // Tuinity + final Component updateMessage = getUpdateStatusMessage("Spottedleaf/Tuinity", GITHUB_BRANCH_NAME, parts[0]); // Tuinity final Component history = getHistory(); return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage; @@ -54,13 +54,10 @@ public class PaperVersionFetcher implements VersionFetcher { private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { int distance; - try { - int jenkinsBuild = Integer.parseInt(versionInfo); - distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion()); - } catch (NumberFormatException ignored) { + // Tuinity - we don't have jenkins setup versionInfo = versionInfo.replace("\"", ""); distance = fetchDistanceFromGitHub(repo, branch, versionInfo); - } + // Tuinity - we don't have jenkins setup switch (distance) { case -1: diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java index da13ff17609b7bc8076d9297edf8decf01a2ed88..b4c69d39eee19339b1de295151d7ed3bf61635c1 100644 --- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java +++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java @@ -312,6 +312,7 @@ public final class PaperTickList extends ServerTickList { // extend to avo toTick.tickState = STATE_SCHEDULED; this.addToNotTickingReady(toTick); } + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick } catch (final Throwable thr) { // start copy from TickListServer // TODO check on update CrashReport crashreport = CrashReport.forThrowable(thr, "Exception while ticking"); diff --git a/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..d9bfe05d4f86229ed743113bfb0bbd983adb7e68 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java @@ -0,0 +1,965 @@ +package com.tuinity.tuinity.chunk; + +import com.destroystokyo.paper.util.misc.PlayerAreaMap; +import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; +import com.tuinity.tuinity.config.TuinityConfig; +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.TickThread; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +import net.minecraft.server.MCUtil; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunk; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; + +public final class PlayerChunkLoader { + + public static final int MIN_VIEW_DISTANCE = 2; + public static final int MAX_VIEW_DISTANCE = 32; + + public static final int TICK_TICKET_LEVEL = 31; + public static final int LOADED_TICKET_LEVEL = 33; + + protected final ChunkMap chunkMap; + protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f); + protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f); + + protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { + if (p1 == p2) { + return 0; + } + + final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst(); + final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst(); + + final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority); + + if (priorityCompare != 0) { + return priorityCompare; + } + + final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); + + if (idCompare != 0) { + return idCompare; + } + + // last resort + return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); + }); + + protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { + if (p1 == p2) { + return 0; + } + + final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget); + if (timeCompare != 0) { + return timeCompare; + } + + final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); + + if (idCompare != 0) { + return idCompare; + } + + // last resort + return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); + }); + + + // no throttling is applied below this VD for loading + + /** + * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded. + */ + public final PlayerAreaMap broadcastMap; + + /** + * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded. + */ + public final PlayerAreaMap loadMap; + + /** + * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus, + * this map is always representing the chunks we are actually going to load. + */ + public final PlayerAreaMap loadTicketCleanup; + + /** + * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen. + */ + public final PlayerAreaMap tickMap; + + /** + * -1 if defaulting to [load distance], else always in [2, load distance] + */ + protected int rawSendDistance = -1; + + /** + * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1] + */ + protected int rawLoadDistance = -1; + + /** + * Never -1, always in [2, 32] + */ + protected int rawTickDistance = -1; + + // methods to bridge for API + + public int getTargetViewDistance() { + return this.getTickDistance(); + } + + public void setTargetViewDistance(final int distance) { + this.setTickDistance(distance); + } + + public int getTargetNoTickViewDistance() { + return this.getLoadDistance() - 1; + } + + public void setTargetNoTickViewDistance(final int distance) { + this.setLoadDistance(distance == -1 ? -1 : distance + 1); + } + + public int getTargetSendDistance() { + return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance; + } + + public void setTargetSendDistance(final int distance) { + this.setSendDistance(distance); + } + + // internal methods + + public int getSendDistance() { + final int loadDistance = this.getLoadDistance(); + return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance); + } + + public void setSendDistance(final int distance) { + if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.rawSendDistance = distance; + } + + public int getLoadDistance() { + final int tickDistance = this.getTickDistance(); + return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance); + } + + public void setLoadDistance(final int distance) { + if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.rawLoadDistance = distance; + } + + public int getTickDistance() { + return this.rawTickDistance; + } + + public void setTickDistance(final int distance) { + if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.rawTickDistance = distance; + } + + /* + Players have 3 different types of view distance: + 1. Sending view distance + 2. Loading view distance + 3. Ticking view distance + + But for configuration purposes (and API) there are: + 1. No-tick view distance + 2. Tick view distance + 3. Broadcast view distance + + These aren't always the same as the types we represent internally. + + Loading view distance is always max(no-tick + 1, tick + 1) + - no-tick has 1 added because clients need an extra radius to render chunks + - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking + + Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means + it loads chunks in radius load-view-distance + 1. + + The maximum value for send view distance is the load view distance. API can set it lower. + */ + + public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) { + this.chunkMap = chunkMap; + this.broadcastMap = new PlayerAreaMap(pooledHashSets, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + if (player.needsChunkCenterUpdate) { + player.needsChunkCenterUpdate = false; + player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ)); + } + PlayerChunkLoader.this.onChunkEnter(player, rangeX, rangeZ); + }, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ); + }); + this.loadMap = new PlayerAreaMap(pooledHashSets, + null, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + if (newState != null) { + return; + } + PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ)); + }); + this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets, + null, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + if (newState != null) { + return; + } + ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); + PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); + if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) { + --PlayerChunkLoader.this.concurrentChunkLoads; + } + }); + this.tickMap = new PlayerAreaMap(pooledHashSets, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + if (newState.size() != 1) { + return; + } + LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); + if (chunk == null || !chunk.areNeighboursLoaded(2)) { + return; + } + + ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); + PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); + }, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + if (newState != null) { + return; + } + ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); + PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); + }); + } + + protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet(); + protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet(); + + // rets whether the chunk is at a loaded stage that is ready to be sent to players + public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); + + if (chunk == null) { + return false; + } + + return chunk.getSendingChunk() != null && this.isTargetedForPlayerLoad.contains(key); + } + + public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerLoaderData data = this.playerMap.get(player); + if (data == null) { + return false; + } + + return data.hasSentChunk(chunkX, chunkZ); + } + + protected int getMaxConcurrentChunkSends() { + double config = TuinityConfig.playerMaxConcurrentChunkSends; + return Math.max(1, config <= 0 ? (int)Math.ceil(-config * this.chunkMap.level.players().size()) : (int)config); + } + + protected int getMaxChunkLoads() { + double config = TuinityConfig.playerMaxConcurrentChunkLoads; + return Math.max(1, (config <= 0 ? (int)Math.ceil(-config * MinecraftServer.getServer().getPlayerCount()) : (int)config) * 9); + } + + protected double getTargetSendRatePerPlayer() { + double config = TuinityConfig.playerTargetChunkSendRate; + return config <= 0 ? -config : config / MinecraftServer.getServer().getPlayerCount(); + } + + public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) { + final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); + } + + public void onChunkSendReady(final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); + + if (playersInSendRange == null) { + return; + } + + final Object[] rawData = playersInSendRange.getBackingSet(); + for (int i = 0, len = rawData.length; i < len; ++i) { + final Object raw = rawData[i]; + + if (!(raw instanceof ServerPlayer)) { + continue; + } + this.onChunkEnter((ServerPlayer)raw, chunkX, chunkZ); + } + + // now let's try and queue mid tick logic again + } + + public void onChunkEnter(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerLoaderData data = this.playerMap.get(player); + + if (data == null) { + return; + } + + if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) { + // if we don't have player tickets, then the load logic will pick this up and queue to send + return; + } + + final long playerPos = this.broadcastMap.getLastCoordinate(player); + final int playerChunkX = CoordinateUtils.getChunkX(playerPos); + final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos); + final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ); + + final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0); + data.sendQueue.add(holder); + } + + public void onChunkLoad(final int chunkX, final int chunkZ) { + if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + --this.concurrentChunkLoads; + } + } + + public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerLoaderData data = this.playerMap.get(player); + + if (data == null) { + return; + } + + data.unloadChunk(chunkX, chunkZ); + } + + public void addPlayer(final ServerPlayer player) { + TickThread.ensureTickThread("Cannot add player async"); + if (!player.isRealPlayer) { + return; + } + final PlayerLoaderData data = new PlayerLoaderData(player, this); + if (this.playerMap.putIfAbsent(player, data) == null) { + data.update(); + } + } + + public void removePlayer(final ServerPlayer player) { + TickThread.ensureTickThread("Cannot remove player async"); + if (!player.isRealPlayer) { + return; + } + + final PlayerLoaderData loaderData = this.playerMap.remove(player); + if (loaderData == null) { + return; + } + loaderData.remove(); + this.chunkLoadQueue.remove(loaderData); + this.chunkSendQueue.remove(loaderData); + this.chunkSendWaitQueue.remove(loaderData); + synchronized (this.sendingChunkCounts) { + final int count = this.sendingChunkCounts.removeInt(loaderData); + if (count != 0) { + concurrentChunkSends.getAndAdd(-count); + } + } + } + + public void updatePlayer(final ServerPlayer player) { + TickThread.ensureTickThread("Cannot update player async"); + if (!player.isRealPlayer) { + return; + } + final PlayerLoaderData loaderData = this.playerMap.get(player); + if (loaderData != null) { + loaderData.update(); + } + } + + public PlayerLoaderData getData(final ServerPlayer player) { + return this.playerMap.get(player); + } + + public void tick() { + TickThread.ensureTickThread("Cannot tick async"); + for (final PlayerLoaderData data : this.playerMap.values()) { + data.update(); + } + this.tickMidTick(); + } + + protected static final AtomicInteger concurrentChunkSends = new AtomicInteger(); + protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>(); + private void trySendChunks() { + final long time = System.nanoTime(); + // drain entries from wait queue + while (!this.chunkSendWaitQueue.isEmpty()) { + final PlayerLoaderData data = this.chunkSendWaitQueue.first(); + + if (data.nextChunkSendTarget > time) { + break; + } + + this.chunkSendWaitQueue.pollFirst(); + + this.chunkSendQueue.add(data); + } + + if (this.chunkSendQueue.isEmpty()) { + return; + } + + final int maxSends = this.getMaxConcurrentChunkSends(); + final double sendRate = this.getTargetSendRatePerPlayer(); + final long nextDeadline = (long)((1 / sendRate) * 1.0e9) + time; + for (;;) { + if (this.chunkSendQueue.isEmpty()) { + break; + } + final int currSends = concurrentChunkSends.get(); + if (currSends >= maxSends) { + break; + } + + if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) { + continue; + } + + // send chunk + + final PlayerLoaderData data = this.chunkSendQueue.removeFirst(); + + final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst(); + if (queuedSend == null) { + concurrentChunkSends.getAndDecrement(); // we never sent, so decrease + // stop iterating over players who have nothing to send + if (this.chunkSendQueue.isEmpty()) { + // nothing left + break; + } + continue; + } + + if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) { + throw new IllegalStateException(); + } + + data.nextChunkSendTarget = nextDeadline; + this.chunkSendWaitQueue.add(data); + + synchronized (this.sendingChunkCounts) { + this.sendingChunkCounts.addTo(data, 1); + } + + data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> { + synchronized (this.sendingChunkCounts) { + final int count = this.sendingChunkCounts.getInt(data); + if (count == 0) { + // disconnected, so we don't need to decrement: it will be decremented for us + return; + } + if (count == 1) { + this.sendingChunkCounts.removeInt(data); + } else { + this.sendingChunkCounts.put(data, count - 1); + } + } + + concurrentChunkSends.getAndDecrement(); + }); + } + } + + protected int concurrentChunkLoads; + private void tryLoadChunks() { + if (this.chunkLoadQueue.isEmpty()) { + return; + } + + final int maxLoads = this.getMaxChunkLoads(); + for (;;) { + final PlayerLoaderData data = this.chunkLoadQueue.pollFirst(); + + final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst(); + if (queuedLoad == null) { + if (this.chunkLoadQueue.isEmpty()) { + break; + } + continue; + } + + if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { + // already loaded! + data.loadQueue.pollFirst(); // already loaded so we just skip + this.chunkLoadQueue.add(data); + + // ensure the chunk is queued to send + this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); + continue; + } + + final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); + + final double priority = queuedLoad.priority; + // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present. + // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the + // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they + // aren't required, it bypasses the limiter system. + boolean unloadedTargetChunk = false; + unloaded_check: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final int offX = queuedLoad.chunkX + dx; + final int offZ = queuedLoad.chunkZ + dz; + if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) { + unloadedTargetChunk = true; + break unloaded_check; + } + } + } + if (unloadedTargetChunk && priority > 0.0) { + // priority > 0.0 implies rate limited chunks + + final int currentChunkLoads = this.concurrentChunkLoads; + if (currentChunkLoads >= maxLoads) { + // don't poll, we didn't load it + this.chunkLoadQueue.add(data); + break; + } + } + + // can only poll after we decide to load + data.loadQueue.pollFirst(); + + // now that we've polled we can re-add to load queue + this.chunkLoadQueue.add(data); + + // add necessary tickets to load chunk up to send-ready + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final int offX = queuedLoad.chunkX + dx; + final int offZ = queuedLoad.chunkZ + dz; + final ChunkPos chunkPos = new ChunkPos(offX, offZ); + + this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); + if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) { + continue; + } + + if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) { + // wont reach here if unloadedTargetChunk is false + ++this.concurrentChunkLoads; + } + } + } + + // mark that we've added tickets here + this.isTargetedForPlayerLoad.add(chunkKey); + + // it's possible all we needed was the player tickets to queue up the send. + if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { + // yup, all we needed. + this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); + } + } + } + + public void tickMidTick() { + // try to send more chunks + this.trySendChunks(); + + // try to queue more chunks to load + this.tryLoadChunks(); + } + + static final class ChunkPriorityHolder { + public final int chunkX; + public final int chunkZ; + public final int manhattanDistanceToPlayer; + public final double priority; + + public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.manhattanDistanceToPlayer = manhattanDistanceToPlayer; + this.priority = priority; + } + } + + public static final class PlayerLoaderData { + + protected static final float FOV = 110.0f; + protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0; + + // Player max sprint speed is approximately 8m/s + protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0); + protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f; + + protected double lastLocX = Double.NEGATIVE_INFINITY; + protected double lastLocZ = Double.NEGATIVE_INFINITY; + + protected int lastChunkX; + protected int lastChunkZ; + + // this is corrected so that 0 is along the positive x-axis + protected float lastYaw = Float.NEGATIVE_INFINITY; + + protected int lastSendDistance = Integer.MIN_VALUE; + protected int lastLoadDistance = Integer.MIN_VALUE; + protected int lastTickDistance = Integer.MIN_VALUE; + protected boolean usingLookingPriority; + + protected final ServerPlayer player; + protected final PlayerChunkLoader loader; + + // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field + // in a comparator! + protected final ArrayDeque loadQueue = new ArrayDeque<>(); + protected final LongOpenHashSet sentChunks = new LongOpenHashSet(); + + protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { + final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer); + if (distanceCompare != 0) { + return distanceCompare; + } + + final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX); + if (coordinateXCompare != 0) { + return coordinateXCompare; + } + + return Integer.compare(p1.chunkZ, p2.chunkZ); + }); + + protected int sendViewDistance = -1; + protected int loadViewDistance = -1; + protected int tickViewDistance = -1; + + protected long nextChunkSendTarget; + + public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) { + this.player = player; + this.loader = loader; + } + + // these view distance methods are for api + public int getTargetSendViewDistance() { + final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; + final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); + final int clientViewDistance = this.getClientViewDistance(); + final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!TuinityConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); + return sendViewDistance; + } + + public void setTargetSendViewDistance(final int distance) { + if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.sendViewDistance = distance; + } + + public int getTargetNoTickViewDistance() { + return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1; + } + + public void setTargetNoTickViewDistance(final int distance) { + if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.loadViewDistance = distance == -1 ? -1 : distance + 1; + } + + public int getTargetTickViewDistance() { + return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; + } + + public void setTargetTickViewDistance(final int distance) { + if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + this.tickViewDistance = distance; + } + + protected int getLoadDistance() { + final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; + + return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); + } + + public boolean hasSentChunk(final int chunkX, final int chunkZ) { + return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) { + if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, + new ChunkPos(chunkX, chunkZ), new Packet[2], false, true); // unloaded, loaded + this.player.connection.connection.execute(onChunkSend); + } else { + throw new IllegalStateException(); + } + } + + public void unloadChunk(final int chunkX, final int chunkZ) { + if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, + new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded + } + } + + protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point + final double p2x, final double p2z, // triangle point + final double p3x, final double p3z, // triangle point + + final double targetX, final double targetZ) { // point + // from barycentric coordinates: + // targetX = a*p1x + b*p2x + c*p3x + // targetZ = a*p1z + b*p2z + c*p3z + // 1.0 = a*1.0 + b*1.0 + c*1.0 + // where a, b, c >= 0.0 + // so, if any of a, b, c are less-than zero then there is no intersection. + + // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z)) + // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d + // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d + // c = 1.0 - a - b + + final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z); + final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d; + + if (a < 0.0 || a > 1.0) { + return false; + } + + final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d; + if (b < 0.0 || b > 1.0) { + return false; + } + + final double c = 1.0 - a - b; + + return c >= 0.0 && c <= 1.0; + } + + public void remove() { + this.loader.broadcastMap.remove(this.player); + this.loader.loadMap.remove(this.player); + this.loader.loadTicketCleanup.remove(this.player); + this.loader.tickMap.remove(this.player); + } + + protected int getClientViewDistance() { + return this.player.clientViewDistance == null ? -1 : this.player.clientViewDistance.intValue(); + } + + public void update() { + final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; + // load view cannot be less-than tick view + 1 + final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = this.getClientViewDistance(); + final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!TuinityConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); + + final double posX = this.player.getX(); + final double posZ = this.player.getZ(); + final float yaw = MCUtil.normalizeYaw(this.player.yRot + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis + + // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them. + final boolean useLookPriority = TuinityConfig.playerFrustumPrioritisation && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD || + this.player.getAbilities().flying); + + // make sure we're in the send queue + this.loader.chunkSendWaitQueue.add(this); + + if ( + // has view distance stayed the same? + sendViewDistance == this.lastSendDistance + && loadViewDistance == this.lastLoadDistance + && tickViewDistance == this.lastTickDistance + + && (this.usingLookingPriority ? ( + // has our block stayed the same (this also accounts for chunk change)? + Mth.floor(this.lastLocX) == Mth.floor(posX) + && Mth.floor(this.lastLocZ) == Mth.floor(posZ) + ) : ( + // has our chunk stayed the same + (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4) + && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4) + )) + + // has our decision about look priority changed? + && this.usingLookingPriority == useLookPriority + + // if we are currently using look priority, has our yaw stayed within recalc threshold? + && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD) + ) { + // nothing we care about changed, so we're not re-calculating + return; + } + + final int centerChunkX = Mth.floor(posX) >> 4; + final int centerChunkZ = Mth.floor(posZ) >> 4; + + this.player.needsChunkCenterUpdate = true; + this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance); + this.player.needsChunkCenterUpdate = false; + this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance); + this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1); + this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance); + + if (sendViewDistance != this.lastSendDistance) { + // update the view radius for client + // note that this should be after the map calls because the client wont expect unload calls not in its VD + // and it's possible we decreased VD here + this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance - 1)); // client already expects the 1 radius neighbours, so subtract 1. + } + + this.lastLocX = posX; + this.lastLocZ = posZ; + this.lastYaw = yaw; + this.lastSendDistance = sendViewDistance; + this.lastLoadDistance = loadViewDistance; + this.lastTickDistance = tickViewDistance; + this.usingLookingPriority = useLookPriority; + + this.lastChunkX = centerChunkX; + this.lastChunkZ = centerChunkZ; + + // points for player "view" triangle: + + // obviously, the player pos is a vertex + final double p1x = posX; + final double p1z = posZ; + + // to the left of the looking direction + final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector + + p1x; // offset vector + final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector + + p1z; // offset vector + + // to the right of the looking direction + final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector + + p1x; // offset vector + final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector + + p1z; // offset vector + + // now that we have all of our points, we can recalculate the load queue + + final List loadQueue = new ArrayList<>(); + + // clear send queue, we are re-sorting + this.sendQueue.clear(); + + final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance); + + for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) { + for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) { + final int chunkX = dx + centerChunkX; + final int chunkZ = dz + centerChunkZ; + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + + if (this.hasSentChunk(chunkX, chunkZ)) { + // already sent (which means it is also loaded) + continue; + } + + final boolean loadChunk = squareDistance <= loadViewDistance; + final boolean sendChunk = squareDistance <= sendViewDistance; + + final boolean prioritised = useLookPriority && triangleIntersects( + // prioritisation triangle + p1x, p1z, p2x, p2z, p3x, p3z, + + // center of chunk + (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8) + ); + + + final int manhattanDistance = (Math.abs(dx) + Math.abs(dz)); + + final double priority; + + if (squareDistance <= TuinityConfig.playerMinChunkLoadRadius) { + // priority should be negative, and we also want to order it from center outwards + // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest + priority = -((2 * TuinityConfig.playerMinChunkLoadRadius + 1) - (dx + dz)); + } else { + if (prioritised) { + // we don't prioritise these chunks above others because we also want to make sure some chunks + // will be loaded if the player changes direction + priority = (double)manhattanDistance / 6.0; + } else { + priority = (double)manhattanDistance; + } + } + + final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority); + + if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) { + if (loadChunk) { + loadQueue.add(holder); + } + } else { + // loaded but not sent: so queue it! + if (sendChunk) { + this.sendQueue.add(holder); + } + } + } + } + + loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { + return Double.compare(p1.priority, p2.priority); + }); + + // we're modifying loadQueue, must remove + this.loader.chunkLoadQueue.remove(this); + + this.loadQueue.clear(); + this.loadQueue.addAll(loadQueue); + + // must re-add + this.loader.chunkLoadQueue.add(this); + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..49dc783a312ed62415d28cdd801dad6a96f3cc16 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java @@ -0,0 +1,477 @@ +package com.tuinity.tuinity.chunk; + +import com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.MCUtil; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +public final class SingleThreadChunkRegionManager { + + protected final int regionSectionMergeRadius; + protected final int regionSectionChunkSize; + public final int regionChunkShift; // log2(REGION_CHUNK_SIZE) + + public final ServerLevel world; + public final String name; + + protected final Long2ObjectOpenHashMap regionsBySection = new Long2ObjectOpenHashMap<>(); + protected final ReferenceLinkedOpenHashSet needsRecalculation = new ReferenceLinkedOpenHashSet<>(); + protected final int minSectionRecalcCount; + protected final double maxDeadRegionPercent; + protected final Supplier regionDataSupplier; + protected final Supplier regionSectionDataSupplier; + + public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount, + final double maxDeadRegionPercent, final int sectionMergeRadius, + final int regionSectionChunkShift, + final String name, final Supplier regionDataSupplier, + final Supplier regionSectionDataSupplier) { + this.regionSectionMergeRadius = sectionMergeRadius; + this.regionSectionChunkSize = 1 << regionSectionChunkShift; + this.regionChunkShift = regionSectionChunkShift; + this.world = world; + this.name = name; + this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); + this.maxDeadRegionPercent = maxDeadRegionPercent; + this.regionDataSupplier = regionDataSupplier; + this.regionSectionDataSupplier = regionSectionDataSupplier; + } + + // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f + + /* + protected void check() { + ReferenceOpenHashSet> checked = new ReferenceOpenHashSet<>(); + + for (RegionSection section : this.regionsBySection.values()) { + if (!checked.add(section.region)) { + section.region.check(); + } + } + for (Region region : this.needsRecalculation) { + region.check(); + } + } + */ + + protected void addToRecalcQueue(final Region region) { + this.needsRecalculation.add(region); + } + + protected void removeFromRecalcQueue(final Region region) { + this.needsRecalculation.remove(region); + } + + public RegionSection getRegionSection(final int chunkX, final int chunkZ) { + return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)); + } + + public Region getRegion(final int chunkX, final int chunkZ) { + final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift)); + return section != null ? section.region : null; + } + + private final List toMerge = new ArrayList<>(); + + protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) { + final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ); + + if (force == null) { + RegionSection region = this.regionsBySection.get(sectionKey); + if (region != null) { + return region; + } + } + + int mergeCandidateSectionSize = -1; + Region mergeIntoCandidate = null; + + // find optimal candidate to merge into + + final int minX = sectionX - this.regionSectionMergeRadius; + final int maxX = sectionX + this.regionSectionMergeRadius; + final int minZ = sectionZ - this.regionSectionMergeRadius; + final int maxZ = sectionZ + this.regionSectionMergeRadius; + for (int currX = minX; currX <= maxX; ++currX) { + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ)); + if (section == null) { + continue; + } + final Region region = section.region; + if (region.dead) { + throw new IllegalStateException("Dead region should not be in live region manager state: " + region); + } + final int sections = region.sections.size(); + + if (sections > mergeCandidateSectionSize) { + mergeCandidateSectionSize = sections; + mergeIntoCandidate = region; + } + this.toMerge.add(region); + } + } + + // merge + if (mergeIntoCandidate != null) { + for (int i = 0; i < this.toMerge.size(); ++i) { + final Region region = this.toMerge.get(i); + if (region.dead || mergeIntoCandidate == region) { + continue; + } + region.mergeInto(mergeIntoCandidate); + } + this.toMerge.clear(); + } else { + mergeIntoCandidate = new Region(this); + } + + final RegionSection section; + if (force == null) { + this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this)); + } else { + final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force); + if (existing != null) { + throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() + + ", with " + force.toStringWithRegion()); + } + + section = force; + } + + mergeIntoCandidate.addRegionSection(section); + //mergeIntoCandidate.check(); + //this.check(); + + return section; + } + + public void addChunk(final int chunkX, final int chunkZ) { + this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ); + } + + public void removeChunk(final int chunkX, final int chunkZ) { + final RegionSection section = this.regionsBySection.get( + MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift) + ); + if (section != null) { + section.removeChunk(chunkX, chunkZ); + } else { + throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist"); + } + } + + public void recalculateRegions() { + for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) { + final Region region = this.needsRecalculation.removeFirst(); + + this.recalculateRegion(region); + //this.check(); + } + } + + protected void recalculateRegion(final Region region) { + region.markedForRecalc = false; + //region.check(); + // clear unused regions + for (final Iterator iterator = region.deadSections.iterator(); iterator.hasNext();) { + final RegionSection deadSection = iterator.next(); + + if (deadSection.hasChunks()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); + } + if (!region.removeRegionSection(deadSection)) { + throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); + } + if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) { + throw new IllegalStateException("Cannot remove dead section '" + + deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + + this.regionsBySection.get(deadSection.regionCoordinate)); + } + } + region.deadSections.clear(); + + // implicitly cover cases where size == 0 + if (region.sections.size() < this.minSectionRecalcCount) { + //region.check(); + return; + } + + // run a test to see if we actually need to recalculate + // TODO + + // destroy and rebuild the region + region.dead = true; + + // destroy region state + for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection aliveSection = iterator.next(); + if (!aliveSection.hasChunks()) { + throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!"); + } + if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) { + throw new IllegalStateException("Cannot remove alive section '" + + aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " + + this.regionsBySection.get(aliveSection.regionCoordinate)); + } + } + + // rebuild regions + for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection aliveSection = iterator.next(); + this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection); + } + } + + public static final class Region { + protected final IteratorSafeOrderedReferenceSet sections = new IteratorSafeOrderedReferenceSet<>(); + protected final ReferenceOpenHashSet deadSections = new ReferenceOpenHashSet<>(16, 0.7f); + protected boolean dead; + protected boolean markedForRecalc; + + public final SingleThreadChunkRegionManager regionManager; + public final RegionData regionData; + + protected Region(final SingleThreadChunkRegionManager regionManager) { + this.regionManager = regionManager; + this.regionData = regionManager.regionDataSupplier.get(); + } + + public IteratorSafeOrderedReferenceSet.Iterator getSections() { + return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); + } + + protected final double getDeadSectionPercent() { + return (double)this.deadSections.size() / (double)this.sections.size(); + } + + /* + protected void check() { + if (this.dead) { + throw new IllegalStateException("Dead region!"); + } + for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + if (section.region != this) { + throw new IllegalStateException("Region section must point to us!"); + } + if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) { + throw new IllegalStateException("Region section must match the regionmanager state!"); + } + } + } + */ + + // note: it is not true that the region at this point is not in any region. use the region field on the section + // to see if it is currently in another region. + protected final boolean addRegionSection(final RegionSection section) { + if (!this.sections.add(section)) { + return false; + } + + section.sectionData.addToRegion(section, section.region, this); + + section.region = this; + return true; + } + + protected final boolean removeRegionSection(final RegionSection section) { + if (!this.sections.remove(section)) { + return false; + } + + section.sectionData.removeFromRegion(section, this); + + return true; + } + + protected void mergeInto(final Region mergeTarget) { + if (this == mergeTarget) { + throw new IllegalStateException("Cannot merge a region onto itself"); + } + if (this.dead) { + throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget); + } else if (mergeTarget.dead) { + throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); + } + this.dead = true; + if (this.markedForRecalc) { + this.regionManager.removeFromRecalcQueue(this); + } + + for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + + if (!mergeTarget.addRegionSection(section)) { + throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget); + } + } + + for (final RegionSection deadSection : this.deadSections) { + if (!this.sections.contains(deadSection)) { + throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); + } + mergeTarget.deadSections.add(deadSection); + } + //mergeTarget.check(); + } + + protected void markSectionAlive(final RegionSection section) { + this.deadSections.remove(section); + if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) { + this.regionManager.removeFromRecalcQueue(this); + this.markedForRecalc = false; + } + } + + protected void markSectionDead(final RegionSection section) { + this.deadSections.add(section); + if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) { + this.regionManager.addToRecalcQueue(this); + this.markedForRecalc = true; + } + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(128); + + ret.append("Region{"); + ret.append("dead=").append(this.dead).append(','); + ret.append("markedForRecalc=").append(this.markedForRecalc).append(','); + + ret.append("sectionCount=").append(this.sections.size()).append(','); + ret.append("sections=["); + for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + ret.append(section); + if (iterator.hasNext()) { + ret.append(','); + } + } + ret.append(']'); + + ret.append('}'); + return ret.toString(); + } + } + + public static final class RegionSection { + protected final long regionCoordinate; + protected final long[] chunksBitset; + protected int chunkCount; + protected Region region; + + public final SingleThreadChunkRegionManager regionManager; + public final RegionSectionData sectionData; + + protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) { + this.regionCoordinate = regionCoordinate; + this.regionManager = regionManager; + this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)]; + this.sectionData = regionManager.regionSectionDataSupplier.get(); + } + + public int getSectionX() { + return MCUtil.getCoordinateX(this.regionCoordinate); + } + + public int getSectionZ() { + return MCUtil.getCoordinateZ(this.regionCoordinate); + } + + public Region getRegion() { + return this.region; + } + + private int getChunkIndex(final int chunkX, final int chunkZ) { + return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift); + } + + protected boolean hasChunks() { + return this.chunkCount != 0; + } + + protected void addChunk(final int chunkX, final int chunkZ) { + final int index = this.getChunkIndex(chunkX, chunkZ); + final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); + if (after == bitset) { + throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); + } + if (++this.chunkCount != 1) { + return; + } + this.region.markSectionAlive(this); + } + + protected void removeChunk(final int chunkX, final int chunkZ) { + final int index = this.getChunkIndex(chunkX, chunkZ); + final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); + if (before == bitset) { + throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); + } + if (--this.chunkCount != 0) { + return; + } + this.region.markSectionDead(this); + } + + @Override + public String toString() { + return "RegionSection{" + + "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + + "}"; + } + + public String toStringWithRegion() { + return "RegionSection{" + + "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + "," + + "region=" + this.region + + "}"; + } + + private static String toString(final long[] array) { + final StringBuilder ret = new StringBuilder(); + for (final long value : array) { + // zero pad the hex string + final char[] zeros = new char[Long.SIZE / 4]; + Arrays.fill(zeros, '0'); + final String string = Long.toHexString(value); + System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); + + ret.append(zeros); + } + + return ret.toString(); + } + } + + public static interface RegionData { + + } + + public static interface RegionSectionData { + + public void removeFromRegion(final RegionSection section, final Region from); + + // removal from the old region is handled via removeFromRegion + public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion); + + } +} diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..356a6118f1b0b091f7527aec747659025562eafc --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java @@ -0,0 +1,432 @@ +package com.tuinity.tuinity.config; + +import com.destroystokyo.paper.util.SneakyThrow; +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.logging.Level; + +public final class TuinityConfig { + + public static final String CONFIG_HEADER = "Configuration file for Tuinity."; + public static final int CURRENT_CONFIG_VERSION = 2; + + private static final Object[] EMPTY = new Object[0]; + + private static File configFile; + public static YamlConfiguration config; + private static int configVersion; + public static boolean createWorldSections = true; + + public static void init(final File file) { + // TODO remove this in the future... + final File tuinityConfig = new File(file.getParent(), "tuinity.yml"); + if (!tuinityConfig.exists()) { + final File oldConfig = new File(file.getParent(), "concrete.yml"); + oldConfig.renameTo(tuinityConfig); + } + TuinityConfig.configFile = file; + final YamlConfiguration config = new YamlConfiguration(); + config.options().header(CONFIG_HEADER); + config.options().copyDefaults(true); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Failure to create tuinity config", ex); + } + } else { + try { + config.load(file); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Failure to load tuinity config", ex); + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + TuinityConfig.load(config); + } + + public static void load(final YamlConfiguration config) { + TuinityConfig.config = config; + TuinityConfig.configVersion = TuinityConfig.getInt("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION); + TuinityConfig.set("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION); + + for (final Method method : TuinityConfig.class.getDeclaredMethods()) { + if (method.getReturnType() != void.class || method.getParameterCount() != 0 || + !Modifier.isPrivate(method.getModifiers()) || !Modifier.isStatic(method.getModifiers())) { + continue; + } + + try { + method.setAccessible(true); + method.invoke(null, EMPTY); + } catch (final Exception ex) { + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + /* We re-save to add new options */ + try { + config.save(TuinityConfig.configFile); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex); + } + } + + static void set(final String path, final Object value) { + TuinityConfig.config.set(path, value); + } + + static boolean getBoolean(final String path, final boolean dfl) { + TuinityConfig.config.addDefault(path, Boolean.valueOf(dfl)); + return TuinityConfig.config.getBoolean(path, dfl); + } + + static int getInt(final String path, final int dfl) { + TuinityConfig.config.addDefault(path, Integer.valueOf(dfl)); + return TuinityConfig.config.getInt(path, dfl); + } + + static long getLong(final String path, final long dfl) { + TuinityConfig.config.addDefault(path, Long.valueOf(dfl)); + return TuinityConfig.config.getLong(path, dfl); + } + + static double getDouble(final String path, final double dfl) { + TuinityConfig.config.addDefault(path, Double.valueOf(dfl)); + return TuinityConfig.config.getDouble(path, dfl); + } + + static String getString(final String path, final String dfl) { + TuinityConfig.config.addDefault(path, dfl); + return TuinityConfig.config.getString(path, dfl); + } + + public static int playerMinChunkLoadRadius; + public static double playerMaxConcurrentChunkSends; + public static double playerMaxConcurrentChunkLoads; + public static boolean playerAutoConfigureSendViewDistance; + public static boolean enableMC162253Workaround; + public static double playerTargetChunkSendRate; + public static boolean playerFrustumPrioritisation; + + private static void newPlayerChunkManagement() { + playerMinChunkLoadRadius = TuinityConfig.getInt("player-chunks.min-load-radius", 2); + playerMaxConcurrentChunkSends = TuinityConfig.getDouble("player-chunks.max-concurrent-sends", 5.0); + playerMaxConcurrentChunkLoads = TuinityConfig.getDouble("player-chunks.max-concurrent-loads", -6.0); + playerAutoConfigureSendViewDistance = TuinityConfig.getBoolean("player-chunks.autoconfig-send-distance", true); + // this costs server bandwidth. latest phosphor or starlight on the client fixes mc162253 anyways. + enableMC162253Workaround = TuinityConfig.getBoolean("player-chunks.enable-mc162253-workaround", true); + playerTargetChunkSendRate = TuinityConfig.getDouble("player-chunks.target-chunk-send-rate", -35.0); + playerFrustumPrioritisation = TuinityConfig.getBoolean("player-chunks.enable-frustum-priority", false); + } + + public static final class PacketLimit { + public final double packetLimitInterval; + public final double maxPacketRate; + public final ViolateAction violateAction; + + public PacketLimit(final double packetLimitInterval, final double maxPacketRate, final ViolateAction violateAction) { + this.packetLimitInterval = packetLimitInterval; + this.maxPacketRate = maxPacketRate; + this.violateAction = violateAction; + } + + public static enum ViolateAction { + KICK, DROP; + } + } + + public static String kickMessage; + public static PacketLimit allPacketsLimit; + public static java.util.Map>, PacketLimit> packetSpecificLimits = new java.util.HashMap<>(); + + private static void packetLimiter() { + packetSpecificLimits.clear(); + kickMessage = org.bukkit.ChatColor.translateAlternateColorCodes('&', TuinityConfig.getString("packet-limiter.kick-message", "&cSent too many packets")); + allPacketsLimit = new PacketLimit( + TuinityConfig.getDouble("packet-limiter.limits.all.interval", 7.0), + TuinityConfig.getDouble("packet-limiter.limits.all.max-packet-rate", 500.0), + PacketLimit.ViolateAction.KICK + ); + if (allPacketsLimit.maxPacketRate <= 0.0 || allPacketsLimit.packetLimitInterval <= 0.0) { + allPacketsLimit = null; + } + final ConfigurationSection section = TuinityConfig.config.getConfigurationSection("packet-limiter.limits"); + + // add default packets + + // auto recipe limiting + TuinityConfig.getDouble("packet-limiter.limits." + + "PacketPlayInAutoRecipe" + ".interval", 4.0); + TuinityConfig.getDouble("packet-limiter.limits." + + "PacketPlayInAutoRecipe" + ".max-packet-rate", 5.0); + TuinityConfig.getString("packet-limiter.limits." + + "PacketPlayInAutoRecipe" + ".action", PacketLimit.ViolateAction.DROP.name()); + + final String canonicalName = MinecraftServer.class.getCanonicalName(); + final String nmsPackage = canonicalName.substring(0, canonicalName.lastIndexOf(".")); + for (final String packetClassName : section.getKeys(false)) { + if (packetClassName.equals("all")) { + continue; + } + Class packetClazz = null; + + try { + packetClazz = Class.forName(nmsPackage + "." + packetClassName); + } catch (final ClassNotFoundException ex) { + for (final String subpackage : java.util.Arrays.asList("game", "handshake", "login", "status")) { + try { + packetClazz = Class.forName("net.minecraft.network.protocol." + subpackage + "." + packetClassName); + } catch (final ClassNotFoundException ignore) {} + } + if (packetClazz == null) { + MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml"); + continue; + } + } + + if (!net.minecraft.network.protocol.Packet.class.isAssignableFrom(packetClazz)) { + MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml"); + continue; + } + + if (!(section.get(packetClassName.concat(".interval")) instanceof Number) || !(section.get(packetClassName.concat(".max-packet-rate")) instanceof Number)) { + throw new RuntimeException("Packet limit setting " + packetClassName + " is missing interval or max-packet-rate!"); + } + + final String actionString = section.getString(packetClassName.concat(".action"), "KICK"); + PacketLimit.ViolateAction action = PacketLimit.ViolateAction.KICK; + for (PacketLimit.ViolateAction test : PacketLimit.ViolateAction.values()) { + if (actionString.equalsIgnoreCase(test.name())) { + action = test; + break; + } + } + + final double interval = section.getDouble(packetClassName.concat(".interval")); + final double rate = section.getDouble(packetClassName.concat(".max-packet-rate")); + + if (interval > 0.0 && rate > 0.0) { + packetSpecificLimits.put((Class)packetClazz, new PacketLimit(interval, rate, action)); + } + } + } + + public static boolean lagCompensateBlockBreaking; + + private static void lagCompensateBlockBreaking() { + lagCompensateBlockBreaking = TuinityConfig.getBoolean("lag-compensate-block-breaking", true); + } + + public static boolean sendFullPosForHardCollidingEntities; + + private static void sendFullPosForHardCollidingEntities() { + sendFullPosForHardCollidingEntities = TuinityConfig.getBoolean("send-full-pos-for-hard-colliding-entities", true); + } + + public static final class WorldConfig { + + public final String worldName; + public String configPath; + ConfigurationSection worldDefaults; + + public WorldConfig(final String worldName) { + this.worldName = worldName; + this.init(); + } + + public void init() { + this.worldDefaults = TuinityConfig.config.getConfigurationSection("world-settings.default"); + if (this.worldDefaults == null) { + this.worldDefaults = TuinityConfig.config.createSection("world-settings.default"); + } + + String worldSectionPath = TuinityConfig.configVersion < 1 ? this.worldName : "world-settings.".concat(this.worldName); + ConfigurationSection section = TuinityConfig.config.getConfigurationSection(worldSectionPath); + this.configPath = worldSectionPath; + if (TuinityConfig.createWorldSections) { + if (section == null) { + section = TuinityConfig.config.createSection(worldSectionPath); + } + TuinityConfig.config.set(worldSectionPath, section); + } + + this.load(); + } + + public void load() { + for (final Method method : TuinityConfig.WorldConfig.class.getDeclaredMethods()) { + if (method.getReturnType() != void.class || method.getParameterCount() != 0 || + !Modifier.isPrivate(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) { + continue; + } + + try { + method.setAccessible(true); + method.invoke(this, EMPTY); + } catch (final Exception ex) { + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + if (TuinityConfig.configVersion < 1) { + ConfigurationSection oldSection = TuinityConfig.config.getConfigurationSection(this.worldName); + TuinityConfig.config.set("world-settings.".concat(this.worldName), oldSection); + TuinityConfig.config.set(this.worldName, null); + } + + /* We re-save to add new options */ + try { + TuinityConfig.config.save(TuinityConfig.configFile); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex); + } + } + + /** + * update world defaults for the specified path, but also sets this world's config value for the path + * if it exists + */ + void set(final String path, final Object val) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.set(path, val); + if (config != null && config.get(path) != null) { + config.set(path, val); + } + } + + boolean getBoolean(final String path, final boolean dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Boolean.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getBoolean(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getBoolean(path) : config.getBoolean(path, this.worldDefaults.getBoolean(path)); + } + + boolean getBooleanRaw(final String path, final boolean dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getBoolean(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getBoolean(path, dfl) : config.getBoolean(path, this.worldDefaults.getBoolean(path, dfl)); + } + + int getInt(final String path, final int dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Integer.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getInt(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getInt(path) : config.getInt(path, this.worldDefaults.getInt(path)); + } + + int getIntRaw(final String path, final int dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getInt(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getInt(path, dfl) : config.getInt(path, this.worldDefaults.getInt(path, dfl)); + } + + long getLong(final String path, final long dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Long.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getLong(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getLong(path) : config.getLong(path, this.worldDefaults.getLong(path)); + } + + long getLongRaw(final String path, final long dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getLong(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getLong(path, dfl) : config.getLong(path, this.worldDefaults.getLong(path, dfl)); + } + + double getDouble(final String path, final double dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Double.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getDouble(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getDouble(path) : config.getDouble(path, this.worldDefaults.getDouble(path)); + } + + double getDoubleRaw(final String path, final double dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getDouble(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getDouble(path, dfl) : config.getDouble(path, this.worldDefaults.getDouble(path, dfl)); + } + + String getString(final String path, final String dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, dfl); + return config == null ? this.worldDefaults.getString(path) : config.getString(path, this.worldDefaults.getString(path)); + } + + String getStringRaw(final String path, final String dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + return config == null ? this.worldDefaults.getString(path, dfl) : config.getString(path, this.worldDefaults.getString(path, dfl)); + } + + List getList(final String path, final List dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, dfl); + return config == null ? this.worldDefaults.getList(path) : config.getList(path, this.worldDefaults.getList(path)); + } + + List getListRaw(final String path, final List dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + return config == null ? this.worldDefaults.getList(path, dfl) : config.getList(path, this.worldDefaults.getList(path, dfl)); + } + + public int spawnLimitMonsters; + public int spawnLimitAnimals; + public int spawnLimitWaterAmbient; + public int spawnLimitWaterAnimals; + public int spawnLimitAmbient; + + private void perWorldSpawnLimit() { + final String path = "spawn-limits"; + + this.spawnLimitMonsters = this.getIntRaw(path + ".monsters", -1); + this.spawnLimitAnimals = this.getIntRaw(path + ".animals", -1); + this.spawnLimitWaterAmbient = this.getIntRaw(path + ".water-ambient", -1); + this.spawnLimitWaterAnimals = this.getIntRaw(path + ".water-animals", -1); + this.spawnLimitAmbient = this.getIntRaw(path + ".ambient", -1); + } + } + +} diff --git a/src/main/java/com/tuinity/tuinity/util/CachedLists.java b/src/main/java/com/tuinity/tuinity/util/CachedLists.java new file mode 100644 index 0000000000000000000000000000000000000000..01320aea07b51c97ae5f0654b81d2332f545d42e --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java @@ -0,0 +1,57 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.util.UnsafeList; +import java.util.List; + +public final class CachedLists { + + // Tuinity start - optimise collisions + static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); + static boolean tempCollisionListInUse; + + public static UnsafeList getTempCollisionList() { + if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { + return new UnsafeList<>(16); + } + tempCollisionListInUse = true; + return TEMP_COLLISION_LIST; + } + + public static void returnTempCollisionList(List list) { + if (list != TEMP_COLLISION_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempCollisionListInUse = false; + } + + static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); + static boolean tempGetEntitiesListInUse; + + public static UnsafeList getTempGetEntitiesList() { + if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { + return new UnsafeList<>(16); + } + tempGetEntitiesListInUse = true; + return TEMP_GET_ENTITIES_LIST; + } + + public static void returnTempGetEntitiesList(List list) { + if (list != TEMP_GET_ENTITIES_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempGetEntitiesListInUse = false; + } + // Tuinity end - optimise collisions + + public static void reset() { + // Tuinity start - optimise collisions + TEMP_COLLISION_LIST.completeReset(); + TEMP_GET_ENTITIES_LIST.completeReset(); + // Tuinity end - optimise collisions + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e883b2af853666c11a14a541aac331d97c6ad057 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java @@ -0,0 +1,600 @@ +package com.tuinity.tuinity.util; + +import com.tuinity.tuinity.voxel.AABBVoxelShape; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.CollisionGetter; +import net.minecraft.world.level.EntityGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; + + public static boolean isEmpty(final AABB aabb) { + return (aabb.maxX - aabb.minX) < COLLISION_EPSILON && (aabb.maxY - aabb.minY) < COLLISION_EPSILON && (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; + } + + public static boolean isEmpty(final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (maxX - minX) < COLLISION_EPSILON && (maxY - minY) < COLLISION_EPSILON && (maxZ - minZ) < COLLISION_EPSILON; + } + + public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move + return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, + x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false); + } + + /* + A couple of rules for VoxelShape collisions: + Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement + checks. + If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite + direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code + will automatically round it to 0. + */ + + public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, + final double maxY1, final double maxZ1, final double minX2, final double minY2, + final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { + return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && + (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && + (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && + (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && + (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { + return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && + (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + + public static double collideX(final AABB target, final AABB source, final double source_move) { + if (source_move == 0.0) { + return 0.0; + } + + if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static double collideY(final AABB target, final AABB source, final double source_move) { + if (source_move == 0.0) { + return 0.0; + } + + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static double collideZ(final AABB target, final AABB source, final double source_move) { + if (source_move == 0.0) { + return 0.0; + } + + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static AABB offsetX(final AABB box, final double dx) { + return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB offsetY(final AABB box, final double dy) { + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB offsetZ(final AABB box, final double dz) { + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false); + } + + public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false); + } + + public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false); + } + + public static double performCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + + public static boolean addBoxesToIfIntersects(final VoxelShape shape, final AABB aabb, final List list) { + if (shape instanceof AABBVoxelShape) { + final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; + if (voxelShapeIntersect(shapeCasted.aabb, aabb) && !isEmpty(shapeCasted.aabb)) { + list.add(shapeCasted.aabb); + return true; + } + return false; + } else if (shape instanceof ArrayVoxelShape) { + final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; + // this can be optimised by checking an "overall shape" first, but not needed + + final double offX = shapeCasted.getOffsetX(); + final double offY = shapeCasted.getOffsetY(); + final double offZ = shapeCasted.getOffsetZ(); + + boolean ret = false; + + for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { + final double minX, minY, minZ, maxX, maxY, maxZ; + if (voxelShapeIntersect(aabb, minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ, + maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ) + && !isEmpty(minX, minY, minZ, maxX, maxY, maxZ)) { + list.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ, false)); + ret = true; + } + } + + return ret; + } else { + final List boxes = shape.toAabbs(); + + boolean ret = false; + + for (int i = 0, len = boxes.size(); i < len; ++i) { + final AABB box = boxes.get(i); + if (voxelShapeIntersect(box, aabb) && !isEmpty(box)) { + list.add(box); + ret = true; + } + } + + return ret; + } + } + + public static void addBoxesTo(final VoxelShape shape, final List list) { + if (shape instanceof AABBVoxelShape) { + final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; + if (!isEmpty(shapeCasted.aabb)) { + list.add(shapeCasted.aabb); + } + } else if (shape instanceof ArrayVoxelShape) { + final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; + + final double offX = shapeCasted.getOffsetX(); + final double offY = shapeCasted.getOffsetY(); + final double offZ = shapeCasted.getOffsetZ(); + + for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { + final AABB box = boundingBox.move(offX, offY, offZ); + if (!isEmpty(box)) { + list.add(box); + } + } + } else { + final List boxes = shape.toAabbs(); + for (int i = 0, len = boxes.size(); i < len; ++i) { + final AABB box = boxes.get(i); + if (!isEmpty(box)) { + list.add(box); + } + } + } + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) { + return isAlmostCollidingOnBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX(); // -X + final double borderMaxX = worldborder.getMaxX(); // +X + + final double borderMinZ = worldborder.getMinZ(); // -Z + final double borderMaxZ = worldborder.getMaxZ(); // +Z + + return + // Not intersecting if we're smaller + !voxelShapeIntersect( + boxMinX + COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + COLLISION_EPSILON, + boxMaxX - COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ) + && + + // Are intersecting if we're larger + voxelShapeIntersect( + boxMinX - COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - COLLISION_EPSILON, + boxMaxX + COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final AABB boundingBox) { + return isCollidingWithBorderEdge(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX() + COLLISION_EPSILON; // -X + final double borderMaxX = worldborder.getMaxX() - COLLISION_EPSILON; // +X + + final double borderMinZ = worldborder.getMinZ() + COLLISION_EPSILON; // -Z + final double borderMaxZ = worldborder.getMaxZ() - COLLISION_EPSILON; // +Z + + return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ; + } + + public static boolean getCollisionsForBlocksOrWorldBorder(final CollisionGetter getter, final Entity entity, final AABB aabb, + final List into, final boolean loadChunks, final boolean collidesWithUnloaded, + final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) { + boolean ret = false; + + if (entity != null) { + if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) { + if (checkOnly) { + return true; + } else { + CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into); + ret = true; + } + } + } + + int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; + int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; + + int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1; + int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1; + + int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; + int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; + + final int minSection = WorldUtil.getMinSection(getter); + final int maxSection = WorldUtil.getMaxSection(getter); + final int minBlock = minSection << 4; + final int maxBlock = (maxSection << 4) | 15; + + BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + CollisionContext collisionShape = null; + + // special cases: + if (minBlockY > maxBlock || maxBlockY < minBlock) { + // no point in checking + return ret; + } + + int minYIterate = Math.max(minBlock, minBlockY); + int maxYIterate = Math.min(maxBlock, maxBlockY); + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider; + if (getter instanceof WorldGenRegion) { + chunkProvider = (ServerChunkCache)((WorldGenRegion)getter).getChunkSource(); + } else if (getter instanceof ServerLevel) { + chunkProvider = ((ServerLevel)getter).getChunkSource(); + } else { + chunkProvider = null; + } + // TODO special case single chunk? + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk + int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk + + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk + int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk + + int chunkXGlobalPos = currChunkX << 4; + int chunkZGlobalPos = currChunkZ << 4; + ChunkAccess chunk; + if (chunkProvider == null) { + chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ); + } else { + chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); + } + + + if (chunk == null) { + if (collidesWithUnloaded) { + if (checkOnly) { + return true; + } else { + into.add(getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } + continue; + } + + LevelChunkSection[] sections = chunk.getSections(); + + // bound y + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + LevelChunkSection section = sections[(currY >> 4) - minSection]; + if (section == null || section.isEmpty()) { + // empty + // skip to next section + currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one + continue; + } + + net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8); + int blockX = currX | chunkXGlobalPos; + int blockY = currY; + int blockZ = currZ | chunkZGlobalPos; + + int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); + if (edgeCount == 3) { + continue; + } + + BlockState blockData = blocks.get(localBlockIndex); + if (blockData.isAir()) { + continue; + } + + if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { + mutablePos.set(blockX, blockY, blockZ); + if (collisionShape == null) { + collisionShape = entity == null ? CollisionContext.empty() : CollisionContext.of(entity); + } + VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape); + if (voxelshape2 != Shapes.empty()) { + VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ); + + if (predicate != null && !predicate.test(blockData, mutablePos)) { + continue; + } + + if (checkOnly) { + if (voxelshape3.intersects(aabb)) { + return true; + } + } else { + ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); + } + } + } + } + } + } + } + } + + return ret; + } + + public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, + final List into, final boolean checkOnly, final Predicate predicate) { + if (isEmpty(aabb) || !(getter instanceof EntityGetter entityGetter)) { + return false; + } + + boolean ret = false; + + // to comply with vanilla intersection rules, expand by -epsilon so we only get stuff we definitely collide with. + // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems + // specifically with boat collisions. + aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); + final List entities = CachedLists.getTempGetEntitiesList(); + try { + if (entity != null && entity.hardCollides()) { + entityGetter.getEntities(entity, aabb, predicate, entities); + } else { + entityGetter.getHardCollidingEntities(entity, aabb, predicate, entities); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if ((entity == null || otherEntity.canBeCollidedWith()) || entity.canCollideWith(otherEntity)) { + if (checkOnly) { + return true; + } else { + into.add(otherEntity.getBoundingBox()); + ret = true; + } + } + } + } finally { + CachedLists.returnTempGetEntitiesList(entities); + } + + return ret; + } + + public static boolean getCollisions(final CollisionGetter view, final Entity entity, final AABB aabb, + final List into, final boolean loadChunks, final boolean collidesWithUnloadedChunks, + final boolean checkBorder, final boolean checkOnly, final BiPredicate blockPredicate, + final Predicate entityPredicate) { + if (checkOnly) { + return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) + || getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); + } else { + return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) + | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); + } + } + + private CollisionUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..f84060952c947d79bf2dffc61c96a300e8d7fac2 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java @@ -0,0 +1,128 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; + +public final class CoordinateUtils { + + // dx, dz are relative to the target chunk + // dx, dz in [-radius, radius] + public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { + return (dx + radius) + (2 * radius + 1)*(dz + radius); + } + + // the chunk keys are compatible with vanilla + + public static long getChunkKey(final BlockPos pos) { + return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final Entity entity) { + return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final ChunkPos pos) { + return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); + } + + public static long getChunkKey(final SectionPos pos) { + return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); + } + + public static long getChunkKey(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getChunkX(final long chunkKey) { + return (int)chunkKey; + } + + public static int getChunkZ(final long chunkKey) { + return (int)(chunkKey >>> 32); + } + + public static int getChunkCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate) >> 4; + } + + // the section keys are compatible with vanilla's + + static final int SECTION_X_BITS = 22; + static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; + static final int SECTION_Y_BITS = 20; + static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; + static final int SECTION_Z_BITS = 22; + static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; + // format is y,z,x (in order of LSB to MSB) + static final int SECTION_Y_SHIFT = 0; + static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; + static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; + static final int SECTION_TO_BLOCK_SHIFT = 4; + + public static long getChunkSectionKey(final int x, final int y, final int z) { + return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final SectionPos pos) { + return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final ChunkPos pos, final int y) { + return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final BlockPos pos) { + return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static long getChunkSectionKey(final Entity entity) { + return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static int getChunkSectionX(final long key) { + return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); + } + + public static int getChunkSectionY(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); + } + + public static int getChunkSectionZ(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); + } + + // the block coordinates are not necessarily compatible with vanilla's + + public static int getBlockCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate); + } + + public static long getBlockKey(final int x, final int y, final int z) { + return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); + } + + public static long getBlockKey(final BlockPos pos) { + return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); + } + + public static long getBlockKey(final Entity entity) { + return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); + } + + private CoordinateUtils() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java @@ -0,0 +1,226 @@ +package com.tuinity.tuinity.util; + +public final class IntegerUtil { + + public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; + public static final long HIGH_BIT_U64 = Long.MIN_VALUE; + + public static int ceilLog2(final int value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static long ceilLog2(final long value) { + return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final int value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final long value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int roundCeilLog2(final int value) { + // optimized variant of 1 << (32 - leading(val - 1)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) + // HIGH_BIT_32 >>> (-1 + leading(val - 1)) + return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); + } + + public static long roundCeilLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); + } + + public static int roundFloorLog2(final int value) { + // optimized variant of 1 << (31 - leading(val)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - 31 + leading(val)) + return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); + } + + public static long roundFloorLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); + } + + public static boolean isPowerOfTwo(final int n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static boolean isPowerOfTwo(final long n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static long getTrailingBit(final long n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + public static int trailingZeros(final long n) { + return Long.numberOfTrailingZeros(n); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorMultiple(final long numbers) { + return (int)(numbers >>> 32); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorShift(final long numbers) { + return (int)numbers; + } + + // copied from hacker's delight (signed division magic value) + // http://www.hackersdelight.org/hdcodetxt/magic.c.txt + public static long getDivisorNumbers(final int d) { + final int ad = IntegerUtil.branchlessAbs(d); + + if (ad < 2) { + throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); + } + + final int two31 = 0x80000000; + final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour + + int p = 31; + + // all these variables are UNSIGNED! + int t = two31 + (d >>> 31); + int anc = t - 1 - t%ad; + int q1 = (int)((two31 & mask)/(anc & mask)); + int r1 = two31 - q1*anc; + int q2 = (int)((two31 & mask)/(ad & mask)); + int r2 = two31 - q2*ad; + int delta; + + do { + p = p + 1; + q1 = 2*q1; // Update q1 = 2**p/|nc|. + r1 = 2*r1; // Update r1 = rem(2**p, |nc|). + if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) + q1 = q1 + 1; + r1 = r1 - anc; + } + q2 = 2*q2; // Update q2 = 2**p/|d|. + r2 = 2*r2; // Update r2 = rem(2**p, |d|). + if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) + q2 = q2 + 1; + r2 = r2 - ad; + } + delta = ad - r2; + } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); + + int magicNum = q2 + 1; + if (d < 0) { + magicNum = -magicNum; + } + int shift = p - 32; + return ((long)magicNum << 32) | shift; + } + + public static int branchlessAbs(final int val) { + // -n = -1 ^ n + 1 + final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + public static long branchlessAbs(final long val) { + // -n = -1 ^ n + 1 + final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + //https://github.com/skeeto/hash-prospector for hash functions + + //score = ~590.47984224483832 + public static int hash0(int x) { + x *= 0x36935555; + x ^= x >>> 16; + return x; + } + + //score = ~310.01596637036749 + public static int hash1(int x) { + x ^= x >>> 15; + x *= 0x356aaaad; + x ^= x >>> 17; + return x; + } + + public static int hash2(int x) { + x ^= x >>> 16; + x *= 0x7feb352d; + x ^= x >>> 15; + x *= 0x846ca68b; + x ^= x >>> 16; + return x; + } + + public static int hash3(int x) { + x ^= x >>> 17; + x *= 0xed5ad4bb; + x ^= x >>> 11; + x *= 0xac4c1b51; + x ^= x >>> 15; + x *= 0x31848bab; + x ^= x >>> 14; + return x; + } + + //score = ~365.79959673201887 + public static long hash1(long x) { + x ^= x >>> 27; + x *= 0xb24924b71d2d354bL; + x ^= x >>> 28; + return x; + } + + //h2 hash + public static long hash2(long x) { + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + return x; + } + + public static long hash3(long x) { + x ^= x >>> 45; + x *= 0xc161abe5704b6c79L; + x ^= x >>> 41; + x *= 0xe3e5389aedbc90f7L; + x ^= x >>> 56; + x *= 0x1f9aba75a52db073L; + x ^= x >>> 53; + return x; + } + + private IntegerUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java new file mode 100644 index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java @@ -0,0 +1,100 @@ +package com.tuinity.tuinity.util; + +public final class IntervalledCounter { + + protected long[] times; + protected final long interval; + protected long minTime; + protected int sum; + protected int head; // inclusive + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { + this.times = new long[8]; + this.interval = interval; + } + + public void updateCurrentTime() { + this.updateCurrentTime(System.nanoTime()); + } + + public void updateCurrentTime(final long currentTime) { + int sum = this.sum; + int head = this.head; + final int tail = this.tail; + final long minTime = currentTime - this.interval; + + final int arrayLen = this.times.length; + + // guard against overflow by using subtraction + while (head != tail && this.times[head] - minTime < 0) { + head = (head + 1) % arrayLen; + --sum; + } + + this.sum = sum; + this.head = head; + this.minTime = minTime; + } + + public void addTime(final long currTime) { + // guard against overflow by using subtraction + if (currTime - this.minTime < 0) { + return; + } + int nextTail = (this.tail + 1) % this.times.length; + if (nextTail == this.head) { + this.resize(); + nextTail = (this.tail + 1) % this.times.length; + } + + this.times[this.tail] = currTime; + this.tail = nextTail; + } + + public void updateAndAdd(final int count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + for (int i = 0; i < count; ++i) { + this.addTime(currTime); + } + } + + public void updateAndAdd(final int count, final long currTime) { + this.updateCurrentTime(currTime); + for (int i = 0; i < count; ++i) { + this.addTime(currTime); + } + } + + private void resize() { + final long[] oldElements = this.times; + final long[] newElements = new long[this.times.length * 2]; + this.times = newElements; + + final int head = this.head; + final int tail = this.tail; + final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head)); + this.head = 0; + this.tail = size; + + if (tail >= head) { + System.arraycopy(oldElements, head, newElements, 0, size); + } else { + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + } + } + + // returns in units per second + public double getRate() { + return this.size() / (this.interval * 1.0e-9); + } + + public int size() { + final int head = this.head; + final int tail = this.tail; + + return tail >= head ? (tail - head) : (tail + (this.times.length - head)); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java new file mode 100644 index 0000000000000000000000000000000000000000..a08377e4b0d9c2d78cf851e2c72770cf623de51a --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/TickThread.java @@ -0,0 +1,41 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; + +public final class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("tuinity.strict-thread-checks"); + + static { + if (STRICT_THREAD_CHECKS) { + MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer"); + } + } + + public static void softEnsureTickThread(final String reason) { + if (!STRICT_THREAD_CHECKS) { + return; + } + ensureTickThread(reason); + } + + + public static void ensureTickThread(final String reason) { + if (!Bukkit.isPrimaryThread()) { + MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + public TickThread(final Runnable run, final String name, final int id) { + super(run, name); + this.id = id; + } + + public static TickThread getCurrentTickThread() { + return (TickThread)Thread.currentThread(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..958b3aff3dda64323456d7e0ef0346a72d43f3f1 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java @@ -0,0 +1,46 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.world.level.LevelHeightAccessor; + +public final class WorldUtil { + + // min, max are inclusive + + public static int getMaxSection(final LevelHeightAccessor world) { + return world.getMaxSection() - 1; // getMaxSection() is exclusive + } + + public static int getMinSection(final LevelHeightAccessor world) { + return world.getMinSection(); + } + + public static int getMaxLightSection(final LevelHeightAccessor world) { + return getMaxSection(world) + 1; + } + + public static int getMinLightSection(final LevelHeightAccessor world) { + return getMinSection(world) - 1; + } + + + + public static int getTotalSections(final LevelHeightAccessor world) { + return getMaxSection(world) - getMinSection(world) + 1; + } + + public static int getTotalLightSections(final LevelHeightAccessor world) { + return getMaxLightSection(world) - getMinLightSection(world) + 1; + } + + public static int getMinBlockY(final LevelHeightAccessor world) { + return getMinSection(world) << 4; + } + + public static int getMaxBlockY(final LevelHeightAccessor world) { + return (getMaxSection(world) << 4) | 15; + } + + private WorldUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java new file mode 100644 index 0000000000000000000000000000000000000000..98a1be343d81d6431476fea6a68014def8ce923b --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java @@ -0,0 +1,334 @@ +package com.tuinity.tuinity.util.maplist; + +import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import org.bukkit.Bukkit; +import java.util.Arrays; +import java.util.NoSuchElementException; + +public final class IteratorSafeOrderedReferenceSet { + + public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0; + + protected final Reference2IntLinkedOpenHashMap indexMap; + protected int firstInvalidIndex = -1; + + /* list impl */ + protected E[] listElements; + protected int listSize; + + protected final double maxFragFactor; + + protected int iteratorCount; + + private final boolean threadRestricted; + + public IteratorSafeOrderedReferenceSet() { + this(16, 0.75f, 16, 0.2); + } + + public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) { + this(16, 0.75f, 16, 0.2, threadRestricted); + } + + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor) { + this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false); + } + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor, final boolean threadRestricted) { + this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor); + this.indexMap.defaultReturnValue(-1); + this.maxFragFactor = maxFragFactor; + this.listElements = (E[])new Object[arrayCapacity]; + this.threadRestricted = threadRestricted; + } + + /* + public void check() { + int iterated = 0; + ReferenceOpenHashSet check = new ReferenceOpenHashSet<>(); + if (this.listElements != null) { + for (int i = 0; i < this.listSize; ++i) { + Object obj = this.listElements[i]; + if (obj != null) { + iterated++; + if (!check.add((E)obj)) { + throw new IllegalStateException("contains duplicate"); + } + if (!this.contains((E)obj)) { + throw new IllegalStateException("desync"); + } + } + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size()); + } + + check.clear(); + iterated = 0; + for (final java.util.Iterator iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final E element = iterator.next(); + iterated++; + if (!check.add(element)) { + throw new IllegalStateException("contains duplicate (iterator is wrong)"); + } + if (!this.contains(element)) { + throw new IllegalStateException("desync (iterator is wrong)"); + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size()); + } + } + */ + + protected final boolean allowSafeIteration() { + return !this.threadRestricted || Bukkit.isPrimaryThread(); + } + + protected final double getFragFactor() { + return 1.0 - ((double)this.indexMap.size() / (double)this.listSize); + } + + public int createRawIterator() { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + if (this.indexMap.isEmpty()) { + return -1; + } else { + return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0; + } + } + + public int advanceRawIterator(final int index) { + final E[] elements = this.listElements; + int ret = index + 1; + for (int len = this.listSize; ret < len; ++ret) { + if (elements[ret] != null) { + return ret; + } + } + + return -1; + } + + public void finishRawIterator() { + if (this.allowSafeIteration() && --this.iteratorCount == 0) { + if (this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + } + } + + public boolean remove(final E element) { + final int index = this.indexMap.removeInt(element); + if (index >= 0) { + if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) { + this.firstInvalidIndex = index; + } + if (this.listElements[index] != element) { + throw new IllegalStateException(); + } + this.listElements[index] = null; + if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + //this.check(); + return true; + } + return false; + } + + public boolean contains(final E element) { + return this.indexMap.containsKey(element); + } + + public boolean add(final E element) { + final int listSize = this.listSize; + + final int previous = this.indexMap.putIfAbsent(element, listSize); + if (previous != -1) { + return false; + } + + if (listSize >= this.listElements.length) { + this.listElements = Arrays.copyOf(this.listElements, listSize * 2); + } + this.listElements[listSize] = element; + this.listSize = listSize + 1; + + //this.check(); + return true; + } + + protected void defrag() { + if (this.firstInvalidIndex < 0) { + return; // nothing to do + } + + if (this.indexMap.isEmpty()) { + Arrays.fill(this.listElements, 0, this.listSize, null); + this.listSize = 0; + this.firstInvalidIndex = -1; + //this.check(); + return; + } + + final E[] backingArray = this.listElements; + + int lastValidIndex; + java.util.Iterator> iterator; + + if (this.firstInvalidIndex == 0) { + iterator = this.indexMap.reference2IntEntrySet().fastIterator(); + lastValidIndex = 0; + } else { + lastValidIndex = this.firstInvalidIndex; + final E key = backingArray[lastValidIndex - 1]; + iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry() { + @Override + public int getIntValue() { + throw new UnsupportedOperationException(); + } + + @Override + public int setValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public E getKey() { + return key; + } + }); + } + + while (iterator.hasNext()) { + final Reference2IntMap.Entry entry = iterator.next(); + + final int newIndex = lastValidIndex++; + backingArray[newIndex] = entry.getKey(); + entry.setValue(newIndex); + } + + // cleanup end + Arrays.fill(backingArray, lastValidIndex, this.listSize, null); + this.listSize = lastValidIndex; + this.firstInvalidIndex = -1; + //this.check(); + } + + public E rawGet(final int index) { + return this.listElements[index]; + } + + public int size() { + // always returns the correct amount - listSize can be different + return this.indexMap.size(); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator() { + return this.iterator(0); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator(final int flags) { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public java.util.Iterator unsafeIterator() { + return this.unsafeIterator(0); + } + public java.util.Iterator unsafeIterator(final int flags) { + return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public static interface Iterator extends java.util.Iterator { + + public void finishedIterating(); + + } + + protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator { + + protected final IteratorSafeOrderedReferenceSet set; + protected final boolean canFinish; + protected final int maxIndex; + protected int nextIndex; + protected E pendingValue; + protected boolean finished; + protected E lastReturned; + + protected BaseIterator(final IteratorSafeOrderedReferenceSet set, final boolean canFinish, final int maxIndex) { + this.set = set; + this.canFinish = canFinish; + this.maxIndex = maxIndex; + } + + @Override + public boolean hasNext() { + if (this.finished) { + return false; + } + if (this.pendingValue != null) { + return true; + } + + final E[] elements = this.set.listElements; + int index, len; + for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) { + final E element = elements[index]; + if (element != null) { + this.pendingValue = element; + this.nextIndex = index + 1; + return true; + } + } + + this.nextIndex = index; + return false; + } + + @Override + public E next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + final E ret = this.pendingValue; + + this.pendingValue = null; + this.lastReturned = ret; + + return ret; + } + + @Override + public void remove() { + final E lastReturned = this.lastReturned; + if (lastReturned == null) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.set.remove(lastReturned); + } + + @Override + public void finishedIterating() { + if (this.finished || !this.canFinish) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.finished = true; + if (this.set.allowSafeIteration()) { + this.set.finishRawIterator(); + } + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java new file mode 100644 index 0000000000000000000000000000000000000000..7f2aeb5ed6775ddf38f2561aae8a82f99c8413a7 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java @@ -0,0 +1,46 @@ +package com.tuinity.tuinity.util.math; + +import java.util.Random; + +public final class ThreadUnsafeRandom extends Random { + + // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them. + private static final long multiplier = 0x5DEECE66DL; + private static final long addend = 0xBL; + private static final long mask = (1L << 48) - 1; + + private static long initialScramble(long seed) { + return (seed ^ multiplier) & mask; + } + + private long seed; + + @Override + public void setSeed(long seed) { + // note: called by Random constructor + this.seed = initialScramble(seed); + } + + @Override + protected int next(int bits) { + // avoid the expensive CAS logic used by superclass + return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits)); + } + + // Taken from + // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ + // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c + // Original license is public domain + public static int fastRandomBounded(final long randomInteger, final long limit) { + // randomInteger must be [0, pow(2, 32)) + // limit must be [0, pow(2, 32)) + return (int)((randomInteger * limit) >>> 32); + } + + @Override + public int nextInt(int bound) { + // yes this breaks random's spec + // however there's nothing that uses this class that relies on it + return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java new file mode 100644 index 0000000000000000000000000000000000000000..b8fa9cd9bce312fc85b90e17094241216c620a9e --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java @@ -0,0 +1,297 @@ +package com.tuinity.tuinity.util.misc; + +import com.tuinity.tuinity.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; + +public final class Delayed26WayDistancePropagator3D { + + // this map is considered "stale" unless updates are propagated. + protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed26WayDistancePropagator3D() { + this(null); + } + + public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int y, final int z) { + return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z)); + } + + public void setSource(final int x, final int y, final int z, final int level) { + this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int y, final int z) { + this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public boolean propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return false; + } + + boolean ret = false; + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + ret = true; + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + + return ret; + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = CoordinateUtils.getChunkSectionX(coordinate); + final int y = CoordinateUtils.getChunkSectionY(coordinate); + final int z = CoordinateUtils.getChunkSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = CoordinateUtils.getChunkSectionX(coordinate); + final int y = CoordinateUtils.getChunkSectionY(coordinate); + final int z = CoordinateUtils.getChunkSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java new file mode 100644 index 0000000000000000000000000000000000000000..cdd3c4032c1d6b34a10ba415bd4d0e377aa9af3c --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java @@ -0,0 +1,718 @@ +package com.tuinity.tuinity.util.misc; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import net.minecraft.server.MCUtil; + +public final class Delayed8WayDistancePropagator2D { + + // Test + /* + protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference, Delayed8WayDistancePropagator2D test) { + int got = test.getLevel(x, z); + + int expect = 0; + Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet(); + if (nearest != null) { + for (Object _obj : nearest) { + if (_obj instanceof Ticket) { + Ticket ticket = (Ticket)_obj; + long ticketCoord = reference.getLastCoordinate(ticket); + int viewDistance = reference.getLastViewDistance(ticket); + int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x), + com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z)); + int level = viewDistance - distance; + if (level > expect) { + expect = level; + } + } + } + } + + if (expect != got) { + throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got); + } + } + + static class Ticket { + + int x; + int z; + + final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty + = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); + + } + + public static void main(final String[] args) { + com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() { + @Override + protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(Ticket object) { + return object.empty; + } + }; + Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D(); + + final int maxDistance = 64; + // test origin + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + // test single source + reference.add(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test single source decrease + reference.update(originTicket, 0, 0, originDistance/2); + test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(0, 0); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = (i & 1) == 1 ? -i : i; + a.z = (i & 1) == 1 ? -i : i; + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + + // now test at coordinate offsets + // test offset + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + // test single source + reference.add(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test single source decrease + reference.update(originTicket, offX, offZ, originDistance/2); + test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(offX, offZ); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = offX + ((i & 1) == 1 ? -i : i); + a.z = offZ + ((i & 1) == 1 ? -i : i); + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + } + */ + + // this map is considered "stale" unless updates are propagated. + protected final LevelMap levels = new LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed8WayDistancePropagator2D() { + this(null); + } + + public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int z) { + return this.levels.get(MCUtil.getCoordinateKey(x, z)); + } + + public void setSource(final int x, final int z, final int level) { + this.setSource(MCUtil.getCoordinateKey(x, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int z) { + this.removeSource(MCUtil.getCoordinateKey(x, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new WorkQueue(); + } + } + protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public boolean propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return false; + } + + boolean ret = false; + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + ret = true; + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + + return ret; + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } + + protected static final class LevelMap extends Long2ByteOpenHashMap { + public LevelMap() { + super(); + } + + public LevelMap(final int expected, final float loadFactor) { + super(expected, loadFactor); + } + + // copied from superclass + private int find(final long k) { + if (k == 0L) { + return this.containsNullKey ? this.n : -(this.n + 1); + } else { + final long[] key = this.key; + long curr; + int pos; + if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) { + return -(pos + 1); + } else if (k == curr) { + return pos; + } else { + while((curr = key[pos = pos + 1 & this.mask]) != 0L) { + if (k == curr) { + return pos; + } + } + + return -(pos + 1); + } + } + } + + // copied from superclass + private void insert(final int pos, final long k, final byte v) { + if (pos == this.n) { + this.containsNullKey = true; + } + + this.key[pos] = k; + this.value[pos] = v; + if (this.size++ >= this.maxFill) { + this.rehash(HashCommon.arraySize(this.size + 1, this.f)); + } + } + + // copied from superclass + public byte putIfGreater(final long key, final byte value) { + final int pos = this.find(key); + if (pos < 0) { + if (this.defRetValue < value) { + this.insert(-pos - 1, key, value); + } + return this.defRetValue; + } else { + final byte curr = this.value[pos]; + if (value > curr) { + this.value[pos] = value; + return curr; + } + return curr; + } + } + + // copied from superclass + private void removeEntry(final int pos) { + --this.size; + this.shiftKeys(pos); + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + private void removeNullEntry() { + this.containsNullKey = false; + --this.size; + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + public byte removeIfGreaterOrEqual(final long key, final byte value) { + if (key == 0L) { + if (!this.containsNullKey) { + return this.defRetValue; + } + final byte current = this.value[this.n]; + if (value >= current) { + this.removeNullEntry(); + return current; + } + return current; + } else { + long[] keys = this.key; + byte[] values = this.value; + long curr; + int pos; + if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) { + return this.defRetValue; + } else if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } else { + while((curr = keys[pos = pos + 1 & this.mask]) != 0L) { + if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } + } + + return this.defRetValue; + } + } + } + } + + protected static final class WorkQueue { + + public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque(); + public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque(); + + } + + protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public long removeFirstLong() { + // copied from superclass + long t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } + + protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public byte removeFirstByte() { + // copied from superclass + byte t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java new file mode 100644 index 0000000000000000000000000000000000000000..3bd1bfab37c8a3b981c86ff09941590f028d24bc --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java @@ -0,0 +1,160 @@ +package com.tuinity.tuinity.util.table; + +import com.google.common.collect.Table; +import net.minecraft.world.level.block.state.StateHolder; +import net.minecraft.world.level.block.state.properties.Property; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public final class ZeroCollidingReferenceStateTable { + + // upper 32 bits: starting index + // lower 32 bits: bitset for contained ids + protected final long[] this_index_table; + protected final Comparable[] this_table; + protected final StateHolder this_state; + + protected long[] index_table; + protected StateHolder[][] value_table; + + public ZeroCollidingReferenceStateTable(final StateHolder state, final Map, Comparable> this_map) { + this.this_state = state; + this.this_index_table = this.create_table(this_map.keySet()); + + int max_id = -1; + for (final Property property : this_map.keySet()) { + final int id = lookup_vindex(property, this.this_index_table); + if (id > max_id) { + max_id = id; + } + } + + this.this_table = new Comparable[max_id + 1]; + for (final Map.Entry, Comparable> entry : this_map.entrySet()) { + this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); + } + } + + public void loadInTable(final Table, Comparable, StateHolder> table, + final Map, Comparable> this_map) { + final Set> combined = new HashSet<>(table.rowKeySet()); + combined.addAll(this_map.keySet()); + + this.index_table = this.create_table(combined); + + int max_id = -1; + for (final Property property : combined) { + final int id = lookup_vindex(property, this.index_table); + if (id > max_id) { + max_id = id; + } + } + + this.value_table = new StateHolder[max_id + 1][]; + + final Map, Map, StateHolder>> map = table.rowMap(); + for (final Property property : map.keySet()) { + final Map, StateHolder> propertyMap = map.get(property); + + final int id = lookup_vindex(property, this.index_table); + final StateHolder[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; + + for (final Map.Entry, StateHolder> entry : propertyMap.entrySet()) { + if (entry.getValue() == null) { + // TODO what + continue; + } + + states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); + } + } + + + for (final Map.Entry, Comparable> entry : this_map.entrySet()) { + final Property property = entry.getKey(); + final int index = lookup_vindex(property, this.index_table); + + if (this.value_table[index] == null) { + this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; + } + + this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; + } + } + + + protected long[] create_table(final Collection> collection) { + int max_id = -1; + for (final Property property : collection) { + final int id = property.getId(); + if (id > max_id) { + max_id = id; + } + } + + final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) + + for (final Property property : collection) { + final int id = property.getId(); + + ret[id >>> 5] |= (1L << (id & 31)); + } + + int total = 0; + for (int i = 1, len = ret.length; i < len; ++i) { + ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; + } + + return ret; + } + + public Comparable get(final Property state) { + final Comparable[] table = this.this_table; + final int index = lookup_vindex(state, this.this_index_table); + + if (index < 0 || index >= table.length) { + return null; + } + return table[index]; + } + + public StateHolder get(final Property property, final Comparable with) { + final int withId = ((Property)property).getIdFor(with); + if (withId < 0) { + return null; + } + + final int index = lookup_vindex(property, this.index_table); + final StateHolder[][] table = this.value_table; + if (index < 0 || index >= table.length) { + return null; + } + + final StateHolder[] values = table[index]; + + if (withId >= values.length) { + return null; + } + + return values[withId]; + } + + protected static int lookup_vindex(final Property property, final long[] index_table) { + final int id = property.getId(); + final long bitset_mask = (1L << (id & 31)); + final long lower_mask = bitset_mask - 1; + final int index = id >>> 5; + if (index >= index_table.length) { + return -1; + } + final long index_value = index_table[index]; + final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain + + // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id + // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, + // otherwise it comes out as -1. + return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); + } +} diff --git a/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java new file mode 100644 index 0000000000000000000000000000000000000000..370c070dedd169fe85ad2cb488dae7aa1dcf28fc --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java @@ -0,0 +1,200 @@ +package com.tuinity.tuinity.voxel; + +import com.tuinity.tuinity.util.CollisionUtil; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.List; + +public final class AABBVoxelShape extends VoxelShape { + + public final AABB aabb; + + public AABBVoxelShape(AABB aabb) { + super(Shapes.getFullUnoptimisedCube().shape); + this.aabb = aabb; + } + + @Override + public boolean isEmpty() { + return CollisionUtil.isEmpty(this.aabb); + } + + @Override + public double min(Direction.Axis enumdirection_enumaxis) { + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.aabb.minX; + case 1: + return this.aabb.minY; + case 2: + return this.aabb.minZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public double max(Direction.Axis enumdirection_enumaxis) { + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.aabb.maxX; + case 1: + return this.aabb.maxY; + case 2: + return this.aabb.maxZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public AABB bounds() { + return this.aabb; + } + + // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis. + @Override + protected double get(Direction.Axis enumdirection_enumaxis, int i) { + switch (enumdirection_enumaxis.ordinal() | (i << 2)) { + case (0 | (0 << 2)): + return this.aabb.minX; + case (1 | (0 << 2)): + return this.aabb.minY; + case (2 | (0 << 2)): + return this.aabb.minZ; + case (0 | (1 << 2)): + return this.aabb.maxX; + case (1 | (1 << 2)): + return this.aabb.maxY; + case (2 | (1 << 2)): + return this.aabb.maxZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + private DoubleList cachedListX; + private DoubleList cachedListY; + private DoubleList cachedListZ; + + @Override + protected DoubleList getCoords(Direction.Axis enumdirection_enumaxis) { + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.cachedListX == null ? this.cachedListX = DoubleArrayList.wrap(new double[] { this.aabb.minX, this.aabb.maxX }) : this.cachedListX; + case 1: + return this.cachedListY == null ? this.cachedListY = DoubleArrayList.wrap(new double[] { this.aabb.minY, this.aabb.maxY }) : this.cachedListY; + case 2: + return this.cachedListZ == null ? this.cachedListZ = DoubleArrayList.wrap(new double[] { this.aabb.minZ, this.aabb.maxZ }) : this.cachedListZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public VoxelShape move(double d0, double d1, double d2) { + return new AABBVoxelShape(this.aabb.move(d0, d1, d2)); + } + + @Override + public VoxelShape optimize() { + if (this.isEmpty()) { + return Shapes.empty(); + } else if (this == Shapes.BLOCK_OPTIMISED || this.aabb.equals(Shapes.BLOCK_OPTIMISED.aabb)) { + return Shapes.BLOCK_OPTIMISED; + } + return this; + } + + @Override + public void forAllBoxes(Shapes.DoubleLineConsumer voxelshapes_a) { + voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ); + } + + @Override + public List toAabbs() { // getAABBs + List ret = new ArrayList<>(1); + ret.add(this.aabb); + return ret; + } + + @Override + protected int findIndex(Direction.Axis enumdirection_enumaxis, double d0) { // findPointIndexAfterOffset + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return d0 < this.aabb.maxX ? (d0 < this.aabb.minX ? -1 : 0) : 1; + case 1: + return d0 < this.aabb.maxY ? (d0 < this.aabb.minY ? -1 : 0) : 1; + case 2: + return d0 < this.aabb.maxZ ? (d0 < this.aabb.minZ ? -1 : 0) : 1; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + protected VoxelShape calculateFace(Direction direction) { + if (this.isEmpty()) { + return Shapes.empty(); + } + if (this == Shapes.BLOCK_OPTIMISED) { + return this; + } + switch (direction) { + case EAST: // +X + case WEST: { // -X + final double from = direction == Direction.EAST ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; + if (from > this.aabb.maxX || this.aabb.minX > from) { + return Shapes.empty(); + } + return new AABBVoxelShape(new AABB(0.0, this.aabb.minY, this.aabb.minZ, 1.0, this.aabb.maxY, this.aabb.maxZ)).optimize(); + } + case UP: // +Y + case DOWN: { // -Y + final double from = direction == Direction.UP ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; + if (from > this.aabb.maxY || this.aabb.minY > from) { + return Shapes.empty(); + } + return new AABBVoxelShape(new AABB(this.aabb.minX, 0.0, this.aabb.minZ, this.aabb.maxX, 1.0, this.aabb.maxZ)).optimize(); + } + case SOUTH: // +Z + case NORTH: { // -Z + final double from = direction == Direction.SOUTH ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; + if (from > this.aabb.maxZ || this.aabb.minZ > from) { + return Shapes.empty(); + } + return new AABBVoxelShape(new AABB(this.aabb.minX, this.aabb.minY, 0.0, this.aabb.maxX, this.aabb.maxY, 1.0)).optimize(); + } + default: { + throw new IllegalStateException("Unknown axis requested"); + } + } + } + + @Override + public double collide(Direction.Axis enumdirection_enumaxis, AABB axisalignedbb, double d0) { + if (CollisionUtil.isEmpty(this.aabb) || CollisionUtil.isEmpty(axisalignedbb)) { + return d0; + } + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return CollisionUtil.collideX(this.aabb, axisalignedbb, d0); + case 1: + return CollisionUtil.collideY(this.aabb, axisalignedbb, d0); + case 2: + return CollisionUtil.collideZ(this.aabb, axisalignedbb, d0); + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public boolean intersects(AABB axisalingedbb) { + return CollisionUtil.voxelShapeIntersect(this.aabb, axisalingedbb); + } +} diff --git a/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java new file mode 100644 index 0000000000000000000000000000000000000000..ad711b6c0628a9cd93ff0d5484769807e5e5b9c0 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java @@ -0,0 +1,500 @@ +package com.tuinity.tuinity.world; + +import com.destroystokyo.paper.util.maplist.EntityList; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.phys.AABB; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public final class ChunkEntitySlices { + + protected final int minSection; + protected final int maxSection; + protected final int chunkX; + protected final int chunkZ; + protected final ServerLevel world; + + protected final EntityCollectionBySection allEntities; + protected final EntityCollectionBySection hardCollidingEntities; + protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; + protected final EntityList entities = new EntityList(); + + public ChunkHolder.FullChunkStatus status; + + // TODO implement container search optimisations + + public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus status, + final int minSection, final int maxSection) { // inclusive, inclusive + this.minSection = minSection; + this.maxSection = maxSection; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + + this.allEntities = new EntityCollectionBySection(this); + this.hardCollidingEntities = new EntityCollectionBySection(this); + this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); + + this.status = status; + } + + // Tuinity start - optimise CraftChunk#getEntities + public org.bukkit.entity.Entity[] getChunkEntities() { + List ret = new java.util.ArrayList<>(); + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + if (entity == null) { + continue; + } + final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); + if (bukkit != null && bukkit.isValid()) { + ret.add(bukkit); + } + } + + return ret.toArray(new org.bukkit.entity.Entity[0]); + } + // Tuinity end - optimise CraftChunk#getEntities + + public boolean isEmpty() { + return this.entities.size() == 0; + } + + private void updateTicketLevels() { + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + entity.chunkStatus = this.status; + } + } + + public synchronized void updateStatus(final ChunkHolder.FullChunkStatus status) { + this.status = status; + this.updateTicketLevels(); + } + + public synchronized void addEntity(final Entity entity, final int chunkSection) { + if (!this.entities.add(entity)) { + return; + } + entity.chunkStatus = this.status; + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.addEntity(entity, sectionIndex); + + if (entity.hardCollides()) { + this.hardCollidingEntities.addEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().addEntity(entity, sectionIndex); + } + } + } + + public synchronized void removeEntity(final Entity entity, final int chunkSection) { + if (!this.entities.remove(entity)) { + return; + } + entity.chunkStatus = ChunkHolder.FullChunkStatus.INACCESSIBLE; + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.removeEntity(entity, sectionIndex); + + if (entity.hardCollides()) { + this.hardCollidingEntities.removeEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().removeEntity(entity, sectionIndex); + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.hardCollidingEntities.getEntities(except, box, into, predicate); + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); + } + + protected EntityCollectionBySection initClass(final Class clazz) { + final EntityCollectionBySection ret = new EntityCollectionBySection(this); + + for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { + final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; + if (sectionEntities == null) { + continue; + } + + final Entity[] storage = sectionEntities.storage; + + for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (clazz.isInstance(entity)) { + ret.addEntity(entity, sectionIndex); + } + } + } + + return ret; + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } else { + synchronized (this) { + this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); + } + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } + } + + public synchronized void updateEntity(final Entity entity) { + /*// TODO + if (prev aabb != entity.getBoundingBox()) { + this.entityMap.delete(entity, prev aabb); + this.entityMap.insert(entity, prev aabb = entity.getBoundingBox()); + }*/ + } + + protected static final class BasicEntityList { + + protected static final Entity[] EMPTY = new Entity[0]; + protected static final int DEFAULT_CAPACITY = 4; + + protected E[] storage; + protected int size; + + public BasicEntityList() { + this(0); + } + + public BasicEntityList(final int cap) { + this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); + } + + public boolean isEmpty() { + return this.size == 0; + } + + public int size() { + return this.size; + } + + private void resize() { + if (this.storage == EMPTY) { + this.storage = (E[])new Entity[DEFAULT_CAPACITY]; + } else { + this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); + } + } + + public void add(final E entity) { + final int idx = this.size++; + if (idx >= this.storage.length) { + this.resize(); + this.storage[idx] = entity; + } else { + this.storage[idx] = entity; + } + } + + public int indexOf(final E entity) { + final E[] storage = this.storage; + + for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { + if (storage[i] == entity) { + return i; + } + } + + return -1; + } + + public boolean remove(final E entity) { + final int idx = this.indexOf(entity); + if (idx == -1) { + return false; + } + + final int size = --this.size; + final E[] storage = this.storage; + if (idx != size) { + System.arraycopy(storage, idx + 1, storage, idx, size - idx); + } + + storage[size] = null; + + return true; + } + + public boolean has(final E entity) { + return this.indexOf(entity) != -1; + } + } + + protected static final class EntityCollectionBySection { + + protected final ChunkEntitySlices manager; + protected final long[] nonEmptyBitset; + protected final BasicEntityList[] entitiesBySection; + protected int count; + + public EntityCollectionBySection(final ChunkEntitySlices manager) { + this.manager = manager; + + final int sectionCount = manager.maxSection - manager.minSection + 1; + + this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE + this.entitiesBySection = new BasicEntityList[sectionCount]; + } + + public void addEntity(final Entity entity, final int sectionIndex) { + BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list != null && list.has(entity)) { + return; + } + + if (list == null) { + this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); + this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); + } + + list.add(entity); + ++this.count; + } + + public void removeEntity(final Entity entity, final int sectionIndex) { + final BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list == null || !list.remove(entity)) { + return; + } + + --this.count; + + if (list.isEmpty()) { + this.entitiesBySection[sectionIndex] = null; + this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + } + } + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { + if (part == except || !part.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { + if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test((T)entity)) { + continue; + } + + into.add((T)entity); + } + } + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f188ff6b08abddd06a3120fb15825e0f71196893 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java @@ -0,0 +1,391 @@ +package com.tuinity.tuinity.world; + +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.AABB; +import java.util.List; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Predicate; + +public final class EntitySliceManager { + + protected static final int REGION_SHIFT = 5; + protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; + protected static final int REGION_SIZE = 1 << REGION_SHIFT; + + public final ServerLevel world; + + private final StampedLock stateLock = new StampedLock(); + protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(64, 0.7f); + + private final int minSection; // inclusive + private final int maxSection; // inclusive + + protected final Long2ObjectOpenHashMap statusMap = new Long2ObjectOpenHashMap<>(); + { + this.statusMap.defaultReturnValue(ChunkHolder.FullChunkStatus.INACCESSIBLE); + } + + public EntitySliceManager(final ServerLevel world) { + this.world = world; + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + } + + public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) { + if (newStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { + this.statusMap.remove(CoordinateUtils.getChunkKey(x, z)); + } else { + this.statusMap.put(CoordinateUtils.getChunkKey(x, z), newStatus); + final ChunkEntitySlices slices = this.getChunk(x, z); + if (slices != null) { + slices.updateStatus(newStatus); + } + } + } + + public synchronized void addEntity(final Entity entity) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); + final int sectionZ = pos.getZ() >> 4; + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + slices.addEntity(entity, sectionY); + + entity.sectionX = sectionX; + entity.sectionY = sectionY; + entity.sectionZ = sectionZ; + } + + public synchronized void removeEntity(final Entity entity) { + final ChunkEntitySlices slices = this.getChunk(entity.sectionX, entity.sectionZ); + slices.removeEntity(entity, entity.sectionY); + if (slices.isEmpty()) { + this.removeChunk(entity.sectionX, entity.sectionZ); + } + } + + public void moveEntity(final Entity entity) { + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; + final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); + final int newSectionZ = newPos.getZ() >> 4; + + if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { + return; + } + + synchronized (this) { + // are we changing chunks? + if (newSectionX != entity.sectionX || newSectionZ != entity.sectionZ) { + final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); + final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); + synchronized (old) { + old.removeEntity(entity, entity.sectionY); + if (old.isEmpty()) { + this.removeChunk(entity.sectionX, entity.sectionZ); + } + } + + synchronized (slices) { + slices.addEntity(entity, newSectionY); + + entity.sectionX = newSectionX; + entity.sectionY = newSectionY; + entity.sectionZ = newSectionZ; + } + } else { + final ChunkEntitySlices slices = this.getChunk(newSectionX, newSectionZ); + // same chunk + synchronized (slices) { + slices.removeEntity(entity, entity.sectionY); + slices.addEntity(entity, newSectionY); + } + entity.sectionY = newSectionY; + } + } + + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(except, box, into, predicate); + } + } + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getHardCollidingEntities(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(type, box, (List)into, (Predicate)predicate); + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(clazz, except, box, into, predicate); + } + } + } + } + } + + public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + if (region == null) { + return null; + } + + return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); + } + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + ChunkEntitySlices ret; + if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { + ret = new ChunkEntitySlices(this.world, chunkX, chunkZ, this.statusMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)), + WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + return ret; + } + + public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { + final long key = CoordinateUtils.getChunkKey(regionX, regionZ); + final long attempt = this.stateLock.tryOptimisticRead(); + if (attempt != 0L) { + try { + final ChunkSlicesRegion ret = this.regions.get(key); + + if (this.stateLock.validate(attempt)) { + return ret; + } + } catch (final Error error) { + throw error; + } catch (final Throwable thr) { + // ignore + } + } + + this.stateLock.readLock(); + try { + return this.regions.get(key); + } finally { + this.stateLock.tryUnlockRead(); + } + } + + public synchronized void removeChunk(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + final ChunkSlicesRegion region = this.regions.get(key); + final int remaining = region.remove(relIndex); + + if (remaining == 0) { + this.stateLock.writeLock(); + try { + this.regions.remove(key); + } finally { + this.stateLock.tryUnlockWrite(); + } + } + } + + public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkSlicesRegion region = this.regions.get(key); + if (region != null) { + region.add(relIndex, slices); + } else { + region = new ChunkSlicesRegion(); + region.add(relIndex, slices); + this.stateLock.writeLock(); + try { + this.regions.put(key, region); + } finally { + this.stateLock.tryUnlockWrite(); + } + } + } + + public static final class ChunkSlicesRegion { + + protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; + protected int sliceCount; + + public ChunkEntitySlices get(final int index) { + return this.slices[index]; + } + + public int remove(final int index) { + final ChunkEntitySlices slices = this.slices[index]; + if (slices == null) { + throw new IllegalStateException(); + } + + this.slices[index] = null; + + return --this.sliceCount; + } + + public void add(final int index, final ChunkEntitySlices slices) { + final ChunkEntitySlices curr = this.slices[index]; + if (curr != null) { + throw new IllegalStateException(); + } + + this.slices[index] = slices; + + ++this.sliceCount; + } + } +} diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java index b70aa66732fb5e957aed0901f4c76358b2c56f8e..b01d7da333bac7820e42b6f645634a15ef88ae4f 100644 --- a/src/main/java/net/minecraft/core/BlockPos.java +++ b/src/main/java/net/minecraft/core/BlockPos.java @@ -478,9 +478,9 @@ public class BlockPos extends Vec3i { } public BlockPos.MutableBlockPos set(int x, int y, int z) { - this.setX(x); - this.setY(y); - this.setZ(z); + this.x = x; // Tuinity - force inline + this.y = y; // Tuinity - force inline + this.z = z; // Tuinity - force inline return this; } @@ -544,19 +544,19 @@ public class BlockPos extends Vec3i { // Paper start - comment out useless overrides @Override - TODO figure out why this is suddenly important to keep @Override public BlockPos.MutableBlockPos setX(int i) { - super.setX(i); + this.x = i; // Tuinity return this; } @Override public BlockPos.MutableBlockPos setY(int i) { - super.setY(i); + this.y = i; // Tuinity return this; } @Override public BlockPos.MutableBlockPos setZ(int i) { - super.setZ(i); + this.z = i; // Tuinity return this; } // Paper end diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java index 5e09890ba2fe326503a49b2dbec09845f5c8c5eb..3ad3652f8074de10222fb01c50548b4312103cc3 100644 --- a/src/main/java/net/minecraft/core/Vec3i.java +++ b/src/main/java/net/minecraft/core/Vec3i.java @@ -17,9 +17,9 @@ public class Vec3i implements Comparable { return IntStream.of(vec3i.getX(), vec3i.getY(), vec3i.getZ()); }); public static final Vec3i ZERO = new Vec3i(0, 0, 0); - private int x; - private int y; - private int z; + protected int x; // Tuinity - protected + protected int y; // Tuinity - protected + protected int z; // Tuinity - protected // Paper start public boolean isValidLocation(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) { @@ -84,17 +84,17 @@ public class Vec3i implements Comparable { return this.z; } - public Vec3i setX(int x) { + protected Vec3i setX(int x) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type this.x = x; return this; } - public Vec3i setY(int y) { + protected Vec3i setY(int y) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type this.y = y; return this; } - public Vec3i setZ(int z) { + protected Vec3i setZ(int z) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type this.z = z; return this; } diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java index c203a78a28e6457bd25b34b5c5ecaa35e3f9211e..0232fb8123c7dfa735802442f8575c6ce1566847 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java @@ -49,6 +49,8 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; + +import io.netty.util.concurrent.AbstractEventExecutor; // Tuinity public class Connection extends SimpleChannelInboundHandler> { private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F; @@ -93,6 +95,77 @@ public class Connection extends SimpleChannelInboundHandler> { public boolean queueImmunity = false; public ConnectionProtocol protocol; // Paper end + // Tuinity start - add pending task queue + private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); + public void execute(final Runnable run) { + if (this.channel == null || !this.channel.isRegistered()) { + run.run(); + return; + } + final boolean queue = !this.queue.isEmpty(); + if (!queue) { + this.channel.eventLoop().execute(run); + } else { + this.pendingTasks.add(run); + if (this.queue.isEmpty()) { + // something flushed async, dump tasks now + Runnable r; + while ((r = this.pendingTasks.poll()) != null) { + this.channel.eventLoop().execute(r); + } + } + } + } + // Tuinity end - add pending task queue + + // Tuinity start - allow controlled flushing + volatile boolean canFlush = true; + private final java.util.concurrent.atomic.AtomicInteger packetWrites = new java.util.concurrent.atomic.AtomicInteger(); + private int flushPacketsStart; + private final Object flushLock = new Object(); + + public void disableAutomaticFlush() { + synchronized (this.flushLock) { + this.flushPacketsStart = this.packetWrites.get(); // must be volatile and before canFlush = false + this.canFlush = false; + } + } + + public void enableAutomaticFlush() { + synchronized (this.flushLock) { + this.canFlush = true; + if (this.packetWrites.get() != this.flushPacketsStart) { // must be after canFlush = true + this.flush(); // only make the flush call if we need to + } + } + } + + private final void flush() { + if (this.channel.eventLoop().inEventLoop()) { + this.channel.flush(); + } else { + this.channel.eventLoop().execute(() -> { + this.channel.flush(); + }); + } + } + // Tuinity end - allow controlled flushing + // Tuinity start - packet limiter + protected final Object PACKET_LIMIT_LOCK = new Object(); + protected final com.tuinity.tuinity.util.IntervalledCounter allPacketCounts = com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit != null ? new com.tuinity.tuinity.util.IntervalledCounter( + (long)(com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.packetLimitInterval * 1.0e9) + ) : null; + protected final java.util.Map>, com.tuinity.tuinity.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>(); + + private boolean stopReadingPackets; + private void killForPacketSpam() { + this.sendPacket(new ClientboundDisconnectPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]), (future) -> { + this.disconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]); + }); + this.setReadOnly(); + this.stopReadingPackets = true; + } + // Tuinity end - packet limiter public Connection(PacketFlow side) { this.receiving = side; @@ -173,6 +246,45 @@ public class Connection extends SimpleChannelInboundHandler> { protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet packet) { if (this.channel.isOpen()) { + // Tuinity start - packet limiter + if (this.stopReadingPackets) { + return; + } + if (this.allPacketCounts != null || + com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.containsKey(packet.getClass())) { + long time = System.nanoTime(); + synchronized (PACKET_LIMIT_LOCK) { + if (this.allPacketCounts != null) { + this.allPacketCounts.updateAndAdd(1, time); + if (this.allPacketCounts.getRate() >= com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.maxPacketRate) { + this.killForPacketSpam(); + return; + } + } + + for (Class check = packet.getClass(); check != Object.class; check = check.getSuperclass()) { + com.tuinity.tuinity.config.TuinityConfig.PacketLimit packetSpecificLimit = + com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.get(check); + if (packetSpecificLimit == null) { + continue; + } + com.tuinity.tuinity.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> { + return new com.tuinity.tuinity.util.IntervalledCounter((long)(packetSpecificLimit.packetLimitInterval * 1.0e9)); + }); + counter.updateAndAdd(1, time); + if (counter.getRate() >= packetSpecificLimit.maxPacketRate) { + switch (packetSpecificLimit.violateAction) { + case DROP: + return; + case KICK: + this.killForPacketSpam(); + return; + } + } + } + } + } + // Tuinity end - packet limiter try { Connection.genericsFtw(packet, this.packetListener); } catch (RunningOnDifferentThreadException cancelledpackethandleexception) { @@ -255,7 +367,7 @@ public class Connection extends SimpleChannelInboundHandler> { net.minecraft.server.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty()) ))) { - this.sendPacket(packet, callback); + this.writePacket(packet, callback, null); // Tuinity return; } // write the packets to the queue, then flush - antixray hooks there already @@ -279,6 +391,14 @@ public class Connection extends SimpleChannelInboundHandler> { } private void sendPacket(Packet packet, @Nullable GenericFutureListener> callback) { + // Tuinity start - add flush parameter + this.writePacket(packet, callback, Boolean.TRUE); + } + private void writePacket(Packet packet, @Nullable GenericFutureListener> callback, Boolean flushConditional) { + this.packetWrites.getAndIncrement(); // must be befeore using canFlush + boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue(); + final boolean flush = effectiveFlush || packet instanceof net.minecraft.network.protocol.game.ClientboundKeepAlivePacket || packet instanceof ClientboundDisconnectPacket; // no delay for certain packets + // Tuinity end - add flush parameter ConnectionProtocol enumprotocol = ConnectionProtocol.getProtocolForPacket(packet); ConnectionProtocol enumprotocol1 = this.getCurrentProtocol(); @@ -289,16 +409,31 @@ public class Connection extends SimpleChannelInboundHandler> { } if (this.channel.eventLoop().inEventLoop()) { - this.a(packet, callback, enumprotocol, enumprotocol1); + this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter } else { + // Tuinity start - optimise packets that are not flushed + // note: since the type is not dynamic here, we need to actually copy the old executor code + // into two branches. On conflict, just re-copy - no changes were made inside the executor code. + if (!flush) { + AbstractEventExecutor.LazyRunnable run = () -> { + this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter + }; + this.channel.eventLoop().execute(run); + } else { // Tuinity end - optimise packets that are not flushed this.channel.eventLoop().execute(() -> { - this.a(packet, callback, enumprotocol, enumprotocol1); + this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter // Tuinity - diff on change }); + } // Tuinity } } private void a(Packet packet, @Nullable GenericFutureListener> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1) { + // Tuinity start - add flush parameter + this.a(packet, genericfuturelistener, enumprotocol, enumprotocol1, true); + } + private void a(Packet packet, @Nullable GenericFutureListener> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1, boolean flush) { + // Tuinity end - add flush parameter if (enumprotocol != enumprotocol1) { this.setProtocol(enumprotocol); } @@ -312,7 +447,7 @@ public class Connection extends SimpleChannelInboundHandler> { try { // Paper end - ChannelFuture channelfuture = this.channel.writeAndFlush(packet); + ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter if (genericfuturelistener != null) { channelfuture.addListener(genericfuturelistener); @@ -353,7 +488,12 @@ public class Connection extends SimpleChannelInboundHandler> { return false; } private boolean processQueue() { + try { // Tuinity - add pending task queue if (this.queue.isEmpty()) return true; + // Tuinity start - make only one flush call per sendPacketQueue() call + final boolean needsFlush = this.canFlush; + boolean hasWrotePacket = false; + // Tuinity end - make only one flush call per sendPacketQueue() call // If we are on main, we are safe here in that nothing else should be processing queue off main anymore // But if we are not on main due to login/status, the parent is synchronized on packetQueue java.util.Iterator iterator = this.queue.iterator(); @@ -361,19 +501,31 @@ public class Connection extends SimpleChannelInboundHandler> { PacketHolder queued = iterator.next(); // poll -> peek // Fix NPE (Spigot bug caused by handleDisconnection()) - if (queued == null) { + if (false && queued == null) { // Tuinity - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here return true; } Packet packet = queued.packet; if (!packet.isReady()) { + // Tuinity start - make only one flush call per sendPacketQueue() call + if (hasWrotePacket && (needsFlush || this.canFlush)) { + this.flush(); + } + // Tuinity end - make only one flush call per sendPacketQueue() call return false; } else { iterator.remove(); - this.sendPacket(packet, queued.listener); + this.writePacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Tuinity - make only one flush call per sendPacketQueue() call + hasWrotePacket = true; // Tuinity - make only one flush call per sendPacketQueue() call } } return true; + } finally { // Tuinity start - add pending task queue + Runnable r; + while ((r = this.pendingTasks.poll()) != null) { + this.channel.eventLoop().execute(r); + } + } // Tuinity end - add pending task queue } // Paper end @@ -396,7 +548,14 @@ public class Connection extends SimpleChannelInboundHandler> { } if (this.packetListener instanceof ServerGamePacketListenerImpl) { + // Tuinity start - detailed watchdog information + net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); + try { + // Tuinity end - detailed watchdog information ((ServerGamePacketListenerImpl) this.packetListener).tick(); + } finally { // Tuinity start - detailed watchdog information + net.minecraft.network.protocol.PacketUtils.packetProcessing.pop(); + } // Tuinity start - detailed watchdog information } if (!this.isConnected() && !this.disconnectionHandled) { diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java index bcf53ec07b8eeec7a88fb67e6fb908362e6f51b0..7265bee436d61d33645fa2d9ed4240529834dbf5 100644 --- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java +++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java @@ -20,6 +20,24 @@ public class PacketUtils { private static final Logger LOGGER = LogManager.getLogger(); + // Tuinity start - detailed watchdog information + public static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>(); + static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong(); + + public static long getTotalProcessedPackets() { + return totalMainThreadPacketsProcessed.get(); + } + + public static java.util.List getCurrentPacketProcessors() { + java.util.List ret = new java.util.ArrayList<>(4); + for (PacketListener listener : packetProcessing) { + ret.add(listener); + } + + return ret; + } + // Tuinity end - detailed watchdog information + public PacketUtils() {} public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { @@ -30,6 +48,8 @@ public class PacketUtils { if (!engine.isSameThread()) { Timing timing = MinecraftTimings.getPacketTiming(packet); // Paper - timings engine.execute(() -> { + packetProcessing.push(listener); // Tuinity - detailed watchdog information + try { // Tuinity - detailed watchdog information if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590 if (listener.getConnection().isConnected()) { try (Timing ignored = timing.startTiming()) { // Paper - timings @@ -53,6 +73,12 @@ public class PacketUtils { } else { PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet); } + // Tuinity start - detailed watchdog information + } finally { + totalMainThreadPacketsProcessed.getAndIncrement(); + packetProcessing.pop(); + } + // Tuinity end - detailed watchdog information }); throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java index d8be2ad889f46491e50404916fb4ae0de5f42098..5b9ea0af272c5e7a85d2a954a9214bf875bc7e9f 100644 --- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java +++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java @@ -32,25 +32,17 @@ public class ClientboundLightUpdatePacket implements Packet { - if (remainingSends.get() == 0) { - cleaner1.run(); - cleaner2.run(); - } - }, "Light Packet Release"); - } + // Tuinity - rewrite light engine } @Override public boolean hasFinishListener() { - return true; + return false; // Tuinity - rewrite light engine } // Paper end @@ -63,8 +55,8 @@ public class ClientboundLightUpdatePacket implements Packet com.mojang.serialization.MapCodec fieldWithFallbacks(com.mojang.serialization.Codec codec, String name, String ...fallback) { + return com.mojang.serialization.MapCodec.of( + new com.mojang.serialization.codecs.FieldEncoder<>(name, codec), + new FieldFallbackDecoder<>(name, java.util.Arrays.asList(fallback), codec), + () -> "FieldFallback[" + name + ": " + codec.toString() + "]" + ); + } + + // This is likely a common occurrence, sadly + public static final class FieldFallbackDecoder extends com.mojang.serialization.MapDecoder.Implementation { + protected final String name; + protected final List fallback; + private final com.mojang.serialization.Decoder elementCodec; + + public FieldFallbackDecoder(final String name, final List fallback, final com.mojang.serialization.Decoder elementCodec) { + this.name = name; + this.fallback = fallback; + this.elementCodec = elementCodec; + } + + @Override + public com.mojang.serialization.DataResult decode(final com.mojang.serialization.DynamicOps ops, final com.mojang.serialization.MapLike input) { + T value = input.get(name); + if (value == null) { + for (String fall : fallback) { + value = input.get(fall); + if (value != null) { + break; + } + } + if (value == null) { + return com.mojang.serialization.DataResult.error("No key " + name + " in " + input); + } + } + return elementCodec.parse(ops, value); + } + + @Override + public java.util.stream.Stream keys(final com.mojang.serialization.DynamicOps ops) { + return java.util.stream.Stream.of(ops.createString(name)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final FieldFallbackDecoder that = (FieldFallbackDecoder)o; + return java.util.Objects.equals(name, that.name) && java.util.Objects.equals(elementCodec, that.elementCodec) + && java.util.Objects.equals(fallback, that.fallback); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(name, fallback, elementCodec); + } + + @Override + public String toString() { + return "FieldDecoder[" + name + ": " + elementCodec + ']'; + } + } } diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java index cfd43069ee2b6f79afb12e10d223f6bf75100034..75c31ec2553c0959f1ac34b554a39a73144da8bd 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java @@ -57,7 +57,7 @@ import org.apache.logging.log4j.Logger; // CraftBukkit start import net.minecraft.SharedConstants; -public class Main { +public class Main { // private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 7682bd72c3932a9b20f14e552711d74f70b969b1..f3bf6270c7735869083559f907c65d95144dbe6f 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -297,6 +297,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public boolean serverAutoSave = false; // Paper @@ -331,6 +332,76 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { + lastMidTickExecuteFailure = currTime; + } + + // note: negative values reduce the time + long overuse = diff - MAX_CHUNK_EXEC_TIME; + if (overuse >= (10L * 1000L * 1000L)) { // 10ms + // make sure something like a GC or dumb plugin doesn't screw us over... + overuse = 10L * 1000L * 1000L; // 10ms + } + + double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; + long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); + + lastMidTickExecute = currTime + extraSleep; + return; + } + } + } finally { + co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming(); + } + } + // Tuinity end - execute chunk tasks mid tick + public MinecraftServer(OptionSet options, DataPackConfig datapackconfiguration, Thread thread, RegistryAccess.RegistryHolder iregistrycustom_dimension, LevelStorageSource.LevelStorageAccess convertable_conversionsession, WorldData savedata, PackRepository resourcepackrepository, Proxy proxy, DataFixer datafixer, ServerResources datapackresources, @Nullable MinecraftSessionService minecraftsessionservice, @Nullable GameProfileRepository gameprofilerepository, @Nullable GameProfileCache usercache, ChunkProgressListenerFactory worldloadlistenerfactory) { super("Server"); SERVER = this; // Paper - better singleton @@ -1133,6 +1204,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - midTickLoadChunks(); // will only do loads since we are still considered !canSleepForTick + // Tuinity - replace logic return !this.canOversleep(); }); isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); @@ -1457,6 +1516,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Spigot - Spigot > // CraftBukkit - cb > vanilla! + return "Tuinity"; // Tuinity - Tuinity > //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! } public SystemReport fillSystemReport(SystemReport systemreport) { diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index 6a1fd7a983724d9e16e8aef06052108ba7ed46f1..9bb0ff063da162f2b5c91d367d9555c1cf1a3ab1 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -222,6 +222,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.getClass(); // load mappings for stacktrace deobf // Paper end + com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config this.setPvpAllowed(dedicatedserverproperties.pvp); this.setFlightAllowed(dedicatedserverproperties.allowFlight); diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index 9fe60d058ea1702930981dbd06093dc594e6bf8e..2dd5909a08a8d4bc250b36d297ef9f3f04aed8bf 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -41,6 +41,8 @@ import net.minecraft.world.level.lighting.LevelLightEngine; import net.minecraft.server.MinecraftServer; // CraftBukkit end +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; // Tuinity + public class ChunkHolder { public static final Either UNLOADED_CHUNK = Either.right(ChunkHolder.ChunkLoadingFailure.UNLOADED); @@ -55,7 +57,7 @@ public class ChunkHolder { private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage - private CompletableFuture chunkToSave; + public CompletableFuture chunkToSave; // Tuinity - public @Nullable private final DebugBuffer chunkToSaveHistory; public int oldTicketLevel; @@ -238,6 +240,12 @@ public class ChunkHolder { long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos); this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); + // Tuinity start - optimise checkDespawn + LevelChunk chunk = this.getFullChunkUnchecked(); + if (chunk != null) { + chunk.updateGeneralAreaCache(); + } + // Tuinity end - optimise checkDespawn } // Paper end - optimise isOutsideOfRange long lastAutoSaveTime; // Paper - incremental autosave @@ -388,7 +396,7 @@ public class ChunkHolder { if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 if (this.changedBlocksPerSection[i] == null) { this.hasChangedSections = true; - this.changedBlocksPerSection[i] = new ShortArraySet(); + this.changedBlocksPerSection[i] = new ShortOpenHashSet(); // Tuinity - use a set to make setting constant-time } this.changedBlocksPerSection[i].add(SectionPos.sectionRelativePos(pos)); @@ -489,7 +497,7 @@ public class ChunkHolder { // Paper start - per player view distance // there can be potential desync with player's last mapped section and the view distance map, so use the // view distance map here. - com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerViewDistanceBroadcastMap; + com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Tuinity - replace old player chunk manager com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); if (players == null) { return; @@ -506,6 +514,7 @@ public class ChunkHolder { int viewDistance = viewDistanceMap.getLastViewDistance(player); long lastPosition = viewDistanceMap.getLastCoordinate(player); + if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Tuinity - replace player chunk management int distX = Math.abs(net.minecraft.server.MCUtil.getCoordinateX(lastPosition) - this.pos.x); int distZ = Math.abs(net.minecraft.server.MCUtil.getCoordinateZ(lastPosition) - this.pos.z); @@ -522,6 +531,7 @@ public class ChunkHolder { continue; } ServerPlayer player = (ServerPlayer)temp; + if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Tuinity - replace player chunk management player.connection.send(packet); } } @@ -597,7 +607,13 @@ public class ChunkHolder { CompletableFuture completablefuture1 = new CompletableFuture(); completablefuture1.thenRunAsync(() -> { + // Tuinity start - do not allow ticket level changes + boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk; + this.chunkMap.unloadingPlayerChunk = true; + try { + // Tuinity end - do not allow ticket level changes playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); + } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes }, executor); this.pendingFullStateConfirmation = completablefuture1; completablefuture.thenAccept((either) -> { @@ -607,12 +623,23 @@ public class ChunkHolder { }); } + private boolean loadCallbackScheduled = false; + private boolean unloadCallbackScheduled = false; + private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) { this.pendingFullStateConfirmation.cancel(false); + // Tuinity start - do not allow ticket level changes + boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk; + this.chunkMap.unloadingPlayerChunk = true; + try { // Tuinity end - do not allow ticket level changes playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); + } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes } - protected void updateFutures(ChunkMap chunkStorage, Executor executor) { + protected long updateCount; // Tuinity - correctly handle recursion + public void updateFutures(ChunkMap chunkStorage, Executor executor) { // Tuinity + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticket level update"); // Tuinity + long updateCount = ++this.updateCount; // Tuinity - correctly handle recursion ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel); ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel); boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; @@ -622,10 +649,23 @@ public class ChunkHolder { // CraftBukkit start // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { - this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main + this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity LevelChunk chunk = (LevelChunk)either.left().orElse(null); - if (chunk != null) { + if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Tuinity - only invoke unload if load was called + // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.unloadCallbackScheduled) { + return; + } + ChunkHolder.this.unloadCallbackScheduled = true; + // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... chunkStorage.callbackExecutor.execute(() -> { + // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + ChunkHolder.this.unloadCallbackScheduled = false; + if (ChunkHolder.this.ticketLevel <= 33) { + return; + } + // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. // These actions may however happen deferred, so we manually set the needsSaving flag already here. @@ -641,6 +681,12 @@ public class ChunkHolder { // Run callback right away if the future was already done chunkStorage.callbackExecutor.run(); + // Tuinity start - correctly handle recursion + if (this.updateCount != updateCount) { + // something else updated ticket level for us. + return; + } + // Tuinity end - correctly handle recursion } // CraftBukkit end CompletableFuture completablefuture; @@ -681,7 +727,8 @@ public class ChunkHolder { this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); // Paper start - cache ticking ready status - ensureMain(this.fullChunkFuture).thenAccept(either -> { // Paper - ensure main + this.fullChunkFuture.thenAccept(either -> { // Paper - ensure main // Tuinity - always fired on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full chunk future completion"); // Tuinity final Optional left = either.left(); if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { // note: Here is a very good place to add callbacks to logic waiting on this. @@ -712,7 +759,8 @@ public class ChunkHolder { this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); // Paper start - cache ticking ready status - ensureMain(this.tickingChunkFuture).thenAccept(either -> { // Paper - ensure main + this.tickingChunkFuture.thenAccept(either -> { // Paper - ensure main // Tuinity - always completed on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticking chunk future completion"); // Tuinity either.ifLeft(chunk -> { // note: Here is a very good place to add callbacks to logic waiting on this. ChunkHolder.this.isTickingReady = true; @@ -720,6 +768,9 @@ public class ChunkHolder { // Paper start - rewrite ticklistserver ChunkHolder.this.chunkMap.level.onChunkSetTicking(ChunkHolder.this.pos.x, ChunkHolder.this.pos.z); // Paper end - rewrite ticklistserver + // Tuinity start - ticking chunk set + ChunkHolder.this.chunkMap.level.getChunkSource().tickingChunks.add(chunk); + // Tuinity end - ticking chunk set }); }); // Paper end @@ -729,6 +780,12 @@ public class ChunkHolder { if (flag4 && !flag5) { this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + // Tuinity start - ticking chunk set + LevelChunk chunkIfCached = this.getFullChunkUnchecked(); + if (chunkIfCached != null) { + this.chunkMap.level.getChunkSource().tickingChunks.remove(chunkIfCached); + } + // Tuinity end - ticking chunk set } boolean flag6 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING); @@ -742,9 +799,13 @@ public class ChunkHolder { this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos); this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); // Paper start - cache ticking ready status - ensureMain(this.entityTickingChunkFuture).thenAccept(either -> { // Paper ensureMain + this.entityTickingChunkFuture.thenAccept(either -> { // Paper ensureMain // Tuinity - always completed on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async entity ticking chunk future completion"); // Tuinity either.ifLeft(chunk -> { ChunkHolder.this.isEntityTickingReady = true; + // Tuinity start - entity ticking chunk set + ChunkHolder.this.chunkMap.level.getChunkSource().entityTickingChunks.add(chunk); + // Tuinity end - entity ticking chunk set }); }); // Paper end @@ -754,6 +815,12 @@ public class ChunkHolder { if (flag6 && !flag7) { this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + // Tuinity start - entity ticking chunk set + LevelChunk chunkIfCached = this.getFullChunkUnchecked(); + if (chunkIfCached != null) { + this.chunkMap.level.getChunkSource().entityTickingChunks.remove(chunkIfCached); + } + // Tuinity end - entity ticking chunk set } if (!playerchunk_state1.isOrAfter(playerchunk_state)) { @@ -783,11 +850,19 @@ public class ChunkHolder { // CraftBukkit start // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { - this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main + this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity LevelChunk chunk = (LevelChunk)either.left().orElse(null); - if (chunk != null) { + if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Tuinity - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33 + // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.loadCallbackScheduled) { + return; + } + ChunkHolder.this.loadCallbackScheduled = true; + // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... chunkStorage.callbackExecutor.execute(() -> { - chunk.loadCallback(); + ChunkHolder.this.loadCallbackScheduled = false; // Tuinity - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Tuinity " }); } }).exceptionally((throwable) -> { diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 7f7bc04a30a0422b2d589adb488082c0aa5326dc..1430411a6f7ef3730d87c022774d7d623f2f415f 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -103,6 +103,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bukkit.entity.Player; // CraftBukkit +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Tuinity public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { @@ -115,8 +116,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public static final int MAX_VIEW_DISTANCE = 33; public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance(); // Paper start - faster copying - public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<>(); // Paper - faster copying - public final Long2ObjectLinkedOpenHashMap visibleChunkMap = new ProtectedVisibleChunksMap(); // Paper - faster copying + // Tuinity start - Don't copy + public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>(); + // Tuinity end - Don't copy private class ProtectedVisibleChunksMap extends com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy { @Override @@ -139,8 +141,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } // Paper end - public final com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy pendingVisibleChunks = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy(); // Paper - this is used if the visible chunks is updated while iterating only - public transient com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy visibleChunksClone; // Paper - used for async access of visible chunks, clone and cache only when needed + // Tuinity - Don't copy public static final int FORCED_TICKET_LEVEL = 31; // public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); // Paper - moved up // public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; // Paper - moved up @@ -148,7 +149,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final LongSet entitiesInLevel; public final ServerLevel level; private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop mainThreadExecutor; + public final BlockableEventLoop mainThreadExecutor; // Tuinity - public final java.util.concurrent.Executor mainInvokingExecutor; // Paper public final ChunkGenerator generator; public final Supplier overworldDataStorage; @@ -181,32 +182,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final CallbackExecutor callbackExecutor = new CallbackExecutor(); public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { - // Paper start - replace impl with recursive safe multi entry queue - // it's possible to schedule multiple tasks currently, so it's vital we change this impl - // If we recurse into the executor again, we will append to another queue, ensuring task order consistency - private java.util.Queue queue = new java.util.ArrayDeque<>(); // Paper - remove final + // Tuinity start - revert paper's change + private Runnable queued; @Override public void execute(Runnable runnable) { org.spigotmc.AsyncCatcher.catchOp("Callback Executor execute"); - if (this.queue == null) { - this.queue = new java.util.ArrayDeque<>(); + if (queued != null) { + MinecraftServer.LOGGER.fatal("Failed to schedule runnable", new IllegalStateException("Already queued")); // Paper - make sure this is printed + throw new IllegalStateException("Already queued"); } - this.queue.add(runnable); + queued = runnable; } + // Tuinity end - revert paper's change @Override public void run() { org.spigotmc.AsyncCatcher.catchOp("Callback Executor run"); - if (this.queue == null) { - return; - } - java.util.Queue queue = this.queue; - this.queue = null; - // Paper end - Runnable task; - while ((task = queue.poll()) != null) { // Paper + // Tuinity start - revert paper's change + Runnable task = queued; + queued = null; + if (task != null) { task.run(); + // Tuinity end - revert paper's change } } }; @@ -215,22 +213,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider final CallbackExecutor chunkLoadConversionCallbackExecutor = new CallbackExecutor(); // Paper // Paper start - distance maps private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); - // Paper start - no-tick view distance - int noTickViewDistance; - public final int getRawNoTickViewDistance() { - return this.noTickViewDistance; - } - public final int getEffectiveNoTickViewDistance() { - return this.noTickViewDistance == -1 ? this.getEffectiveViewDistance() : this.noTickViewDistance; - } - public final int getLoadViewDistance() { - return Math.max(this.getEffectiveViewDistance(), this.getEffectiveNoTickViewDistance()); - } - - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap; - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap; - // Paper end - no-tick view distance + public final com.tuinity.tuinity.chunk.PlayerChunkLoader playerChunkManager = new com.tuinity.tuinity.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Tuinity - replace chunk loader // Paper start - use distance map to optimise tracker public static boolean isLegacyTrackingEntity(Entity entity) { return entity.isLegacyTrackingEntity; @@ -239,7 +222,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // inlined EnumMap, TrackingRange.TrackingRangeType static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; - final int[] entityTrackerTrackRanges; + final int[] entityTrackerTrackRanges; public int getEntityTrackerRange(final int ordinal) { return this.entityTrackerTrackRanges[ordinal]; } // Tuinity - public read private int convertSpigotRangeToVanilla(final int vanilla) { return MinecraftServer.getServer().getScaledTrackingDistance(vanilla); @@ -256,6 +239,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; // Paper end - optimise PlayerChunkMap#isOutsideRange + // Tuinity start - optimise checkDespawn + public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; + // Tuinity end - optimise checkDespawn void addPlayerToDistanceMaps(ServerPlayer player) { int chunkX = MCUtil.getChunkCoordinate(player.getX()); @@ -266,7 +255,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; int trackRange = this.entityTrackerTrackRanges[i]; - trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); + trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances } // Paper end - use distance map to optimise entity tracker // Paper start - optimise PlayerChunkMap#isOutsideRange @@ -275,19 +264,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - optimise PlayerChunkMap#isOutsideRange this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper end - optimise PlayerChunkMap#isOutsideRange - // Paper start - no-tick view distance - int effectiveTickViewDistance = this.getEffectiveViewDistance(); - int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); - - if (!this.skipPlayer(player)) { - this.playerViewDistanceTickMap.add(player, chunkX, chunkZ, effectiveTickViewDistance); - this.playerViewDistanceNoTickMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) - } - - player.needsChunkCenterUpdate = true; - this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured - player.needsChunkCenterUpdate = false; - // Paper end - no-tick view distance + this.playerChunkManager.addPlayer(player); // Tuinity - replace chunk loader + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); + // Tuinity end - optimise checkDespawn } void removePlayerFromDistanceMaps(ServerPlayer player) { @@ -300,11 +280,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerMobSpawnMap.remove(player); this.playerChunkTickRangeMap.remove(player); // Paper end - optimise PlayerChunkMap#isOutsideRange - // Paper start - no-tick view distance - this.playerViewDistanceBroadcastMap.remove(player); - this.playerViewDistanceTickMap.remove(player); - this.playerViewDistanceNoTickMap.remove(player); - // Paper end - no-tick view distance + this.playerChunkManager.removePlayer(player); // Tuinity - replace chunk loader + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.remove(player); + // Tuinity end - optimise checkDespawn } void updateMaps(ServerPlayer player) { @@ -316,27 +295,50 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; int trackRange = this.entityTrackerTrackRanges[i]; - trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); + trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances } // Paper end - use distance map to optimise entity tracker // Paper start - optimise PlayerChunkMap#isOutsideRange this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper end - optimise PlayerChunkMap#isOutsideRange - // Paper start - no-tick view distance - int effectiveTickViewDistance = this.getEffectiveViewDistance(); - int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); + this.playerChunkManager.updatePlayer(player); // Tuinity - replace chunk loader + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); + // Tuinity end - optimise checkDespawn + } + // Paper end + // Tuinity start + public final List regionManagers = new java.util.ArrayList<>(); + public final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager dataRegionManager; + + public static final class DataRegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionData { + } + + public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData { + + @Override + public void removeFromRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, + final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region from) { + final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; + final DataRegionData fromData = (DataRegionData)from.regionData; + } - if (!this.skipPlayer(player)) { - this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveTickViewDistance); - this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) + @Override + public void addToRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, + final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region oldRegion, + final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region newRegion) { + final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; + final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData; + final DataRegionData newRegionData = (DataRegionData)newRegion.regionData; } + } - player.needsChunkCenterUpdate = true; - this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured - player.needsChunkCenterUpdate = false; - // Paper end - no-tick view distance + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { + return this.pendingUnloads.get(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); } - // Paper end + // Tuiniy end + + boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { super(new File(session.getDimensionPath(world.dimension()), "region"), dataFixer, dsync); @@ -453,53 +455,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } }); // Paper end - optimise PlayerChunkMap#isOutsideRange - // Paper start - no-tick view distance - this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); - this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, + this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); // Tuinity - replace chunk loading system + // Tuinity start + this.dataRegionManager = new com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); + this.regionManagers.add(this.dataRegionManager); + // Tuinity end + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - checkHighPriorityChunks(player); - if (newState.size() != 1) { - return; - } - LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); - if (chunk == null || !chunk.areNeighboursLoaded(2)) { - return; + LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); } - - ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); - ChunkMap.this.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update }, (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - if (newState != null) { - return; + LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); } - ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); - ChunkMap.this.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update - // Paper start - ChunkMap.this.level.getChunkSource().clearPriorityTickets(chunkPos); - }, - (player, prevPos, newPos) -> { - player.lastHighPriorityChecked = -1; // reset and recheck - checkHighPriorityChunks(player); - }); - // Paper end - this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); - this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - if (player.needsChunkCenterUpdate) { - player.needsChunkCenterUpdate = false; - player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ)); - } - ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded - }, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), null, true, false); // unloaded, loaded }); - // Paper end - no-tick view distance + // Tuinity end - optimise checkDespawn } // Paper start - Chunk Prioritization @@ -533,6 +510,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void checkHighPriorityChunks(ServerPlayer player) { + if (true) return; // Tuinity - replace player chunk loader int currentTick = MinecraftServer.currentTick; if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players return; @@ -540,7 +518,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider player.lastHighPriorityChecked = currentTick; it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); - int viewDistance = getEffectiveNoTickViewDistance(); + int viewDistance = 10;//int viewDistance = getEffectiveNoTickViewDistance(); // Tuinity - replace player chunk loader net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos(); // Prioritize circular near @@ -606,7 +584,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } private boolean shouldSkipPrioritization(ChunkPos coord) { - if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true; + if (true) return true; // Tuinity - replace player chunk loader - unused outside paper player loader logic ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong()); return chunk != null && (chunk.isFullChunkReady()); } @@ -674,7 +652,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @Nullable public ChunkHolder getUpdatingChunkIfPresent(long pos) { - return (ChunkHolder) this.updatingChunkMap.get(pos); + return this.updatingChunks.getUpdating(pos); // Tuinity - Don't copy } // Paper start - remove cloning of visible chunks unless accessed as a collection async @@ -682,47 +660,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private boolean isIterating = false; private boolean hasPendingVisibleUpdate = false; public void forEachVisibleChunk(java.util.function.Consumer consumer) { - org.spigotmc.AsyncCatcher.catchOp("forEachVisibleChunk"); - boolean prev = isIterating; - isIterating = true; - try { - for (ChunkHolder value : this.visibleChunkMap.values()) { - consumer.accept(value); - } - } finally { - this.isIterating = prev; - if (!this.isIterating && this.hasPendingVisibleUpdate) { - ((ProtectedVisibleChunksMap)this.visibleChunkMap).copyFrom(this.pendingVisibleChunks); - this.pendingVisibleChunks.clear(); - this.hasPendingVisibleUpdate = false; - } - } + throw new UnsupportedOperationException(); // Tuinity - Don't copy } public Long2ObjectLinkedOpenHashMap getVisibleChunks() { - if (Thread.currentThread() == this.level.thread) { - return this.visibleChunkMap; - } else { - synchronized (this.visibleChunkMap) { - if (DEBUG_ASYNC_VISIBLE_CHUNKS) new Throwable("Async getVisibleChunks").printStackTrace(); - if (this.visibleChunksClone == null) { - this.visibleChunksClone = this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.clone() : ((ProtectedVisibleChunksMap)this.visibleChunkMap).clone(); - } - return this.visibleChunksClone; - } + // Tuinity start - Don't copy (except in rare cases) + synchronized (this.updatingChunks) { + return this.updatingChunks.getVisibleMap().clone(); } + // Tuinity end - Don't copy (except in rare cases) } // Paper end @Nullable public ChunkHolder getVisibleChunkIfPresent(long pos) { - // Paper start - mt safe get - if (Thread.currentThread() != this.level.thread) { - synchronized (this.visibleChunkMap) { - return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos)); - } + // Tuinity start - Don't copy + if (Thread.currentThread() == this.level.thread) { + return this.updatingChunks.getVisible(pos); } - return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos)); - // Paper end + return this.updatingChunks.getVisibleAsync(pos); + // Tuinity end - Don't copy } protected IntSupplier getChunkQueueLevel(long pos) { @@ -844,12 +800,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @Nullable ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { + if (this.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity if (k > ChunkMap.MAX_CHUNK_DISTANCE && level > ChunkMap.MAX_CHUNK_DISTANCE) { return holder; } else { if (holder != null) { holder.setTicketLevel(level); - holder.updateRanges(); // Paper - optimise isOutsideOfRange + // Tuinity - move to correct place } if (holder != null) { @@ -864,11 +821,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider holder = (ChunkHolder) this.pendingUnloads.remove(pos); if (holder != null) { holder.setTicketLevel(level); + holder.updateRanges(); // Paper - optimise isOutsideOfRange // Tuinity - move to correct place } else { holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); + // Tuinity start + for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { + this.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z); + } + // Tuinity end } - this.updatingChunkMap.put(pos, holder); + this.updatingChunks.queueUpdate(pos, holder); // Tuinity - Don't copy this.modified = true; } @@ -1023,7 +986,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider while (longiterator.hasNext()) { // Spigot long j = longiterator.nextLong(); longiterator.remove(); // Spigot - ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); + ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Tuinity - Don't copy if (playerchunk != null) { this.pendingUnloads.put(j, playerchunk); @@ -1060,7 +1023,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.level, chunkPos.x, chunkPos.z, - poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority if (!chunk.isUnsaved()) { return; @@ -1081,7 +1044,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider asyncSaveData = ChunkSerializer.getAsyncSaveData(this.level, chunk); } - this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY, + this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, // Tuinity - use normal priority asyncSaveData, chunk); chunk.setUnsaved(false); @@ -1097,7 +1060,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (completablefuture1 != completablefuture) { this.scheduleUnload(pos, holder); } else { - if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) { + // Tuinity start - do not allow ticket level changes while unloading chunks + org.spigotmc.AsyncCatcher.catchOp("playerchunk unload"); + boolean unloadingBefore = this.unloadingPlayerChunk; + this.unloadingPlayerChunk = true; + try { + // Tuinity end - do not allow ticket level changes while unloading chunks + // Tuinity start + boolean removed; + if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { + for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { + this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); + } + // Tuinity end if (ichunkaccess instanceof LevelChunk) { ((LevelChunk) ichunkaccess).setLoaded(false); } @@ -1122,7 +1097,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); this.lightEngine.tryScheduleUpdate(); this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); + } else if (removed) { // Tuinity start + for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { + this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); + } } + // Tuinity end + } finally { this.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes while unloading chunks } }; @@ -1141,19 +1122,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (!this.modified) { return false; } else { - // Paper start - stop cloning visibleChunks - synchronized (this.visibleChunkMap) { - if (isIterating) { - hasPendingVisibleUpdate = true; - this.pendingVisibleChunks.copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy)this.updatingChunkMap); - } else { - hasPendingVisibleUpdate = false; - this.pendingVisibleChunks.clear(); - ((ProtectedVisibleChunksMap)this.visibleChunkMap).copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy)this.updatingChunkMap); - this.visibleChunksClone = null; - } + // Tuinity start - Don't copy + synchronized (this.updatingChunks) { + this.updatingChunks.performUpdates(); } - // Paper end + // Tuinity end - Don't copy this.modified = false; return true; @@ -1166,11 +1139,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (requiredStatus == ChunkStatus.EMPTY) { return this.scheduleChunkLoad(chunkcoordintpair); } else { + // Tuinity start - revert 1.17 chunk system changes + CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this); + return future.thenComposeAsync((either) -> { + Optional optional = either.left(); + if (!optional.isPresent()) { + return CompletableFuture.completedFuture(either); + } + // Tuinity end - revert 1.17 chunk system changes + + // Tuinity start - revert 1.17 chunk system changes if (requiredStatus == ChunkStatus.LIGHT) { this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.FEATURES), chunkcoordintpair); } - - Optional optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); + // Tuinity end - revert 1.17 chunk system changes if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) { CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> { @@ -1182,6 +1164,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { return this.scheduleChunkGeneration(holder, requiredStatus); } + }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Tuinity - revert 1.17 chunk system changes } } @@ -1282,7 +1265,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }); Executor executor = (runnable) -> { // Paper start - optimize chunk status progression without jumping through thread pool - if (holder.canAdvanceStatus()) { + if (false && holder.canAdvanceStatus()) { // Tuinity - these optimisations will be revisited later this.mainInvokingExecutor.execute(runnable); return; } @@ -1313,7 +1296,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.releaseLightTicket(chunkcoordintpair); return CompletableFuture.completedFuture(Either.right(playerchunk_failure)); }); - }, executor); + }, executor).thenComposeAsync((either) -> { // Tuinity start - force competion on the main thread + return CompletableFuture.completedFuture(either); + }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute + // Tuinity end - force competion on the main thread } protected void releaseLightTicket(ChunkPos pos) { @@ -1472,9 +1458,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider chunk.unpackTicks(); return chunk; }); - }, (runnable) -> { - this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(playerchunk, runnable)); - }); + }, this.mainThreadExecutor); // Tuinity - queue to execute immediately so this doesn't delay chunk unloading } public int getTickingGenerated() { @@ -1559,7 +1543,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider int k = this.viewDistance; this.viewDistance = j; - this.setNoTickViewDistance(this.getRawNoTickViewDistance()); //Paper - no-tick view distance - propagate changes to no-tick, which does the actual chunk loading/sending + this.playerChunkManager.setTickDistance(Mth.clamp(viewDistance, 2, 32)); // Tuinity - replace player loader system } } @@ -1567,26 +1551,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - no-tick view distance public final void setNoTickViewDistance(int viewDistance) { viewDistance = viewDistance == -1 ? -1 : Mth.clamp(viewDistance, 2, 32); - - this.noTickViewDistance = viewDistance; - int loadViewDistance = this.getLoadViewDistance(); - this.distanceManager.setNoTickViewDistance(loadViewDistance + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2 - - if (this.level != null && this.level.players != null) { // this can be called from constructor, where these aren't set - for (ServerPlayer player : this.level.players) { - net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; - if (connection != null) { - // moved in from PlayerList - connection.send(new net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket(loadViewDistance)); - } - this.updateMaps(player); - // Paper end - no-tick view distance - } - } + this.playerChunkManager.setLoadDistance(viewDistance == -1 ? -1 : viewDistance + 1); // Tuinity - replace player loader system - add 1 here, we need an extra one to send to clients for chunks in this viewDistance to render } - protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { + public void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { // Tuinity - public if (player.level == this.level) { if (withinViewDistance && !withinMaxWatchDistance) { ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); @@ -1610,7 +1579,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public int size() { - return this.visibleChunkMap.size(); + return this.updatingChunks.getVisibleMap().size(); // Tuinity - Don't copy } protected DistanceManager getDistanceManager() { @@ -1915,6 +1884,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider */ // Paper end - replaced by distance map this.updateMaps(player); // Paper - distance maps + this.playerChunkManager.updatePlayer(player); // Tuinity - respond to movement immediately } @@ -1923,7 +1893,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - per player view distance // there can be potential desync with player's last mapped section and the view distance map, so use the // view distance map here. - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkPos); + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); // Tuinity - replace player chunk loader system if (inRange == null) { return Stream.empty(); @@ -1939,8 +1909,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider continue; } ServerPlayer player = (ServerPlayer)temp; - int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player); - long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player); + if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management + int viewDistance = this.playerChunkManager.broadcastMap.getLastViewDistance(player); // Tuinity - replace player chunk loader system + long lastPosition = this.playerChunkManager.broadcastMap.getLastCoordinate(player); // Tuinity - replace player chunk loader system int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - chunkPos.x); int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkPos.z); @@ -1955,6 +1926,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider continue; } ServerPlayer player = (ServerPlayer)temp; + if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management players.add(player); } } @@ -2265,7 +2237,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially final Entity entity; private final int range; SectionPos lastSectionPos; - public final Set seenBy = Sets.newIdentityHashSet(); + public final Set seenBy = new ReferenceOpenHashSet<>(); // Tuinity - optimise map impl public TrackedEntity(Entity entity, int i, int j, boolean flag) { this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit @@ -2365,7 +2337,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially double vec3d_dy = player.getY() - this.entity.getY(); double vec3d_dz = player.getZ() - this.entity.getZ(); // Paper end - remove allocation of Vec3D here - int i = Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); + int i = Math.min(this.getEffectiveRange(), player.getBukkitEntity().getViewDistance() * 16); // Tuinity - per player view distance boolean flag = vec3d_dx >= (double) (-i) && vec3d_dx <= (double) i && vec3d_dz >= (double) (-i) && vec3d_dz <= (double) i && this.entity.broadcastToPlayer(player); // Paper - remove allocation of Vec3D here // CraftBukkit start - respect vanish API @@ -2400,7 +2372,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially int j = entity.getType().clientTrackingRange() * 16; j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper - if (j < i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic + if (j > i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic // Tuinity - not anymore! i = j; } } diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index 1cc4e0a1f3d8235ef88b48e01ca8b78a263d2676..428d94c60b826ddf3797d6713661dff1ca835ac2 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -36,6 +36,7 @@ import net.minecraft.world.level.chunk.LevelChunk; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Tuinity public abstract class DistanceManager { static final Logger LOGGER = LogManager.getLogger(); @@ -44,9 +45,9 @@ public abstract class DistanceManager { private static final int INITIAL_TICKET_LIST_CAPACITY = 4; final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); - private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); + //private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Tuinity - replace ticket level propagator public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used - private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); + //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Tuinity - no longer used // Paper start use a queue, but still keep unique requirement public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() { @Override @@ -77,6 +78,46 @@ public abstract class DistanceManager { this.mainThreadExecutor = mainThreadExecutor; } + // Tuinity start - replace ticket level propagator + protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() { + @Override + protected void rehash(int newN) { + // no downsizing allowed + if (newN < this.n) { + return; + } + super.rehash(newN); + } + }; + protected final com.tuinity.tuinity.util.misc.Delayed8WayDistancePropagator2D ticketLevelPropagator = new com.tuinity.tuinity.util.misc.Delayed8WayDistancePropagator2D( + (long coordinate, byte oldLevel, byte newLevel) -> { + DistanceManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel)); + } + ); + // function for converting between ticket levels and propagator levels and vice versa + // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects + // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator + // and the levels we get out of the propagator + + // this maps so that GOLDEN_TICKET + 1 will be 0 in the propagator, GOLDEN_TICKET will be 1, and so on + // we need GOLDEN_TICKET+1 as 0 because anything >= GOLDEN_TICKET+1 should be unloaded + public static int convertBetweenTicketLevels(final int level) { + return ChunkMap.MAX_CHUNK_DISTANCE - level + 1; + } + + protected final int getPropagatedTicketLevel(final long coordinate) { + return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate)); + } + + protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { + if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) { + this.ticketLevelPropagator.removeSource(coordinate); + } else { + this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel)); + } + } + // Tuinity end - replace ticket level propagator + protected void purgeStaleTickets() { ++this.ticketTickCounter; ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); @@ -87,7 +128,7 @@ public abstract class DistanceManager { if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error return ticket.timedOut(this.ticketTickCounter); })) { - this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); + this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(entry.getValue())); // Tuinity - replace ticket level propagator } if (((SortedArraySet) entry.getValue()).isEmpty()) { @@ -110,60 +151,93 @@ public abstract class DistanceManager { @Nullable protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + protected long ticketLevelUpdateCount; // Tuinity - replace ticket level propagator public boolean runAllUpdates(ChunkMap playerchunkmap) { //this.f.a(); // Paper - no longer used org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper - this.playerTicketManager.runAllUpdates(); - int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); - boolean flag = i != 0; + //this.playerTicketManager.runAllUpdates(); // Tuinity - no longer used + boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Tuinity - replace ticket level propagator if (flag) { ; } - // Paper start - if (!this.pendingChunkUpdates.isEmpty()) { - this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority - while(!this.pendingChunkUpdates.isEmpty()) { - ChunkHolder remove = this.pendingChunkUpdates.remove(); - remove.isUpdateQueued = false; - remove.updateFutures(playerchunkmap, this.mainThreadExecutor); - } - } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority - // Paper end - return true; - } else { - if (!this.ticketsToRelease.isEmpty()) { - LongIterator longiterator = this.ticketsToRelease.iterator(); + // Tuinity start - replace level propagator + ticket_update_loop: + while (!this.ticketLevelUpdates.isEmpty()) { + flag = true; - while (longiterator.hasNext()) { - long j = longiterator.nextLong(); + boolean oldPolling = this.pollingPendingChunkUpdates; + this.pollingPendingChunkUpdates = true; + try { + for (java.util.Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { + Long2IntMap.Entry entry = iterator.next(); + long key = entry.getLongKey(); + int newLevel = entry.getIntValue(); + ChunkHolder chunk = this.getChunk(key); + + if (chunk == null && newLevel > ChunkMap.MAX_CHUNK_DISTANCE) { + // not loaded and it shouldn't be loaded! + continue; + } + + int currentLevel = chunk == null ? ChunkMap.MAX_CHUNK_DISTANCE + 1 : chunk.getTicketLevel(); + + if (currentLevel == newLevel) { + // nothing to do + continue; + } - if (this.getTickets(j).stream().anyMatch((ticket) -> { - return ticket.getType() == TicketType.PLAYER; - })) { - ChunkHolder playerchunk = playerchunkmap.getUpdatingChunkIfPresent(j); + this.updateChunkScheduling(key, newLevel, chunk, currentLevel); + } - if (playerchunk == null) { - throw new IllegalStateException(); + long recursiveCheck = ++this.ticketLevelUpdateCount; + while (!this.ticketLevelUpdates.isEmpty()) { + long key = this.ticketLevelUpdates.firstLongKey(); + int newLevel = this.ticketLevelUpdates.removeFirstInt(); + ChunkHolder chunk = this.getChunk(key); + + if (chunk == null) { + if (newLevel <= ChunkMap.MAX_CHUNK_DISTANCE) { + throw new IllegalStateException("Expected chunk holder to be created"); } + // not loaded and it shouldn't be loaded! + continue; + } - CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); + int currentLevel = chunk.oldTicketLevel; - completablefuture.thenAccept((either) -> { - this.mainThreadExecutor.execute(() -> { - this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { - }, j, false)); - }); - }); + if (currentLevel == newLevel) { + // nothing to do + continue; + } + + chunk.updateFutures(playerchunkmap, this.mainThreadExecutor); + if (recursiveCheck != this.ticketLevelUpdateCount) { + // back to the start, we must create player chunks and update the ticket level fields before + // processing the actual level updates + continue ticket_update_loop; } } - this.ticketsToRelease.clear(); - } + for (;;) { + if (recursiveCheck != this.ticketLevelUpdateCount) { + continue ticket_update_loop; + } + ChunkHolder pendingUpdate = this.pendingChunkUpdates.poll(); + if (pendingUpdate == null) { + break; + } - return flag; + pendingUpdate.updateFutures(playerchunkmap, this.mainThreadExecutor); + } + } finally { + this.pollingPendingChunkUpdates = oldPolling; + } } + + return flag; + // Tuinity end - replace level propagator } boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority @@ -175,7 +249,7 @@ public abstract class DistanceManager { ticket1.setCreatedTick(this.ticketTickCounter); if (ticket.getTicketLevel() < j) { - this.ticketTracker.update(i, ticket.getTicketLevel(), true); + this.updateTicketLevel(i, ticket.getTicketLevel()); // Tuinity - replace ticket level propagator } return ticket == ticket1; // CraftBukkit @@ -219,7 +293,7 @@ public abstract class DistanceManager { // Paper start - Chunk priority int newLevel = getTicketLevelAt(arraysetsorted); if (newLevel > oldLevel) { - this.ticketTracker.update(i, newLevel, false); + this.updateTicketLevel(i, newLevel); // Paper // Tuinity - replace ticket level propagator } // Paper end return removed; // CraftBukkit @@ -313,7 +387,7 @@ public abstract class DistanceManager { org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket"); long pair = coords.toLong(); ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair); - boolean needsTicket = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null && !hasPlayerTicket(coords, 33); + boolean needsTicket = false; // Tuinity - replace old loader system if (needsTicket) { Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, coords); @@ -411,7 +485,7 @@ public abstract class DistanceManager { return new ObjectOpenHashSet(); })).add(player); //this.f.update(i, 0, true); // Paper - no longer used - this.playerTicketManager.update(i, 0, true); + //this.playerTicketManager.update(i, 0, true); // Tuinity - no longer used } public void removePlayer(SectionPos pos, ServerPlayer player) { @@ -423,7 +497,7 @@ public abstract class DistanceManager { if (objectset == null || objectset.isEmpty()) { // Paper this.playersPerChunk.remove(i); //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used - this.playerTicketManager.update(i, Integer.MAX_VALUE, false); + //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Tuinity - no longer used } } @@ -442,7 +516,7 @@ public abstract class DistanceManager { } protected void setNoTickViewDistance(int i) { // Paper - force abi breakage on usage change - this.playerTicketManager.updateViewDistance(i); + throw new UnsupportedOperationException("use world api"); // Tuinity - no longer relevant } public int getNaturalSpawnChunkCount() { @@ -507,7 +581,7 @@ public abstract class DistanceManager { SortedArraySet> tickets = entry.getValue(); if (tickets.remove(target)) { // copied from removeTicket - this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); + this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Tuinity - replace ticket level propagator // can't use entry after it's removed if (tickets.isEmpty()) { @@ -563,6 +637,7 @@ public abstract class DistanceManager { } } + /* Tuinity - replace old loader system private class FixedPlayerDistanceChunkTracker extends ChunkTracker { protected final Long2ByteMap chunks = new Long2ByteOpenHashMap(); @@ -858,4 +933,5 @@ public abstract class DistanceManager { } // Paper end } + */ // Tuinity - replace old loader system } diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 82d3cfb2d346a8b929e9469ae09369f6a639f81d..958b7044c196ebd66f60391c33c64ad2ff82d4e8 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper public class ServerChunkCache extends ChunkSource { + public static final org.apache.logging.log4j.Logger LOGGER = org.apache.logging.log4j.LogManager.getLogger(); // Tuinity public static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); private final DistanceManager distanceManager; @@ -139,7 +140,7 @@ public class ServerChunkCache extends ChunkSource { return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true); } - private long chunkFutureAwaitCounter; + long chunkFutureAwaitCounter; // Tuinity - private -> package private public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { if (Thread.currentThread() != this.mainThread) { @@ -201,9 +202,9 @@ public class ServerChunkCache extends ChunkSource { try { if (onLoad != null) { - chunkMap.callbackExecutor.execute(() -> { + // Tuinity - revert incorrect use of callback executor onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. - }); + // Tuinity - revert incorrect use of callback executor } } catch (Throwable thr) { if (thr instanceof ThreadDeath) { @@ -227,6 +228,166 @@ public class ServerChunkCache extends ChunkSource { } // Paper end - rewrite ticklistserver + // Tuinity start + // this will try to avoid chunk neighbours for lighting + public final ChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) { + LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + ChunkAccess empty = this.getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, true); + if (empty != null && empty.getStatus().isOrAfter(ChunkStatus.FULL)) { + return empty; + } + return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); + } + + public final ChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) { + LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + ChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ); + if (ret != null && ret.getStatus().isOrAfter(ChunkStatus.FULL)) { + return ret; + } else { + return null; + } + } + + void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, + java.util.function.Consumer consumer) { + this.getChunkAtAsynchronously(chunkX, chunkZ, ticketLevel, (ChunkHolder chunkHolder) -> { + if (ticketLevel <= 33) { + return (CompletableFuture)chunkHolder.getFullChunkFuture(); + } else { + return chunkHolder.getOrScheduleFuture(ChunkHolder.getStatus(ticketLevel), ServerChunkCache.this.chunkMap); + } + }, consumer); + } + + void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, + java.util.function.Function>> function, + java.util.function.Consumer consumer) { + if (Thread.currentThread() != this.mainThread) { + throw new IllegalStateException(); + } + + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++); + this.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); + this.runDistanceManagerUpdates(); + + ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()); + + if (chunk == null) { + throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.level.getWorld().getName() + "'"); + } + + CompletableFuture> future = function.apply(chunk); + + future.whenCompleteAsync((either, throwable) -> { + try { + if (throwable != null) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", throwable); + } else if (either.right().isPresent()) { + LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "': " + either.right().get().toString()); + } + + try { + if (consumer != null) { + consumer.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. + } + } catch (Throwable thr) { + if (thr instanceof ThreadDeath) { + throw (ThreadDeath)thr; + } + LOGGER.fatal("Load callback for future await failed " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", thr); + return; + } + } finally { + // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback. + ServerChunkCache.this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + ServerChunkCache.this.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); + } + }, this.mainThreadProcessor); + } + + void chunkLoadAccept(int chunkX, int chunkZ, ChunkAccess chunk, java.util.function.Consumer consumer) { + try { + consumer.accept(chunk); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.level.getWorld().getName() + "' threw an exception", throwable); + } + } + + public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer onLoad) { + // try to fire sync + int chunkStatusTicketLevel = 33 + ChunkStatus.getDistance(status); + ChunkHolder playerChunk = this.chunkMap.getUpdatingChunkIfPresent(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (playerChunk != null) { + ChunkStatus holderStatus = playerChunk.getChunkHolderStatus(); + ChunkAccess immediate = playerChunk.getAvailableChunkNow(); + if (immediate != null) { + if (allowSubTicketLevel ? immediate.getStatus().isOrAfter(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isOrAfter(status))) { + this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad); + return; + } else { + if (gen || (!allowSubTicketLevel && immediate.getStatus().isOrAfter(status))) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } + } + } + + // need to fire async + + if (gen && !allowSubTicketLevel) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + + this.getChunkAtAsynchronously(chunkX, chunkZ, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (ChunkAccess chunk) -> { + if (chunk == null) { + throw new IllegalStateException("Chunk cannot be null"); + } + + if (!chunk.getStatus().isOrAfter(status)) { + if (gen) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } else { + if (allowSubTicketLevel) { + ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad); + return; + } else { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + } + }); + } + + final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + // Tuinity end + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, boolean flag, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkstatusupdatelistener, Supplier supplier) { this.level = world; this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); @@ -570,6 +731,8 @@ public class ServerChunkCache extends ChunkSource { return completablefuture; } + private long syncLoadCounter; // Tuinity - prevent plugin unloads from removing our ticket + private CompletableFuture> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag) { // Paper start - add isUrgent - old sig left in place for dirty nms plugins return getChunkFutureMainThread(i, j, chunkstatus, flag, false); @@ -588,9 +751,12 @@ public class ServerChunkCache extends ChunkSource { ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel()); currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); } + final Long identifier; // Tuinity - prevent plugin unloads from removing our ticket if (flag && !currentlyUnloading) { // CraftBukkit end this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); + identifier = Long.valueOf(this.syncLoadCounter++); // Tuinity - prevent plugin unloads from removing our ticket + this.distanceManager.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity - prevent plugin unloads from removing our ticket if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority if (this.chunkAbsent(playerchunk, l)) { ProfilerFiller gameprofilerfiller = this.level.getProfiler(); @@ -601,12 +767,20 @@ public class ServerChunkCache extends ChunkSource { playerchunk = this.getVisibleChunkIfPresent(k); gameprofilerfiller.pop(); if (this.chunkAbsent(playerchunk, l)) { + this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity throw (IllegalStateException) Util.pauseInIde((Throwable) (new IllegalStateException("No chunk holder after ticket has been added"))); } } - } + } else { identifier = null; } // Tuinity - prevent plugin unloads from removing our ticket // Paper start - Chunk priority CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap); + // Tuinity start - prevent plugin unloads from removing our ticket + if (flag && !currentlyUnloading) { + future.thenAcceptAsync((either) -> { + ServerChunkCache.this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); + }, ServerChunkCache.this.mainThreadProcessor); + } + // Tuinity end - prevent plugin unloads from removing our ticket if (isUrgent) { future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); } @@ -664,6 +838,8 @@ public class ServerChunkCache extends ChunkSource { public boolean runDistanceManagerUpdates() { if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority + if (this.chunkMap.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity + co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Tuinity - add timings for distance manager boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); boolean flag1 = this.chunkMap.promoteChunkMap(); @@ -673,6 +849,7 @@ public class ServerChunkCache extends ChunkSource { this.clearCache(); return true; } + } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Tuinity - add timings for distance manager } // Paper start - helper @@ -730,6 +907,7 @@ public class ServerChunkCache extends ChunkSource { // CraftBukkit start - modelled on below public void purgeUnload() { + if (true) return; // Tuinity - tickets will be removed later, this behavior isn't really well accounted for by the chunk system this.level.getProfiler().push("purge"); this.distanceManager.purgeStaleTickets(); this.runDistanceManagerUpdates(); @@ -745,17 +923,18 @@ public class ServerChunkCache extends ChunkSource { this.level.getProfiler().push("purge"); this.level.timings.doChunkMap.startTiming(); // Spigot this.distanceManager.purgeStaleTickets(); - this.level.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.runDistanceManagerUpdates(); this.level.timings.doChunkMap.stopTiming(); // Spigot this.level.getProfiler().popPush("chunks"); this.level.timings.chunks.startTiming(); // Paper - timings + this.chunkMap.playerChunkManager.tick(); // Tuinity - this is mostly is to account for view distance changes this.tickChunks(); this.level.timings.chunks.stopTiming(); // Paper - timings this.level.timings.doChunkUnload.startTiming(); // Spigot this.level.getProfiler().popPush("unload"); this.chunkMap.tick(booleansupplier); - this.level.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.level.timings.doChunkUnload.stopTiming(); // Spigot this.level.getProfiler().pop(); this.clearCache(); @@ -833,12 +1012,15 @@ public class ServerChunkCache extends ChunkSource { //Collections.shuffle(list); // Paper // Paper - moved up this.level.timings.chunkTicks.startTiming(); // Paper - final int[] chunksTicked = {0}; this.chunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping - Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); - - if (optional.isPresent()) { + // Tuinity start + int chunksTicked = 0; + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickingChunks.iterator(); + try { while (iterator.hasNext()) { + LevelChunk chunk = iterator.next(); + ChunkHolder playerchunk = chunk.playerChunk; + if (playerchunk != null) { this.level.getProfiler().push("broadcast"); - LevelChunk chunk = (LevelChunk) optional.get(); + // Tuinity end this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings playerchunk.broadcastChanges(chunk); @@ -846,11 +1028,11 @@ public class ServerChunkCache extends ChunkSource { this.level.getProfiler().pop(); ChunkPos chunkcoordintpair = chunk.getPos(); - if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange + if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange // Tuinity - we only iterate entity ticking chunks chunk.setInhabitedTime(chunk.getInhabitedTime() + j); if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag2); - if (chunksTicked[0]++ % 10 == 0) this.level.getServer().midTickLoadChunks(); // Paper + if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper // Tuinity } // this.level.timings.doTickTiles.startTiming(); // Spigot // Paper @@ -858,7 +1040,11 @@ public class ServerChunkCache extends ChunkSource { // this.level.timings.doTickTiles.stopTiming(); // Spigot // Paper } } - }); + } // Tuinity start - optimise chunk tick iteration + } finally { + iterator.finishedIterating(); + } + // Tuinity end - optimise chunk tick iteration this.level.timings.chunkTicks.stopTiming(); // Paper this.level.getProfiler().push("customSpawners"); if (flag1) { @@ -871,7 +1057,24 @@ public class ServerChunkCache extends ChunkSource { this.level.getProfiler().pop(); } + // Tuinity start - controlled flush for entity tracker packets + List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); + for (ServerPlayer player : this.level.players) { + net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; + if (connection != null) { + connection.connection.disableAutomaticFlush(); + disabledFlushes.add(connection.connection); + } + } + try { // Tuinity end - controlled flush for entity tracker packets this.chunkMap.tick(); + // Tuinity start - controlled flush for entity tracker packets + } finally { + for (net.minecraft.network.Connection networkManager : disabledFlushes) { + networkManager.enableAutomaticFlush(); + } + } + // Tuinity end - controlled flush for entity tracker packets } private void getFullChunk(long pos, Consumer chunkConsumer) { @@ -1018,46 +1221,14 @@ public class ServerChunkCache extends ChunkSource { super.doRunTask(task); } - // Paper start - private long lastMidTickChunkTask = 0; - public boolean pollChunkLoadTasks() { - if (com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask()) { - try { - ServerChunkCache.this.runDistanceManagerUpdates(); - } finally { - // from below: process pending Chunk loadCallback() and unloadCallback() after each run task - chunkMap.callbackExecutor.run(); - } - return true; - } - return false; - } - public void midTickLoadChunks() { - net.minecraft.server.MinecraftServer server = ServerChunkCache.this.level.getServer(); - // always try to load chunks, restrain generation/other updates only. don't count these towards tick count - //noinspection StatementWithEmptyBody - while (pollChunkLoadTasks()) {} - - if (System.nanoTime() - lastMidTickChunkTask < 200000) { - return; - } - - for (;server.midTickChunksTasksRan < com.destroystokyo.paper.PaperConfig.midTickChunkTasks && server.haveTime();) { - if (this.pollTask()) { - server.midTickChunksTasksRan++; - lastMidTickChunkTask = System.nanoTime(); - } else { - break; - } - } - } - // Paper end + // Tuinity - replace logic @Override // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task public boolean pollTask() { try { boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper + ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Tuinity if (ServerChunkCache.this.runDistanceManagerUpdates()) { return true; } else { diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java index 44aa0c4ec6f0e4df2541c74fa7de852dae59bda5..a00627e0fa38632449042f59c053b4dac13e58bf 100644 --- a/src/main/java/net/minecraft/server/level/ServerEntity.java +++ b/src/main/java/net/minecraft/server/level/ServerEntity.java @@ -173,7 +173,7 @@ public class ServerEntity { // Paper end - remove allocation of Vec3D here boolean flag4 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; - if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround()) { + if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround() && !(com.tuinity.tuinity.config.TuinityConfig.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Tuinity - send full pos for hard colliding entities to prevent collision problems due to desync if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { if (flag2) { packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.isOnGround()); diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 8154ca39ec7e2e8559cd125d73a59b8d2b00714c..07b0eae123e310809dc28506ebe2e0878dcaa224 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -115,6 +115,7 @@ import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.TickingBlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; @@ -165,6 +166,7 @@ import org.bukkit.event.server.MapInitializeEvent; import org.bukkit.event.weather.LightningStrikeEvent; import org.bukkit.event.world.TimeSkipEvent; // CraftBukkit end +import it.unimi.dsi.fastutil.ints.IntArrayList; // Tuinity public class ServerLevel extends net.minecraft.world.level.Level implements WorldGenLevel { @@ -193,7 +195,9 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl final Int2ObjectMap dragonParts; private final StructureFeatureManager structureFeatureManager; private final boolean tickTime; - + // Tuinity start - execute chunk tasks mid tick + public long lastMidTickExecuteFailure; + // Tuinity end - execute chunk tasks mid tick // CraftBukkit start private int tickPosition; @@ -304,6 +308,172 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } } // Paper end - rewrite ticklistserver + // Tuinity start + public final boolean areChunksLoadedForMove(AABB axisalignedbb) { + // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override + // ICollisionAccess methods for VoxelShapes) + // be more strict too, add a block (dumb plugins in move events?) + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { + return false; + } + } + } + + return true; + } + + public final void loadChunksForMoveAsync(AABB axisalignedbb, double toX, double toZ, + java.util.function.Consumer> onLoad) { + if (Thread.currentThread() != this.thread) { + this.getChunkSource().mainThreadProcessor.execute(() -> { + this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad); + }); + return; + } + List ret = new java.util.ArrayList<>(); + IntArrayList ticketLevels = new IntArrayList(); + + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); + int[] loadedChunks = new int[1]; + + Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); + + java.util.function.Consumer consumer = (ChunkAccess chunk) -> { + if (chunk != null) { + int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); + ret.add(chunk); + ticketLevels.add(ticketLevel); + chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + } + if (++loadedChunks[0] == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + ChunkPos chunkPos = ret.get(i).getPos(); + int ticketLevel = ticketLevels.getInt(i); + + chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); + } + } + } + }; + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + chunkProvider.getChunkAtAsynchronously(cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, false, consumer); + } + } + } + // Tuinity end + // Tuinity start - optimise checkDespawn + public final List playersAffectingSpawning = new java.util.ArrayList<>(); + // Tuinity end - optimise checkDespawn + // Tuinity start - optimise get nearest players for entity AI + @Override + public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source, + double centerX, double centerY, double centerZ) { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; + nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); + + if (nearby == null) { + return null; + } + + Object[] backingSet = nearby.getBackingSet(); + + double closestDistanceSquared = Double.MAX_VALUE; + ServerPlayer closest = null; + + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof ServerPlayer)) { + continue; + } + ServerPlayer player = (ServerPlayer)_player; + + double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ); + if (distanceSquared < closestDistanceSquared && condition.test(source, player)) { + closest = player; + closestDistanceSquared = distanceSquared; + } + } + + return closest; + } + + @Override + public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) { + return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ()); + } + + @Override + public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, + double d0, double d1, double d2) { + return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2); + } + + @Override + public List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; + double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5; + double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5; + nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); + + List ret = new java.util.ArrayList<>(); + + if (nearby == null) { + return ret; + } + + Object[] backingSet = nearby.getBackingSet(); + + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof ServerPlayer)) { + continue; + } + ServerPlayer player = (ServerPlayer)_player; + + if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) { + ret.add(player); + } + } + + return ret; + } + // Tuinity end - optimise get nearest players for entity AI // Add env and gen to constructor, WorldData -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) { @@ -351,7 +521,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl DataFixer datafixer = minecraftserver.getFixerUpper(); EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, new File(convertable_conversionsession.getDimensionPath(resourcekey), "entities"), datafixer, flag2, minecraftserver); - this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); + this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Tuinity StructureManager definedstructuremanager = minecraftserver.getStructureManager(); int j = this.spigotConfig.viewDistance; // Spigot PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; @@ -386,6 +556,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper } + // Tuinity start - optimise collision + + // Tuinity end - optimise collision + // CraftBukkit start @Override public BlockEntity getTileEntity(BlockPos pos, boolean validate) { @@ -439,6 +613,14 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } public void tick(BooleanSupplier shouldKeepTicking) { + // Tuinity start - optimise checkDespawn + this.playersAffectingSpawning.clear(); + for (ServerPlayer player : this.players) { + if (net.minecraft.world.entity.EntitySelector.affectsSpawning.test(player)) { + this.playersAffectingSpawning.add(player); + } + } + // Tuinity end - optimise checkDespawn ProfilerFiller gameprofilerfiller = this.getProfiler(); this.handlingTick = true; @@ -588,7 +770,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } timings.scheduledBlocks.stopTiming(); // Paper - this.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic gameprofilerfiller.popPush("raid"); this.timings.raids.startTiming(); // Paper - timings this.raids.tick(); @@ -597,7 +779,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl timings.doSounds.startTiming(); // Spigot this.runBlockEvents(); timings.doSounds.stopTiming(); // Spigot - this.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.handlingTick = false; gameprofilerfiller.pop(); boolean flag3 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players @@ -644,12 +826,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl timings.entityTick.stopTiming(); // Spigot timings.tickEntities.stopTiming(); // Spigot gameprofilerfiller.pop(); - this.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.tickBlockEntities(); } gameprofilerfiller.push("entityManagement"); - this.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.entityManager.tick(); gameprofilerfiller.pop(); } @@ -694,6 +876,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl entityplayer.stopSleepInBed(false, false); }); } + // Paper start - optimise random block ticking + private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); + private final com.tuinity.tuinity.util.math.ThreadUnsafeRandom randomTickRandom = new com.tuinity.tuinity.util.math.ThreadUnsafeRandom(); + // Paper end public void tickChunk(LevelChunk chunk, int randomTickSpeed) { ChunkPos chunkcoordintpair = chunk.getPos(); @@ -703,10 +889,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl ProfilerFiller gameprofilerfiller = this.getProfiler(); gameprofilerfiller.push("thunder"); - BlockPos blockposition; + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change if (!this.paperConfig.disableThunder && flag && this.isThundering() && this.random.nextInt(100000) == 0) { // Paper - Disable thunder - blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); + blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper if (this.isRainingAt(blockposition)) { DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * paperConfig.skeleHorseSpawnChance && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper @@ -729,64 +915,76 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } gameprofilerfiller.popPush("iceandsnow"); - if (!this.paperConfig.disableIceAndSnow && this.random.nextInt(16) == 0) { // Paper - Disable ice and snow - blockposition = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.getBlockRandomPos(j, 0, k, 15)); - BlockPos blockposition1 = blockposition.below(); + if (!this.paperConfig.disableIceAndSnow && this.randomTickRandom.nextInt(16) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking + // Paper start - optimise chunk ticking + this.getRandomBlockPosition(j, 0, k, 15, blockposition); + int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15); + int downY = normalY - 1; + blockposition.setY(normalY); + // Paper end Biome biomebase = this.getBiome(blockposition); - if (biomebase.shouldFreeze((LevelReader) this, blockposition1)) { - org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit + // Paper start - optimise chunk ticking + blockposition.setY(downY); + if (biomebase.shouldFreeze(this, blockposition)) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.ICE.defaultBlockState(), null); // CraftBukkit + // Paper end } if (flag) { + blockposition.setY(normalY); // Paper if (biomebase.shouldSnow(this, blockposition)) { org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit } - BlockState iblockdata = this.getBlockState(blockposition1); + blockposition.setY(downY); // Paper + BlockState iblockdata = this.getBlockState(blockposition); // Paper Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation(); - if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) { + if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition)) { // Paper biomebase_precipitation = Biome.Precipitation.SNOW; } - iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition1, biomebase_precipitation); + iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition, biomebase_precipitation); // Paper } } - gameprofilerfiller.popPush("tickBlocks"); + // Paper start - optimise random block ticking + gameprofilerfiller.popPush("randomTick"); timings.chunkTicksBlocks.startTiming(); // Paper if (randomTickSpeed > 0) { - LevelChunkSection[] achunksection = chunk.getSections(); - int l = achunksection.length; - - for (int i1 = 0; i1 < l; ++i1) { - LevelChunkSection chunksection = achunksection[i1]; - - if (chunksection != LevelChunk.EMPTY_SECTION && chunksection.isRandomlyTicking()) { - int j1 = chunksection.bottomBlockY(); - - for (int k1 = 0; k1 < randomTickSpeed; ++k1) { - BlockPos blockposition2 = this.getBlockRandomPos(j, j1, k, 15); - - gameprofilerfiller.push("randomTick"); - BlockState iblockdata1 = chunksection.getBlockState(blockposition2.getX() - j, blockposition2.getY() - j1, blockposition2.getZ() - k); + LevelChunkSection[] sections = chunk.getSections(); + int minSection = com.tuinity.tuinity.util.WorldUtil.getMinSection(this); + for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) { + LevelChunkSection section = sections[sectionIndex]; + if (section == null || section.tickingList.size() == 0) { + continue; + } - if (iblockdata1.isRandomlyTicking()) { - iblockdata1.randomTick(this, blockposition2, this.random); - } + int yPos = (sectionIndex + minSection) << 4; + for (int a = 0; a < randomTickSpeed; ++a) { + int tickingBlocks = section.tickingList.size(); + int index = this.randomTickRandom.nextInt(16 * 16 * 16); + if (index >= tickingBlocks) { + continue; + } - FluidState fluid = iblockdata1.getFluidState(); + long raw = section.tickingList.getRaw(index); + int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); + int randomX = location & 15; + int randomY = ((location >>> (4 + 4)) & 255) | yPos; + int randomZ = (location >>> 4) & 15; - if (fluid.isRandomlyTicking()) { - fluid.randomTick(this, blockposition2, this.random); - } + BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); + BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); - gameprofilerfiller.pop(); - } + iblockdata.randomTick(this, blockposition2, this.randomTickRandom); + // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). + // TODO CHECK ON UPDATE } } } + // Paper end - optimise random block ticking timings.chunkTicksBlocks.stopTiming(); // Paper gameprofilerfiller.pop(); } @@ -912,7 +1110,26 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } + // Tuinity start - log detailed entity tick information + // TODO replace with varhandle + static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); + + public static List getCurrentlyTickingEntities() { + Entity ticking = currentlyTickingEntity.get(); + List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); + + return ret; + } + // Tuinity end - log detailed entity tick information + public void tickNonPassenger(Entity entity) { + // Tuinity start - log detailed entity tick information + com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); + try { + if (currentlyTickingEntity.get() == null) { + currentlyTickingEntity.lazySet(entity); + } + // Tuinity end - log detailed entity tick information ++TimingHistory.entityTicks; // Paper - timings // Spigot start co.aikar.timings.Timing timer; // Paper @@ -953,7 +1170,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl } // } finally { timer.stopTiming(); } // Paper - timings - move up - + // Tuinity start - log detailed entity tick information + } finally { + if (currentlyTickingEntity.get() == entity) { + currentlyTickingEntity.lazySet(null); + } + } + // Tuinity end - log detailed entity tick information } private void tickPassenger(Entity vehicle, Entity passenger) { @@ -1245,9 +1468,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl // Spigot Start for (BlockEntity tileentity : chunk.getBlockEntities().values()) { if (tileentity instanceof net.minecraft.world.Container) { + // Tuinity start - this area looks like it can load chunks, change the behavior + // chests for example can apply physics to the world + // so instead we just change the active container and call the event for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((net.minecraft.world.Container) tileentity).getViewers())) { - h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper + ((org.bukkit.craftbukkit.entity.CraftHumanEntity)h).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper } + // Tuiniy end } } // Spigot End diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 935b22199ebdf84db591f8442e0506d8fcc92e02..217af30c81438836864312e125cf512b491a3ab4 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -255,7 +255,7 @@ public class ServerPlayer extends Player { public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper - boolean needsChunkCenterUpdate; // Paper - no-tick view distance + public boolean needsChunkCenterUpdate; // Paper - no-tick view distance // Tuinity - public public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { @@ -418,7 +418,7 @@ public class ServerPlayer extends Player { if (blockposition1 != null) { this.moveTo(blockposition1, 0.0F, 0.0F); - if (world.noCollision(this)) { + if (world.noCollision(this, this.getBoundingBox(), null, true)) { // Tuinity - make sure this loads chunks, we default to NOT loading now break; } } @@ -426,7 +426,7 @@ public class ServerPlayer extends Player { } else { this.moveTo(blockposition, 0.0F, 0.0F); - while (!world.noCollision(this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { + while (!world.noCollision(this, this.getBoundingBox(), null, true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Tuinity - make sure this loads chunks, we default to NOT loading now this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); } } @@ -1557,6 +1557,18 @@ public class ServerPlayer extends Player { this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); this.doCloseContainer(); } + // Tuinity start - special close for unloaded inventory + @Override + public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { + // copied from above + CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit + // Paper end + // copied from below + this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); + this.containerMenu = this.inventoryMenu; + // do not run close logic + } + // Tuinity end - special close for unloaded inventory public void doCloseContainer() { this.containerMenu.removed((Player) this); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java index e572088cad8b9e09b1d64f7971bacac2f10c5b17..b2c8cae1a777cd63a35ed1340caf205b1b3bb0ad 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java @@ -56,14 +56,28 @@ public class ServerPlayerGameMode { @Nullable private GameType previousGameModeForPlayer; private boolean isDestroyingBlock; - private int destroyProgressStart; + private int destroyProgressStart; private long lastDigTime; // Tuinity - lag compensate block breaking private BlockPos destroyPos; private int gameTicks; private boolean hasDelayedDestroy; private BlockPos delayedDestroyPos; - private int delayedTickStart; + private int delayedTickStart; private long hasDestroyedTooFastStartTime; // Tuinity - lag compensate block breaking private int lastSentState; + // Tuinity start - lag compensate block breaking + private int getTimeDiggingLagCompensate() { + int lagCompensated = (int)((System.nanoTime() - this.lastDigTime) / (50L * 1000L * 1000L)); + int tickDiff = this.gameTicks - this.destroyProgressStart; + return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to + } + + private int getTimeDiggingTooFastLagCompensate() { + int lagCompensated = (int)((System.nanoTime() - this.hasDestroyedTooFastStartTime) / (50L * 1000L * 1000L)); + int tickDiff = this.gameTicks - this.delayedTickStart; + return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to + } + // Tuinity end + public ServerPlayerGameMode(ServerPlayer player) { this.gameModeForPlayer = GameType.DEFAULT_MODE; this.destroyPos = BlockPos.ZERO; @@ -130,7 +144,7 @@ public class ServerPlayerGameMode { if (iblockdata == null || iblockdata.isAir()) { // Paper this.hasDelayedDestroy = false; } else { - float f = this.incrementDestroyProgress(iblockdata, this.delayedDestroyPos, this.delayedTickStart); + float f = this.updateBlockBreakAnimation(iblockdata, this.delayedDestroyPos, this.getTimeDiggingTooFastLagCompensate()); // Tuinity - lag compensate destroying blocks if (f >= 1.0F) { this.hasDelayedDestroy = false; @@ -150,7 +164,7 @@ public class ServerPlayerGameMode { this.lastSentState = -1; this.isDestroyingBlock = false; } else { - this.incrementDestroyProgress(iblockdata, this.destroyPos, this.destroyProgressStart); + this.updateBlockBreakAnimation(iblockdata, this.destroyPos, this.getTimeDiggingLagCompensate()); // Tuinity - lag compensate destroying } } @@ -158,6 +172,12 @@ public class ServerPlayerGameMode { private float incrementDestroyProgress(BlockState state, BlockPos pos, int i) { int j = this.gameTicks - i; + // Tuinity start - change i (startTime) to totalTime + return this.updateBlockBreakAnimation(state, pos, j); + } + private float updateBlockBreakAnimation(BlockState state, BlockPos pos, int totalTime) { + int j = totalTime; + // Tuinity end float f = state.getDestroyProgress(this.player, this.player.level, pos) * (float) (j + 1); int k = (int) (f * 10.0F); @@ -225,7 +245,7 @@ public class ServerPlayerGameMode { return; } - this.destroyProgressStart = this.gameTicks; + this.destroyProgressStart = this.gameTicks; this.lastDigTime = System.nanoTime(); // Tuinity - lag compensate block breaking float f = 1.0F; iblockdata = this.level.getBlockState(pos); @@ -278,12 +298,12 @@ public class ServerPlayerGameMode { int j = (int) (f * 10.0F); this.level.destroyBlockProgress(this.player.getId(), pos, j); - this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying")); + if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying")); this.lastSentState = j; } } else if (action == ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK) { if (pos.equals(this.destroyPos)) { - int k = this.gameTicks - this.destroyProgressStart; + int k = this.getTimeDiggingLagCompensate(); // Tuinity - lag compensate block breaking iblockdata = this.level.getBlockState(pos); if (!iblockdata.isAir()) { @@ -300,12 +320,18 @@ public class ServerPlayerGameMode { this.isDestroyingBlock = false; this.hasDelayedDestroy = true; this.delayedDestroyPos = pos; - this.delayedTickStart = this.destroyProgressStart; + this.delayedTickStart = this.destroyProgressStart; this.hasDestroyedTooFastStartTime = this.lastDigTime; // Tuinity - lag compensate block breaking } } } + // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block + if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { + this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); + } else { this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "stopped destroying")); + } + // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block } else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) { this.isDestroyingBlock = false; if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) { @@ -317,7 +343,7 @@ public class ServerPlayerGameMode { } this.level.destroyBlockProgress(this.player.getId(), pos, -1); - this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying")); + if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying")); // Tuinity - this can cause clients on a lagging server to think they stopped destroying a block they're currently destroying } } @@ -327,7 +353,13 @@ public class ServerPlayerGameMode { public void destroyAndAck(BlockPos pos, ServerboundPlayerActionPacket.Action action, String reason) { if (this.destroyBlock(pos)) { + // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block + if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { + this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); + } else { this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, reason)); + } + // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block } else { this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // CraftBukkit - SPIGOT-5196 } diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java index de228b677810ce49c4e953ca0b4e590413b20e45..580165d0a728a4558031dac11f5edaf2923b5ad0 100644 --- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java @@ -23,6 +23,17 @@ import net.minecraft.world.level.lighting.LevelLightEngine; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +// Tuinity start +import ca.spottedleaf.starlight.light.StarLightEngine; +import com.tuinity.tuinity.util.CoordinateUtils; +import java.util.function.Supplier; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import net.minecraft.world.level.chunk.ChunkStatus; +// Tuinity end + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { private static final Logger LOGGER = LogManager.getLogger(); private final ProcessorMailbox taskMailbox; @@ -32,13 +43,166 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl private volatile int taskPerBatch = 5; private final AtomicBoolean scheduled = new AtomicBoolean(); + // Tuinity start - replace light engine impl + protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine; + public final boolean hasBlockLight; + public final boolean hasSkyLight; + // Tuinity end - replace light engine impl + public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { - super(chunkProvider, true, hasBlockLight); + super(chunkProvider, false, false); // Tuinity - destroy vanilla light engine state this.chunkMap = chunkStorage; this.sorterMailbox = executor; this.taskMailbox = processor; + // Tuinity start - replace light engine impl + this.hasBlockLight = true; + this.hasSkyLight = hasBlockLight; // Nice variable name. + this.theLightEngine = new ca.spottedleaf.starlight.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); + // Tuinity end - replace light engine impl + } + + // Tuinity start - replace light engine impl + protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { + return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); + } + + protected long relightCounter; + + public int relight(java.util.Set chunks_param, + java.util.function.Consumer chunkLightCallback, + java.util.function.IntConsumer onComplete) { + if (!org.bukkit.Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Must only be called on the main thread"); + } + + java.util.Set chunks = new java.util.LinkedHashSet<>(chunks_param); + // add tickets + java.util.Map ticketIds = new java.util.HashMap<>(); + int totalChunks = 0; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkPos chunkPos = iterator.next(); + + final ChunkAccess chunk = ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + final Long id = Long.valueOf(this.relightCounter++); + + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); + ticketIds.put(chunkPos, id); + + ++totalChunks; + } + + this.taskMailbox.tell(() -> { + this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { + chunkLightCallback.accept(chunkPos); + ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); + }); + }, onComplete); + }); + this.tryScheduleUpdate(); + + return totalChunks; + } + + private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { + final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); + + final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); + if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing + // chunk scheduling, we could be lighting and generating a chunk at the same time + return; + } + + if (center.getStatus() != ChunkStatus.FULL) { + // do not keep chunk loaded, we are probably in a gen thread + // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) + runnable.get(); + return; + } + + if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { + // ticket logic is not safe to run off-main, re-schedule + world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { + this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); + }); + return; + } + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final CompletableFuture updateFuture = runnable.get(); + + if (updateFuture == null) { + // not scheduled + return; + } + + final int references = this.chunksBeingWorkedOn.addTo(key, 1); + if (references == 0) { + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } + + // append future to this chunk and 1 radius neighbours chunk save futures + // this prevents us from saving the world without first waiting for the light engine + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ)); + if (neighbour != null) { + neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> { + return curr; + }); + } + } + } + + updateFuture.thenAcceptAsync((final Void ignore) -> { + final int newReferences = this.chunksBeingWorkedOn.get(key); + if (newReferences == 1) { + this.chunksBeingWorkedOn.remove(key); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } else { + this.chunksBeingWorkedOn.put(key, newReferences - 1); + } + }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { + if (thr != null) { + LOGGER.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); + } + }); + } + + @Override + public boolean hasLightWork() { + // route to new light engine + return this.theLightEngine.hasUpdates() || !this.lightTasks.isEmpty(); } + @Override + public LayerLightEventListener getLayerListener(final LightLayer lightType) { + return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); + } + + @Override + public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { + // need to use new light hooks for this + final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; + final int block = this.theLightEngine.getBlockReader().getLightValue(pos); + return Math.max(sky, block); + } + // Tuinity end - replace light engine impl + @Override public void close() { } @@ -55,15 +219,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void checkBlock(BlockPos pos) { - BlockPos blockPos = pos.immutable(); - this.addTask(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ThreadedLevelLightEngine.TaskType.POST_UPDATE, Util.name(() -> { - super.checkBlock(blockPos); - }, () -> { - return "checkBlock " + blockPos; - })); + // Tuinity start - replace light engine impl + final BlockPos posCopy = pos.immutable(); + this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { + return this.theLightEngine.blockChange(posCopy); + }); + // Tuinity end - replace light engine impl } protected void updateChunkStatus(ChunkPos pos) { + if (true) return; // Tuinity - replace light engine impl this.addTask(pos.x, pos.z, () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -86,17 +251,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void updateSectionStatus(SectionPos pos, boolean notReady) { - this.addTask(pos.x(), pos.z(), () -> { - return 0; - }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - super.updateSectionStatus(pos, notReady); - }, () -> { - return "updateSectionStatus " + pos + " " + notReady; - })); + // Tuinity start - replace light engine impl + this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { + return this.theLightEngine.sectionChange(pos, notReady); + }); + // Tuinity end - replace light engine impl } @Override public void enableLightSources(ChunkPos chunkPos, boolean bl) { + if (true) return; // Tuinity - replace light engine impl this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { super.enableLightSources(chunkPos, bl); }, () -> { @@ -106,6 +270,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) { + if (true) return; // Tuinity - replace light engine impl this.addTask(pos.x(), pos.z(), () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -131,6 +296,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void retainData(ChunkPos pos, boolean retainData) { + if (true) return; // Tuinity - replace light engine impl this.addTask(pos.x, pos.z, () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -141,6 +307,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { + // Tuinity start - replace light engine impl + if (true) { + boolean lit = excludeBlocks; + final ChunkPos chunkPos = chunk.getPos(); + + return CompletableFuture.supplyAsync(() -> { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); + if (!lit) { + chunk.setLightCorrect(false); + this.theLightEngine.lightChunk(chunk, emptySections); + chunk.setLightCorrect(true); + } else { + this.theLightEngine.forceLoadInChunk(chunk, emptySections); + // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have + // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should + // catch what we miss here. + this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z); + } + + this.chunkMap.releaseLightTicket(chunkPos); + return chunk; + }, (runnable) -> { + this.theLightEngine.scheduleChunkLight(chunkPos, runnable); + this.tryScheduleUpdate(); + }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { + if (throwable != null) { + LOGGER.fatal("Failed to light chunk " + chunkPos, throwable); + } + }); + } + // Tuinity end - replace light engine impl ChunkPos chunkPos = chunk.getPos(); chunk.setLightCorrect(false); this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -175,7 +372,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public void tryScheduleUpdate() { - if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { + if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Tuinity - rewrite light engine this.taskMailbox.tell(() -> { this.runUpdate(); this.scheduled.set(false); @@ -197,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } objectListIterator.back(j); - super.runUpdates(Integer.MAX_VALUE, true, true); + this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) { Pair pair2 = objectListIterator.next(); diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index 3c1698ba0d3bc412ab957777d9b5211dbc555208..c438cbfa1f964a5bea98bca85e688d8019e1c4fb 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -31,6 +31,8 @@ public class TicketType { public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper + public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Tuinity - ensure chunks stay loaded for lighting + public static final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail public static TicketType create(String name, Comparator argumentComparator) { return new TicketType<>(name, argumentComparator, 0L); diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java index 0f6b534a4c789a2f09f6c4624e5d58b99c7ed0e6..fea852674098fe411841d8e5ebeace7d11d94e4f 100644 --- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java +++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java @@ -77,6 +77,23 @@ public class WorldGenRegion implements WorldGenLevel { @Nullable private Supplier currentlyGenerating; + // Tuinity start + // No-op, this class doesn't provide entity access + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + return Collections.emptyList(); + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} + // Tuinity end + public WorldGenRegion(ServerLevel world, List list, ChunkStatus chunkstatus, int i) { this.generatingStatus = chunkstatus; this.writeRadiusCutoff = i; diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 35fa416a8ce332e823ed5077a8fd3492683d7ad0..f78119970da27ef66a9d9093e2e42ce129d4cf31 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -537,6 +537,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1); // Paper end - fix large move vectors killing the server + // Tuinity start - fix large move vectors killing the server + double otherFieldX = d3 - this.vehicleLastGoodX; + double otherFieldY = d4 - this.vehicleLastGoodY - 1.0E-6D; + double otherFieldZ = d5 - this.vehicleLastGoodZ; + d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1); + // Tuinity end - fix large move vectors killing the server // CraftBukkit start - handle custom speeds and skipped ticks this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; @@ -577,12 +583,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser return; } - boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); + AABB oldBox = entity.getBoundingBox(); // Tuinity - copy from player movement packet - d6 = d3 - this.vehicleLastGoodX; - d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; - d8 = d5 - this.vehicleLastGoodZ; + d6 = d3 - this.vehicleLastGoodX; // Tuinity - diff on change, used for checking large move vectors above + d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Tuinity - diff on change, used for checking large move vectors above + d8 = d5 - this.vehicleLastGoodZ; // Tuinity - diff on change, used for checking large move vectors above entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); + boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... double d11 = d7; d6 = d3 - entity.getX(); @@ -596,16 +603,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser boolean flag1 = false; if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot - flag1 = true; + flag1 = true; // Tuinity - diff on change, this should be moved wrongly ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)); } Location curPos = this.getCraftPlayer().getLocation(); // Spigot entity.absMoveTo(d3, d4, d5, f, f1); this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit - boolean flag2 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); - - if (flag && (flag1 || !flag2)) { + // Tuinity start - optimise out extra getCubes + boolean teleportBack = flag1; // violating this is always a fail + if (!teleportBack) { + // note: only call after setLocation, or else getBoundingBox is wrong + AABB newBox = entity.getBoundingBox(); + if (didCollide || !oldBox.equals(newBox)) { + teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); + } // else: no collision at all detected, why do we care? + } + if (teleportBack) { // Tuinity end - optimise out extra getCubes entity.absMoveTo(d0, d1, d2, f, f1); this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit this.connection.send(new ClientboundMoveVehiclePacket(entity)); @@ -691,7 +705,32 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } private boolean noBlocksAround(Entity entity) { - return entity.level.getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); + // Tuinity start - stop using streams, this is already a known fixed problem in Entity#move + AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); + int minX = Mth.floor(box.minX); + int minY = Mth.floor(box.minY); + int minZ = Mth.floor(box.minZ); + int maxX = Mth.floor(box.maxX); + int maxY = Mth.floor(box.maxY); + int maxZ = Mth.floor(box.maxZ); + + Level world = entity.level; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + + for (int y = minY; y <= maxY; ++y) { + for (int z = minZ; z <= maxZ; ++z) { + for (int x = minX; x <= maxX; ++x) { + pos.set(x, y, z); + BlockState type = world.getTypeIfLoaded(pos); + if (type != null && !type.isAir()) { + return false; + } + } + } + } + + return true; + // Tuinity end - stop using streams, this is already a known fixed problem in Entity#move } @Override @@ -1247,7 +1286,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } if (this.awaitingPositionFromClient != null) { - if (this.tickCount - this.awaitingTeleportTime > 20) { + if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Tuinity - this will greatly screw with clients with > 1000ms RTT this.awaitingTeleportTime = this.tickCount; this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); } @@ -1286,6 +1325,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser double currDeltaZ = toZ - prevZ; double d11 = Math.max(d7 * d7 + d8 * d8 + d9 * d9, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1); // Paper end - fix large move vectors killing the server + // Tuinity start - fix large move vectors killing the server + double otherFieldX = d0 - this.lastGoodX; + double otherFieldY = d1 - this.lastGoodY; + double otherFieldZ = d2 - this.lastGoodZ; + d11 = Math.max(d11, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1); + // Tuinity end - fix large move vectors killing the server if (this.player.isSleeping()) { if (d11 > 1.0D) { @@ -1335,11 +1380,11 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } } - AABB axisalignedbb = this.player.getBoundingBox(); + AABB axisalignedbb = this.player.getBoundingBox(); // Tuinity - diff on change, should be old AABB - d7 = d0 - this.lastGoodX; - d8 = d1 - this.lastGoodY; - d9 = d2 - this.lastGoodZ; + d7 = d0 - this.lastGoodX; // Tuinity - diff on change, used for checking large move vectors above + d8 = d1 - this.lastGoodY; // Tuinity - diff on change, used for checking large move vectors above + d9 = d2 - this.lastGoodZ; // Tuinity - diff on change, used for checking large move vectors above boolean flag = d8 > 0.0D; if (this.player.isOnGround() && !packet.isOnGround() && flag) { @@ -1374,6 +1419,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } this.player.move(MoverType.PLAYER, new Vec3(d7, d8, d9)); + boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... this.player.setOnGround(packet.isOnGround()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move // Paper start - prevent position desync if (this.awaitingPositionFromClient != null) { @@ -1393,12 +1439,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser boolean flag1 = false; if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot - flag1 = true; + flag1 = true; // Tuinity - diff on change, this should be moved wrongly ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); } this.player.absMoveTo(d0, d1, d2, f, f1); - if (!this.player.noPhysics && !this.player.isSleeping() && (flag1 && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew((LevelReader) worldserver, axisalignedbb))) { + // Tuinity start - optimise out extra getCubes + // Original for reference: + // boolean teleportBack = flag1 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb)); + boolean teleportBack = flag1; // violating this is always a fail + if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { + AABB newBox = this.player.getBoundingBox(); + if (didCollide || !axisalignedbb.equals(newBox)) { + // note: only call after setLocation, or else getBoundingBox is wrong + teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); + } // else: no collision at all detected, why do we care? + } + if (!this.player.noPhysics && !this.player.isSleeping() && teleportBack) { // Tuinity end - optimise out extra getCubes this.teleport(d3, d4, d5, f, f1); } else { // CraftBukkit start - fire PlayerMoveEvent @@ -1485,6 +1542,27 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } } + // Tuinity start - optimise out extra getCubes + private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { + final List collisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList(); + try { + com.tuinity.tuinity.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, true, false, + true, false, null, null); + + for (int i = 0, len = collisions.size(); i < len; ++i) { + final AABB box = collisions.get(i); + if (!com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { + return true; + } + } + + return false; + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(collisions); + } + } + // Tuinity end - optimise out extra getCubes + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) { Stream stream = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D), (entity) -> { return true; diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java index 9a428e166561b4bc028732ec563d3b2e99f81a8e..771c575ffe46db94d9c91f3fd0440d4deb460de7 100644 --- a/src/main/java/net/minecraft/server/players/GameProfileCache.java +++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java @@ -60,6 +60,11 @@ public class GameProfileCache { @Nullable private Executor executor; + // Tuinity start + protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock(); + protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock(); + // Tuinity end + public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) { this.profileRepository = profileRepository; this.file = cacheFile; @@ -67,6 +72,7 @@ public class GameProfileCache { } private void safeAdd(GameProfileCache.GameProfileInfo entry) { + try { this.stateLock.lock(); // Tuinity - allow better concurrency GameProfile gameprofile = entry.getProfile(); entry.setLastAccess(this.getNextOperation()); @@ -81,6 +87,7 @@ public class GameProfileCache { if (uuid != null) { this.profilesByUUID.put(uuid, entry); } + } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency } @@ -118,7 +125,7 @@ public class GameProfileCache { return com.destroystokyo.paper.PaperConfig.isProxyOnlineMode(); // Paper } - public synchronized void add(GameProfile profile) { // Paper - synchronize + public void add(GameProfile profile) { // Paper - synchronize // Tuinity - allow better concurrency Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); @@ -135,8 +142,9 @@ public class GameProfileCache { } @Nullable - public synchronized GameProfile get(String name) { // Paper - synchronize + public GameProfile get(String name) { // Paper - synchronize // Tuinity start - allow better concurrency String s1 = name.toLowerCase(Locale.ROOT); + boolean stateLocked = true; try { this.stateLock.lock(); // Tuinity - allow better concurrency GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1); boolean flag = false; @@ -150,10 +158,14 @@ public class GameProfileCache { GameProfile gameprofile; if (usercache_usercacheentry != null) { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency usercache_usercacheentry.setLastAccess(this.getNextOperation()); gameprofile = usercache_usercacheentry.getProfile(); } else { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency + try { this.lookupLock.lock(); // Tuinity - allow better concurrency gameprofile = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players + } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency if (gameprofile != null) { this.add(gameprofile); flag = false; @@ -165,6 +177,7 @@ public class GameProfileCache { } return gameprofile; + } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency } public void getAsync(String s, Consumer consumer) { @@ -326,7 +339,9 @@ public class GameProfileCache { } private Stream getTopMRUProfiles(int limit) { + try { this.stateLock.lock(); // Tuinity - allow better concurrency return ImmutableList.copyOf(this.profilesByUUID.values()).stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit((long) limit); + } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency } private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) { diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index b46e64a386d256d30eac330c463c71396452563d..570cea8ee6a442b2dc3c6ef849294ef0c02027ca 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -175,6 +175,7 @@ public abstract class PlayerList { abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor public void placeNewPlayer(Connection connection, ServerPlayer player) { + player.isRealPlayer = true; // Paper // Tuinity - this is a better place to write this that works and isn't overriden by plugins ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper if (prev != null) { disconnectPendingPlayer(prev); @@ -264,7 +265,7 @@ public abstract class PlayerList { boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); // Spigot - view distance - playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.getLoadViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance + playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance // Tuinity - replace old player chunk management player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); @@ -723,7 +724,7 @@ public abstract class PlayerList { SocketAddress socketaddress = loginlistener.connection.getRemoteAddress(); ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameprofile); - entity.isRealPlayer = true; // Paper - Chunk priority + // Tuinity - some plugins (namely protocolsupport) bypass this logic completely! So this needs to be moved. Player player = entity.getBukkitEntity(); PlayerLoginEvent event = new PlayerLoginEvent(player, hostname, ((java.net.InetSocketAddress) socketaddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.getRawAddress()).getAddress()); @@ -927,13 +928,13 @@ public abstract class PlayerList { worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper entityplayer1.forceCheckHighPriority(); // Player - Chunk priority - while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { + while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), null, true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Tuinity - make sure this loads chunks, we default to NOT loading now entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); } // CraftBukkit start LevelData worlddata = worldserver1.getLevelData(); entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag)); - entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.getLoadViewDistance())); // Spigot // Paper - no-tick view distance + entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance())); // Spigot // Paper - no-tick view distance// Tuinity - replace old player chunk management entityplayer1.setLevel(worldserver1); entityplayer1.unsetRemoved(); entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); @@ -1208,7 +1209,7 @@ public abstract class PlayerList { // Really shouldn't happen... backingSet = world != null ? world.players.toArray() : players.toArray(); } else { - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); // Tuinity - replace old player chunk management if (nearbyPlayers == null) { return; } diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java index 07e1374ac3430662edd9f585e59b785e329f0820..9f9c0b56f0891e9c423d79f8ae4c3643a2b91048 100644 --- a/src/main/java/net/minecraft/util/BitStorage.java +++ b/src/main/java/net/minecraft/util/BitStorage.java @@ -104,4 +104,32 @@ public class BitStorage { } } + + // Paper start + public final void forEach(DataBitConsumer consumer) { + int i = 0; + long[] along = this.data; + int j = along.length; + + for (int k = 0; k < j; ++k) { + long l = along[k]; + + for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { + consumer.accept(i, (int) (l & this.mask)); + l >>= this.bits; + ++i; + if (i >= this.size) { + return; + } + } + } + } + + @FunctionalInterface + public static interface DataBitConsumer { + + void accept(int location, int data); + + } + // Paper end } diff --git a/src/main/java/net/minecraft/util/valueproviders/IntProvider.java b/src/main/java/net/minecraft/util/valueproviders/IntProvider.java index 020a19cd683dd3779c5116d12b3cdcd3b3ca69b4..17d209c347b07acef451180c97835f41b8bf8433 100644 --- a/src/main/java/net/minecraft/util/valueproviders/IntProvider.java +++ b/src/main/java/net/minecraft/util/valueproviders/IntProvider.java @@ -9,13 +9,44 @@ import net.minecraft.core.Registry; public abstract class IntProvider { private static final Codec> CONSTANT_OR_DISPATCH_CODEC = Codec.either(Codec.INT, Registry.INT_PROVIDER_TYPES.dispatch(IntProvider::getType, IntProviderType::codec)); - public static final Codec CODEC = CONSTANT_OR_DISPATCH_CODEC.xmap((either) -> { + public static final Codec CODEC_REAL = CONSTANT_OR_DISPATCH_CODEC.xmap((either) -> { // Paper - used by CODEC below return either.map(ConstantInt::of, (intProvider) -> { return intProvider; }); }, (intProvider) -> { return intProvider.getType() == IntProviderType.CONSTANT ? Either.left(((ConstantInt)intProvider).getValue()) : Either.right(intProvider); }); + // Tuinity start + public static final Codec CODEC = new Codec<>() { + @Override + public DataResult> decode(com.mojang.serialization.DynamicOps ops, T input) { + /* + UniformInt: + count -> { (old format) + base, spread + } -> {UniformInt} { (new format & type) + base, base + spread + } */ + + + if (ops.get(input, "base").result().isPresent() && ops.get(input, "spread").result().isPresent()) { + // detected old format + int base = ops.getNumberValue(ops.get(input, "base").result().get()).result().get().intValue(); + int spread = ops.getNumberValue(ops.get(input, "spread").result().get()).result().get().intValue(); + return DataResult.success(new com.mojang.datafixers.util.Pair<>(UniformInt.of(base, base + spread), input)); + } + + // not old format, forward to real codec + return CODEC_REAL.decode(ops, input); + } + + @Override + public DataResult encode(IntProvider input, com.mojang.serialization.DynamicOps ops, T prefix) { + // forward to real codec + return CODEC_REAL.encode(input, ops, prefix); + } + }; + // Tuinity end public static final Codec NON_NEGATIVE_CODEC = codec(0, Integer.MAX_VALUE); public static final Codec POSITIVE_CODEC = codec(1, Integer.MAX_VALUE); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 4fd030ef9537d9b31c6167d73349f4c4a6b33a15..ca7718053a6a2eb715ea3671bd4bc15304ede420 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -356,8 +356,27 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { - return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] - .getObjectsInRange(MCUtil.getCoordinateKey(this)); + // Tuinity start - determine highest range of passengers + if (this.passengers.isEmpty()) { + return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] + .getObjectsInRange(MCUtil.getCoordinateKey(this)); + } + Iterable passengers = this.getIndirectPassengers(); + net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; + org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; + int range = chunkMap.getEntityTrackerRange(type.ordinal()); + + for (Entity passenger : passengers) { + org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; + int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); + if (passengerRange > range) { + type = passengerType; + range = passengerRange; + } + } + + return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); + // Tuinity end - determine highest range of passengers } // Paper end - optimise entity tracking @@ -392,6 +411,56 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // Paper end - make end portalling safe + // Tuinity start + public final AABB getBoundingBoxAt(double x, double y, double z) { + return this.dimensions.makeBoundingBox(x, y, z); + } + // Tuinity end + + // Tuinity start + /** + * Overriding this field will cause memory leaks. + */ + private final boolean hardCollides; + + private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); + { + /* // Goodbye, broken on reobf... + Boolean hardCollides = cachedOverrides.get(this.getClass()); + if (hardCollides == null) { + try { + java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); + java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); + if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) + || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { + hardCollides = Boolean.TRUE; + } else { + hardCollides = Boolean.FALSE; + } + cachedOverrides.put(this.getClass(), hardCollides); + } + catch (ThreadDeath thr) { throw thr; } + catch (Throwable thr) { + // shouldn't happen, just explode + throw new RuntimeException(thr); + } + } */ + this.hardCollides = this instanceof Boat + || this instanceof net.minecraft.world.entity.monster.Shulker + || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart; + } + + public final boolean hardCollides() { + return this.hardCollides; + } + + public net.minecraft.server.level.ChunkHolder.FullChunkStatus chunkStatus; + + public int sectionX = Integer.MIN_VALUE; + public int sectionY = Integer.MIN_VALUE; + public int sectionZ = Integer.MIN_VALUE; + // Tuinity end + public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); this.passengers = ImmutableList.of(); @@ -813,7 +882,42 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return this.onGround; } + // Tuinity start - detailed watchdog information + public final Object posLock = new Object(); // Tuinity - log detailed entity tick information + + private Vec3 moveVector; + private double moveStartX; + private double moveStartY; + private double moveStartZ; + + public final Vec3 getMoveVector() { + return this.moveVector; + } + + public final double getMoveStartX() { + return this.moveStartX; + } + + public final double getMoveStartY() { + return this.moveStartY; + } + + public final double getMoveStartZ() { + return this.moveStartZ; + } + // Tuinity end - detailed watchdog information + public void move(MoverType movementType, Vec3 movement) { + // Tuinity start - detailed watchdog information + com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot move an entity off-main"); + synchronized (this.posLock) { + this.moveStartX = this.getX(); + this.moveStartY = this.getY(); + this.moveStartZ = this.getZ(); + this.moveVector = movement; + } + try { + // Tuinity end - detailed watchdog information if (this.noPhysics) { this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); } else { @@ -949,9 +1053,44 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n float f2 = this.getBlockSpeedFactor(); this.setDeltaMovement(this.getDeltaMovement().multiply((double) f2, 1.0D, (double) f2)); - if (this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata1) -> { - return iblockdata1.is((Tag) BlockTags.FIRE) || iblockdata1.is(Blocks.LAVA); - })) { + // Tuinity start - remove expensive streams from here + boolean noneMatch = true; + AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); + { + int minX = Mth.floor(fireSearchBox.minX); + int minY = Mth.floor(fireSearchBox.minY); + int minZ = Mth.floor(fireSearchBox.minZ); + int maxX = Mth.floor(fireSearchBox.maxX); + int maxY = Mth.floor(fireSearchBox.maxY); + int maxZ = Mth.floor(fireSearchBox.maxZ); + fire_search_loop: + for (int fz = minZ; fz <= maxZ; ++fz) { + for (int fx = minX; fx <= maxX; ++fx) { + for (int fy = minY; fy <= maxY; ++fy) { + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); + if (chunk == null) { + // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true + // even if we're in lava/fire + noneMatch = true; + break fire_search_loop; + } + if (!noneMatch) { + // don't do get type, we already know we're in fire - we just need to check the chunks + // loaded state + continue; + } + + BlockState type = chunk.getType(fx, fy, fz); + if (type.is((Tag) BlockTags.FIRE) || type.is(Blocks.LAVA)) { + noneMatch = false; + // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded + } + } + } + } + } + if (noneMatch) { + // Tuinity end - remove expensive streams from here if (this.remainingFireTicks <= 0) { this.setRemainingFireTicks(-this.getFireImmuneTicks()); } @@ -968,6 +1107,13 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n this.level.getProfiler().pop(); } } + // Tuinity start - detailed watchdog information + } finally { + synchronized (this.posLock) { // Tuinity + this.moveVector = null; + } // Tuinity + } + // Tuinity end - detailed watchdog information } protected void tryCheckInsideBlocks() { @@ -1073,39 +1219,79 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return offsetFactor; } - private Vec3 collide(Vec3 movement) { - AABB axisalignedbb = this.getBoundingBox(); - CollisionContext voxelshapecollision = CollisionContext.of(this); - VoxelShape voxelshape = this.level.getWorldBorder().getCollisionShape(); - Stream stream = !this.level.getWorldBorder().isWithinBounds(axisalignedbb) ? Stream.empty() : Stream.of(voxelshape); // Paper - Stream stream1 = this.level.getEntityCollisions(this, axisalignedbb.expandTowards(movement), (entity) -> { - return true; - }); - RewindableStream streamaccumulator = new RewindableStream<>(Stream.concat(stream1, stream)); - Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBoxHeuristically(this, movement, axisalignedbb, this.level, voxelshapecollision, streamaccumulator); - boolean flag = movement.x != vec3d1.x; - boolean flag1 = movement.y != vec3d1.y; - boolean flag2 = movement.z != vec3d1.z; - boolean flag3 = this.onGround || flag1 && movement.y < 0.0D; - - if (this.maxUpStep > 0.0F && flag3 && (flag || flag2)) { - Vec3 vec3d2 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, (double) this.maxUpStep, movement.z), axisalignedbb, this.level, voxelshapecollision, streamaccumulator); - Vec3 vec3d3 = Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, (double) this.maxUpStep, 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level, voxelshapecollision, streamaccumulator); - - if (vec3d3.y < (double) this.maxUpStep) { - Vec3 vec3d4 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level, voxelshapecollision, streamaccumulator).add(vec3d3); - - if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { - vec3d2 = vec3d4; + private Vec3 collide(Vec3 moveVector) { + // Tuinity start - optimise collisions + // This is a copy of vanilla's except that it uses strictly AABB math + if (moveVector.x == 0.0 && moveVector.y == 0.0 && moveVector.z == 0.0) { + return moveVector; + } + + final Level world = this.level; + final AABB currBoundingBox = this.getBoundingBox(); + + if (com.tuinity.tuinity.util.CollisionUtil.isEmpty(currBoundingBox)) { + return moveVector; + } + + final List potentialCollisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList(); + try { + final double stepHeight = (double)this.maxUpStep; + final AABB collisionBox; + + if (moveVector.x == 0.0 && moveVector.z == 0.0 && moveVector.y != 0.0) { + if (moveVector.y > 0.0) { + collisionBox = com.tuinity.tuinity.util.CollisionUtil.cutUpwards(currBoundingBox, moveVector.y); + } else { + collisionBox = com.tuinity.tuinity.util.CollisionUtil.cutDownwards(currBoundingBox, moveVector.y); + } + } else { + if (stepHeight > 0.0 && (this.onGround || (moveVector.y < 0.0)) && (moveVector.x != 0.0 || moveVector.z != 0.0)) { + // don't bother getting the collisions if we don't need them. + if (moveVector.y <= 0.0) { + collisionBox = com.tuinity.tuinity.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z), stepHeight); + } else { + collisionBox = currBoundingBox.expandTowards(moveVector.x, Math.max(stepHeight, moveVector.y), moveVector.z); + } + } else { + collisionBox = currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z); } } - if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { - return vec3d2.add(Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level, voxelshapecollision, streamaccumulator)); + com.tuinity.tuinity.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, true, + false, false, null, null); + + if (com.tuinity.tuinity.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { + com.tuinity.tuinity.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); } - } - return vec3d1; + final Vec3 limitedMoveVector = com.tuinity.tuinity.util.CollisionUtil.performCollisions(moveVector, currBoundingBox, potentialCollisions); + + if (stepHeight > 0.0 + && (this.onGround || (limitedMoveVector.y != moveVector.y && moveVector.y < 0.0)) + && (limitedMoveVector.x != moveVector.x || limitedMoveVector.z != moveVector.z)) { + Vec3 vec3d2 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, stepHeight, moveVector.z), currBoundingBox, potentialCollisions); + final Vec3 vec3d3 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(moveVector.x, 0.0, moveVector.z), potentialCollisions); + + if (vec3d3.y < stepHeight) { + final Vec3 vec3d4 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, 0.0D, moveVector.z), currBoundingBox.move(vec3d3), potentialCollisions).add(vec3d3); + + if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { + vec3d2 = vec3d4; + } + } + + if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { + return vec3d2.add(com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + moveVector.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisions)); + } + + return limitedMoveVector; + } else { + return limitedMoveVector; + } + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(potentialCollisions); + } + // Tuinity end - optimise collisions } public static Vec3 collideBoundingBoxHeuristically(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, CollisionContext context, RewindableStream collisions) { @@ -2244,9 +2430,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n float f = this.dimensions.width * 0.8F; AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); - return this.level.getBlockCollisions(this, axisalignedbb, (iblockdata, blockposition) -> { - return iblockdata.isSuffocating(this.level, blockposition); - }).findAny().isPresent(); + // Tuinity start + return com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this.level, this, axisalignedbb, null, + false, false, false, true, (iblockdata, blockposition) -> { + return iblockdata.isSuffocating(this.level, blockposition); + }); + // Tuinity end } } @@ -2254,11 +2443,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return InteractionResult.PASS; } - public boolean canCollideWith(Entity other) { + public boolean canCollideWith(Entity other) { // Tuinity - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); } - public boolean canBeCollidedWith() { + public boolean canBeCollidedWith() { // Tuinity - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override return false; } @@ -3727,7 +3916,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } public void setDeltaMovement(Vec3 velocity) { + synchronized (this.posLock) { // Tuinity this.deltaMovement = velocity; + } // Tuinity } public void setDeltaMovement(double x, double y, double z) { @@ -3789,7 +3980,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n public final void setPosRaw(double x, double y, double z) { // Paper start - fix MC-4 if (this instanceof ItemEntity) { - if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { + if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert // encode/decode from PacketPlayOutEntity x = Mth.lfloor(x * 4096.0D) * (1 / 4096.0D); y = Mth.lfloor(y * 4096.0D) * (1 / 4096.0D); @@ -3804,7 +3995,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // Paper end if (this.position.x != x || this.position.y != y || this.position.z != z) { + synchronized (this.posLock) { // Tuinity this.position = new Vec3(x, y, z); + } // Tuinity int i = Mth.floor(x); int j = Mth.floor(y); int k = Mth.floor(z); diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index c4c5c35e37b793f3b74349ff03c0829f4913b91c..154b3c767d079f72643c826b962892c1029b0a1b 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -784,7 +784,12 @@ public abstract class Mob extends LivingEntity { if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { this.discard(); } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { - Player entityhuman = this.level.findNearbyPlayer(this, -1.0D, EntitySelector.affectsSpawning); // Paper + // Tuinity start - optimise checkDespawn + Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig.hardDespawnDistance + 1, EntitySelector.affectsSpawning); // Paper + if (entityhuman == null) { + entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0); + } + // Tuinity end - optimise checkDespawn if (entityhuman != null) { double d0 = entityhuman.distanceToSqr((Entity) this); // CraftBukkit - decompile error diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java index 6c3455823f996e0421975b7f4a00f4e333e9f514..7db31cc24e68aab55a9ba735165da23059bdc626 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java @@ -183,7 +183,7 @@ public class PoiManager extends SectionStorage { data = this.getData(chunkcoordintpair); } com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, - chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority } // Paper end this.distanceTracker.runAllUpdates(); diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java index 925f16d5eb092518ef774f69a8d99689feb0f5d7..01d8af06f19427354cac95d691e65d31253fef94 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java +++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java @@ -91,7 +91,7 @@ public class Turtle extends Animal { } public void setHomePos(BlockPos pos) { - this.entityData.set(Turtle.HOME_POS, pos); + this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... } public BlockPos getHomePos() { // Paper - public diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java index 1bccd932851045c374e3092d33dc77fab680d0db..069f658003d96a05aac0b30af1d89f15ea554475 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -498,6 +498,11 @@ public abstract class Player extends LivingEntity { this.containerMenu = this.inventoryMenu; } // Paper end + // Tuinity start - special close for unloaded inventory + public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { + this.containerMenu = this.inventoryMenu; + } + // Tuinity end - special close for unloaded inventory public void closeContainer() { this.containerMenu = this.inventoryMenu; diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java index 7ccfe737fdf7f07b731ea0ff82e897564350705c..abcc3dac7c7369a3f37e85ddeecbe272833298c9 100644 --- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java +++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java @@ -60,9 +60,10 @@ public class EnderEyeItem extends Item { // CraftBukkit start - Use relative location for far away sounds // world.b(1038, blockposition1.c(1, 0, 1), 0); - int viewDistance = world.getCraftServer().getViewDistance() * 16; + //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Tuinity - apply view distance patch BlockPos soundPos = blockposition1.offset(1, 0, 1); for (ServerPlayer player : world.getServer().getPlayerList().players) { + final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch double deltaX = soundPos.getX() - player.getX(); double deltaZ = soundPos.getZ() - player.getZ(); double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java index fe4dba491b586757a16aa36e62682f364daa2602..ec781ab232d12cedb5f0236860377c4917c576d7 100644 --- a/src/main/java/net/minecraft/world/level/BlockGetter.java +++ b/src/main/java/net/minecraft/world/level/BlockGetter.java @@ -84,7 +84,8 @@ public interface BlockGetter extends LevelHeightAccessor { return BlockHitResult.miss(raytrace1.getTo(), Direction.getNearest(vec3d.x, vec3d.y, vec3d.z), new BlockPos(raytrace1.getTo())); } // Paper end - FluidState fluid = this.getFluidState(blockposition); + if (iblockdata.isAir()) return null; // Tuinity - optimise air cases + FluidState fluid = iblockdata.getFluidState(); // Tuinity - don't need to go to world state again Vec3 vec3d = raytrace1.getFrom(); Vec3 vec3d1 = raytrace1.getTo(); VoxelShape voxelshape = raytrace1.getBlockShape(iblockdata, this, blockposition); diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java index 2a784a8342e708e0813c7076a2ca8e429446ffd3..b909bd7bf10adc9165df49a210df0d73912cd626 100644 --- a/src/main/java/net/minecraft/world/level/CollisionGetter.java +++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java @@ -36,28 +36,40 @@ public interface CollisionGetter extends BlockGetter { return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); } + // Tuinity start - optimise collisions + default boolean noCollision(Entity entity, AABB box, Predicate filter, boolean loadChunks) { + return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, loadChunks, false, entity != null, true, null) + && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); + } + // Tuinity end - optimise collisions + default boolean noCollision(AABB box) { - return this.noCollision((Entity)null, box, (e) -> { - return true; - }); + // Tuinity start - optimise collisions + return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, null, box, null, false, false, false, true, null) + && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, null, box, null, true, null); + // Tuinity end - optimise collisions } default boolean noCollision(Entity entity) { - return this.noCollision(entity, entity.getBoundingBox(), (e) -> { - return true; - }); + // Tuinity start - optimise collisions + AABB box = entity.getBoundingBox(); + return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) + && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); + // Tuinity end - optimise collisions } default boolean noCollision(Entity entity, AABB box) { - return this.noCollision(entity, box, (e) -> { - return true; - }); + // Tuinity start - optimise collisions + return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) + && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); + // Tuinity end - optimise collisions } default boolean noCollision(@Nullable Entity entity, AABB box, Predicate filter) { - try { if (entity != null) entity.collisionLoadChunks = true; // Paper - return this.getCollisions(entity, box, filter).allMatch(VoxelShape::isEmpty); - } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper + // Tuinity start - optimise collisions + return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) + && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); + // Tuinity end - optimise collisions } Stream getEntityCollisions(@Nullable Entity entity, AABB box, Predicate predicate); diff --git a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java index e420c98d9ccc45d570984dc30fdb928883edec9f..ac83704692cf60c34b579ed11689863ef191cad3 100644 --- a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java +++ b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java @@ -99,7 +99,7 @@ public class CollisionSpliterator extends AbstractSpliterator { VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); if (voxelShape == Shapes.block()) { - if (!this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { + if (!com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(this.box, (double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { // Tuinity - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil continue; } diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java index 325e244c46ec208a2e7e18d71ccbbfcc25fc1bce..6a4e44dd8935018d1b5283761dfb8e855be62987 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java @@ -18,6 +18,18 @@ import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; public interface EntityGetter { + + // Tuinity start + List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); + + void getEntities(Entity except, AABB box, Predicate predicate, List into); + + void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); + + void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, + Predicate predicate); + // Tuinity end + List getEntities(@Nullable Entity except, AABB box, Predicate predicate); List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); @@ -37,7 +49,7 @@ public interface EntityGetter { return true; } else { for(Entity entity2 : this.getEntities(entity, shape.bounds())) { - if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && Shapes.joinIsNotEmpty(shape, Shapes.create(entity2.getBoundingBox()), BooleanOp.AND)) { + if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && shape.intersects(entity2.getBoundingBox())) { // Tuinity return false; } } @@ -54,9 +66,9 @@ public interface EntityGetter { if (box.getSize() < 1.0E-7D) { return Stream.empty(); } else { - AABB aABB = box.inflate(1.0E-7D); - return this.getEntities(entity, aABB, predicate.and((entityx) -> { - if (entityx.getBoundingBox().intersects(aABB)) { + AABB aABB = box.inflate(-1.0E-7D); // Tuinity - needs to be negated, or else we get things we don't collide with + Predicate hardCollides = (entityx) -> { // Tuinity - optimise entity hard collisions + if (true || entityx.getBoundingBox().intersects(aABB)) { // Tuinity - always true if (entity == null) { if (entityx.canBeCollidedWith()) { return true; @@ -67,7 +79,11 @@ public interface EntityGetter { } return false; - })).stream().map(Entity::getBoundingBox).map(Shapes::create); + }; // Tuinity start - optimise entity hard collisions + predicate = predicate == null ? hardCollides : hardCollides.and(predicate); + return (entity != null && entity.hardCollides() ? this.getEntities(entity, aABB, predicate) : this.getHardCollidingEntities(entity, aABB, predicate)) + .stream().map(Entity::getBoundingBox).map(Shapes::create); + // Tuinity end - optimise entity hard collisions } } diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 17281575ff83bbf1e720335619a78a6d0a0e5077..38753e10b1597a2f3bd2cde208c6e30b26a03b43 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -166,6 +166,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray + public final com.tuinity.tuinity.config.TuinityConfig.WorldConfig tuinityConfig; // Tuinity - Server Config + public final co.aikar.timings.WorldTimingsHandler timings; // Paper public static BlockPos lastPhysicsProblem; // Spigot private org.spigotmc.TickLimiter entityLimiter; @@ -202,9 +204,117 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.typeKey; } + // Tuinity start + protected final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager; + + // Tuinity start - optimise CraftChunk#getEntities + public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { + com.tuinity.tuinity.world.ChunkEntitySlices slices = this.entitySliceManager.getChunk(chunkX, chunkZ); + if (slices == null) { + return new org.bukkit.entity.Entity[0]; + } + return slices.getChunkEntities(); + } + // Tuinity end - optimise CraftChunk#getEntities + + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + List ret = new java.util.ArrayList<>(); + this.entitySliceManager.getEntities(except, box, ret, predicate); + return ret; + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) { + this.entitySliceManager.getEntities(except, box, into, predicate); + } + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { + this.entitySliceManager.getHardCollidingEntities(except, box, into, predicate); + } + + @Override + public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, + Predicate predicate) { + this.entitySliceManager.getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); + } + + @Override + public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { + List ret = new java.util.ArrayList<>(); + this.entitySliceManager.getEntities(entityClass, null, box, ret, predicate); + return ret; + } + // Tuinity end + // Tuinity start - optimise checkDespawn + public final List getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + LevelChunk chunk; + if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || + (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { + return this.getNearbyPlayersSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); + } + + List ret = new java.util.ArrayList<>(); + chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret); + return ret; + } + + private List getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + List ret = new java.util.ArrayList<>(); + double maxRangeSquared = maxRange * maxRange; + + for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) { + if (predicate == null || predicate.test(player)) { + ret.add(player); + } + } + } + + return ret; + } + + private net.minecraft.server.level.ServerPlayer getNearestPlayerSlow(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + net.minecraft.server.level.ServerPlayer closest = null; + double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + + for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) { + closest = player; + closestRangeSquared = distanceSquared; + } + } + + return closest; + } + + + public final net.minecraft.server.level.ServerPlayer getNearestPlayer(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + LevelChunk chunk; + if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || + (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { + return this.getNearestPlayerSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); + } + + return chunk.findNearestPlayer(sourceX, sourceY, sourceZ, maxRange, predicate); + } + + @Override + public @Nullable Player getNearestPlayer(double d0, double d1, double d2, double d3, @Nullable Predicate predicate) { + return this.getNearestPlayer(null, d0, d1, d2, d3, predicate); + } + // Tuinity end - optimise checkDespawn + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, final DimensionType dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper + this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData)worlddatamutable).getLevelName()); // Tuinity - Server Config this.generator = gen; this.world = new CraftWorld((ServerLevel) this, gen, env); this.ticksPerAnimalSpawns = this.getCraftServer().getTicksPerAnimalSpawns(); // CraftBukkit @@ -278,6 +388,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.chunkPacketBlockController = this.paperConfig.antiXray ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray + this.entitySliceManager = new com.tuinity.tuinity.world.EntitySliceManager((ServerLevel)this); // Tuinity } // Paper start @@ -363,6 +474,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline + // Tuinity start - make sure loaded chunks get the inlined variant of this function + net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource(); + if (cps.mainThread == Thread.currentThread()) { + LevelChunk ifLoaded = cps.getChunkAtIfLoadedMainThread(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + } + // Tuinity end - make sure loaded chunks get the inlined variant of this function return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump } @@ -551,7 +671,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance // if copied from above - } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { + } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Tuinity - replace old player chunk management ((ServerLevel)this).getChunkSource().blockChanged(blockposition); // Paper end - per player view distance } @@ -862,6 +982,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public void guardEntityTick(Consumer tickConsumer, T entity) { try { tickConsumer.accept(entity); + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick } catch (Throwable throwable) { if (throwable instanceof ThreadDeath) throw throwable; // Paper // Paper start - Prevent tile entity and entity crashes @@ -991,26 +1112,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { this.getProfiler().incrementCounter("getEntities"); List list = Lists.newArrayList(); - - this.getEntities().get(box, (entity1) -> { - if (entity1 != except && predicate.test(entity1)) { - list.add(entity1); - } - - if (entity1 instanceof EnderDragon) { - EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); - int i = aentitycomplexpart.length; - - for (int j = 0; j < i; ++j) { - EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; - - if (entity1 != except && predicate.test(entitycomplexpart)) { - list.add(entitycomplexpart); - } - } - } - - }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper + this.entitySliceManager.getEntities(except, box, list, predicate); // Tuinity - optimise this call return list; } @@ -1019,26 +1121,22 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.getProfiler().incrementCounter("getEntities"); List list = Lists.newArrayList(); - this.getEntities().get(filter, box, (entity) -> { - if (predicate.test(entity)) { - list.add(entity); - } - - if (entity instanceof EnderDragon) { - EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity).getSubEntities(); - int i = aentitycomplexpart.length; - - for (int j = 0; j < i; ++j) { - EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; - T t0 = filter.tryCast(entitycomplexpart); - - if (t0 != null && predicate.test(t0)) { - list.add(t0); - } - } + // Tuinity start - optimise this call + if (filter instanceof net.minecraft.world.entity.EntityType) { + this.entitySliceManager.getEntities((net.minecraft.world.entity.EntityType)filter, box, list, predicate); + } else { + Predicate test = (obj) -> { + return filter.tryCast(obj) != null; + }; + predicate = predicate == null ? test : test.and((Predicate)predicate); + Class base; + if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { + this.entitySliceManager.getEntities((Entity) null, box, (List)list, (Predicate)predicate); + } else { + this.entitySliceManager.getEntities(base, null, box, (List)list, (Predicate)predicate); // Tuinity - optimise this call } - - }); + } + // Tuinity end - optimise this call return list; } @@ -1326,10 +1424,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public abstract TagContainer getTagManager(); public BlockPos getBlockRandomPos(int x, int y, int z, int l) { + // Paper start - allow use of mutable pos + BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); + this.getRandomBlockPosition(x, y, z, l, ret); + return ret.immutable(); + } + public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { + // Paper end this.randValue = this.randValue * 3 + 1013904223; int i1 = this.randValue >> 2; - return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); + out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call + return out; // Paper } public boolean noSave() { diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index 55b937f802ee7066cb13b9a497932038b2905ff0..b2329de658df71ce03a6fe0c41c02997afdf868f 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -262,7 +262,7 @@ public final class NaturalSpawner { blockposition_mutableblockposition.set(l, i, i1); double d0 = (double) l + 0.5D; double d1 = (double) i1 + 0.5D; - Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); + Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Tuinity - use chunk's player cache to optimize search in range if (entityhuman != null) { double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); @@ -335,7 +335,7 @@ public final class NaturalSpawner { } private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) { - return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); + return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); // Tuinity - diff on change, copy into caller } private static Boolean isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureFeatureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) { // Paper diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java index aa1ba8b74ab70b6cede99e4853ac0203f388ab06..a242a80b16c7d074d52a52728646224b1a0091d4 100644 --- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java +++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java @@ -139,19 +139,28 @@ public class FarmBlock extends Block { } private static boolean isNearWater(LevelReader world, BlockPos pos) { - Iterator iterator = BlockPos.betweenClosed(pos.offset(-4, 0, -4), pos.offset(4, 1, 4)).iterator(); - - BlockPos blockposition1; - - do { - if (!iterator.hasNext()) { - return false; + // Tuinity start - remove abstract block iteration + int xOff = pos.getX(); + int yOff = pos.getY(); + int zOff = pos.getZ(); + + for (int dz = -4; dz <= 4; ++dz) { + int z = dz + zOff; + for (int dx = -4; dx <= 4; ++dx) { + int x = xOff + dx; + for (int dy = 0; dy <= 1; ++dy) { + int y = dy + yOff; + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)world.getChunk(x >> 4, z >> 4); + net.minecraft.world.level.material.FluidState fluid = chunk.getBlockData(x, y, z).getFluidState(); + if (fluid.is(FluidTags.WATER)) { + return true; + } + } } + } - blockposition1 = (BlockPos) iterator.next(); - } while (!world.getFluidState(blockposition1).is((Tag) FluidTags.WATER)); - - return true; + return false; + // Tuinity end - remove abstract block iteration } @Override diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java index 1179c62695da4dcf02590c97d8da3c6fcdbee9ef..04d5ef90cd4171f9360017ac0c01ce48ae6ec983 100644 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -610,14 +610,14 @@ public abstract class BlockBehaviour { public abstract static class BlockStateBase extends StateHolder { - private final int lightEmission; - private final boolean useShapeForLightOcclusion; + private final int lightEmission; public final int getEmittedLight() { return this.lightEmission; } // Tuinity - OBFHELPER + private final boolean useShapeForLightOcclusion; public final boolean isTransparentOnSomeFaces() { return this.useShapeForLightOcclusion; } // Tuinity - OBFHELPER private final boolean isAir; private final Material material; private final MaterialColor materialColor; public final float destroySpeed; private final boolean requiresCorrectToolForDrops; - private final boolean canOcclude; + private final boolean canOcclude; public final boolean isOpaque() { return this.canOcclude; } // Tuinity - OBFHELPER private final BlockBehaviour.StatePredicate isRedstoneConductor; private final BlockBehaviour.StatePredicate isSuffocating; private final BlockBehaviour.StatePredicate isViewBlocking; @@ -643,6 +643,7 @@ public abstract class BlockBehaviour { this.isViewBlocking = blockbase_info.isViewBlocking; this.hasPostProcess = blockbase_info.hasPostProcess; this.emissiveRendering = blockbase_info.emissiveRendering; + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Tuinity } // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; @@ -658,13 +659,34 @@ public abstract class BlockBehaviour { protected FluidState fluid; // Paper end + // Tuinity start + protected boolean shapeExceedsCube = true; + public final boolean shapeExceedsCube() { + return this.shapeExceedsCube; + } + // Tuinity end + // Tuinity start + protected int opacityIfCached = -1; + // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] + public final int getOpacityIfCached() { + return this.opacityIfCached; + } + + protected final boolean conditionallyFullOpaque; + public final boolean isConditionallyFullOpaque() { + return this.conditionallyFullOpaque; + } + // Tuinity end + public void initCache() { this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() this.isTicking = this.getBlock().isRandomlyTicking(this.asState()); // Paper - moved from isTicking() if (!this.getBlock().hasDynamicShape()) { this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); } - + this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Tuinity - moved from actual method to here + this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Tuinity - cache opacity for light + // TODO optimise light } public Block getBlock() { @@ -700,7 +722,7 @@ public abstract class BlockBehaviour { } public final boolean hasLargeCollisionShape() { // Paper - return this.cache == null || this.cache.largeCollisionShape; + return this.shapeExceedsCube; // Tuinity - moved into shape cache init } public final boolean useShapeForLightOcclusion() { // Paper diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java index baf1cb77eb170a44d821eae572d059f18ea46d7e..5d25223cb2f31e78b1608bd2846effba5b4301a4 100644 --- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java @@ -40,11 +40,13 @@ public abstract class StateHolder { private final ImmutableMap, Comparable> values; private Table, Comparable, S> neighbours; protected final MapCodec propertiesCodec; + protected final com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Tuinity - optimise state lookup protected StateHolder(O owner, ImmutableMap, Comparable> entries, MapCodec codec) { this.owner = owner; this.values = entries; this.propertiesCodec = codec; + this.optimisedTable = new com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable(this, entries); // Tuinity - optimise state lookup } public > S cycle(Property property) { @@ -85,11 +87,11 @@ public abstract class StateHolder { } public > boolean hasProperty(Property property) { - return this.values.containsKey(property); + return this.optimisedTable.get(property) != null; // Tuinity - optimise state lookup } public > T getValue(Property property) { - Comparable comparable = this.values.get(property); + Comparable comparable = this.optimisedTable.get(property); // Tuinity - optimise state lookup if (comparable == null) { throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); } else { @@ -98,24 +100,18 @@ public abstract class StateHolder { } public > Optional getOptionalValue(Property property) { - Comparable comparable = this.values.get(property); + Comparable comparable = this.optimisedTable.get(property); // Tuinity - optimise state lookup return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); } public , V extends T> S setValue(Property property, V value) { - Comparable comparable = this.values.get(property); - if (comparable == null) { - throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); - } else if (comparable == value) { - return (S)this; - } else { - S object = this.neighbours.get(property, value); - if (object == null) { - throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); - } else { - return object; - } + // Tuinity start - optimise state lookup + final S ret = (S)this.optimisedTable.get(property, value); + if (ret == null) { + throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); } + return ret; + // Tuinity end - optimise state lookup } public void populateNeighbours(Map, Comparable>, S> states) { @@ -134,7 +130,7 @@ public abstract class StateHolder { } } - this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); + this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Tuinity - optimise state lookup } } diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java index ff1a0d125edd2ea10c870cbb62ae9aa23644b6dc..90c5d20d92dd0dba3503c0f8bc16ed533ca59869 100644 --- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java @@ -7,6 +7,13 @@ import java.util.Optional; public class BooleanProperty extends Property { private final ImmutableSet values = ImmutableSet.of(true, false); + // Tuinity start - optimise iblockdata state lookup + @Override + public final int getIdFor(final Boolean value) { + return value.booleanValue() ? 1 : 0; + } + // Tuinity end - optimise iblockdata state lookup + protected BooleanProperty(String name) { super(name, Boolean.class); } diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java index bcf8b24e9f9e9870c1a1d27c721a6a433305d55a..32aa07141682ebdd99c2fce9b64c9f283a5d5707 100644 --- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java @@ -17,6 +17,15 @@ public class EnumProperty & StringRepresentable> extends Prope private final ImmutableSet values; private final Map names = Maps.newHashMap(); + // Tuinity start - optimise iblockdata state lookup + private int[] idLookupTable; + + @Override + public final int getIdFor(final T value) { + return this.idLookupTable[value.ordinal()]; + } + // Tuinity end - optimise iblockdata state lookup + protected EnumProperty(String name, Class type, Collection values) { super(name, type); this.values = ImmutableSet.copyOf(values); @@ -31,6 +40,14 @@ public class EnumProperty & StringRepresentable> extends Prope this.names.put(string, enum_); } + // Tuinity start - optimise iblockdata state lookup + int id = 0; + this.idLookupTable = new int[type.getEnumConstants().length]; + java.util.Arrays.fill(this.idLookupTable, -1); + for (final T value : this.getPossibleValues()) { + this.idLookupTable[value.ordinal()] = id++; + } + // Tuinity end - optimise iblockdata state lookup } @Override diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java index 72f508321ebffcca31240fbdd068b4d185454cbc..346ae8ff58afd1c1f439c150c3d21143b41c3295 100644 --- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java @@ -13,6 +13,16 @@ public class IntegerProperty extends Property { public final int min; public final int max; + // Tuinity start - optimise iblockdata state lookup + @Override + public final int getIdFor(final Integer value) { + final int val = value.intValue(); + final int ret = val - this.min; + + return ret | ((this.max - ret) >> 31); + } + // Tuinity end - optimise iblockdata state lookup + protected IntegerProperty(String name, int min, int max) { super(name, Integer.class); this.min = min; diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java index 81b43e0b0146729a8a1c6ade82634c86cde67857..9d5e76877bc06b3318c817c40821a453ac4c4a97 100644 --- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java @@ -20,6 +20,17 @@ public abstract class Property> { }, this::getName); private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); + // Tuinity start - optimise iblockdata state lookup + private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); + private final int id = ID_GENERATOR.getAndIncrement(); + + public final int getId() { + return this.id; + } + + public abstract int getIdFor(final T value); + // Tuinity end - optimise state lookup + protected Property(String name, Class type) { this.clazz = type; this.name = name; diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java index 63203172a127d812fd59cea0546b67e855ce3ad5..498988b70617f086f047d8d293e525377971e66e 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java @@ -1,5 +1,6 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import it.unimi.dsi.fastutil.shorts.ShortList; import java.util.Collection; @@ -42,6 +43,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess { } // Paper end + // Tuinity start + default SWMRNibbleArray[] getBlockNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setBlockNibbles(SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + default SWMRNibbleArray[] getSkyNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setSkyNibbles(SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default boolean[] getSkyEmptinessMap() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default void setSkyEmptinessMap(final boolean[] emptinessMap) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + public default boolean[] getBlockEmptinessMap() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + public default void setBlockEmptinessMap(final boolean[] emptinessMap) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + // Tuinity end + BlockState getType(final int x, final int y, final int z); // Paper @Nullable BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java index c2b0b1adcff5baf169901710d492317d44b93846..c7636191fa2ba92db95a7f779d0e5a1bd45198aa 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -196,7 +196,7 @@ public abstract class ChunkGenerator { // Get origin location (re)defined by event call. center = new BlockPos(event.getOrigin().getBlockX(), event.getOrigin().getBlockY(), event.getOrigin().getBlockZ()); // Get world (re)defined by event call. - world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle(); + //world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle(); // Tuinity - callers and this function don't expect this to change // Get radius and whether to find unexplored structures (re)defined by event call. radius = event.getRadius(); skipExistingChunks = event.shouldFindUnexplored(); diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java index 25570730f376665ca6477263d3b3f94d725ecd21..21d85b2f70e5ffe46220905b27715579d7fcdc59 100644 --- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java @@ -11,7 +11,7 @@ public class DataLayer { public static final int LAYER_SIZE = 128; private static final int NIBBLE_SIZE = 4; @Nullable - protected byte[] data; + protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Tuinity - provide accessor // Paper start public static byte[] EMPTY_NIBBLE = new byte[2048]; private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072); @@ -54,6 +54,7 @@ public class DataLayer { boolean poolSafe = false; public java.lang.Runnable cleaner; private void registerCleaner() { + if (true) return; // Tuinity - purge cleaner usage if (!poolSafe) { cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes); } else { @@ -68,7 +69,7 @@ public class DataLayer { } public DataLayer(byte[] bytes, boolean isSafe) { this.data = bytes; - if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety + // Tuinity - purge cleaner usage registerCleaner(); // Paper end if (bytes.length != 2048) { @@ -153,7 +154,7 @@ public class DataLayer { } // Paper end public DataLayer copy() { - return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor + return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Tuinity - no longer clone in constructor } public String toString() { diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java index 8245c5834ec69beb8e3b95fb3900601009a9273f..88f30cd8e57ccb69da633daac49f8bc9e44111da 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java @@ -1,5 +1,6 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; import it.unimi.dsi.fastutil.longs.LongSet; import java.util.BitSet; import java.util.Map; @@ -29,6 +30,48 @@ public class ImposterProtoChunk extends ProtoChunk { this.wrapped = wrapped; } + // Tuinity start - rewrite light engine + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.getWrapped().getBlockNibbles(); + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.getWrapped().setBlockNibbles(nibbles); + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.getWrapped().getSkyNibbles(); + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.getWrapped().setSkyNibbles(nibbles); + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.getWrapped().getSkyEmptinessMap(); + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.getWrapped().setSkyEmptinessMap(emptinessMap); + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.getWrapped().getBlockEmptinessMap(); + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.getWrapped().setBlockEmptinessMap(emptinessMap); + } + // Tuinity end - rewrite light engine + @Nullable @Override public BlockEntity getBlockEntity(BlockPos pos) { diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java index cc02b577453fa251f0f1b508281ddea2513138a1..54e23d303aad286ab46c3e5f9b17a5f9922e2942 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -1,5 +1,7 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.starlight.light.StarLightEngine; import com.google.common.collect.ImmutableList; import com.destroystokyo.paper.exception.ServerInternalException; import com.google.common.collect.Maps; @@ -17,7 +19,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -28,7 +29,6 @@ import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.core.BlockPos; -import net.minecraft.core.DefaultedRegistry; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; @@ -125,11 +125,62 @@ public class LevelChunk implements ChunkAccess { private volatile boolean isLightCorrect; private final Int2ObjectMap gameEventDispatcherSections; + // Tuinity start - rewrite light engine + protected volatile SWMRNibbleArray[] blockNibbles; + protected volatile SWMRNibbleArray[] skyNibbles; + protected volatile boolean[] skyEmptinessMap; + protected volatile boolean[] blockEmptinessMap; + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.skyEmptinessMap; + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.skyEmptinessMap = emptinessMap; + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.blockEmptinessMap; + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.blockEmptinessMap = emptinessMap; + } + // Tuinity end - rewrite light engine + public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) { this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null); } public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes, UpgradeData upgradeData, TickList blockTickScheduler, TickList fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer loadToWorldConsumer) { + // Tuinity start + this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); + this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); + // Tuinity end this.pendingBlockEntities = Maps.newHashMap(); this.tickersInLevel = Maps.newHashMap(); this.heightmaps = Maps.newEnumMap(Heightmap.Types.class); @@ -192,7 +243,7 @@ public class LevelChunk implements ChunkAccess { return NEIGHBOUR_CACHE_RADIUS; } - boolean loadedTicketLevel; + boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Tuinity - public accessor private long neighbourChunksLoadedBitset; private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; @@ -242,11 +293,12 @@ public class LevelChunk implements ChunkAccess { ChunkMap chunkMap = chunkProviderServer.chunkMap; // this code handles the addition of ticking tickets - the distance map handles the removal if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { - if (chunkMap.playerViewDistanceTickMap.getObjectsInRange(this.coordinateKey) != null) { + if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Tuinity - replace old player chunk loading system // now we're ready for entity ticking chunkProviderServer.mainThreadProcessor.execute(() -> { // double check that this condition still holds. - if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerViewDistanceTickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { + if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Tuinity - replace old player chunk loading system + chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Tuinity - replace old player chunk chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update } }); @@ -255,31 +307,18 @@ public class LevelChunk implements ChunkAccess { // this code handles the chunk sending if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { - if (chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(this.coordinateKey) != null) { - // now we're ready to send - chunkMap.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkMap.getUpdatingChunkIfPresent(this.coordinateKey), (() -> { // Copied frm PlayerChunkMap - // double check that this condition still holds. - if (!LevelChunk.this.areNeighboursLoaded(1)) { - return; - } - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(LevelChunk.this.coordinateKey); - if (inRange == null) { - return; - } - - // broadcast - Object[] backingSet = inRange.getBackingSet(); - Packet[] chunkPackets = new Packet[2]; - for (int index = 0, len = backingSet.length; index < len; ++index) { - Object temp = backingSet[index]; - if (!(temp instanceof net.minecraft.server.level.ServerPlayer)) { - continue; - } - net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)temp; - chunkMap.playerLoadedChunk(player, chunkPackets, LevelChunk.this); - } - }))); - } + // Tuinity start - replace old player chunk loading system + chunkProviderServer.mainThreadProcessor.execute(() -> { + if (!LevelChunk.this.areNeighboursLoaded(1)) { + return; + } + LevelChunk.this.postProcessGeneration(); + if (!LevelChunk.this.areNeighboursLoaded(1)) { + return; + } + chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); + }); + // Tuinity end - replace old player chunk loading system } // Paper end - no-tick view distance } @@ -330,9 +369,102 @@ public class LevelChunk implements ChunkAccess { } } // Paper end + // Tuinity start - optimise checkDespawn + private boolean playerGeneralAreaCacheSet; + private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; + + public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayerGeneralAreaCache() { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + return this.playerGeneralAreaCache; + } + + public void updateGeneralAreaCache() { + this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); + } + + public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { + this.playerGeneralAreaCacheSet = true; + this.playerGeneralAreaCache = value; + } + + public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, + double maxRange, java.util.function.Predicate predicate) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return null; + } + + Object[] backingSet = nearby.getBackingSet(); + double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + net.minecraft.server.level.ServerPlayer closest = null; + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { + continue; + } + net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + + double distance = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distance < closestDistance && predicate.test(player)) { + closest = player; + closestDistance = distance; + } + } + + return closest; + } + + public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate predicate, + double range, java.util.List ret) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return; + } + + double rangeSquared = range * range; + + Object[] backingSet = nearby.getBackingSet(); + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { + continue; + } + net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + + if (range >= 0.0) { + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distanceSquared > rangeSquared) { + continue; + } + } + + if (predicate == null || predicate.test(player)) { + ret.add(player); + } + } + } + // Tuinity end - optimise checkDespawn public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer consumer) { this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer); + // Tuinity start - copy over protochunk light + this.setBlockNibbles(protoChunk.getBlockNibbles()); + this.setSkyNibbles(protoChunk.getSkyNibbles()); + this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); + this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); + // Tuinity end - copy over protochunk light Iterator iterator = protoChunk.getBlockEntities().values().iterator(); while (iterator.hasNext()) { @@ -788,6 +920,7 @@ public class LevelChunk implements ChunkAccess { // CraftBukkit start public void loadCallback() { + if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Tuinity // Paper start - neighbour cache int chunkX = this.chunkPos.x; int chunkZ = this.chunkPos.z; @@ -807,6 +940,7 @@ public class LevelChunk implements ChunkAccess { // Paper end - neighbour cache org.bukkit.Server server = this.level.getCraftServer(); this.level.getChunkSource().addLoadedChunk(this); // Paper + ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Tuinity - rewrite player chunk management if (server != null) { /* * If it's a new world, the first few chunks are generated inside @@ -842,6 +976,7 @@ public class LevelChunk implements ChunkAccess { } public void unloadCallback() { + if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Tuinity org.bukkit.Server server = this.level.getCraftServer(); org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved()); server.getPluginManager().callEvent(unloadEvent); diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java index cdac1f7b30e4c043dcb12ac9e29af926df8170bd..5d2f76eeb4aef0a5ee8c202c1c682171d4d5b2ea 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java @@ -18,7 +18,8 @@ public class LevelChunkSection { short nonEmptyBlockCount; // Paper - package-private private short tickingBlockCount; private short tickingFluidCount; - final PalettedContainer states; // Paper - package-private + public final PalettedContainer states; // Paper - package-private // Tuinity - public + public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper // Paper start - Anti-Xray - Add parameters @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere @@ -79,6 +80,9 @@ public class LevelChunkSection { --this.nonEmptyBlockCount; if (blockState.isRandomlyTicking()) { --this.tickingBlockCount; + // Paper start + this.tickingList.remove(x, y, z); + // Paper end } } @@ -90,6 +94,9 @@ public class LevelChunkSection { ++this.nonEmptyBlockCount; if (state.isRandomlyTicking()) { ++this.tickingBlockCount; + // Paper start + this.tickingList.add(x, y, z, state); + // Paper end } } @@ -125,22 +132,28 @@ public class LevelChunkSection { } public void recalcBlockCounts() { + // Paper start + this.tickingList.clear(); + // Paper end this.nonEmptyBlockCount = 0; this.tickingBlockCount = 0; this.tickingFluidCount = 0; - this.states.count((state, count) -> { + this.states.forEachLocation((state, location) -> { // Paper FluidState fluidState = state.getFluidState(); if (!state.isAir()) { - this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); + this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper if (state.isRandomlyTicking()) { - this.tickingBlockCount = (short)(this.tickingBlockCount + count); + // Paper start + this.tickingBlockCount = (short)(this.tickingBlockCount + 1); + this.tickingList.add(location, state); + // Paper end } } if (!fluidState.isEmpty()) { - this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); + this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper if (fluidState.isRandomlyTicking()) { - this.tickingFluidCount = (short)(this.tickingFluidCount + count); + this.tickingFluidCount = (short)(this.tickingFluidCount + 1); // Paper } } diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java index 554474d4b2e57d8a005b3c3b9b23f32a62243058..ebeb3e3b0619b034a9681da999e9ac33cc241718 100644 --- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java @@ -174,7 +174,7 @@ public class PalettedContainer implements PaletteResize { return this.get(y << 8 | z << 4 | x); // Paper - inline } - protected T get(int index) { + public T get(int index) { // Tuinity - public T object = this.palette.valueFor(this.storage.get(index)); return (T)(object == null ? this.defaultValue : object); } @@ -320,4 +320,12 @@ public class PalettedContainer implements PaletteResize { public interface CountConsumer { void accept(T object, int count); } + + // Paper start + public void forEachLocation(PalettedContainer.CountConsumer datapaletteblock_a) { + this.storage.forEach((int location, int data) -> { + datapaletteblock_a.accept(this.palette.valueFor(data), location); + }); + } + // Paper end } diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java index 78bd3274866fed3d627a3eda7b96b92716507d38..ccdadf5d7c07d74f5bea94fc21784114b6d520da 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java @@ -1,5 +1,7 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.starlight.light.StarLightEngine; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -65,6 +67,53 @@ public class ProtoChunk implements ChunkAccess { private volatile boolean isLightCorrect; final net.minecraft.world.level.Level level; // Paper - Add level + // Tuinity start - rewrite light engine + protected volatile SWMRNibbleArray[] blockNibbles; + protected volatile SWMRNibbleArray[] skyNibbles; + protected volatile boolean[] skyEmptinessMap; + protected volatile boolean[] blockEmptinessMap; + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.skyEmptinessMap; + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.skyEmptinessMap = emptinessMap; + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.blockEmptinessMap; + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.blockEmptinessMap = emptinessMap; + } + // Tuinity end - rewrite light engine + // Paper start - add level @Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { this(pos, upgradeData, world, null); } public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { @@ -79,6 +128,10 @@ public class ProtoChunk implements ChunkAccess { // Paper start - add level @Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world) { this(pos, upgradeData, levelChunkSections, blockTickScheduler, fluidTickScheduler, world, null); } public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { + // Tuinity start + this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); + this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); + // Tuinity end this.level = level; // Paper end this.chunkPos = pos; @@ -176,7 +229,7 @@ public class ProtoChunk implements ChunkAccess { LevelChunkSection levelChunkSection = this.getOrCreateSection(l); BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state); - if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { + if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Tuinity - move block updates to only happen after lighting occurs (or during, thanks chunk system) this.lightEngine.checkBlock(pos); } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 18432dc1e5067d65cda709b7b3bcc2dd37b77d02..917fa5a3106259c01d6a01acf770890dbdf50f1a 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -65,6 +65,21 @@ import org.apache.logging.log4j.Logger; public class ChunkSerializer { + // Tuinity start - replace light engine impl + private static final int STARLIGHT_LIGHT_VERSION = 5; + + private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; + private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; + private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + // Tuinity end - replace light engine impl + // Tuinity start + // TODO: Check on update + public static long getLastWorldSaveTime(CompoundTag chunkData) { + CompoundTag levelData = chunkData.getCompound("Level"); + return levelData.getLong("LastUpdate"); + } + // Tuinity end + private static final Logger LOGGER = LogManager.getLogger(); public static final String TAG_UPGRADE_DATA = "UpgradeData"; @@ -118,7 +133,7 @@ public class ChunkSerializer { } // Paper end BiomeSource worldchunkmanager = chunkgenerator.getBiomeSource(); - CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate + CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate // Tuinity - diff on change ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos")); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate if (!Objects.equals(pos, chunkcoordintpair1)) { @@ -133,13 +148,20 @@ public class ChunkSerializer { ProtoTickList protochunkticklist1 = new ProtoTickList<>((fluidtype) -> { return fluidtype == null || fluidtype == Fluids.EMPTY; }, pos, nbttagcompound1.getList("LiquidsToBeTicked", 9), world); - boolean flag = nbttagcompound1.getBoolean("isLightOn"); + boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbttagcompound1.get("isLightOn") != null && nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Tuinity ListTag nbttaglist = nbttagcompound1.getList("Sections", 10); int i = world.getSectionsCount(); LevelChunkSection[] achunksection = new LevelChunkSection[i]; boolean flag1 = world.dimensionType().hasSkyLight(); ServerChunkCache chunkproviderserver = world.getChunkSource(); LevelLightEngine lightengine = chunkproviderserver.getLightEngine(); + // Tuinity start + ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Tuinity - replace light impl + ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Tuinity - replace light impl + final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world); + final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world); + boolean canReadSky = world.dimensionType().hasSkyLight(); + // Tuinity end if (flag) { tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main @@ -148,7 +170,7 @@ public class ChunkSerializer { } for (int j = 0; j < nbttaglist.size(); ++j) { - CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); + CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound2; // Tuinity byte b0 = nbttagcompound2.getByte("Y"); if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) { @@ -166,23 +188,29 @@ public class ChunkSerializer { } if (flag) { - if (nbttagcompound2.contains("BlockLight", 7)) { - // Paper start - delay this task since we're executing off-main - DataLayer blockLight = new DataLayer(nbttagcompound2.getByteArray("BlockLight")); - tasksToExecuteOnMain.add(() -> { - lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkcoordintpair1, b0), blockLight, true); - }); - // Paper end - delay this task since we're executing off-main + // Tuinity start - rewrite light engine + int y = sectionData.getByte("Y"); + + if (sectionData.contains("BlockLight", 7)) { + // this is where our diff is + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety + } else { + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); } - if (flag1 && nbttagcompound2.contains("SkyLight", 7)) { - // Paper start - delay this task since we're executing off-main - DataLayer skyLight = new DataLayer(nbttagcompound2.getByteArray("SkyLight")); - tasksToExecuteOnMain.add(() -> { - lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkcoordintpair1, b0), skyLight, true); - }); - // Paper end - delay this task since we're executing off-main + if (canReadSky) { + if (sectionData.contains("SkyLight", 7)) { + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety + } else { + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); + } } + // Tuinity end - rewrite light engine } } @@ -226,8 +254,12 @@ public class ChunkSerializer { object = new LevelChunk(world.getLevel(), pos, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, k, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys. createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here );// Paper end + ((LevelChunk)object).setBlockNibbles(blockNibbles); // Tuinity - replace light impl + ((LevelChunk)object).setSkyNibbles(skyNibbles); // Tuinity - replace light impl } else { ProtoChunk protochunk = new ProtoChunk(pos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, world); // Paper - add level + protochunk.setBlockNibbles(blockNibbles); // Tuinity - replace light impl + protochunk.setSkyNibbles(skyNibbles); // Tuinity - replace light impl protochunk.setBiomes(biomestorage); object = protochunk; @@ -408,7 +440,7 @@ public class ChunkSerializer { DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; - for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { + for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Tuinity - don't run loop, we don't need to - light data is per chunk now DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i)); DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i)); @@ -457,6 +489,12 @@ public class ChunkSerializer { return saveChunk(world, chunk, null); } public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, AsyncSaveData asyncsavedata) { + // Tuinity start - rewrite light impl + final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world); + final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world); + ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); + ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); + // Tuinity end - rewrite light impl // Paper end ChunkPos chunkcoordintpair = chunk.getPos(); CompoundTag nbttagcompound = new CompoundTag(); @@ -466,7 +504,7 @@ public class ChunkSerializer { nbttagcompound.put("Level", nbttagcompound1); nbttagcompound1.putInt("xPos", chunkcoordintpair.x); nbttagcompound1.putInt("zPos", chunkcoordintpair.z); - nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading + nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Tuinity - diff on change nbttagcompound1.putLong("InhabitedTime", chunk.getInhabitedTime()); nbttagcompound1.putString("Status", chunk.getStatus().getName()); UpgradeData chunkconverter = chunk.getUpgradeData(); @@ -485,32 +523,33 @@ public class ChunkSerializer { LevelChunkSection chunksection = (LevelChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> { return chunksection1 != null && SectionPos.blockToSectionCoord(chunksection1.bottomBlockY()) == finalI; // CraftBukkit - decompile errors }).findFirst().orElse(LevelChunk.EMPTY_SECTION); - // Paper start - async chunk save for unload - DataLayer nibblearray; // block light - DataLayer nibblearray1; // sky light - if (asyncsavedata == null) { - nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) - nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) - } else { - nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()]; - nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()]; - } - // Paper end - if (chunksection != LevelChunk.EMPTY_SECTION || nibblearray != null || nibblearray1 != null) { - CompoundTag nbttagcompound2 = new CompoundTag(); + // Tuinity start - replace light engine + ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); + ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); + if (chunksection != LevelChunk.EMPTY_SECTION || blockNibble != null || skyNibble != null) { + // Tuinity end - replace light engine + CompoundTag nbttagcompound2 = new CompoundTag(); CompoundTag section = nbttagcompound2; // Tuinity nbttagcompound2.putByte("Y", (byte) (i & 255)); if (chunksection != LevelChunk.EMPTY_SECTION) { chunksection.getStates().write(nbttagcompound2, "Palette", "BlockStates"); } - if (nibblearray != null && !nibblearray.isEmpty()) { - nbttagcompound2.putByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper + // Tuinity start - replace light engine + if (blockNibble != null) { + if (blockNibble.data != null) { + section.putByteArray("BlockLight", blockNibble.data); + } + section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); } - if (nibblearray1 != null && !nibblearray1.isEmpty()) { - nbttagcompound2.putByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper + if (skyNibble != null) { + if (skyNibble.data != null) { + section.putByteArray("SkyLight", skyNibble.data); + } + section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); } + // Tuinity end - replace light engine nbttaglist.add(nbttagcompound2); } @@ -518,7 +557,8 @@ public class ChunkSerializer { nbttagcompound1.put("Sections", nbttaglist); if (flag) { - nbttagcompound1.putBoolean("isLightOn", true); + nbttagcompound1.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Tuinity + nbttagcompound1.putBoolean("isLightOn", false); // Tuinity - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) } ChunkBiomeContainer biomestorage = chunk.getBiomes(); diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java index 176610b31f66b890afe61f4de46c412382bb8d22..70ec2feef1553afca2c8cca3a7f19498637b41d5 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java @@ -34,12 +34,13 @@ public class ChunkStorage implements AutoCloseable { this.fixerUpper = dataFixer; // Paper start - async chunk io // remove IO worker - this.regionFileCache = new RegionFileStorage(directory, dsync); // Paper - nuke IOWorker + this.regionFileCache = new RegionFileStorage(directory, dsync, true); // Paper - nuke IOWorker // Tuinity // Paper end - async chunk io } // CraftBukkit start private boolean check(ServerChunkCache cps, int x, int z) throws IOException { + if (true) return true; // Tuinity - this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full" ChunkPos pos = new ChunkPos(x, z); if (cps != null) { //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java index c8298a597818227de33a4afce4698ec0666cf758..b49b0c4cac8aec09ffe970c92e5a75047c0e1f1d 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java @@ -9,6 +9,27 @@ import java.util.BitSet; public class RegionBitmap { private final BitSet used = new BitSet(); + // Tuinity start + public final void copyFrom(RegionBitmap other) { + BitSet thisBitset = this.used; + BitSet otherBitset = other.used; + + for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { + thisBitset.set(i, otherBitset.get(i)); + } + } + + public final boolean tryAllocate(int from, int length) { + BitSet bitset = this.used; + int firstSet = bitset.nextSetBit(from); + if (firstSet > 0 && firstSet < (from + length)) { + return false; + } + bitset.set(from, from + length); + return true; + } + // Tuinity end + public void force(int start, int size) { this.used.set(start, start + size); } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java index c22391a0d4b7db49bd3994b0887939a7d8019391..118adb6fbdc56ca03652f114c1b7ced0ef26a628 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java @@ -55,6 +55,341 @@ public class RegionFile implements AutoCloseable { public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper public final File regionFile; // Paper + // Tuinity start - try to recover from RegionFile header corruption + private static long roundToSectors(long bytes) { + long sectors = bytes >>> 12; // 4096 = 2^12 + long remainingBytes = bytes & 4095; + long sign = -remainingBytes; // sign is 1 if nonzero + return sectors + (sign >>> 63); + } + + private static final CompoundTag OVERSIZED_COMPOUND = new CompoundTag(); + + private CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException { + try { + if (chunkDataLength < 0) { + return null; + } + + long offset = sector * 4096L + 4L; // offset for chunk data + + if ((offset + chunkDataLength) > fileLength) { + return null; + } + + ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength); + if (chunkDataLength != this.file.read(chunkData, offset)) { + return null; + } + + ((java.nio.Buffer)chunkData).flip(); + + byte compressionType = chunkData.get(); + if (compressionType < 0) { // compressionType & 128 != 0 + // oversized chunk + return OVERSIZED_COMPOUND; + } + + RegionFileVersion compression = RegionFileVersion.fromId(compressionType); + if (compression == null) { + return null; + } + + InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); + + return NbtIo.read((java.io.DataInput)new DataInputStream(new BufferedInputStream(input))); + } catch (Exception ex) { + return null; + } + } + + private int getLength(long sector) throws IOException { + ByteBuffer length = ByteBuffer.allocate(4); + if (4 != this.file.read(length, sector * 4096L)) { + return -1; + } + + return length.getInt(0); + } + + private void backupRegionFile() { + File backup = new File(this.regionFile.getParent(), this.regionFile.getName() + "." + new java.util.Random().nextLong() + ".backup"); + this.backupRegionFile(backup); + } + + private void backupRegionFile(File to) { + try { + this.file.force(true); + LOGGER.warn("Backing up regionfile \"" + this.regionFile.getAbsolutePath() + "\" to " + to.getAbsolutePath()); + java.nio.file.Files.copy(this.regionFile.toPath(), to.toPath()); + LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath()); + } catch (IOException ex) { + LOGGER.error("Failed to backup to " + to.getAbsolutePath(), ex); + } + } + + // note: only call for CHUNK regionfiles + void recalculateHeader() throws IOException { + if (!this.canRecalcHeader) { + return; + } + synchronized (this) { + LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.regionFile.getAbsolutePath(), new Throwable()); + + // try to backup file so maybe it could be sent to us for further investigation + + this.backupRegionFile(); + CompoundTag[] compounds = new CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data) + int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes + int[] sectorOffsets = new int[32 * 32]; // in sectors + boolean[] hasAikarOversized = new boolean[32 * 32]; + + long fileLength = this.file.size(); + long totalSectors = roundToSectors(fileLength); + + // search the regionfile from start to finish for the most up-to-date chunk data + + for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip + int chunkDataLength = this.getLength(i); + CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength); + if (compound == null || compound == OVERSIZED_COMPOUND) { + continue; + } + + ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound); + int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); + + CompoundTag otherCompound = compounds[location]; + + if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) { + continue; // don't overwrite newer data. + } + + // aikar oversized? + File aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); + boolean isAikarOversized = false; + if (aikarOversizedFile.exists()) { + try { + CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); + if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) { + // best we got for an id. hope it's good enough + isAikarOversized = true; + } + } catch (Exception ex) { + LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.regionFile.getAbsolutePath() + ", oversized data for this chunk will be lost", ex); + // fall through, if we can't read aikar oversized we can't risk corrupting chunk data + } + } + + hasAikarOversized[location] = isAikarOversized; + compounds[location] = compound; + rawLengths[location] = chunkDataLength + 4; + sectorOffsets[location] = (int)i; + + int chunkSectorLength = (int)roundToSectors(rawLengths[location]); + i += chunkSectorLength; + --i; // gets incremented next iteration + } + + // forge style oversized data is already handled by the local search, and aikar data we just hope + // we get it right as aikar data has no identifiers we could use to try and find its corresponding + // local data compound + + java.nio.file.Path containingFolder = this.externalFileDir; + File[] regionFiles = containingFolder.toFile().listFiles(); + boolean[] oversized = new boolean[32 * 32]; + RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; + + if (regionFiles != null) { + ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.regionFile); + + if (ourLowerLeftPosition == null) { + LOGGER.fatal("Unable to get chunk location of regionfile " + this.regionFile.getAbsolutePath() + ", cannot recover oversized chunks"); + } else { + int lowerXBound = ourLowerLeftPosition.x; // inclusive + int lowerZBound = ourLowerLeftPosition.z; // inclusive + int upperXBound = lowerXBound + 32 - 1; // inclusive + int upperZBound = lowerZBound + 32 - 1; // inclusive + + // read mojang oversized data + for (File regionFile : regionFiles) { + ChunkPos oversizedCoords = getOversizedChunkPair(regionFile); + if (oversizedCoords == null) { + continue; + } + + if ((oversizedCoords.x < lowerXBound || oversizedCoords.x > upperXBound) || (oversizedCoords.z < lowerZBound || oversizedCoords.z > upperZBound)) { + continue; // not in our regionfile + } + + // ensure oversized data is valid & is newer than data in the regionfile + + int location = (oversizedCoords.x & 31) | ((oversizedCoords.z & 31) << 5); + + byte[] chunkData; + try { + chunkData = Files.readAllBytes(regionFile.toPath()); + } catch (Exception ex) { + LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", data will be lost", ex); + continue; + } + + CompoundTag compound = null; + + // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them + RegionFileVersion compression = null; + for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) { + try { + DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java + compound = NbtIo.read((java.io.DataInput)in); + compression = compressionType; + break; // reaches here iff readNBT does not throw + } catch (Exception ex) { + continue; + } + } + + if (compound == null) { + LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", it's corrupt. Its data will be lost"); + continue; + } + + if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) { + oversized[location] = true; + oversizedCompressionTypes[location] = compression; + } + } + } + } + + // now we need to calculate a new offset header + + int[] calculatedOffsets = new int[32 * 32]; + RegionBitmap newSectorAllocations = new RegionBitmap(); + newSectorAllocations.force(0, 2); // make space for header + + // allocate sectors for normal chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (oversized[location]) { + continue; + } + + int rawLength = rawLengths[location]; // bytes + int sectorOffset = sectorOffsets[location]; // sectors + int sectorLength = (int)roundToSectors(rawLength); + + if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) { + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } else { + LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + ", chunk will be regenerated"); + } + } + } + + // allocate sectors for oversized chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (!oversized[location]) { + continue; + } + + int sectorOffset = newSectorAllocations.allocate(1); + int sectorLength = 1; + + try { + this.file.write(this.createExternalStub(oversizedCompressionTypes[location]), sectorOffset * 4096); + // only allocate in the new offsets if the write succeeds + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } catch (IOException ex) { + newSectorAllocations.free(sectorOffset, sectorLength); + LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + " will be regenerated"); + } + } + } + + // rewrite aikar oversized data + + this.oversizedCount = 0; + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + int isAikarOversized = hasAikarOversized[location] ? 1 : 0; + + this.oversizedCount += isAikarOversized; + this.oversized[location] = (byte)isAikarOversized; + } + } + + if (this.oversizedCount > 0) { + try { + this.writeOversizedMeta(); + } catch (Exception ex) { + LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.regionFile.getAbsolutePath(), ex); + this.getOversizedMetaFile().delete(); + } + } else { + this.getOversizedMetaFile().delete(); + } + + this.usedSectors.copyFrom(newSectorAllocations); + + // before we overwrite the old sectors, print a summary of the chunks that got changed. + + LOGGER.info("Starting summary of changes for regionfile " + this.regionFile.getAbsolutePath()); + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + int oldOffset = this.offsets.get(location); + int newOffset = calculatedOffsets[location]; + + if (oldOffset == newOffset) { + continue; + } + + this.offsets.put(location, newOffset); // overwrite incorrect offset + + if (oldOffset == 0) { + // found lost data + LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath()); + } else if (newOffset == 0) { + LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.regionFile.getAbsolutePath() + ", it will be regenerated"); + } else { + LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.regionFile.getAbsolutePath()); + } + } + } + + LOGGER.info("End of change summary for regionfile " + this.regionFile.getAbsolutePath()); + + // simply destroy the timestamp header, it's not used + + for (int i = 0; i < 32 * 32; ++i) { + this.timestamps.put(i, calculatedOffsets[i] != 0 ? (int)System.currentTimeMillis() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this + } + + // write new header + try { + this.flush(); + this.file.force(true); // try to ensure it goes through... + LOGGER.info("Successfully wrote new header to disk for regionfile " + this.regionFile.getAbsolutePath()); + } catch (IOException ex) { + LOGGER.fatal("Failed to write new header to disk for regionfile " + this.regionFile.getAbsolutePath(), ex); + } + } + } + + final boolean canRecalcHeader; // final forces compile fail on new constructor + // Tuinity end + // Paper start - Cache chunk status private final ChunkStatus[] statuses = new ChunkStatus[32 * 32]; @@ -82,8 +417,19 @@ public class RegionFile implements AutoCloseable { public RegionFile(File file, File directory, boolean dsync) throws IOException { this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync); } + // Tuinity start - add can recalc flag + public RegionFile(File file, File directory, boolean dsync, boolean canRecalcHeader) throws IOException { + this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync, canRecalcHeader); + } + // Tuinity end - add can recalc flag public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException { + // Tuinity start - add can recalc flag + this(file, directory, outputChunkStreamVersion, dsync, false); + } + public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync, boolean canRecalcHeader) throws IOException { + this.canRecalcHeader = canRecalcHeader; + // Tuinity end - add can recalc flag this.header = ByteBuffer.allocateDirect(8192); this.regionFile = file.toFile(); // Paper initOversizedState(); // Paper @@ -112,14 +458,16 @@ public class RegionFile implements AutoCloseable { RegionFile.LOGGER.warn("Region file {} has truncated header: {}", file, i); } - long j = Files.size(file); + final long j = Files.size(file); final long regionFileSize = j; // Tuinity - recalculate header on header corruption + boolean needsHeaderRecalc = false; // Tuinity - recalculate header on header corruption + boolean hasBackedUp = false; // Tuinity - recalculate header on header corruption for (int k = 0; k < 1024; ++k) { - int l = this.offsets.get(k); + final int l = this.offsets.get(k); final int headerLocation = l; // Tuinity - we expect this to be the header location if (l != 0) { - int i1 = RegionFile.getSectorNumber(l); - int j1 = RegionFile.getNumSectors(l); + final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Tuinity - we expect this to be offset in file in sectors + int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Tuinity - diff on change, we expect this to be sector length of region - watch out for reassignments // Spigot start if (j1 == 255) { // We're maxed out, so we need to read the proper length from the section @@ -128,32 +476,102 @@ public class RegionFile implements AutoCloseable { j1 = (realLen.getInt(0) + 4) / 4096 + 1; } // Spigot end + sectorLength = j1; // Tuinity - diff on change, we expect this to be sector length of region if (i1 < 2) { RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", file, k, i1); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change } else if (j1 == 0) { RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", file, k); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change } else if ((long) i1 * 4096L > j) { RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", file, k, i1); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change } else { - this.usedSectors.force(i1, j1); + //this.usedSectors.force(i1, j1); // Tuinity - move this down so we can check if it fails to allocate + } + // Tuinity start - recalculate header on header corruption + if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { + if (canRecalcHeader) { + LOGGER.error("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + "! Recalculating header..."); + needsHeaderRecalc = true; + break; + } else { + // location = chunkX | (chunkZ << 5); + LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too + this.offsets.put(headerLocation, 0); // delete the entry from header + continue; + } } + boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength); + if (failedToAllocate) { + LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.regionFile.getAbsolutePath()); + } + if (failedToAllocate & !canRecalcHeader) { + // location = chunkX | (chunkZ << 5); + LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too + this.offsets.put(headerLocation, 0); // delete the entry from header + continue; + } + needsHeaderRecalc |= failedToAllocate; + // Tuinity end - recalculate header on header corruption } } + // Tuinity start - recalculate header on header corruption + // we move the recalc here so comparison to old header is correct when logging to console + if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues + LOGGER.error("Recalculating regionfile " + this.regionFile.getAbsolutePath() + ", header gave erroneous offsets & locations"); + this.recalculateHeader(); + } + // Tuinity end } } } private Path getExternalChunkPath(ChunkPos chunkPos) { - String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; + String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Tuinity - diff on change return this.externalFileDir.resolve(s); } + // Tuinity start + private static ChunkPos getOversizedChunkPair(File file) { + String fileName = file.getName(); + + if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) { + return null; + } + + String[] split = fileName.split("\\."); + + if (split.length != 4) { + return null; + } + + try { + int x = Integer.parseInt(split[1]); + int z = Integer.parseInt(split[2]); + + return new ChunkPos(x, z); + } catch (NumberFormatException ex) { + return null; + } + } + // Tuinity end + @Nullable public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { int i = this.getOffset(pos); @@ -177,6 +595,12 @@ public class RegionFile implements AutoCloseable { ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error if (bytebuffer.remaining() < 5) { RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", pos, l, bytebuffer.remaining()); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else { int i1 = bytebuffer.getInt(); @@ -184,6 +608,12 @@ public class RegionFile implements AutoCloseable { if (i1 == 0) { RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else { int j1 = i1 - 1; @@ -191,17 +621,49 @@ public class RegionFile implements AutoCloseable { if (RegionFile.isExternalStreamChunk(b0)) { if (j1 != 0) { RegionFile.LOGGER.warn("Chunk has both internal and external streams"); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Tuinity end - recalculate header on regionfile corruption } - return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); + // Tuinity start - recalculate header on regionfile corruption + final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + return ret; + // Tuinity end - recalculate header on regionfile corruption } else if (j1 > bytebuffer.remaining()) { RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", pos, j1, bytebuffer.remaining()); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else if (j1 < 0) { RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else { - return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); + // Tuinity start - recalculate header on regionfile corruption + final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + return ret; + // Tuinity end - recalculate header on regionfile corruption } } } @@ -376,10 +838,15 @@ public class RegionFile implements AutoCloseable { } private ByteBuffer createExternalStub() { + // Tuinity start - add version param + return this.createExternalStub(this.version); + } + private ByteBuffer createExternalStub(RegionFileVersion version) { + // Tuinity end - add version param ByteBuffer bytebuffer = ByteBuffer.allocate(5); bytebuffer.putInt(1); - bytebuffer.put((byte) (this.version.getId() | 128)); + bytebuffer.put((byte) (version.getId() | 128)); // Tuinity - replace with version param ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error return bytebuffer; } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java index 6496108953effae82391b5c1ea6fdec8482731cd..a6f831fea2245e2d1f44ffa60f96b6f1243b888b 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -25,7 +25,15 @@ public class RegionFileStorage implements AutoCloseable { private final File folder; private final boolean sync; + private final boolean isChunkData; // Tuinity + RegionFileStorage(File directory, boolean dsync) { + // Tuinity start - add isChunkData param + this(directory, dsync, false); + } + RegionFileStorage(File directory, boolean dsync, boolean isChunkData) { + this.isChunkData = isChunkData; + // Tuinity end - add isChunkData param this.folder = directory; this.sync = dsync; } @@ -90,9 +98,9 @@ public class RegionFileStorage implements AutoCloseable { File file = this.folder; int j = chunkcoordintpair.getRegionX(); - File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); + File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Tuinity - diff on change if (existingOnly && !file1.exists()) return null; // CraftBukkit - RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync); + RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync, this.isChunkData); // Tuinity - allow for chunk regionfiles to regen header this.regionCache.putAndMoveToFirst(i, regionfile1); // Paper start @@ -180,6 +188,13 @@ public class RegionFileStorage implements AutoCloseable { if (regionfile == null) { return null; } + // Tuinity start - Add regionfile parameter + return this.read(pos, regionfile); + } + public CompoundTag read(ChunkPos pos, RegionFile regionfile) throws IOException { + // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile + // if we decide to re-read + // Tuinity end // CraftBukkit end try { // Paper DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); @@ -196,6 +211,17 @@ public class RegionFileStorage implements AutoCloseable { try { if (datainputstream != null) { nbttagcompound = NbtIo.read((DataInput) datainputstream); + // Tuinity start - recover from corrupt regionfile header + if (this.isChunkData) { + ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); + if (!chunkPos.equals(pos)) { + MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.getAbsolutePath()); + regionfile.recalculateHeader(); + regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. + return this.read(pos, regionfile); + } + } + // Tuinity end - recover from corrupt regionfile header break label43; } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java index b7835b9b904e7d4bff64f7189049e334f5ab4d6f..ae638ac0a0557de204471fef4b03bdb0ad310b2b 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java @@ -12,7 +12,7 @@ import java.util.zip.InflaterInputStream; import javax.annotation.Nullable; public class RegionFileVersion { - private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); + public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Tuinity - public public static final RegionFileVersion VERSION_GZIP = register(new RegionFileVersion(1, GZIPInputStream::new, GZIPOutputStream::new)); public static final RegionFileVersion VERSION_DEFLATE = register(new RegionFileVersion(2, InflaterInputStream::new, DeflaterOutputStream::new)); public static final RegionFileVersion VERSION_NONE = register(new RegionFileVersion(3, (inputStream) -> { diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java index f01182a0ac8a14bcd5b1deb778306e7bf1bf70ed..2cfc54a577d0a63a504e24bc54fd763fe51083e5 100644 --- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java @@ -9,54 +9,40 @@ import javax.annotation.Nullable; import net.minecraft.world.entity.Entity; public class EntityTickList { - private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>(); - private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); - @Nullable - private Int2ObjectMap iterated; + private final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entities = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Tuinity - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? private void ensureActiveIsNotIterated() { - if (this.iterated == this.active) { - this.passive.clear(); - - for(Entry entry : Int2ObjectMaps.fastIterable(this.active)) { - this.passive.put(entry.getIntKey(), entry.getValue()); - } - - Int2ObjectMap int2ObjectMap = this.active; - this.active = this.passive; - this.passive = int2ObjectMap; - } + // Tuinity - replace with better logic, do not delay removals } public void add(Entity entity) { this.ensureActiveIsNotIterated(); - this.active.put(entity.getId(), entity); + this.entities.add(entity); // Tuinity - replace with better logic, do not delay removals/additions } public void remove(Entity entity) { this.ensureActiveIsNotIterated(); - this.active.remove(entity.getId()); + this.entities.remove(entity); // Tuinity - replace with better logic, do not delay removals/additions } public boolean contains(Entity entity) { - return this.active.containsKey(entity.getId()); + return this.entities.contains(entity); // Tuinity - replace with better logic, do not delay removals/additions } public void forEach(Consumer action) { - if (this.iterated != null) { - throw new UnsupportedOperationException("Only one concurrent iteration supported"); - } else { - this.iterated = this.active; - - try { - for(Entity entity : this.active.values()) { - action.accept(entity); - } - } finally { - this.iterated = null; + // Tuinity start - replace with better logic, do not delay removals/additions + // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... + // (by dfl iterator() is configured to not iterate over new entries) + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); + try { + while (iterator.hasNext()) { + action.accept(iterator.next()); } - + } finally { + iterator.finishedIterating(); } + + // Tuinity end - replace with better logic, do not delay removals/additions } } diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java index 79e733b3ea2e6589d60f3b322244479d2b3b9f86..a99d0a00bbdb90588b87a3f85c62bdc1468b5e5a 100644 --- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java @@ -41,8 +41,10 @@ public class PersistentEntitySectionManager implements A private final Long2ObjectMap chunkLoadStatuses = new Long2ObjectOpenHashMap<>(); private final LongSet chunksToUnload = new LongOpenHashSet(); private final Queue> loadingInbox = Queues.newConcurrentLinkedQueue(); + public final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager; // Tuinity - public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess) { + public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess, com.tuinity.tuinity.world.EntitySliceManager entitySliceManager) { // Tuinity + this.entitySliceManager = entitySliceManager; // Tuinity this.visibleEntityStorage = new EntityLookup<>(); this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility); this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN); @@ -93,6 +95,7 @@ public class PersistentEntitySectionManager implements A long l = SectionPos.asLong(entity.blockPosition()); EntitySection entitySection = this.sectionStorage.getOrCreateSection(l); entitySection.add(entity); + this.entitySliceManager.addEntity((Entity)entity); // Tuinity entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, l, entitySection)); if (!existing) { this.callbacks.onCreated(entity); @@ -147,6 +150,7 @@ public class PersistentEntitySectionManager implements A public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) { Visibility visibility = Visibility.fromFullChunkStatus(levelType); + this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Tuinity this.updateChunkStatus(chunkPos, visibility); } @@ -383,6 +387,7 @@ public class PersistentEntitySectionManager implements A BlockPos blockPos = this.entity.blockPosition(); long l = SectionPos.asLong(blockPos); if (l != this.currentSectionKey) { + PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Tuinity Visibility visibility = this.currentSection.getStatus(); if (!this.currentSection.remove(this.entity)) { PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l); @@ -426,6 +431,7 @@ public class PersistentEntitySectionManager implements A if (!this.currentSection.remove(this.entity)) { PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (destroying due to {})", this.entity, SectionPos.of(this.currentSectionKey), reason); } + PersistentEntitySectionManager.this.entitySliceManager.removeEntity((Entity)this.entity); // Tuinity Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, this.currentSection.getStatus()); if (visibility.isTicking()) { diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java b/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java index 05bba5410fbd9f8e333584ccbd65a909f3040322..de3122f450edacaf2eed6f60b0680ebe64f7d214 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java +++ b/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java @@ -10,11 +10,28 @@ import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.state.BlockState; public class ColumnPlacer extends BlockPlacer { + // Tuinity start public static final Codec CODEC = RecordCodecBuilder.create((instance) -> { - return instance.group(IntProvider.NON_NEGATIVE_CODEC.fieldOf("size").forGetter((columnPlacer) -> { - return columnPlacer.size; - })).apply(instance, ColumnPlacer::new); + return instance.group( + IntProvider.NON_NEGATIVE_CODEC.optionalFieldOf("size").forGetter((columnPlacer) -> { + return java.util.Optional.of(columnPlacer.size); + }), + Codec.INT.optionalFieldOf("min_size").forGetter((columnPlacer) -> { + return java.util.Optional.empty(); + }), + Codec.INT.optionalFieldOf("extra_size").forGetter((columnPlacer) -> { + return java.util.Optional.empty(); + }) + ).apply(instance, ColumnPlacer::new); }); + public ColumnPlacer(java.util.Optional size, java.util.Optional minSize, java.util.Optional extraSize) { + if (size.isPresent()) { + this.size = size.get(); + } else { + this.size = net.minecraft.util.valueproviders.BiasedToBottomInt.of(minSize.get().intValue(), minSize.get().intValue() + extraSize.get().intValue()); + } + } + // Tuinity end private final IntProvider size; public ColumnPlacer(IntProvider size) { diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java index 5da68897148192905c2747676c1ee2ee649f923f..b990099cf274f8cb0d96c139345cf0bf328affd6 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java +++ b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java @@ -18,13 +18,13 @@ public class TreeConfiguration implements FeatureConfiguration { return treeConfiguration.trunkProvider; }), TrunkPlacer.CODEC.fieldOf("trunk_placer").forGetter((treeConfiguration) -> { return treeConfiguration.trunkPlacer; - }), BlockStateProvider.CODEC.fieldOf("foliage_provider").forGetter((treeConfiguration) -> { + }), net.minecraft.server.MCUtil.fieldWithFallbacks(BlockStateProvider.CODEC, "foliage_provider", "leaves_provider").forGetter((treeConfiguration) -> { // Paper - provide fallback for rename return treeConfiguration.foliageProvider; - }), BlockStateProvider.CODEC.fieldOf("sapling_provider").forGetter((treeConfiguration) -> { + }), BlockStateProvider.CODEC.optionalFieldOf("sapling_provider", new SimpleStateProvider(Blocks.OAK_SAPLING.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide default - it looks like for now this is OK because it's just used to check canSurvive. Same check happens in 1.16.5 for the default we provide - so it should retain behavior... return treeConfiguration.saplingProvider; }), FoliagePlacer.CODEC.fieldOf("foliage_placer").forGetter((treeConfiguration) -> { return treeConfiguration.foliagePlacer; - }), BlockStateProvider.CODEC.fieldOf("dirt_provider").forGetter((treeConfiguration) -> { + }), BlockStateProvider.CODEC.optionalFieldOf("dirt_provider", new SimpleStateProvider(Blocks.DIRT.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide defaults, old data DOES NOT have this key (thankfully ALL OLD DATA used DIRT) return treeConfiguration.dirtProvider; }), FeatureSize.CODEC.fieldOf("minimum_size").forGetter((treeConfiguration) -> { return treeConfiguration.minimumSize; diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java index 120498a39b7ca7aee9763084507508d4a1c425aa..6f7e6429c35eea346517cbf08cf223fc6d838a8c 100644 --- a/src/main/java/net/minecraft/world/phys/AABB.java +++ b/src/main/java/net/minecraft/world/phys/AABB.java @@ -25,6 +25,17 @@ public class AABB { this.maxZ = Math.max(z1, z2); } + // Tuinity start + public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + // Tuinity end + public AABB(BlockPos pos) { this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); } diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java index 99427b6130895ddecee8bcf77db72d809c24c375..af1ef430e81cb9bdd749aa235577c63fa381f4c5 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java @@ -6,6 +6,9 @@ import java.util.Arrays; import net.minecraft.Util; import net.minecraft.core.Direction; +// Tuinity start +import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; +// Tuinity end public class ArrayVoxelShape extends VoxelShape { private final DoubleList xs; private final DoubleList ys; @@ -16,6 +19,11 @@ public class ArrayVoxelShape extends VoxelShape { } ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { + // Tuinity start - optimise multi-aabb shapes + this(shape, xPoints, yPoints, zPoints, null, 0.0, 0.0, 0.0); + } + ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints, net.minecraft.world.phys.AABB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) { + // Tuinity end - optimise multi-aabb shapes super(shape); int i = shape.getXSize() + 1; int j = shape.getYSize() + 1; @@ -27,6 +35,12 @@ public class ArrayVoxelShape extends VoxelShape { } else { throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")); } + // Tuinity start - optimise multi-aabb shapes + this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.toAabbs().toArray(EMPTY) : boundingBoxesRepresentation; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + // Tuinity end - optimise multi-aabb shapes } @Override @@ -42,4 +56,152 @@ public class ArrayVoxelShape extends VoxelShape { throw new IllegalArgumentException(); } } + + // Tuinity start + public static final class DoubleListOffsetExposed extends AbstractDoubleList { + + public final DoubleArrayList list; + public final double offset; + + public DoubleListOffsetExposed(final DoubleArrayList list, final double offset) { + this.list = list; + this.offset = offset; + } + + @Override + public double getDouble(final int index) { + return this.list.getDouble(index) + this.offset; + } + + @Override + public int size() { + return this.list.size(); + } + } + + static final net.minecraft.world.phys.AABB[] EMPTY = new net.minecraft.world.phys.AABB[0]; + final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation; + + final double offsetX; + final double offsetY; + final double offsetZ; + + public final net.minecraft.world.phys.AABB[] getBoundingBoxesRepresentation() { + return this.boundingBoxesRepresentation; + } + + public final double getOffsetX() { + return this.offsetX; + } + + public final double getOffsetY() { + return this.offsetY; + } + + public final double getOffsetZ() { + return this.offsetZ; + } + + @Override + public java.util.List toAabbs() { + if (this.boundingBoxesRepresentation == null) { + return super.toAabbs(); + } + java.util.List ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length); + + double offX = this.offsetX; + double offY = this.offsetY; + double offZ = this.offsetZ; + + for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { + ret.add(boundingBox.move(offX, offY, offZ)); + } + + return ret; + } + + protected static DoubleArrayList getList(DoubleList from) { + if (from instanceof DoubleArrayList) { + return (DoubleArrayList)from; + } else { + return DoubleArrayList.wrap(from.toDoubleArray()); + } + } + + @Override + public VoxelShape move(double x, double y, double z) { + if (x == 0.0 && y == 0.0 && z == 0.0) { + return this; + } + DoubleListOffsetExposed xPoints, yPoints, zPoints; + double offsetX, offsetY, offsetZ; + + if (this.xs instanceof DoubleListOffsetExposed) { + xPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.xs).list, offsetX = this.offsetX + x); + yPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.ys).list, offsetY = this.offsetY + y); + zPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.zs).list, offsetZ = this.offsetZ + z); + } else { + xPoints = new DoubleListOffsetExposed(getList(this.xs), offsetX = x); + yPoints = new DoubleListOffsetExposed(getList(this.ys), offsetY = y); + zPoints = new DoubleListOffsetExposed(getList(this.zs), offsetZ = z); + } + + return new ArrayVoxelShape(this.shape, xPoints, yPoints, zPoints, this.boundingBoxesRepresentation, offsetX, offsetY, offsetZ); + } + + @Override + public final boolean intersects(net.minecraft.world.phys.AABB axisalingedbb) { + // this can be optimised by checking an "overall shape" first, but not needed + double offX = this.offsetX; + double offY = this.offsetY; + double offZ = this.offsetZ; + + for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { + if (com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(axisalingedbb, boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ, + boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) { + return true; + } + } + + return false; + } + + @Override + public void forAllBoxes(Shapes.DoubleLineConsumer doubleLineConsumer) { + if (this.boundingBoxesRepresentation == null) { + super.forAllBoxes(doubleLineConsumer); + return; + } + for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { + doubleLineConsumer.consume(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, + boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ); + } + } + + @Override + public VoxelShape optimize() { + if (this == Shapes.empty() || this.boundingBoxesRepresentation.length == 0) { + return this; + } + + VoxelShape simplified = Shapes.empty(); + for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { + simplified = Shapes.joinUnoptimized(simplified, Shapes.box(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, + boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ), BooleanOp.OR); + } + + if (!(simplified instanceof ArrayVoxelShape)) { + return simplified; + } + + final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation = ((ArrayVoxelShape)simplified).getBoundingBoxesRepresentation(); + + if (boundingBoxesRepresentation.length == 1) { + return new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBoxesRepresentation[0]).optimize(); + } + + return simplified; + } + // Tuinity end + } diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java index 16bc18cacbf7a23fb744c8a12e7fd8da699b2fea..472c47a585da7d95b3f4774d3caef1d864b6337a 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java @@ -26,16 +26,17 @@ public final class Shapes { DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); discreteVoxelShape.fill(0, 0, 0); return new CubeVoxelShape(discreteVoxelShape); - }); + }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Tuinity - OBFHELPER public static final VoxelShape INFINITY = box(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); private static final VoxelShape EMPTY = new ArrayVoxelShape(new BitSetDiscreteVoxelShape(0, 0, 0), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D}))); + public static final com.tuinity.tuinity.voxel.AABBVoxelShape BLOCK_OPTIMISED = new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); // Tuinity public static VoxelShape empty() { return EMPTY; } public static VoxelShape block() { - return BLOCK; + return BLOCK_OPTIMISED; // Tuinity } public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { @@ -47,30 +48,11 @@ public final class Shapes { } public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { - if (!(maxX - minX < 1.0E-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) { - int i = findBits(minX, maxX); - int j = findBits(minY, maxY); - int k = findBits(minZ, maxZ); - if (i >= 0 && j >= 0 && k >= 0) { - if (i == 0 && j == 0 && k == 0) { - return block(); - } else { - int l = 1 << i; - int m = 1 << j; - int n = 1 << k; - BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(l, m, n, (int)Math.round(minX * (double)l), (int)Math.round(minY * (double)m), (int)Math.round(minZ * (double)n), (int)Math.round(maxX * (double)l), (int)Math.round(maxY * (double)m), (int)Math.round(maxZ * (double)n)); - return new CubeVoxelShape(bitSetDiscreteVoxelShape); - } - } else { - return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ})); - } - } else { - return empty(); - } + return new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Tuinity } public static VoxelShape create(AABB box) { - return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + return new com.tuinity.tuinity.voxel.AABBVoxelShape(box); // Tuinity } @VisibleForTesting @@ -132,6 +114,20 @@ public final class Shapes { } public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { + // Tuinity start - optimise voxelshape + if (predicate == BooleanOp.AND) { + if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { + return com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb, ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb); + } else if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof ArrayVoxelShape) { + return ((ArrayVoxelShape)shape2).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb); + } else if (shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape1 instanceof ArrayVoxelShape) { + return ((ArrayVoxelShape)shape1).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb); + } + } + return joinIsNotEmptyVanilla(shape1, shape2, predicate); + } + public static boolean joinIsNotEmptyVanilla(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { + // Tuinity end - optimise voxelshape if (predicate.apply(false, false)) { throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); } else { @@ -285,6 +281,43 @@ public final class Shapes { } public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { + // Tuinity start - optimise shape creation here for lighting, as this shape is going to be used + // for transparency checks + if (shape == BLOCK || shape == BLOCK_OPTIMISED) { + return BLOCK_OPTIMISED; + } else if (shape == empty()) { + return empty(); + } + + if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { + final AABB box = ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape).aabb; + switch (direction) { + case WEST: // -X + case EAST: { // +X + final boolean useEmpty = direction == Direction.EAST ? !DoubleMath.fuzzyEquals(box.maxX, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : + !DoubleMath.fuzzyEquals(box.minX, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); + return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, box.minY, box.minZ, 1.0, box.maxY, box.maxZ)).optimize(); + } + case DOWN: // -Y + case UP: { // +Y + final boolean useEmpty = direction == Direction.UP ? !DoubleMath.fuzzyEquals(box.maxY, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : + !DoubleMath.fuzzyEquals(box.minY, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); + return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, 0.0, box.minZ, box.maxX, 1.0, box.maxZ)).optimize(); + } + case NORTH: // -Z + case SOUTH: { // +Z + final boolean useEmpty = direction == Direction.SOUTH ? !DoubleMath.fuzzyEquals(box.maxZ, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : + !DoubleMath.fuzzyEquals(box.minZ,0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); + return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, box.minY, 0.0, box.maxX, box.maxY, 1.0)).optimize(); + } + } + } + + // fall back to vanilla + return getFaceShapeVanilla(shape, direction); + } + public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) { + // Tuinity end if (shape == block()) { return block(); } else { @@ -299,7 +332,7 @@ public final class Shapes { i = 0; } - return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); + return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Tuinity - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape } } @@ -324,6 +357,53 @@ public final class Shapes { } public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { + // Tuinity start - try to optimise for the case where the shapes do _not_ occlude + // which is _most_ of the time in lighting + if (one == getFullUnoptimisedCube() || one == BLOCK_OPTIMISED + || two == getFullUnoptimisedCube() || two == BLOCK_OPTIMISED) { + return true; + } + boolean v1Empty = one == empty(); + boolean v2Empty = two == empty(); + if (v1Empty && v2Empty) { + return false; + } + if ((one instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v1Empty) + && (two instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v2Empty)) { + if (!v1Empty && !v2Empty && (one != two)) { + AABB boundingBox1 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb; + AABB boundingBox2 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb; + // can call it here in some cases + + // check overall bounding box + double minY = Math.min(boundingBox1.minY, boundingBox2.minY); + double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY); + if (minY > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxY < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { + return false; + } + double minX = Math.min(boundingBox1.minX, boundingBox2.minX); + double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX); + if (minX > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxX < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { + return false; + } + double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ); + double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ); + if (minZ > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxZ < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { + return false; + } + // fall through to full merge check + } else { + AABB boundingBox = v1Empty ? ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb : ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb; + // check if the bounding box encloses the full cube + return (boundingBox.minY <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) && + (boundingBox.minX <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) && + (boundingBox.minZ <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)); + } + } + return faceShapeOccludesVanilla(one, two); + } + public static boolean faceShapeOccludesVanilla(VoxelShape one, VoxelShape two) { + // Tuinity end if (one != block() && two != block()) { if (one.isEmpty() && two.isEmpty()) { return false; diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java index f325d76c79d63629200262a77eab7cdcc9beedfa..ad23eafd6d9e7901f726977ad8404fa34dc0874e 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java @@ -16,11 +16,17 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; public abstract class VoxelShape { - protected final DiscreteVoxelShape shape; + public final DiscreteVoxelShape shape; // Tuinity - public @Nullable private VoxelShape[] faces; - VoxelShape(DiscreteVoxelShape voxels) { + // Tuinity start + public boolean intersects(AABB shape) { + return Shapes.joinIsNotEmpty(this, new com.tuinity.tuinity.voxel.AABBVoxelShape(shape), BooleanOp.AND); + } + // Tuinity end + + protected VoxelShape(DiscreteVoxelShape voxels) { // Tuinity - protected this.shape = voxels; } @@ -163,7 +169,7 @@ public abstract class VoxelShape { } } - private VoxelShape calculateFace(Direction direction) { + protected VoxelShape calculateFace(Direction direction) { // Tuinity Direction.Axis axis = direction.getAxis(); DoubleList doubleList = this.getCoords(axis); if (doubleList.size() == 2 && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0D, 1.0E-7D)) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java index 40d6dfe30e8f388fb2014ba81f9ea4a986354b88..9de4b1c9402e78c661b4d2dc7d70439e75768bc8 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java @@ -110,13 +110,7 @@ public class CraftChunk implements Chunk { this.getWorld().getChunkAt(x, z); // Transient load for this tick } - // Paper start - improve CraftChunk#getEntities - return this.worldServer.entityManager.sectionStorage.getExistingSectionsInChunk(ChunkPos.asLong(this.x, this.z)) - .flatMap(net.minecraft.world.level.entity.EntitySection::getEntities) - .map(net.minecraft.world.entity.Entity::getBukkitEntity) - .filter(entity -> entity != null && entity.isValid()) - .toArray(Entity[]::new); - // Paper end + return ((CraftWorld)this.getWorld()).getHandle().getChunkEntities(this.x, this.z); // Tuinity - optimise this better than paper :) } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 2ba3b01092cef23bcc958244992ef44103bc7e74..130a088e694b85f7d56620352f044161ea56caf3 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -230,7 +230,7 @@ import javax.annotation.Nullable; // Paper import javax.annotation.Nonnull; // Paper public final class CraftServer implements Server { - private final String serverName = "Paper"; // Paper + private final String serverName = "Tuinity"; // Tuinity // Paper private final String serverVersion; private final String bukkitVersion = Versioning.getBukkitVersion(); private final Logger logger = Logger.getLogger("Minecraft"); @@ -875,6 +875,7 @@ public final class CraftServer implements Server { org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper + com.tuinity.tuinity.config.TuinityConfig.init((File) console.options.valueOf("tuinity-settings")); // Tuinity - Server Config for (ServerLevel world : this.console.getAllLevels()) { world.serverLevelData.setDifficulty(config.difficulty); world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); @@ -909,6 +910,7 @@ public final class CraftServer implements Server { } world.spigotConfig.init(); // Spigot world.paperConfig.init(); // Paper + world.tuinityConfig.init(); // Tuinity - Server Config } Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper @@ -2374,6 +2376,14 @@ public final class CraftServer implements Server { return com.destroystokyo.paper.PaperConfig.config; } + // Tuinity start - add config to timings report + @Override + public YamlConfiguration getTuinityConfig() + { + return com.tuinity.tuinity.config.TuinityConfig.config; + } + // Tuinity end - add config to timings report + @Override public void restart() { org.spigotmc.RestartCommand.restart(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 3403b75c8311f1e52a0533363c5f0307442f8a15..92cb1fd2419eb3a3e64ebc0c5e699a79483f8c44 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -289,7 +289,7 @@ public class CraftWorld implements World { public int getTileEntityCount() { return net.minecraft.server.MCUtil.ensureMain(() -> { // We don't use the full world tile entity list, so we must iterate chunks - Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.visibleChunkMap; + Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map int size = 0; for (ChunkHolder playerchunk : chunks.values()) { net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk(); @@ -312,7 +312,7 @@ public class CraftWorld implements World { return net.minecraft.server.MCUtil.ensureMain(() -> { int ret = 0; - for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) { + for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().values()) { // Tuinity - change updating chunks map if (chunkHolder.getTickingChunk() != null) { ++ret; } @@ -351,13 +351,20 @@ public class CraftWorld implements World { this.generator = gen; this.environment = env; + // Tuinity start - per world spawn limits + this.monsterSpawn = world.tuinityConfig.spawnLimitMonsters; + this.animalSpawn = world.tuinityConfig.spawnLimitAnimals; + this.waterAmbientSpawn = world.tuinityConfig.spawnLimitWaterAmbient; + this.waterAnimalSpawn = world.tuinityConfig.spawnLimitWaterAnimals; + this.ambientSpawn = world.tuinityConfig.spawnLimitAmbient; // Paper start - per world spawn limits - this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters; - this.animalSpawn = this.world.paperConfig.spawnLimitAnimals; - this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals; - this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient; - this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient; + if (this.monsterSpawn == -1) this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters; + if (this.animalSpawn == -1) this.animalSpawn = this.world.paperConfig.spawnLimitAnimals; + if (this.waterAnimalSpawn == -1) this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals; + if (this.waterAmbientSpawn == -1) this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient; + if (this.ambientSpawn == -1) this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient; // Paper end + // Tuinity end - per world spawn limits } @Override @@ -431,14 +438,7 @@ public class CraftWorld implements World { @Override public Chunk getChunkAt(int x, int z) { - // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it - net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); - if (chunk == null) { - addTicket(x, z); - chunk = this.world.getChunkSource().getChunk(x, z, true); - } - return chunk.bukkitChunk; - // Paper end + return this.world.getChunkSource().getChunk(x, z, true).bukkitChunk; // Tuinity - revert paper diff } // Paper start @@ -486,13 +486,16 @@ public class CraftWorld implements World { public Chunk[] getLoadedChunks() { // Paper start if (Thread.currentThread() != world.getLevel().thread) { - synchronized (world.getChunkSource().chunkMap.visibleChunkMap) { - Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.visibleChunkMap; - return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); + // Tuinity start - change updating chunks map + Long2ObjectLinkedOpenHashMap chunks; + synchronized (world.getChunkSource().chunkMap.updatingChunks) { + chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().clone(); } + return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); + // Tuinity end - change updating chunks map } // Paper end - Long2ObjectLinkedOpenHashMap chunks = this.world.getChunkSource().chunkMap.visibleChunkMap; + Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); } @@ -2671,7 +2674,7 @@ public class CraftWorld implements World { // Paper end return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> { net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null); - if (chunk != null) addTicket(x, z); // Paper + if (false && chunk != null) addTicket(x, z); // Paper // Tuinity - revert return java.util.concurrent.CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk()); }, net.minecraft.server.MinecraftServer.getServer()); } @@ -2696,14 +2699,14 @@ public class CraftWorld implements World { throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); } net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; - if (viewDistance != chunkMap.getEffectiveViewDistance()) { + if (true) { // Tuinity - replace old player chunk management chunkMap.setViewDistance(viewDistance); } } @Override public int getNoTickViewDistance() { - return getHandle().getChunkSource().chunkMap.getEffectiveNoTickViewDistance(); + return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Tuinity - replace old player chunk management } @Override @@ -2712,11 +2715,22 @@ public class CraftWorld implements World { throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); } net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; - if (viewDistance != chunkMap.getRawNoTickViewDistance()) { + if (true) { // Tuinity - replace old player chunk management chunkMap.setNoTickViewDistance(viewDistance); } } // Paper end - per player view distance + // Tuinity start - add view distances + @Override + public int getSendViewDistance() { + return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); + } + + @Override + public void setSendViewDistance(int viewDistance) { + getHandle().getChunkSource().chunkMap.playerChunkManager.setTargetSendDistance(viewDistance); + } + // Tuinity end - add view distances // Spigot start private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot() diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java index c3c7b34ceb1b8f0ed042b29924c633fa7519dc30..c59deadcfbfd5afbf951a167979a4eceb0c63579 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java @@ -146,6 +146,13 @@ public class Main { .defaultsTo(new File("paper.yml")) .describedAs("Yml file"); // Paper end + // Tuinity start - Server Config + acceptsAll(asList("tuinity", "tuinity-settings"), "File for tuinity settings") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File("tuinity.yml")) + .describedAs("Yml file"); + // Tuinity end - Server Config // Paper start acceptsAll(asList("server-name"), "Name of the server") @@ -269,7 +276,7 @@ public class Main { if (buildDate.before(deadline.getTime())) { // Paper start - This is some stupid bullshit System.err.println("*** Warning, you've not updated in a while! ***"); - System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads ***"); // Paper + System.err.println("*** Please download a new build ***"); // Paper // Tuinity //System.err.println("*** Server will start in 20 seconds ***"); //Thread.sleep(TimeUnit.SECONDS.toMillis(20)); // Paper End diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 8246ad7ebecdfc0b7519fe4412fef7b07407e850..c0a508295d2e68d92ec8d24e14f9b7626911f548 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -517,27 +517,36 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { this.entity.setYHeadRot(yaw); } - @Override// Paper start - public java.util.concurrent.CompletableFuture teleportAsync(Location loc, @javax.annotation.Nonnull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { - net.minecraft.server.level.ChunkMap playerChunkMap = ((CraftWorld) loc.getWorld()).getHandle().getChunkSource().chunkMap; - java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); - - loc.getWorld().getChunkAtAsyncUrgently(loc).thenCompose(chunk -> { - net.minecraft.world.level.ChunkPos pair = new net.minecraft.world.level.ChunkPos(chunk.getX(), chunk.getZ()); - ((CraftWorld) loc.getWorld()).getHandle().getChunkSource().addTicketAtLevel(TicketType.POST_TELEPORT, pair, 31, 0); - net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pair.toLong()); - if (updatingChunk != null) { - return updatingChunk.getEntityTickingChunkFuture(); - } else { - return java.util.concurrent.CompletableFuture.completedFuture(com.mojang.datafixers.util.Either.left(((org.bukkit.craftbukkit.CraftChunk)chunk).getHandle())); + // Tuinity start - implement teleportAsync better + @Override + public java.util.concurrent.CompletableFuture teleportAsync(Location location, TeleportCause cause) { + Preconditions.checkArgument(location != null, "location"); + location.checkFinite(); + Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. + + net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); + + world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), location.getX(), location.getZ(), (list) -> { + net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource(); + for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { + chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); } - }).thenAccept((chunk) -> future.complete(teleport(loc, cause))).exceptionally(ex -> { - future.completeExceptionally(ex); - return null; + net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + try { + ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + ret.completeExceptionally(throwable); + } + }); }); - return future; + + return ret; } - // Paper end + // Tuinity end - implement teleportAsync better @Override public boolean teleport(Location location) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 4e95bf2eb6434d8ca44d478262329c56b0b0a079..1da5b6f73e78a697031f7662e68c546543fb9d1a 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -516,15 +516,70 @@ public class CraftPlayer extends CraftHumanEntity implements Player { } } + // Tuinity start - implement view distances + @Override + public int getSendViewDistance() { + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + return chunkMap.playerChunkManager.getTargetSendDistance(); + } + return data.getTargetSendViewDistance(); + } + + @Override + public void setSendViewDistance(int viewDistance) { + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + throw new IllegalStateException("Player is not attached to world"); + } + + data.setTargetSendViewDistance(viewDistance); + } + + @Override + public int getNoTickViewDistance() { + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); + } + return data.getTargetNoTickViewDistance(); + } + + @Override + public void setNoTickViewDistance(int viewDistance) { + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + throw new IllegalStateException("Player is not attached to world"); + } + + data.setTargetNoTickViewDistance(viewDistance); + } + @Override public int getViewDistance() { - throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + return chunkMap.playerChunkManager.getTargetViewDistance(); + } + return data.getTargetTickViewDistance(); } @Override public void setViewDistance(int viewDistance) { - throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO + net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; + com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); + if (data == null) { + throw new IllegalStateException("Player is not attached to world"); + } + + data.setTargetTickViewDistance(viewDistance); } + // Tuinity end - implement view distances @Override public T getClientOption(com.destroystokyo.paper.ClientOption type) { diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java index 2f3e2a404f55f09ae4db8261e495275e31228034..eb6cde923012d34a53a31f72b86870837e5f0824 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java @@ -25,7 +25,10 @@ class CraftAsyncTask extends CraftTask { @Override public void run() { final Thread thread = Thread.currentThread(); - synchronized (this.workers) { + // Tuinity start - name threads according to running plugin + final String nameBefore = thread.getName(); + thread.setName(nameBefore + " - " + this.getOwner().getName()); + try { synchronized (this.workers) { // Tuinity end - name threads according to running plugin if (getPeriod() == CraftTask.CANCEL) { // Never continue running after cancelled. // Checking this with the lock is important! @@ -92,6 +95,7 @@ class CraftAsyncTask extends CraftTask { } } } + } finally { thread.setName(nameBefore); } // Tuinity - name worker thread according } LinkedList getWorkers() { diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java index 8ccfe9488db44d7d2cf4040a5b4cead33da1d5f4..d8c572b686c332eca722922c8a96d4629232856a 100644 --- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java @@ -113,9 +113,18 @@ public final class CraftScoreboardManager implements ScoreboardManager { // CraftBukkit method public void getScoreboardScores(ObjectiveCriteria criteria, String name, Consumer consumer) { + // Tuinity start - add timings for scoreboard search + // plugins leaking scoreboards will make this very expensive, let server owners debug it easily + co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); + try { + // Tuinity end - add timings for scoreboard search for (CraftScoreboard scoreboard : this.scoreboards) { Scoreboard board = scoreboard.board; board.forAllObjectives(criteria, name, (score) -> consumer.accept(score)); } + } finally { // Tuinity start - add timings for scoreboard search + co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); + } + // Tuinity end - add timings for scoreboard search } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java index a430506c31d9ce7a5c90d726a68f097498629545..e8c5109c36d437287e3eec23a5d1031f197a6162 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java @@ -1,5 +1,6 @@ package org.bukkit.craftbukkit.util; +import java.util.Collections; import java.util.List; import java.util.Random; import java.util.function.Predicate; @@ -234,4 +235,20 @@ public class DummyGeneratorAccess implements LevelAccessor { public boolean destroyBlock(BlockPos pos, boolean drop, Entity breakingEntity, int maxUpdateDepth) { return false; // SPIGOT-6515 } + + // Tuinity start + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + return Collections.emptyList(); + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} + // Tuinity end } diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java index d40c0d8be1b0153d62021b8bcb6e8b37fd0acb4e..025540a62e805816cb93307c472bf0de64e2b01f 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java @@ -119,6 +119,32 @@ public class UnsafeList extends AbstractList implements List, RandomAcc return this.indexOf(o) >= 0; } + // Tuinity start + protected transient int maxSize; + public void setSize(int size) { + if (this.maxSize < this.size) { + this.maxSize = this.size; + } + this.size = size; + } + + public void completeReset() { + if (this.data != null) { + Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null); + } + this.size = 0; + this.maxSize = 0; + if (this.iterPool != null) { + for (Iterator temp : this.iterPool) { + if (temp == null) { + continue; + } + ((Itr)temp).valid = false; + } + } + } + // Tuinity end + @Override public void clear() { // Create new array to reset memory usage to initial capacity diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java index 774556a62eb240da42e84db4502e2ed43495be17..001b1e5197eaa51bfff9031aa6c69876c9a47960 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java @@ -11,7 +11,7 @@ public final class Versioning { public static String getBukkitVersion() { String result = "Unknown-Version"; - InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); + InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.tuinity/tuinity-api/pom.properties"); // Tuinity Properties properties = new Properties(); if (stream != null) { diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index a08583863f9fa08016bdfc7949a273eaa4429927..7361bf04de16d0526dc4cdbd0f564713df041d90 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -205,7 +205,7 @@ public class ActivationRange ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, waterActivationRange ); // Paper end - world.getEntities().get(maxBB, ActivationRange::activateEntity); + world.getEntities(null, maxBB).forEach(ActivationRange::activateEntity); // Tuinity } MinecraftTimings.entityActivationCheckTimer.stopTiming(); } diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java index 7a23b56752f6733ee626a8b1e4c3b78591855c4e..17151d9dc8c7030b54f1ba0956424d9d704c81ab 100644 --- a/src/main/java/org/spigotmc/AsyncCatcher.java +++ b/src/main/java/org/spigotmc/AsyncCatcher.java @@ -10,8 +10,9 @@ public class AsyncCatcher public static void catchOp(String reason) { - if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread ) + if ( (AsyncCatcher.enabled || com.tuinity.tuinity.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Tuinity { + MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed thread check for reason: Asynchronous " + reason, new Throwable()); // Tuinity - not all exceptions are printed throw new IllegalStateException( "Asynchronous " + reason + "!" ); } } diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java index dcfbe77bdb25d9c58ffb7b75c48bdb580bc0de47..fcd3917e0af080ae4726e7f511abf586df4dc7de 100644 --- a/src/main/java/org/spigotmc/WatchdogThread.java +++ b/src/main/java/org/spigotmc/WatchdogThread.java @@ -23,6 +23,78 @@ public class WatchdogThread extends Thread private volatile long lastTick; private volatile boolean stopping; + // Tuinity start - log detailed tick information + private void dumpEntity(net.minecraft.world.entity.Entity entity) { + Logger log = Bukkit.getServer().getLogger(); + double posX, posY, posZ; + net.minecraft.world.phys.Vec3 mot; + double moveStartX, moveStartY, moveStartZ; + net.minecraft.world.phys.Vec3 moveVec; + synchronized (entity.posLock) { + posX = entity.getX(); + posY = entity.getY(); + posZ = entity.getZ(); + mot = entity.getDeltaMovement(); + moveStartX = entity.getMoveStartX(); + moveStartY = entity.getMoveStartY(); + moveStartZ = entity.getMoveStartZ(); + moveVec = entity.getMoveVector(); + } + + String entityType = entity.getMinecraftKey().toString(); + java.util.UUID entityUUID = entity.getUUID(); + net.minecraft.world.level.Level world = entity.level; + + log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName()); + log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger()); + log.log(Level.SEVERE, "Entity UUID: " + entityUUID); + log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")"); + log.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)"); + log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox()); + if (moveVec != null) { + log.log(Level.SEVERE, "Move call information: "); + log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")"); + log.log(Level.SEVERE, "Move vector: " + moveVec.toString()); + } + } + + private void dumpTickingInfo() { + Logger log = Bukkit.getServer().getLogger(); + + // ticking entities + for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) { + this.dumpEntity(entity); + net.minecraft.world.entity.Entity vehicle = entity.getVehicle(); + if (vehicle != null) { + log.log(Level.SEVERE, "Detailing vehicle for above entity:"); + this.dumpEntity(vehicle); + } + } + + // packet processors + for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) { + if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) { + net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player; + long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets(); + if (player == null) { + log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener); + log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); + } else { + this.dumpEntity(player); + net.minecraft.world.entity.Entity vehicle = player.getVehicle(); + if (vehicle != null) { + log.log(Level.SEVERE, "Detailing vehicle for above entity:"); + this.dumpEntity(vehicle); + } + log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); + } + } else { + log.log(Level.SEVERE, "Handling packet for connection: " + packetListener); + } + } + } + // Tuinity end - log detailed tick information + private WatchdogThread(long timeoutTime, boolean restart) { super( "Paper Watchdog Thread" ); @@ -121,6 +193,7 @@ public class WatchdogThread extends Thread log.log( Level.SEVERE, "------------------------------" ); log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper + this.dumpTickingInfo(); // Tuinity - log detailed tick information WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log ); log.log( Level.SEVERE, "------------------------------" ); // diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png index a7d785f60c884ee4ee487cc364402d66c3dc2ecc..1e6ee83b1a207eca59d82b25c06895ce894e8173 100644 GIT binary patch literal 23418 zcmeFYWmH_-wk=#}0m0oV+}+*X-CYZUyF+mI1a}V}+)04oE&+lQG(i$vzslbGp0n?J z=ic{jYv22KpcSyj9HY-U`xv9I#adM{YAUiQNQ6iL002c^PD%svd*t^E0Uq+RRpJCnpDr?!(Jf-{(g()1mU$77W~Nxy+`GL;Hd+mDj%< z`hrC2`H9Xp$xKhL+Bo|HKmFo7=M-Xnd+W#BZSvAC(iiMjve~^o$4Z89_W8CRjpga8 zv+emII+5u5MEB{@eX9E4yRDb7&Bx>ddEq2BvbJprC(KVzdi!1Hv=?V?eMFm!UV_HW z<0^u;`E~qf&`C*71k3BLYs^6^yabk)Yj{C^qJi!8i)sPCIKOO3EVK#shU4l~4n!S! zBamlrZCbkCbo>x;?i@>dx2tj1p0!K)a=rGr2p4?tkPD7Uai7@eu^4La)+NpTh=0lZ zE(P78GpKSAt^%r$xC~`T`{upc*$uzT!TfrA3JV59PrYKoJM^id?usWu%mjxS3 zGIWly9E+e~H|x~U79-+u{MV>MAb|`0-4G}cnLI5ADba_gfd16vBrVvaLTf}7i67f- zUV>Jio#tHA0!~sm0$WpoKSN7XaeXh@QicB@RolHM98-~@BV9w0u_uzyp}OO(fy4Wr zJ6yWa1vkdp)+N7#EaB{Yfr3+`*@tt=c)q!jl6ZmDL#*2gzWIp{69Q}B$Lkw*6d3E9 zj_uz&w}qyzxK`wOuXwhtqv;8552mSzd|Q}T3AX)_u}HT0BEIyM(c#%fVL8Kl$)^Q- z{cz)BjBTo$%y58*RW}_BV3pCfwG+RGqat@@zbbZ*z)HjE?T7VtC8S+15t5s!b=&=C zuMOnJ=l9)reQEq93r~0)2a~9Mdp~&f4J|(Ua$ai{S=hrYe&$}+ef~hR0L`9fAlnp& zU~_CYxN1ByF`|jyx`3l081=asXX^0V#MfW0Y0VrDu1g;K6Cwtlws(!g$mm8Fc@kmk zHk0P``>>$trX zOKO@8`AeIqg~@X}4s0Zdc8nGX+gS2a=5c65je_?zXn&ci*C*B5)?qG%26@N56+L?} z_?X#4UxDE@4hnDzJay4|Cf4;AR@J+8U27CLTh9I}A5l!L!2P|^Ua+g%qMzCQE@)hL zA~-&6za6OQ9%Z{iDi=UpL2anxJKdKrIBFb}_xMXd-+yzVJCa`;xB$c`45(a{uL_?H z^UnJDof4?Z78^HM^tq81kY(+E0IU@LG@-q&gT3lP`C0BLFBi5s1Hd|u9H{b+){b^? zVfdKsCmro&{*7zt>f#S%YdjlEHqY`MS=yCESuK;xK&3U=$+|Z6v2q#ROz^|kPh-CI z>sUvd=>Cp2gT~p^Qg3DBWn_`9>(sprp=nf0XDapeDmb1HW5tc+++|m7V{%gSN~|TR z2bCXzg_I)5(8c;v>pwESE4h025uEHWm_tcvSv0-E6WR7g3T6}C=sFV%aA~|;mXOgp zbsW9;LREE09ya@GgH3vFkvo)fU+79v^vy#Rp>OwSYk=3Vr;1)qSCZpdl#Z+8kNuNv zr^K;X5$X^A^f#^C!Z%c=re|~`tieWGWEBCuDKtv+o*#NWII)B$(msOTpG_kyd|Tr} zl)isjXV6?~MrnDMVo}|bMAWHywlydwi}d}AA=Ng?^q9luER>~(DYgLsJPsoth{ z^L0l>OMO@3z^=mT0BbDwT+^};TfH95K77HfImHl;O0l6Aa0i~unu9BP)>#ALajXQY z(IAA-I$io%FzIkRDIA|X@Q&P^4W(}lk35PWxacGM{hEaQV`_!->QPK!o8{@lbl2vF zAcCregL@&8&6g|$_T4n2`{CTidQJRh@uSlrAblg^dVxZ_UF{=zb0Tl@30I07F>s); zB&Kq)g3KwCC(YS#qB6%v(@u$+^UMy-IEbUBQRDrBd@SheXBkgd<+C)wHq7gZ(RrO} zX!YuqAdL^xp06x-d@$GZi>I*hU^L$u?W*m7&ycfFm$3!GKI38q6HzRhlyDjtX3lk zl_?UG_9#t6uvIWv*&fx{vD>Y@YE%u*tmbl(Bt~m|8qUIR0E|ALe6o5B=)H;yd_lB9t3rQ!Y&l&## zzEU*<$W>9t0Q=QZHACiN3EfqjA0BYIZ~f*20=YI6 z{Em>2YX%d;v8O`|!SJKCxu8K^-rF~)StR^C_LzNYw?u|A-=xtK-E-CjvDW|v%}GaV zP>LRLIvV@eU*8rL+c>f^D~|Rho639&VFj+l>yKJ4eIbWwD1l)MhOwTp9h+ zK!7t0Oq{PSv;PgWzj^ zi2;$Q6@7i22*WE_EZhAtRz=}@`u0)mfGB7G72hJ-GiF`^`eUMM(=a7(D(M}eL3wZG zFgJ!gwvo5oi;MJpB%3^>s1ziW!x29I>H*20qpye1QzPY>*ui(rBWy_*%W{S`Y{OcR z;zbDin`pyGAs3FMCRuEgvf!aoa@6cdNO8fd3@T}3!-A}!yy%eqNH6iIQ=srUm7A-3 z!8WfxcESolm%~pvfNKS|ZTrS^QSb(nHCJKWNSZP1GS$UHdHlbS(qQ*g+7V7wT2`i! zCxckawhv=4=em7PU-BOj;j-nuX7I#<1U2USu!s`sw|Qj0q(K7!e^j9MdH2b zX}Up?>zr*gO%5&!lvoTUo{7b6mpGhRS1CzUtDJF4sWML|SaLy?OkcEgdVghZenRPl zT*Mv9rd-4+A|eydl#C&mDzu6tWq+O)Y&AkmU>Bp27=kM5vFtu`ZzpZj@vGCo%vv{! zzD)2e@LRT0LJOtL8t(XrO%vUysrYBGv+PiA9+s_+d00Q+Os}~m2GF=$VK(3)kO;U) zjZ(o|6V%dWCy8&B&RJwqs2^}HSFy}|r+o@ApT97fjefwV(2k_5KvPxZ-@*}agR2!XW+R!#l>B$r!C#W9v@`sABQLji9L~ z)E1^q%&2Ce@bIuybSCHF!L{|^5@~kU7GR9=h@2P6w~h<7IaY-IJ<_^ooUV+zZ666S zPKvBHrs$lAcyJSiemBCBcrQMAiG$%VZ0yLA$2k;--khf~luvd6wPK;ZDyK5m5)H1l z<8p|zfHgHurFh+Dl3F&5`tfWjh%Edv0XKZ&3cWqpoOL`l`+Y7Se&h#|a#-sSP{z-G ziX!wTBm`(FcNsQuP1zVT*(2S!mL~Je1pw;qTk?&33{E4vlsj$W zD3L=e#5ax!b$WJjG1qUw*eHu{+CGOFQ05#}RKFBY}O*@~B?zB{4V4QR~FA90s~- zTKiB$qixoEEs{0}1i#-YNRS~(W z+{it>$5Jd1@V3{zx5SblB_?Fu@}BAY>4MKV7)uaD@B0o|-X&pb@X`s!e#p za}k=v@U~28t!OD3agfASR#`p6-YSbF+?KXJ0zQs@_^7C>zkHs;MZi&y>RCd9LkN`Q zJRK@|*Nj#HK83KbiBT-{+q#NH`_$I98#vTp~vq(WNYq`8zQ!0Z_?>pjekqE&Jk z6>6!8e$D3%XC;=(2A4j?jm*Hbk9?*^SL`5wFqWEiFL_TdMyfL_rVlL{*ObwlYlxAh zVH!e4p`PV~jdCFi4Wy0ML>51?9<~8#Ag!|4C%nF!S_?IAf&I&r4;c`LVFeW9L zajZu)Pq4%1+b_!)#4 zV5`C%5cC}Rf|#gMmiRC}MHb?qMNtZR3GT8u8tlCp_pM&%K3$7kGKyx_>gilk&0Q3M zT52YLk0#NRvl?xnB^|!Sp+lvsiqP|!Ib-zj-ICTVI;u}U6|gJ21zvgZ2=$zd{5n)+ z+#wEOy%@GD6J$Ub>x~*&iLtd@+xN|4b4G^E7Z`M> z#%ESGzLCqN5C%_G*l)|8+_vjhJvprN{H*nTr_}qic0be&HDBX%p;jgBt45ycsTmaH zHCCI~uF*<}&178-@c5V^wC9;K_$vf*&5LQzzkG4x8EchNJ<05*$Y2lF>%8Bi6-Ll3 zL1S@6ChLdHvxaH50O!`9;4?a5*H@zl$i&e3$gJq@mq`gxNLMvNO&F13kjC?;>M{li zk=Z4eoD}aXu7wXN8_9U9xV*yZ~=F;wK)KM@Rc?AOHCXJ|CY z?hshFPBFrd*z49R)eok%Eli-{C7VRFEBcda;lRV{&HVy&#rLoAefF-PkhLtrznBe2 zw8$Cw=?*e?BZWa})K4;^p7eN&R9? zD^F$@_(;-5){)bDug8pywS=C!bbs8Lua;QlFY@IN>3*{r-Q|^$znkGx7uVMX8BScP ztPzM42}my8tn80z=Hu0>>>mx>wV2JJe?0lHyxv3Pcq-XqpFU!^|MPW0f8HR*Xk+tc z9ZA$WJy9*&Y<`*Q5u05|4Dm!I?>&8PLl=Kw zn3#G`ePZiV<*SCvxPM|rm6Z(^ca~V%n<_u#Z1K#>weTPYHK#|CUk=>ztkM5^J692u zu1V2wDf?nTKhM%;JL3=r@9Om&ehaT3>93f-eP0@U0bhv6H{fk>gc1^RB;8GJuMq`$ zGuTDABs2W={xdz}lR)H-RA?w>z!gCw<)Iq6Cs>ns;?3ED%N7&)3*J{}!^!%Fwy!D_ zxZk352@DLZ9tFILkv}SmrN;4U_3O1PhBWf&`>!Ba*f*1EJ0_p1=i4D(S0An75>$`? zYXf~+dXw8CR@fcV;CIp)E-{$7KhB;-ONcxBFH#Mk{@53GWn)9_G*0@dGc8lBV}M`6 zDfaDB-q?frPE6|Y`t=pRqo^M%4u?m!6ej$OR9taQUN^Qeg)z|a9(9Ybgsc0n zO!3-sFuAT@F+E{Q_Fbm*`J+i3io#Xi;S@pBadS~cn4y=#-&8mY)LXyf;~Z?N`VrF7 z+OgDiI)ROBkh-aUwYNihC5Ztm|18l={{Sq5TcPB}Zen?eRb%c)D%^i1@h;-{&5aA}224huiur53b(2brgGoHD-H9le75aejeU-313HaR?~OWN*gVryiulZz4N_cM$T4d2 zA+Y9$@O{OW8#Q?F&f;}xhswErtv)cVaott4#wF<#+33e}iCk3f7W(?I$&s&f5Kctv`YB@nB{(|$VJQYEmw=-`{@Ovm-R|fzz{$er(YBw3T1F-;98A-h^HA6flZT2^ z1M`+o$;eAeP)c6GA`JVLLCe^Vuu}X~-^?oWn5)3om2iFfX4#I&F1@Nn)e3z5c&p{H zuea!<6QIqvzCQ4ISwS(MhSv+li#kOPQDwWB94Sp_=%PC<=NO}KOI`{}4zPswUqVSM zC{=%Y2_gD2y`N(joV}}xxDJD-j1-3j^>m>!D;M|R=9w$HrK~bIt)`JwerC^&-<-tp zsuaorH593+$W(nRsr_OP1yejXhwk*Wlt7ZncB8To8ytA zytu(cLN?tr$G&cJ-`i_ZHH!{i*Q3$*tj>#-h)I=sFYGf%_CoM+pb}bgaf1gpsqXTL zium=nhJ(3W@O+{*hoF81@>1A?1zHTrIXlxj;_?xqv??})960xo6DbWS54n5pu2NO&g_dytq09An2Sr#iVIwp{9rSUbiGkS za>{_uZVpZ=8}=vt@c?jc@gEaBRgt>*9*Y`eIB8rF_pPj*9owF%I{n!%b(x=Y%=yS? zj)*k0vV1mg@<)#cEY7I$*)JUSeKIkb=tcv@LGsNj-7Jlgd7vJ_&M z3Q#+wbQFNoe68YJu`dGMvIOroH5CN~z8<&CzEKi3@CPC^185`c&4>&pd^klX+Qu@y zJRaRpVnkMA)x!LyUQtTCE^Zy|3Umy!4-DX^ylKCgZRMW`mLqx6GaiZ(*n90i!{Cbf z~xJ9nMmsDIjVhJ3*<7BXsV>hyj- zy=uR_Io=%^227&XsLnC3N^UX*`mOg_^0Er z<)UdsVJ3V_;yjDM(^rojm)u9rZ9TIrUHL#>t*xQSS3JC{0?n7iGl44it=~yo-HDhK zZ+xPWZ>9Lfkz7Fwh7nn6#=j0rm1U;b1DWkq(9Sp=N~t|V#+IRYO5fqOHs^6@@l8Xm ztX>8?ZN2il159#ae47qP8aNPYw!EF#rFEJ)M@JG$&E2c3$gcLC;(*CjB;G89ak=cn z-d=o*W>B*5Y?pTX7`>SgxV7(EVlR63YBgz!--3of+P3xT?#ty(u1FmZ>>|ZkzygW~ zEP|kBj$(4T8~kQH!xcrsDG>D;0rHL`7lpJ1O6pa4TM55yLT1Ucyd0=>>R74 zXqTK2@JrPFQg!rx&&Km`=kO5)Q^RnKk%u^ViQH!u1zUMdZ2Cy*<4Ln7t?ud&+Y3;} zi{j-TsiYGCfC>joN~+0AO8&>gLdYY++`trJxldxmL#Bph#W+zm=&mDbc}&p?uIUW8 z0}^1y=3_IrBRnp!oQ4rG-13_+vN&xZo~<#75uErr%;4mt{H(OXqp5}+#_oWt=*GJL z*B+S1Ne0cye0i}&Cd$0%xQ@qa`~YbH41%PXl0|v`1e47+fA8t`?ul`_t#84DJ2C@Y z8k9(iPvTLebhkM1TC>O0!yq)HBGLu_MYuQ~RnT7N_n$-ush@)K>G4-Cg&M_}-gfGp zTIld-?GWrl7u%Nlp0G!(jlxAu$=^QKGez4_Ya=hk%r zDCp__jHQd^?EdexM+u3z{LqoDp>ZdPG-pun1pCj^SO`)n;q30?q_>{pd+E`_okO3w z+uE*0RLDY@JX-=_h>4+hsmDxie7sG)onP|7H#acv2HjdCx2GVFaM8hF$irJbWhDVi zXGa!uD`yL979U3!$Or%+B`# zXE$3`c7A?-RyGb+4i09B2eZ4clZUwvvy(gJZ-_rJq^#X7-M}s$U}q=LZ%lIwXHO4d z3JSj(tpCZ${{;lZ{XYS?y!Lee`=UMFt$&;T*!XKJ8&=4!{#g3oCzX{~R{KZH?>yLo z9bNv2`0f37Pblh zb6fNN4MNoo3~3+p*Z*oId~7z>JeHQc%}aj&Vg8#cb{=+aE`ByH zes*47HZI=3#p_tRxkGy6H!3?D3&)>3x3UzFfnb_LY832fZfniz;$-^=?(bv>K(+&+ z)BJZ!L6HBjLwFUCbh9@1aCXyjc782P@q4wP-;jTFB1q`39urV#QNVk|8Gc|cFx{T|93e5bo~d4xSNN!vzvpeo2rGqwWY^@kMpmb|AC|d zkw|wBH(&Yx!>0ZhKcPR1R1OmB?B@Ho`8BOw|2+Cr$6kZ~pb7-~Lpub_E&rt7-Q3IC z>W>sac>Ht7($3t;)*2$xe>K>D*unpzkokBytj*cEd70TPAsWiXZehdB$IZ{f%*AiX zYt3$H$-~F?zo5H2+jw}JyIG6dLTUh#XNUm&k!KLyU-d`-pI75;XZ^dl*f`jk**KZm z*|pf&1vuCRIN2%w%r;1f^|vhl$8Lpw>yEOrz~A}!iy|p&H!x(gGH%X}e^Bv<0t#6D z*WJzY-*z{IEr=uv{3}-eQU~t;GXAeZ{jHY*^1mBI)6>P}HQ3tiKj-g1>iNI$6Z)t9 zuip7Th5y~{&uB?!7hj0F*muQ{|m=I5tP7|)=uuu|J~C6?(%24{&rS{toctH zBn{zuOLuC9O6^*>_Z zf8_k{>iYkUE~J0nKd^R!T#tG~?hf3j&(lKgEWleR%1QxVet#GCRHQ*Hh%R#a?f}3m ztluvvKz1%Z#0d0|SC$5TgL#F8L)J^q7!3e`0P<4eT0W~k@_Yld7Vlm}555XMDA}Yo zj3(0A4|WccWH6BHdDkl9XjS8=#~Jo5RKvk0w3M~Bw6ZLAHea8>H^m@j-nX>cw|p@@ zk%14ldi#BpDeG#(|Mc#O9Sb=#dBryM>%rB|)ehW;b@!9@j$^_tB(^u6k}h_McuCQllCx6WPr8fi`+7H|R$TCT;+X9jpc< z;Cg{WkWB@B6vLowwmB>?bkK#f5=qg05G@=_d>rk{Z2$W#2y157K;9MfTvKqk99$hGMBYd~U|5{&@WZKL#cU zRsF5{wriLICSMtb&4Mta{zRXPW9M*LOAY);avRt%Z@$v_5s`iFuCO5la1LV!g@7PY z=Otwj@Zgiwlec>iGDyyVG}hyU(8vc6!+}HNJl6eOyJRbTQhz*apbukp7@@i5_4bpJ zQz7m)PqdYg&ri`#sAlTtF$<1PQAgriNqn(R%vo%00)}pr=oGoLm+ESho0!)e8>~cE z&VD>h$t^m|0Qx5v`(0GwmN-20oQ5}tqbXZ~tL>C`w0Eonc_xETDKOWlN84gl;&|J{ zIFlxa5=FiBDOn~jkyhOGTGHF1rzo$u0*j$E-V`nDlVHwbImE&sUN2a$rlOej6HvBkUOrN#vK7qhp&|SuQaJd0VAY)C(tz z(&aZFn|O1f0r}x24<}3e3{fxccloYN!zMZd_%?tby|~+x46FC4WND z-K(+SUD42K@MhW%KIR4D0~Bg+jENDD;0vfXqns;Vy9cp0UN+93>g~ zV^td=!tDIKT$Yb0d-6QH9ykQL9|g1&%&53de(TL0X0^5_lu{7^MHNqtAJWo-v)7+Y zHLr6S{C1)oXk70AW+Vh1^*2uPC%lbejE*rvl~4-n6<7I)K}OfQefV61$dq3)d5SOg z21fO9Uk%-Qk~z#bbx;*6oX3XikpjZWj}N5KFYYI*IM~Lx31mq z^#lqH=b*q_6um=#-~Q%sBtCsLf=W2@1mFVdsCfWmX}#T5Glh>n>n{MuhMV8`XhgsQ zu*;X7H7ACxW5r*bZzAui;YSc!N9%tST6DNjG$Ou_4$X%Xle?SjaiIe`t*coQ760&! zHG(Fl3fdZQj+faw_W^JxG^)tUu@mN}C=V|x=hR=>g~UtMH!k|JwZwU2P#Ob}+&c*Y zEXZ`}H!7dE@gGe)=QI1~x!z)b4-4oe#|`_m3vUj7t>PxOh)4>dhQoca(8YcEt(4M&J2s=;GEZo{ASO$i+lo0u`eu>oK#h;xYW31JWJ)V z&4PzHjj6nLW4TU?ODvKSpUC^L(pV^GHC%fprG(S(8XZ$`k6w!LqK*{SyKVnv0ABz4 zhl7B;M8`4;gLUBmVhm@PQ^{+d+H{nA*6x$K9YWA2^7Pf_uvCro-tjnDi%A$NQPEn+Gf|E&UkLAH@TN zz6r68;WK0lqf_dGsk9g4$VO@AdtBdtTiiK@JCgh!WS^fOu+ zKXQb@8qsFXJ6aT35Rej*mAGYedyR&Q58sM7(%ZNs@y1B)S=9O^|?ChZ9%je z4&(Xlhtzb$HjP(3IO%+DKZ}0IEhJUoymg1|^^ipU9JdBi&*wWN&l4}KrgL@(70FD) z3U9B*BC`4bjwoKP^>*$wm;=y9F8c&VsW+UkL`JaC3b^S_<(I*2(xoVu;aL6%-8+hw zcEZnB5|?*}VpjjSh7>kkkdw?PN+N z^!L#k?sPfg-sund_})-@h-SvuA zZ1+uI0k3J~AL|9B6Q4T+b*7c+2OC(|b>O!I0HV-r?6s(gZlIA5$6$vUGzZ1! zxjHRz7e<@iY3sBj-<#n(a+Ktto?+A+Hihm)y`eHGDy*M6QslJTBQ~D-RIEMV*Q&B3 zJWxIe{jWk7-8l(iuglhGp!ATo9so7;eT{~2>P`M8?y3+?pByoDeWg{7C;FCoAE#-> z=*-wZq8JL=?06rZq!Gb(p2Hk&HqCP2mrrA5xVWN~=COR%z>Pko7Ihi(2`EGq`qtX) z>T3;JMl0*O43rja&x}!N(*!f^ujVG#%U}0nnL4iGU$k!{MoBR zWLT8Ouu#?`H-s`=h_a~nTP-H&1-vA$sC*fI={v@mKvMCPPQi#%e!zU@mp}Ro}CQ6Ddqh!_uD&6;P z?AxxOtxGW^)J%#E_$@rZb8^+x2y{AF-r0>BVA;p2&?v-H8a?H(V@Ex;P1;tVSLyc z{G4Xz=u_{la6_~KQ@1i-GQ?ZYLX~KT)K6W*#v*3tb5rp-@lD<;4+FC|+P#*OfmW+I zD$r0-b8hPj79hN^ezk_#RGB9qg}jpL^T7Sp7Y`};t@_cdhu!|;4rQBe$@5# zF|lvll87X_3o%3A3tT7;*>$V(5b;2+laeE0LGv3PG%EDXh~tok@13T88_~dZ@>vrz zHZ}fq+Gs0g9-}K+4(-knQh}*`4kfCqV}XDvU}01$a*LZCv?bk5C%KFzT#!UDTbR>( zEB0D5fhXF0l#WGOexh!yg8TGCG@g99?;l}Cxjc%tO31@7SF{W&58mqxcKYoAmOv?F zi^(U}YtSJS(3Nlv(EW&1Bl1h*y$|}Ly4rYCdFYrHG*2H~-(Qj648u)Q|IXi3%mVp_ zWc(%Jj8It=vW>Bv7WDp?bh-G4UYB5m#BYEN#F~7;QZyP!quanWpd)*yIC)ysNt#wy z)1CEHXJZH9h5o*dCM1nrW199|vJQ>`>!`9ho~Q^m&Ye`nJR?vLn5L7`%Cr4M|L!ui zR?NJP7XCGF8lEhlwYx{UJ1#bxS~kqv88=B3#@TJ^PAX7=uIEjDsR z*oC$lJL5ZZ_HE*C%qEBav6=pI(G$p>38|w#vhz8c-nM%foL*($b{3^nZE$fCmvTQ9 zRW>z6L|C}*JkKP2B0vS4i|W6>FQ^N|^P32ZJ`tNumQ)68iesU-t;OAhg65DMoTu?0^2S4-Y9(EitJ1@_gHIp%vqo7S#UXlGZ24ZKiJ-cHzXsGsI?b4 zqCO;6F$AivQw5mHZfTBp2vuLrqFo}1n#gqg`R1gvoq;|$MRHdELhLp)1@&`p;lgQ1 zs1XWD2Rb$xIq(i4bZizOiJ;auxtle2FyCgLO>p|1cI)@9Ta`IU;D%AxB zz=j*s>OCobA3U;mn570!!HOn35(xWkD-TL??x$HpiX1p%Bkh}p%tub#cP1{IK3ZCz zKyYf$YDDno?Uu7kC~CJFmF=X&t;oCjm+878n$QjohP|)qWe`AN#*Mj{_(FwaV`&Oh z&kYa5SwB`ArV-L@0N#8~Uw+!swWBfgrp?}N6Bls+`HC7^o00try2`~-tu&Tg=&EaK z)ITwOjb0{WCb(@={ny!$y!6Y^h5%Fu^5VeaKY#o?9* zLtURX9Xg+*S(}UUB;`o!wI73P))9oW;4l=$ITU}!=s6x{sjVibn?y^WUokfkP$9Tz zJDR8dbjOW(neDl)Oyb4oCBqWAgq*Z3yk~tg^Qd^*$gg(d|MSj^E4H4rO zLq(9KLIf(F1ja(Hw?>Qfl)vJc>~t25m7%Oqi2ame{v~Ss;EnG>D%Hfc-ki}s8+|ID zHUL*tny<)2z$YR18oK<{n&0*q75AMkeaaVs=}FnoE#?MDg%fd5J_D}1Z<&)DVHupD zWQVqT5ar-0J1dmLJJz%t1a6A9#ox%CxWLs_lQ1zbr$MSF;T4*{d{_zg)o3>=f)$(; z$17~skW#O=Wi|69{&U!DJ-t->;vRQ*B7Ovoo6QFj&DjSolTz>U8 zfEORGa}O?3Fg)^&0Tx_eMLwt zsZvn+Tyf9?Ls=CXO(fRf-(aFEMG}BT6Pgt-{q$q#0t6d#e=&fDd|;%V$5bZ-x{@+= zd@jOOKmn_f)+a#u3^&@H*wD4ZFqpz7{tP9@m=!k19Q)8W9T@GKoH($(W%R?0-M3>e zDR;x?M?4-EUnB-yaHV4yl((d;3UEk~uU{Hl34I0x(mKj-(U zu5L(LXb)hG8#(KA=2uXZBtt9|;#Y-gn1p2HrpSyOKL|Pz@e8YS4i@xgJ7UADTDJSg zPsqvQb$a_sSCvf~72y=YZkqv0QbatInCWaGo4@N0En*V|XA`)|>llI6FX~539Nz!s z2N6w(3$hW#xNqj@7@5A*eU`&)O6@A!?8Hly0c!}EfYX&qD@|3Z03Y0JeT^tuN=$_5;=6PEvo?B~WomoH9H zivvKf!H+m^Z9xKYm+7Z(}x+@u;#kEL{zv#Ko@|c zH31KzI{pf$XK3K|+|zLdLc%;cM&!$^rdvx_YvwscW1ZcaVWKS*`Yq(uPe5coJc>Ht z0E-LA%Dd3^<85xiFZz9ARKzVMM>E&EAZkp*66;m_w|Hup;Hzmth$z8peMKF1Mlu{9 zyMukh;e2}PKAiwef#QM|n(ERsGN1ucR`9S0eP^XaNigx;7+yr0Z4^fh{4WX{HT8#lV2QOCIHdwNlLz2|-0YjHXf=uStu#ZtS@`K@dG4}+2L=7VCXa)v8V!dYl3xnwN{qDV@) zr9X+&&ux~oFxXmR0{bQA+^Dewm;Fy%kL93uQ+Ih|g%vzh)M9J0^?9Ce8G62%K2zlX zqI&3>?a&k*H`2e>$qMThIAOFkW`aMsrnEiBfPyFTE0|~RZ<98N@pgM)BZ!yvq}E|e z8Trbjxm$C+Sr!qo7>-^EMiSz0;|6>cwi)n~9DpJKyBGz0gtwGSG+iZ0V$$Uk49Ap6 zY*CGrbPi)L(?mr2CWDZU%Nzc4_S}a60qXm=OBZoaFs?LH?p206oaOE9Cjw3xCBCoE zAEgQW(UB%m?cU|x!V5oq;jZu)_TNBYm_5LrLM;nVw)~!^W=tjjq5_s z9^4^}3{`*xD5kT>ACh>s=h$tdJ@Zgm@Y98%Vide-Z_yOkdH($9w&y5sZ3~SKE8*H4 zh{=Fry|sl7Py(naFk~`v=-HlZJW?e^ksm4wAVYspk#%Y`7>(NW;t%%GkPOp4IT8xj z`(zaU?uu?!T7=(?cK2Ak=Y<9Rjq?hvMS9U|R&*Qy6E!>nLg0t0I9;yOD zZ_Bbfm90HMQ{hs7!HjpoWqR2pf>t;tnCwU#7V0yR@*Z~@LH*?F`Z=>Bwq3|&2`5k5 zha^LHBeB?C(&Q4>vFgt0A3fNnpJtbl0WulcH~q_(47E-nGtcBJ-X;39v3mg@vYvO9 zZl1}u{MBrQZpTB2eR=B`el=_wZ^QPhk2cu%UFf>)1iY}twfF&A zofj5f$&b_rm4v@R*YouI3T{jeo^o;vdMPDQ`6Wk2MyV5)YuYnKs9E;@Nn`Ibl5WMd z;4mOyTm-{19`&+qoV`)2N0t@25h1hhmBi4Yv)GOsrxJO{qDo)NvMihuH^Lsh`ryZT zFF{=e%#EkRa{?t8E7&g)izh>s4AWPm+c-^QD)=LD8qmbrJZ!`n0EM8tC^fo#8HAEy z=!f;50sfgii%cQJ1_8U^(pPVyIVUJqYL)Xr6n~S7&Wb$DCvlhNKn={qpillv;!}!fKP_tBqX@0Oh_m&gWHLD2TXVV&OLz^TvenX7gOQ&s(gcUAZf{%5OrjxRvu0!Rwp z`S@IMlOxyk`1WTzPC>PGO^b~7r`n{esOo%qefh*mnD#P|*qNS}~lpY>&-v+k% z#ofBbA!)?>d3gVxnXdHrZ76)V{2>{t>$*Ork)UWNoWMU!TtR* z2J}@!tKQZ|d$rLNzv>Y#F7%e6H5Qv6Kzw7F0g#T;@pfCy@GJTlF_2062TEJ{%(HIW zy{ihmx-2ddG{1N2_+5@3cGAnQ27=yguJXtPU`L_fQzb&&`ZBfjMjX4ZxPi+TT8sE9 z;wq{dRl?$g@@Bt&YkIhJ%=d;x0M)xndI|PbGZ5OB*Lyvj*}1y8=ikm}X?NHFpU}0- z&b&s8AT@$Y4Q67&xMcbEHmpJou@cB;6CI)J5A4MTp1h9YbNaYd|4l4SaUgvZn#n8s z7zT@BORhxB1YNp2aFz6+seMrN?zZ7mWU~lC6Y1W^UEUe@L__yCrl`&cW3!_43^^&N z#Odh7)&*`Pwd@>?;T-q=ZwlYrD+G^XDfri{D9-dIFLf)vdn7{c z5VkMr36j&vlKyy0ad!ERG!==MbDRdbeVRQPh1>NRrSbF(8ZAmQI8Y-k{Ea=0kO8E> zY#LL8r@Z*_3ckN?unXB+bI_`9o8sRuTYJ>l+eJuo+TQ~63P7>V^N?C5BsOp8qV`9U zI~hfM-B&&#Hq3x=!jt67)Gw8yry)e%V~@*tzKW-@{n1!}S_>%*SP))8gL;kd_2RFt zZ!`Yn)X$7%s)T->I@&xU^ds~7FwiuXcHeW+c!V2tc>CuR-)p1^o~9&c9}&z-p}Q0h z$no%x9mQ1yeG}5aI^u6oRQV!hQGHqKMDJmmOMl>UUPINNV!i8{O#gIzQN%RW2jx$N zjC-g;#a8Yrl~N6@tc%znXt_)zJ+5s0y}QQ6P60GJ6a8qUcz0uPr~33Lb4zQyQGzWVXH()vr)3Fr%U9+ zgk&N2lCF_K3(De%=zl%Va#)Hzk0SFkymEI_G*It1jN@D`0wC|6SqNAH@wm-2WnCAAg~7d(dcNiI9zD11;_j{3F^;@ z8sjNRF0tzmXIk~b5ZxCdOE1Z>%E(i4D~yuos3Eki8S%M3!#ViGV$SSaDak`ycq4&% zI=UiKsJ@WmFro7^X@%uLxZ=B;fQ%3bkF>AoXN{$a(YJRW&t$WN3dlex!f6f1AWdnV zq7>5pFXp%=KS;KOgDQ*6yb1=Obg{;%sX(xn$nZZyhLko$OB%OhvAnqsWr;y}Bz*&X zH<`M1KjML+$n*LDbq3+)BXN<8MlYJRW zOo(I&O^8T#USwwwDQgXq#*(R|cx7KQjdilk$dYaBV+$#gJrr3cYqC@H{k;Fc_m}h2 zbDrxt_kEx1xz4$s>n0}Xd&vi}_JC?g#oqjOqtl%DQ^&U2jtjdvhnKk7FH9itn&n8+ zJo=Z3^3NqpPca6OEqdJ0yYlp_`3T{^;rHe&D2mUS8+KlsFKCYX-DXVRtcF}_yk?y~ z*>JMqiiWG$E-}J2V?3O~2~bTj=<2{nb^W_kKGt6mmHuPPeUjB#5e}{ixx|rAvx4e) zmj@jNhM`)`=OnIaKUX>Qh3907B#~2oBO5e4BTI^EixtLRy$UM!Lw_16S%7f^dA$Dt zmiN)%3xRg;Lwz;r?N_X zNGRS=Nk#?OBEs@p_N!>c&hzYiL;5m;gFoyh!<&$8pMfPMP*gB$_Ke~D22 zKc)>tjYcBlQP&9=9~mY1N|sGtm6fh)*^*j+Dk5!p_hxCgiT2C&pz|7iSyRIj!3@2y zo{Ed($nC9#L^_>5AeZTT;h^Qw|h3%~zez?08A z4B|%}HL1&~>$yblVv3BJAF}Fwwbdr1LMHawmfZBd=rk|9C1={NF6l0~X#m$= z$BL$-%lywtgN{ab$8;r?twc-lsDIq2|0bgBuIn zl}tuOm}@B%TT*b{Rqdo$2Z^GUKThs9nj!x_gE5i2bBR#kA~BH^~LcNOK*u;!u+DLn0p- z?5pPs{z@oAtr(j?QVJM?jFM*o?oeW;9|zK*EVKr1YHii(Y=nB^7$(n%0fpDE0C1lunj-a!?$m# zRAHglXz>#~7I@}hI5#WV0Rc#mm$g;71c&`lUCw85M!QebGH#93L1XHEv5gx$OX zb#=o6U(0Q=vrQ4Q0b;!2!FqOKnfSv^#sx(GkAfK6$Zp(I^|fK1RfRiOtm4Es&Vv3W z48AGN(9iO{#g08om*p?o23piA=ejmslX5>PPdQL9d7Gmzhd|-P<)5KoxU-*HTsafRtv4%10MRl^oK= zW2^4_~7F?f6DI+MYJo=+_yB|t#IU_J8GO_uv@sCd}3yecEOFH zF2SOoc#<0!h6CnJ$fv@)8%j90%gi`o`7sP<_M5&3dGufgp2+kGfpnXx*?TmGXql`Z zK-mK5TJ{;#bn?y^nE1=PN};IlEYt z-BHe>`zH(tBjD~}*ad$cldWkY*yGoYa*?&gywKGZ*~xDLnP}ntN+K?$43gz*Len+b z;Z4J3=$lM<1SS;Cm{_P9+u^}_W)V{dDNmsU^k5>$ncjuS~W07QO~BbZYhP!Wj@Fw_q9yk)?=tjyUU>;!iv~ezvMD3et4m<8rJB*+hxi9 zOa=%1VW0(nC6IGmRwjs!n8*+*{42-^Fu|55t4mJ0=P8Ull#YHBQp5Y(eg}BKNIMJ0 z8V0i(4nU0c3dTn5qgZAmfN=oJ-`HJ4*zS1hXi`maIL!~;GK&KCp|ZWok;pGT>oCrva0XmkI$3-QGAjqraem~wa6j_a*bHt^gF>m; zgE~Om`SF>_bgvsE=>c|epIZJk%Vz^$s}|mrhIKn%@7_9o3N!96qf{(~?2fvjx_{#U z?JHs%{cF2opyVHUX4`R5Q^uW33xFIbRu2EWMJ-Els>lx}Y6`});R8U3PPFldzy_0& zT4s&kobB9;9#6|AO^JA+T))d5z#k|r2#8XQ?vagSUS>|iVj_3N0V0EYNcoQOnK){l z*5~dT@>Zu(O2tD_nm_OtDzPY9;u=^!9OGlvd2Z}Glf#!}grVf+1qNWdMa|YN z%r8MU3s=4n_wVWoC46Ot5|ut8lL!&4*~Cj!1(*AF)=(`_$BYI7cxMQ48N^gxpy7I_ zSdw<0D&*iaw`Sp8q)~1fw9;q3TD958x852C{9&URgCh6_a%EC?M1GxEYQB;R%1^e` zN_)obR!Q%iM%As!e|HnW4;JlDQKrDye#{WKVaz%z=X78~Feb9r#on!jKX$ad2~xAi zM<=NTx~jb66DlKxL@9PaA{b_afJ_jZlhnbTq*Kfwl7B$1bPOc}7P*kmXU z14}x`!zEpTP*lh}rn~0pT4`dpjlMEkf~iGl=5m%7;0H3}NF$%v-8?Q|s!Uv$WPjg( z+HF>|ZO=bAcXf{(-+1J^n}4*z^_@mUU}d0$+I&ah2t@XYXJNxKlBFXhqA*|JeN2Sw zx5C}g8$jEn((T{E?AXLp)C})MiSF2P{p@Fg<~maRtHGidF=9gSK~?|r&W)9=U#dl@ z-OR+Z5a%SLd&6Z2cZ6_(!M2p%;!B>Ujh4fzfvJjEj?CL4cJ;{$nR{ig%3E;Jzi2VNm^~p*Q{zrsqZjq^T;;iJYC)?L!{&-#a9vvf^r1In1`EyG zypR3?eJUjPRI1mR+6ncLX>~W@axRUspViH;TfdCMwLZZ<#4!wR>(9Nol@$!J%zp~D zJl1s2TcI=657x0Uo)ck7t?F&91n9)!XX5&}GyZlN?f9rmJ1&p&peEYjrQnCCe6NJu zkgtK}^G3L}Q^8VO`zw^m!`;!SRmK50gN`LP>=J)U~Ti@(v-%DUi zIsWKyJgW)QLwP8*nDomJBHqP2f5p)|?!T$8MaYX--UME`W>p`HGH*hqHhCa|n!Gbi zL(BC%)geM0@7`cST}au1`H&RQ0k>6yarWXW-QUd*`p_3Bm&W@Tp*eN^r45F!j<}xO z>q(*1+!ImgdQV9Tt%v-3OmkG(w6}iivx}5KnY~#0#?D8wSUXx)Nc6>e<9W?;iS};C zH`uXv5DvY7%vpuV0z}IV_7EH5nfNTMUr#oBi&KTqX}k9I_eU#&cmdb!>SG1XN`0<~ zrd|djclMo&%FdZqBrngNb@uO0iavJSbwztTTem*$6*Zp57%EifHTj4@ihzmay|FQ{{!khsA>QJ literal 14310 zcmXY21yoy2uugDycPmm{N^yd_Q`}vP7YOd|PH`>8wLo!q*HRn`6f5rV?*HD)IX5{c zxpy-=`|Zxo_svGBD$Agwkf4A-AaprdNp;|J21vifLD%hMrT$lZeEex|7Xe0mqFAZArC{!qp)zJmbab@utqA`6 z61Z~|e!k$IbXNT?PvGuuzT7G514$8e!}lsR>%nURMm+~pde``@(!O=ISt0%B93;Ez za-qRi4n0Q>zQ2#2^_y08QOl3jT*!Ir5@<8VrFx(6f9sP|H8ttjftN;wrX>jP4BcG1;MfU5x^L`zc09u!bDBt#+ll=7@ zB;}A$BKgu}V?#qfHvm`~pt%wG2y{MOc%B!8I`p|pc zO#?sq!Zd&j8UPmvY4RQnfo>!6{a}GFV!}g@qu<3Wu$07X(O`vikNW$~q!ngF23Ls2 z53p8js<-B_Qd?xX6rtq43Mdz(jOg2QXx#Wng_9^1^^~KqFNq{Kvb@Ap9}bf&xFA-C z5+#cQ`#v$A=kd0O=agATcleBaxXf_(dnqbQz|cL9R&&Ni1omTs+6~YApmk)MCghxj z1}mq&IU>1nEiF=q=PI`%jQbyRd=hVI83Sm{E-4uTc#w;NNwEW)C(C`xvWzY_%`_MmO zD&g-sEaE)}6(&g)y-N&rNy;5@+{M`}!{60Y8wMgF5;HmO#B~hG`W$;7xLG*yF((rq zxP6I#r#o`B3FppK{v(q1!C+YLFSfySDcHyoW!}EfzuCB1B|C5+oP}dtocnwkcNy1EZ6#5JX4=ePl&cu~0tMnt&79+I4%PaK>VqFx;r!QdNmnxlEqdU-QR%Nmu{aWP zJxwXvt5fFTCOVgB)Zq z%H0U=9q7Y0lu&1kc4zYT3*lHA@XJfoK>3WFM&WWf2u6^+wCm8##D$x@Gkw+t^HoO( z4pxDRqg;$5S=t^k22H5^V3V0Qfy%Ogl8I%LD$52=7)J>Ki9Ej1HyEi_ujELlz8$-+?cdD1Zxi02kW0 zaY=caFq4~s^R?zxcc3Z0X|az}Aww<{P$>6rk+5Di5J7$kWor0{Q&>+DWSBH^Gf`SP zT{4}IOFh-hB7xwBdewq%de)q6QvxorV(()2>@j8i!kj)=^hN zl_N{$9xTHHA;V&Zx#tX&1pOO;v^NiOP#_UK@J;;lp+OOhOOO2mlMdxM;Qv-mWG+^vzox|8t`w| z=gPlM3)y6G*hfV1WwuMe>bO-vP9g`h5BqgO9x{ROBD;aPl>XDmvt(3PUxt|4RFRpK z5OEtRz{(Oa_W_!Z4XHf#h;Z-~71XM7wlF*L!-#h_Uy2tGuy-rAZ)4{qE~feNkp}qf zgvBtLkFPI~I7%C=OHZfPZz$j>L9)rb;l z@J^dxncy52;wmHg=wC3|Xn6jPYCR7xc}~D0wNjoYxmoRh_zh=6@8coM1UQIa_z*1)cZPw4v40qoZQp-uy#DLv=oP zX9b3vzFA2r8}|_AO8W1(OMG__0{1AUD&Z%&7-(>s+Z-X6Sv}G5QguIbZ3mYa--?09 z;wNw?n=yAag4%m#w$$-YZ{(ZJUcwHfzu&!gykNjG)e}!=q8xy2_KS=ULsQwv45NK! zVqqD8#S{vRjg4(Q6HM_F&tihNIQns<%DVjE$cv33ET>Dvc^#{z&#u&&9RgXO?ZLuebczKv#;! zCS|2lIa37Bp#3RWj0$V3=I2>o40{(J^LD|EUH?!2;Z&HS*>7*V%{v1)wHaUP85mcX z%q!K}Ntr*IzJD%++btJ;VQO*OjJL1t{GvR3cy@OC-~pe^bV?N`z0QKCr?Tom)4u%A z3mi2k&eIgh0^rGI#Di+&3lrsy-r+}zwBkDQtswtPbkj!Y^l`{f!# zLseC0M;DiifDa!({-G4{W$Wxsgv*(NX%HMyXhArVwY105dUHg?+=@6Sy8n@slS76x zU7%PI8ToKm#qahfR;7kn#|t@9y(0EkooWBDqA1(mpO)>BBz))giBi8xVHlj#dR9U8 zRo%`iBdlj8%_tRn^qa%T>{nsLLwTNld&WHLyfbPzv2W62m6q=Nsdxnk z#{P==5!Lidx3bcr_qlUl%BX!xjywA?jv>FU^mJDa0zQT9Kw8RRHq>7B zb~DXw0(oqBrOQunsm2ghWV2i1VmN{F?)U;0%*j{FEUxazAJ3)KSWomuhklkDi?5h*MTLDS5ma_Nk1sNZYzZ#$maGRyiXBzjG@(G__fuyBl(^A>s&{jF+J%5| zv#7nD1XK806#_U_4#N2ANAxznk%;U$Y$z#{K*O07mADqx6LjACqwP<`HFV#C6Q*wx z8JVP_qGF}V7B?^8)f*2F5AON7v$L~Kr?2}oPai_kG!_6MI(U`LS~+Mo*CSyrw>pPE zllqxy z^&rnDn4XA@AUY7~`1lwTCrm8KlVRqX&!kZFH&;i9@=R}UDxNSh*)Iq2U+#9}@ag1t z%KUOEw0DXT)>hQoLTprY^z=BC=8NAyi3pZWT7A`?;rI<3%65Nqb93%pJ=!+dNtB>W z7f3O-e-S7ZBgBntcyt~wOG_p$AU2zlGH8=%TEm+z8kLYReEMTkIo#2YiA=iKWrH); zS%uT3xAyyY=!U)0Evpgx{{38MPR2nN<3913M<0O#YCO=TSt^4IzV3^D%2zC>t_OO} z_h~AVOk+IIi$Ov;-g93a4j@WaekCC#HFm2_Vu9s)8-GbYtr{LgrxnSIN^PW9)!jYX z?%-yssA~&R3F)C)wj5i|@!atCx?Qy%P1QEGSZm;iUNai`-F(8a%y+_a>CMzx$XEKx z>sW|JbN36s+Y{4SZsrspH%UH=+Q6J`c&_-JLGL&5|$XUA1vFOC+rgoc&xT{dFT&pMaEBKwyD;plX0>2nla;jTlQ{!fn2M=Ak*=K*g% zBm0-$ly1~}CT-5gv){jex9)7&b8u!a+vYHXU>=NF2>g3+_rN{(LUMGwRWKk49sS$v zazyX8zZ1hwZ|U*5{fK@i@hRl*U%Q2cg+!iIfb)6W%S5F{91qinEZE%~4Gl>rBw9S< zMP5$exl1jESyt}d~jo?hf`z^32b!}UGtJH+w9(0UrI#~Ei*ii&6z(AVE?(}k_A zE9Z@mj7HF-ch46I0ipe3gapRj{=zk_J1E^b_JwdrhKi4ytBuwP)m>e$@9v`A{1N{h zwUN6H=_W+h(a?rGaQ%%LP5C4)XiZ*`1uUwgqWvk`LyDD!Ps#Q5oI($KDJ%8n5kBi- zghsLx`~mf<>WT)6-cJBbp|htk1NfkZ@e#B4@l?UH7!MDMpO?1NETGk_Eg{z!N3!D< zWg8gtgS%b(0Bg7dw9u35xq)1vNdnM8iu7Eje*u?#sZ~%^q*HDaZC?5z4ZzhSA%ndS z4&$M&7(|(9nWY%QShCnuN0 z`n9&UeypypUgx;R+x;XM#8uDM{p`9~j<49)^dotHJVO*A@HL&g7F={FP#trj@{dzm zeQUiqRWJ&pkKkA1O-|vOf8O1UQ$$0lIExffio|}F@ROV#MXcPH$ z?$$kxAF@B#KT}u;R@SVyIO>1sw1!i?C(_013w9@?8$bKaLQi34zC$g*^}F&(%NEO6 zQzD-^6}HQMnGJ{h$J*)HjSxjblWegsW&rLC8Ov_r_20jLjUS$Ptnm|p9fK%r0j+4; z57^mjL&lISh8>DC;eB$B69$h4XxE3qU4T&zUpDeV@4g>or%D-x@qhie>6mqD959ck74(h?S0BA0}YQ18d?hr6}%}y{%ZNJ^-(?=Op~; z#2-UNh)jH9>RXmvPJ(Y!8(uhyW|sFpyvv)AaNeljHj^Fx+RC z!`@c->W1C^FUKHmG2w_atkdsMnzY+l!CV8havQ8-Gu)<8t{#V*2Pwp4h?ayXsi5Z> zo!guta>TA~iv#iJpQkN>#)QF%As@2WgU&V_Y^qm#E*O}M_ijJfFWq}ts)-l4>D)kCqJJ@MG2$69ph0jzwI8ry1u8D@CyinC$oT?7S*Z}Eg zYs}PWLqr4u@)w}#!{cMx;KxO6W2H6~3k$laJjAt+C{0mmCRnfs=OJYbh}HMh&e`#> zj;jrpjqKCh41OK{FOS`@_sPP$iCm46G^EMNk8(l-1f>!gEV+4vMVRZ#8infUenP+k zL^tBOHF^=)k&U-Tw{gfijqQ&^ z-RHHII5yp}2|o8pTsf6x7$teW9Em!~iy2DN?D@|U)g%I6VG%JBO$|~;c~1Q^3|x`1 z6HRbq1#~Ke)wWpALcc&@P;m+*sGavR0{aOx3=IwUE3YPWAwV45pzD$~02inxi7(6X z$zk683M=_r#M*+6fQ)&FK0y|lm7JLwS)K=t&ZJk!U_-y%_o@fhr{s37MUEQOF*M)3 zB$;4>Zx;Xk*(hwFjb>1iJ1f*D#nyWL{=>{2|9*^vCNN!%bF8Oe<`xz#s;jFz?;I}4M3lL;!fy_;J-E96Of+;sG%K=fZdR)99pJ}fM( zq%(s8UrsEL{NrdF`!#RY+VjFyPpE_vtqPMM!MQ+QnE)+_g9Z^{4^;k&Sa^=w*yuxB_*Z!U%!3{_9Qr)Jfz4IeS#io4oj_Kqhq`HCUub|Ke!v$1-$v=kc+O#rlCej?%dhY zxxKUTsFPG1nfoFp3%7@gh9S?vM0N27#*fpJyaX;Vy{!pt*}!9_mX9uC#J5RyjknW2Dm3dCvZYU zSW?0kvI9!o2un}*%`AYhr^CQT1aZF=-Nt^atn@Kt%b2!hT(pK!|MclbBv3-<+6{>_ z8toMfWc9rpOk(8|KW>Z-k>Fr(xc_+q9ocf`8!_n}XYUrW?Ax|*_|=5m*4F0V+46wJ z1IGS^Z5t=0Zj86J2MfJc zUq#WKCfhoB<;P2&&`*_G4^_0uqDR20m!>T8ay_rxSzA&9_v5##g6tzXTkx+KRfz32 z9vvpp?+YxHTxDthCBu7)&Q052y4s9*$M4_2w-OdPyK?F-EBoUuSsIk@@(!gA*A_!0 z2eu1y;-Q$Ut(M>8FCOtw?vZR-%*ly^x)<95vK@P0tJoZws@+M*NGhg_NU`!}DZnWBHQz%*@6))$BWN;EM0xAF+B4Mph#S??J?K+&viwPmes*n^HGDL9iBf zCk|mDu46wwughN!isu&G((DO>Ws`(VLY?^#w=RONxUgFGby--Y=5NJ|(>qXOS`;lZhmXyMEyBdVM@jJh71E-})~`?t4w8^Kwy) z<+KACjs!F^TS-;FT24_iWF+=l(nR}j7U#;Vd z)IT3=b&}A}1PUKFa6DKfgHkJci!~7u?a%k9h7Rri^{y`|;;xNDoQbV}+oJ=LdApL}|77o@C= z;~aed)XpbrMtt1x3gHPWxbliQH4nKBCew{9 z*-_PTyn~`1VrwKcc4ZrhI^!MsZ{D0O0%O2!SHHi^Dfyr9*x*DGFKwc()b;q6nM*M7 zvA$x_?$BMJJHN5HIn9Ps{_7-sn79~BZegaa5V;s(BA<5BnU?^AeJHXtd)cIj_UCjA zW|N@MjV~vrJz{sE0Dzv}tXxUDQAXm)1(kX7C_ZVFX%!TlZ850i(P1A0BxaJu)#LcH zoxMFRzxoxw$bM=B6gpuMD#vcsa^00?%=D+T9-dQqV*=zD|)W!3BLun2&^n)~$ z2_^{i9~sGXOAsF_S=k&4mWJ@`mD+G%MiPTlhuomboeFNwHb(< zVpVR!mwf;JmpO3JL|B%L-!;@7TG}+`HZA;-{VIlQGY|T=f|!9!S=!c?sq5|KeEQ*~ zm!1xeZcJPbSsfjU9e>K|=Ni<+YgrIG!|5@|Z>4bjx+`1j^O-{QK8XARf zUG$nLRiTEtt;)9F30rvw>nj)@vCF{$d7>o2n>}~Y2^^C79l@s`uXRZOcuy>^%2@t- zRGv={pKlDXFUgvG_^DWGR==il1rIzn{$p4r(FVOQxZi!_*Ksfl2hR{Aj>01RbFAM= zpr0wzMwlOwlkt4|JLK)$>VL+{4nv>^`yMa)T;(9f*B(9;{T+)_=M4dN>M&&hS-#(G z)-sW(WxVkHR)`x#g)25Lu7qnN;~Q-bvKDZ=;^fyLy@okDpvt&ZU{!U)WVtmnp zAN-CzM{jPFWep9NAKDDq@=kynkGi_GQ@Z2y_Wn)xc_q3-&+9`qdGy_{PF-2c^$)%x zd0sonEJhtG*2|P*Q-f_3`Akk96HzBz2 z!5tnJaCcA2hGQrSw*{F)epvfYX?7toP=O0dN zizY2w`>O@4Vqff!dBhQ^><#TjMP}loM9ProiD-Og@$V=*zQ|Avg0D!+96lr^u(1fl z3J52PHoJYDdvdiIW?q?JIC*r?88VruLx#bp0lys39v$(c6uC*j}2IFFh zViOX|K+DH18cd9%Rgjs$*sXuoW<>p^Fv-7CV|zpgTUnj812pyyX-nhA4TZ^UyYY9; z?}BOarTT1q;0xSTjV_DPWE11?Y2+wSA*ybzebDoy8JwhznKa6SvYxE$WswX7Z6pG$ zsA2GgHFFL3^zA@XTYK{a+6$Q8di%@1-|q9U15y+~R-L7Kwx8*xr(FP{g*JDPa`e((jSl#~?Rx=3ne(nLfeP9k0grubJK zU4euzZqt~$Cl%k^{-!e6YQZi|D3#+MUS}VsYZ)0S>y@)kyqRI?A_esvAu-{`1Uq@! zC+b`wnMK&<_mitl+k@e*$*{&S>vayX*>D>Q5sw2FZ?l(8ff%(8lo<^mBMrwQXOXe+ z*7sZdWzBTIwZO$y^F)qZL1XbOMY<@M_a56y{({Vg@YN<_y}toq41V%~w=+4ZQvg)X zVw~l$z-sId^nKU%dlk7W(mG}eS&KV2BdYqNJnX-p=YrG&&`_m0fzA_|iKD${5?oL* zdS$heR@%Q+(3!!T&k;tIN|v2j=UI))rgkvyC7MTTrKP3g>Fma@_R0`GE5(tL%sS$7 zG41ag%(Y(xZ5cjlk=R~(3XC+$25r*Fo=G5OhGgR}i!nDoG?^sult?Eo*x$x6CH-3L@LtZ0dfq!Bbbw-S}RwlN%lpH8c=4l2qH z1wRszHSPh~=esnWvXD8B{D4<}?}6cA+@Ob1760Is6`g!zl@WL(L&={LA}SxAt0>Tw z%b7i^&yNKM;(vGcNwuxAK{g|S3Y1&pH_6U1G z3M4zx5FU=O;=l_?VzQ-~bx~xN1axPgYI0am3d25BjYmfSTX7Q}==Vcryl6@Se0(Jv zxKW_o%H`jdnC7QXlkFbCsACHN1Dx=0gf<~@PW-&<=`1Hd)@#ypH7%OpalDj-P=ts+3^~yWs~TV}BD20HjkW6zc1L z0#HzMkn3JV%7N-18_@tgE82*YnmEzxirriDSx#_|<|q1vL{k}7>^mRzO(ueTSN2~H zG}kxp)Qn!&)><3|e>62+GXSpQKcemfqU!&BHZ5Ca;DT<63bBM&uV1BDS?MM$M;x8w>gShAPMxJM^BbMZn}Unm{OC9^4x3%% zlmX8!km-u$N4fQXQ>jRe`7)3+RFGjhz z18zf(Fo2<>YV^7LJO^UTZ2Ivd#mpN}o?7pBV&q=f%ID>haV7M8R3jsF*@a%iwIy>| zsZ!-y{!%&j7`B?W8TcF4NH-RHH1xZ{;7BsA<#APu!;cND)te)FhoXz$BIU}2&^7WP zT}TX>ZO58$VNPuh6JV7~s(W$vAj`^%AtUamex3YdVl3~4+pqk?G)qUibNMrj0*M25 zY>5Ac|Dnv6xBQmV#$3JA?&HTN(lYl~J}@$l{*TY^kORrCB)3dDO}^^v!dcLf^CHty zanjllIQeSLmpuG+h&ae`r*v!C*0A&W^a&q>93?BAXzG7n z2*3TGPIcN`-_hY9&oaiv#fiv~>}7`T`4=pInEqWX*3e8+yPm^9h-tr&ts55$l+388 zW)~F}2JH!}VLbQ>?6~H@&k`MnSsTeVj0TRVP4jGbP*!!CwM6`Z11c)yI2w$+R0zxo zT|obYS1&&`{>>Z9(jnVU&=yI*%PGe*f78ie*_9oap?sd7fx7{r^WT>=XHF zl`f{=UJEn2?tRw`Fem?eRE6#*nOes(ebRcmaK3~a3{a3EyE1zXSF0p7I_iDJ&%;3V zU;AS}e?*mH#Yh2P9E3QBigIqu2iXf=@t)2+I~f*_E^JtEP1@IR{CBfTj%T}E3e#n% zUa{@vU?D$l4DEANwkkK@ruP4ta)E*e^KLGg%$PizyPmHvKNMWtuJQ6sPXY=(1m#>W z7V?9E!Vj}>a|KfQx5ESpH+q6$@gAp-P#~lbz`aj1_?xinN>3o8b2-Z3w>UZ3QZ}W0 zWg-!>p>AADDcU^4;0*L4UFgB0QLlXd^y1E&4>txV!T|!`RwjZGl`;-4ZgFf>luHIy zZ8d8Rh{I3r!g-ht6mAZxMB6VxRqnA0UY`h|mJZy2 z17BazT$jMKFL3J6Ue_HL1^)4s%$Jj~Qx~1HG#tS@kwL(KP_ZI3dWz0SH(sqj#-*TNGsIWqPj>cj?!GyWvfdEiNOu4$>MIqL=F&Cc0{g*~L5 zA1wt)=_zMFUkCT5$l!G{1-Y9QtGQ#qm5E(3fYPms_EP*sSVI)bfXN|uNO`BqVuCvd zv)z8IGRgtM1<_trndVhQ^xA)wn~*W~#d*X@E=W)jcQWI8+?kdzHe;DZ`%+JE%gE}m z6H=FO8rJxM{N90S=Gi!Mel)TyanxPa;E}C?hJl@e9UWad->;S|v;axgFjrY$z3(rV{MiJ}3M)t;Q?P5wZy0e3G{dcDO7n}3slDXLMrB$;#*W@Qv)D$=?Xs$F(8eTcyGIQ~IWgD%Gn&E>F9y#o>cR-7spE;Rur<_E~Pu)e0I z#&y1|@8D~8c55<|KMf;&x;hg!A%VOZ38_+uk`jH4#=b9M&xcpxV-7cMN{jXVRnKSe zlKJJ%=VBV{$DNeI1QkiA;DfdVT?$;O#22z6v6bTK9)fjrfIh!Hq__l~KzuNqT{&kA zKs@YV6^1ZLGjTgR%(=NHS-DvWnnP)NM#qbHINqmQdCE5??co$3nuikqgm=s7*#Kd*+j_weKrZjMeLeHEoiJm>zuDRU` zh~ggr^knneWU!Nn}AQt=0Id6Hk; z4bJqse|V$H`stT?NS0yreYvaZ9YF!fw+N}{3#yXRU!C7?exl35BDC%+!jDMGT^DN# zN9FGd#5t#;$h}5UgQ?q-Gr15>C6=nLUszle9<+_!!oi_m@_L^-R>_Qty7_g|C%m|5 z-7^5X5V_ARi?h9_LW%2vByD3X_IvUktqBv{%SYXO1&;e&O#Ll_cfC`Wv1u+l_#RI< zQ5Kly0;P`%TXaQN(heOg~>V&L{d+ZDA%eq-UKo#1)$rkjSm=nzAE2r z5--RyKhxfXoGVU3^ab{5XGlyL1+26foG)4HZvN zG@&I3h0fnK5lIjcrg*XxPy1(gK3_TN`&VYnxP;C|j$~0rT$0f|*#=OzM^NbE-1T5D z%Csnt)n!sx3N#b(8G&+G3W~Q_B#StA6jZZ=p#wuu`DrAMXm{T@#S;ku4Dme@{Njmk zCtrh3z6O>o)~o{&Htx+6kn*)$NNBH-biu^aYtWUq z(G>4rCEKr#tO>!x8A@%W@6g)Xs%2Hq!y#Mbb@9R2@GDWi&!{jhZvzQ1D9nMuPoOS+ z+cj{9nx5X{jJOIavbFf)Kz5Jnbe5Bu#(XE-z$j&iaP%c9W59OoT0~|N#D*(N2kz={ zs(|)nH!_+_g1)#ZH2xk>ZTG#6WN#qa3BxZM{NWxq`*#$H255k6Ky?hw*hSA6`c_fl zT@Ua%E5Ez3;~`kQFmrC#$Nlvc_Uy3#yzhd-6UYuuIwgIBZZC-`dwOBJbfurL(FfhH z{YkjE+9OrOveY`{t{sGw&51YO1@{iO4)Ki=!Z5#q=m_Hi)_j0`>?;t2j);vv%BUif z;wpTZdLQLsGvZ()DCdxYudn^Pt;BZ}Rin$4F8h{R`HxT2z`uc&aMXIQOvwgA5%{&) zFW52MiN!$!EXgx}Px~e1!EMp;#&kY65oDho95j~!qD%YJr`+aK4jCJ4UJ^;q>w@Lf zvDfg|M`S^@DGxu+7aR3Cx#;%?advj&1~L-m zJqCP9&TW3migV*`Z$#)Qa>3>Jf)g9D6Ki28P@iX(uso)hic8Dp1F< zeF;(n8Po8A*~^T{De(J)Z2nqLl@Vv3yoSlGwq0aeOg4ymI(KIkTeur-=J-yp9z?qe)it6gq-wl@I z0D-_I{|T<5kwD9uH3yf1GWXp5*8eOgJf*q0IRoK|+r{}Fug&0WpNDKMTC@(Xc)9K8 zy`lByMn!1fnY)1KYP(0Je1)c~WilUuh<&Q8^OE?L9Q^xK*Y@M$`6D6TDCZ^@l8{|} zxmmNw)mng$hYBii+&ZqedxWT0dnV#LG4zC%+kzcK+-??vEHT>Q-T8zu|s_1IbA#OV)^+1pg1OmmZn`