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 b50463c2356301a1b47a0bf4f50dc1f121d363a1..d658b7502185f1f7c938d510e2f8404fdaa66bb6 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.util.Locale @@ -28,8 +34,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") @@ -80,7 +86,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 date, // Paper "Specification-Title" to "Bukkit", "Specification-Version" to project.version, @@ -105,6 +111,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(DEOBF_NAMESPACE) + toNamespace.set(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 { @@ -176,7 +198,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 e873b23527cc4e56580c3c7dc5b52ecc3f2a9e31..6317988df19da315c057a9cbc55ff77006f45912 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 cfe293881f68c8db337c3a48948362bb7b3e3522..b5728243f01aa6ea75cb42af453fd9348a5f438b 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 5fdaefc128956581be4bb9b34199fd6410563991..8203524862c309bd52fd3a8a47b219aca570f0b1 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..a2f52a68420a2d23d8f54f61f7aeede41567dc0f --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java @@ -0,0 +1,645 @@ +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.item.Item; +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.level.material.FlowingFluid; +import net.minecraft.world.level.material.FluidState; +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.EntityCollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.List; +import java.util.Optional; +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 (checkBorder) { + 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 = null; + } 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 = new LazyEntityCollisionContext(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 != null && 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); + } + } + + public static final class LazyEntityCollisionContext extends EntityCollisionContext { + + private CollisionContext delegate; + private final Entity entity; + + public LazyEntityCollisionContext(final Entity entity) { + super(false, 0.0, null, null, null, Optional.ofNullable(entity)); + this.entity = entity; + } + + public CollisionContext getDelegate() { + return this.delegate == null ? this.delegate = (this.entity == null ? CollisionContext.empty() : CollisionContext.of(this.entity)) : this.delegate; + } + + @Override + public boolean isDescending() { + return this.getDelegate().isDescending(); + } + + @Override + public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { + return this.getDelegate().isAbove(shape, pos, defaultValue); + } + + @Override + public boolean hasItemOnFeet(final Item item) { + return this.getDelegate().hasItemOnFeet(item); + } + + @Override + public boolean isHoldingItem(final Item item) { + return this.getDelegate().isHoldingItem(item); + } + + @Override + public boolean canStandOnFluid(final FluidState state, final FlowingFluid fluid) { + return this.getDelegate().canStandOnFluid(state, fluid); + } + } + + 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/PoiAccess.java b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..a7ae9f0f4e56138465b0d8913d3cea9d5e9b56f2 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java @@ -0,0 +1,748 @@ +package com.tuinity.tuinity.util; + +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Provides optimised access to POI data. All returned values will be identical to vanilla. + */ +public final class PoiAccess { + + protected static double clamp(final double val, final double min, final double max) { + return (val < min ? min : (val > max ? max : val)); + } + + protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, + final double boxMaxX, final double boxMaxY, final double boxMaxZ, + + final double circleX, final double circleY, final double circleZ) { + // is the circle center inside the box? + if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { + return 0.0; + } + + final double boxWidthX = (boxMaxX - boxMinX) / 2.0; + final double boxWidthY = (boxMaxY - boxMinY) / 2.0; + final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; + + final double boxCenterX = (boxMinX + boxMaxX) / 2.0; + final double boxCenterY = (boxMinY + boxMaxY) / 2.0; + final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; + + double centerDiffX = circleX - boxCenterX; + double centerDiffY = circleY - boxCenterY; + double centerDiffZ = circleZ - boxCenterZ; + + centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); + centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); + centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); + + return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); + } + + + // key is: + // upper 32 bits: + // upper 16 bits: max y section + // lower 16 bits: min y section + // lower 32 bits: + // upper 16 bits: section + // lower 16 bits: radius + protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { + return ( + (maxSection & 0xFFFFL) << (64 - 16) + | (minSection & 0xFFFFL) << (64 - 32) + | (section & 0xFFFFL) << (64 - 48) + | (radius & 0xFFFFL) << (64 - 64) + ); + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findClosestPoiDataRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load + ); + + return ret == null ? null : ret.getPos(); + } + + // only includes x/z axis + // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. + public static void findClosestPoiDataPositions(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load, + final Set ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findClosestPoiDataRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(record.getPos()); + } + } + + // only includes x/z axis + // finds the closest poi data by distance. + public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findClosestPoiDataRecords( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, ret + ); + return ret.isEmpty() ? null : ret.get(0); + } + + // only includes x/z axis + // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. + public static void findClosestPoiDataRecords(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load, + final List ret) { + final Predicate occupancyFilter = occupancy.getTest(); + + final List closestRecords = new ArrayList<>(); + double closestDistanceSquared = maxDistance * maxDistance; + + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = WorldUtil.getMinSection(poiStorage.world); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = WorldUtil.getMaxSection(poiStorage.world); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + final int centerX = sourcePosition.getX() >> 4; + final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); + final int centerZ = sourcePosition.getZ() >> 4; + + final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ)); + final LongOpenHashSet seen = new LongOpenHashSet(); + + while (!queue.isEmpty()) { + final long key = queue.dequeueLong(); + final int sectionX = CoordinateUtils.getChunkSectionX(key); + final int sectionY = CoordinateUtils.getChunkSectionY(key); + final int sectionZ = CoordinateUtils.getChunkSectionZ(key); + + if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { + // out of bound chunk + continue; + } + + final double sectionDistanceSquared = getSmallestDistanceSquared( + (sectionX << 4) + 0.5, + (sectionY << 4) + 0.5, + (sectionZ << 4) + 0.5, + (sectionX << 4) + 15.5, + (sectionY << 4) + 15.5, + (sectionZ << 4) + 15.5, + (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() + ); + if (sectionDistanceSquared > closestDistanceSquared) { + continue; + } + + // queue all neighbours + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many + // values are set. we only care about cardinal neighbours, so, we only care if one value is set + if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { + continue; + } + + final int neighbourX = sectionX + dx; + final int neighbourY = sectionY + dy; + final int neighbourZ = sectionZ + dz; + + final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); + if (seen.add(neighbourKey)) { + queue.enqueue(neighbourKey); + } + } + } + } + + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); + + if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { + continue; + } + + final PoiSection poiSection = poiSectionOptional.orElse(null); + + final Map> sectionData = poiSection.getData(); + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! + final double dataRange = poiPosition.distSqr(sourcePosition); + + if (dataRange > closestDistanceSquared) { + // out of range for distance check + continue; + } + + if (positionPredicate != null && !positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + if (dataRange < closestDistanceSquared) { + closestRecords.clear(); + closestDistanceSquared = dataRange; + } + closestRecords.add(poiData); + } + } + } + + // uh oh! we might have multiple records that match the distance sorting! + // we need to re-order our results by the way vanilla would have iterated over them. + closestRecords.sort((record1, record2) -> { + // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section + // is fine and should be preserved (this sort is stable so we're good there) + // but they iterate sections by x then by z (like the following) + // for (int x = -dx; x <= dx; ++x) + // for (int z = -dz; z <= dz; ++z) + // .... + // so we need to reorder such that records with lower chunk z, then lower chunk x come first + final BlockPos pos1 = record1.getPos(); + final BlockPos pos2 = record2.getPos(); + + final int cx1 = pos1.getX() >> 4; + final int cz1 = pos1.getZ() >> 4; + + final int cx2 = pos2.getX() >> 4; + final int cz2 = pos2.getZ() >> 4; + + if (cz2 != cz1) { + // want smaller z + return Integer.compare(cz1, cz2); + } + + if (cx2 != cx1) { + // want smaller x + return Integer.compare(cx1, cx2); + } + + // same chunk + // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y + // so now we just compare section y, wanting smaller y + + return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); + }); + + // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). + ret.addAll(closestRecords); + } + + // finds the closest poi entry pos. + public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findNearestPoiRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load + ); + return ret == null ? null : ret.getPos(); + } + + // finds the closest `max` poi entry positions. + public static void findNearestPoiPositions(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findNearestPoiRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, max, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(record.getPos()); + } + } + + // finds the closest poi entry. + public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findNearestPoiRecords( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, + 1, ret + ); + return ret.isEmpty() ? null : ret.get(0); + } + + // finds the closest `max` poi entries. + public static void findNearestPoiRecords(final PoiManager poiStorage, + final Predicate villagePlaceType, + // position predicate must not modify chunk POI + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final double maxDistance, + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + final Predicate occupancyFilter = occupancy.getTest(); + + final double maxDistanceSquared = maxDistance * maxDistance; + final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); + int totalRecords = 0; + double furthestDistanceSquared = maxDistanceSquared; + + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = WorldUtil.getMinSection(poiStorage.world); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = WorldUtil.getMaxSection(poiStorage.world); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + final int centerX = sourcePosition.getX() >> 4; + final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); + final int centerZ = sourcePosition.getZ() >> 4; + + final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ)); + final LongOpenHashSet seen = new LongOpenHashSet(); + + while (!queue.isEmpty()) { + final long key = queue.dequeueLong(); + final int sectionX = CoordinateUtils.getChunkSectionX(key); + final int sectionY = CoordinateUtils.getChunkSectionY(key); + final int sectionZ = CoordinateUtils.getChunkSectionZ(key); + + if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { + // out of bound chunk + continue; + } + + final double sectionDistanceSquared = getSmallestDistanceSquared( + (sectionX << 4) + 0.5, + (sectionY << 4) + 0.5, + (sectionZ << 4) + 0.5, + (sectionX << 4) + 15.5, + (sectionY << 4) + 15.5, + (sectionZ << 4) + 15.5, + (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() + ); + + if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { + continue; + } + + // queue all neighbours + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many + // values are set. we only care about cardinal neighbours, so, we only care if one value is set + if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { + continue; + } + + final int neighbourX = sectionX + dx; + final int neighbourY = sectionY + dy; + final int neighbourZ = sectionZ + dz; + + final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); + if (seen.add(neighbourKey)) { + queue.enqueue(neighbourKey); + } + } + } + } + + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); + + if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { + continue; + } + + final PoiSection poiSection = poiSectionOptional.orElse(null); + + final Map> sectionData = poiSection.getData(); + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! + final double dataRange = poiPosition.distSqr(sourcePosition); + + if (dataRange > maxDistanceSquared) { + // out of range for distance check + continue; + } + + if (dataRange > furthestDistanceSquared && totalRecords >= max) { + // out of range for distance check + continue; + } + + if (positionPredicate != null && !positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + if (dataRange > furthestDistanceSquared) { + // we know totalRecords < max, so this entry is now our furthest + furthestDistanceSquared = dataRange; + } + + closestRecords.computeIfAbsent(dataRange, (final double unused) -> { + return new ArrayList<>(); + }).add(poiData); + + if (++totalRecords >= max) { + if (closestRecords.size() >= 2) { + int entriesInClosest = 0; + final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); + double nextFurthestDistanceSquared = 0.0; + + for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { + final Double2ObjectMap.Entry> recordEntry = iterator.next(); + entriesInClosest += recordEntry.getValue().size(); + nextFurthestDistanceSquared = recordEntry.getDoubleKey(); + } + + if (entriesInClosest >= max) { + // the last set of entries at range wont even be considered for sure... nuke em + final Double2ObjectMap.Entry> recordEntry = iterator.next(); + totalRecords -= recordEntry.getValue().size(); + iterator.remove(); + + furthestDistanceSquared = nextFurthestDistanceSquared; + } + } + } + } + } + } + + final List closestRecordsUnsorted = new ArrayList<>(); + + // we're done here, so now just flatten the map and sort it. + + for (final List records : closestRecords.values()) { + closestRecordsUnsorted.addAll(records); + } + + // uh oh! we might have multiple records that match the distance sorting! + // we need to re-order our results by the way vanilla would have iterated over them. + closestRecordsUnsorted.sort((record1, record2) -> { + // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section + // is fine and should be preserved (this sort is stable so we're good there) + // but they iterate sections by x then by z (like the following) + // for (int x = -dx; x <= dx; ++x) + // for (int z = -dz; z <= dz; ++z) + // .... + // so we need to reorder such that records with lower chunk z, then lower chunk x come first + final BlockPos pos1 = record1.getPos(); + final BlockPos pos2 = record2.getPos(); + + final int cx1 = pos1.getX() >> 4; + final int cz1 = pos1.getZ() >> 4; + + final int cx2 = pos2.getX() >> 4; + final int cz2 = pos2.getZ() >> 4; + + if (cz2 != cz1) { + // want smaller z + return Integer.compare(cz1, cz2); + } + + if (cx2 != cx1) { + // want smaller x + return Integer.compare(cx1, cx2); + } + + // same chunk + // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y + // so now we just compare section y, wanting smaller section y + + return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); + }); + + // trim out any entries exceeding our maximum + for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { + closestRecordsUnsorted.remove(i); + } + + // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). + ret.addAll(closestRecordsUnsorted); + } + + public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, + final Predicate villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load) { + final PoiRecord ret = findAnyPoiRecord( + poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load + ); + + return ret == null ? null : ret.getPos(); + } + + public static void findAnyPoiPositions(final PoiManager poiStorage, + final Predicate villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + final Set positions = new HashSet<>(); + // pos predicate is last thing that runs before adding to ret. + final Predicate newPredicate = (final BlockPos pos) -> { + if (positionPredicate != null && !positionPredicate.test(pos)) { + return false; + } + return positions.add(pos.immutable()); + }; + + final List toConvert = new ArrayList<>(); + findAnyPoiRecords( + poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert + ); + + for (final PoiRecord record : toConvert) { + ret.add(record.getPos()); + } + } + + public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, + final Predicate villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load) { + final List ret = new ArrayList<>(); + findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); + return ret.isEmpty() ? null : ret.get(0); + } + + public static void findAnyPoiRecords(final PoiManager poiStorage, + final Predicate villagePlaceType, + final Predicate positionPredicate, + final BlockPos sourcePosition, + final int range, // distance on x y z axis + final PoiManager.Occupancy occupancy, + final boolean load, + final int max, + final List ret) { + // the biggest issue with the original mojang implementation is that they chain so many streams together + // the amount of streams chained just rolls performance, even if nothing is iterated over + final Predicate occupancyFilter = occupancy.getTest(); + final double rangeSquared = range * range; + + int added = 0; + + // First up, we need to iterate the chunks + // all the values here are in chunk sections + final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; + final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4); + final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; + final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; + final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4); + final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; + + // Vanilla iterates by x until max is reached then increases z + // vanilla also searches by increasing Y section value + for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { + for (int currX = lowerX; currX <= upperX; ++currX) { + for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need + final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : + poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); + final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); + if (poiSection == null) { + continue; + } + + final Map> sectionData = poiSection.getData(); + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (final Map.Entry> entry : sectionData.entrySet()) { + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (final PoiRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + final BlockPos poiPosition = poiData.getPos(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + if (poiPosition.distSqr(sourcePosition) > rangeSquared) { + // out of range for distance check + continue; + } + + if (positionPredicate != null && !positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + // found one! + ret.add(poiData); + if (++added >= max) { + return; + } + } + } + } + } + } + } + + private PoiAccess() { + throw new RuntimeException(); + } +} 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 9d09ec3b127e3440bef6b248578dec109407f9ff..4b6bbdbdf581b8a751c08708ee24e8b2a85534a0 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 3dded5c491ace6b073a7bc3178976bd70f0b9393..f25bb4214cffd0050241ea229b6acb0c16b2b0a5 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -293,6 +293,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public boolean serverAutoSave = false; // Paper @@ -327,6 +328,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 @@ -1141,6 +1212,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(); @@ -1459,6 +1518,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 details) { diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index 7b6c547e71230fbb3733f99a4597b3f5b51547b8..1b324839e37d510552f5f5497de009add69ecda5 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -223,6 +223,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // load mappings for stacktrace deobf and etc. // 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 574434760cb91234b994f101a5ddef595337b42e..fd24a282d28254182cdb88cb500b3f3c32ce958e 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; @@ -722,6 +770,9 @@ public class ChunkHolder { 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 @@ -731,6 +782,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); @@ -744,9 +801,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 @@ -756,6 +817,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)) { @@ -786,11 +853,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 42fd259f4492e539112b5bcb310aaaadab58a443..b9b985268f5627a238c302f81400a05bfd7c592d 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -104,6 +104,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 { @@ -116,8 +117,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 @@ -140,8 +142,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 @@ -149,7 +150,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; @@ -182,32 +183,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 } } }; @@ -216,22 +214,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; @@ -240,7 +223,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); @@ -257,6 +240,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()); @@ -267,7 +256,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 @@ -276,19 +265,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) { @@ -301,11 +281,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) { @@ -317,28 +296,125 @@ 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; - 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) + public static final class DataRegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionData { + // Tuinity start - optimise notify() + private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators; + + public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { + return this.navigators; + } + + public boolean addToNavigators(final Mob navigator) { + if (this.navigators == null) { + this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(); + } + return this.navigators.add(navigator); } - 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 boolean removeFromNavigators(final Mob navigator) { + if (this.navigators == null) { + return false; + } + return this.navigators.remove(navigator); + } + // Tuinity end - optimise notify() } - // Paper end + public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData { + + // Tuinity start - optimise notify() + private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators; + + public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { + return this.navigators; + } + + public boolean addToNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { + if (this.navigators == null) { + this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(); + } + final boolean ret = this.navigators.add(navigator); + if (ret) { + final DataRegionData data = (DataRegionData)section.getRegion().regionData; + if (!data.addToNavigators(navigator)) { + throw new IllegalStateException(); + } + } + return ret; + } + + public boolean removeFromNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { + if (this.navigators == null) { + return false; + } + final boolean ret = this.navigators.remove(navigator); + if (ret) { + final DataRegionData data = (DataRegionData)section.getRegion().regionData; + if (!data.removeFromNavigators(navigator)) { + throw new IllegalStateException(); + } + } + return ret; + } + // Tuinity end - optimise notify() + + @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; + // Tuinity start - optimise notify() + if (sectionData.navigators != null) { + for (final Iterator iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + if (!fromData.removeFromNavigators(iterator.next())) { + throw new IllegalStateException(); + } + } + } + // Tuinity end - optimise notify() + } + + @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; + // Tuinity start - optimise notify() + if (sectionData.navigators != null) { + for (final Iterator iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + if (!newRegionData.addToNavigators(iterator.next())) { + throw new IllegalStateException(); + } + } + } + // Tuinity end - optimise notify() + } + } + + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { + return this.pendingUnloads.get(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + // Tuiniy end + + boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks private final java.util.concurrent.ExecutorService lightThread; // Paper 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); @@ -465,53 +541,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, - (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; - } - - ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); - ChunkMap.this.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update - }, + 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) -> { - 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)); + LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); } - 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 @@ -545,6 +596,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; @@ -552,7 +604,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 @@ -618,7 +670,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()); } @@ -686,7 +738,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 @@ -694,47 +746,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) { @@ -856,12 +886,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) { @@ -876,11 +907,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; } @@ -1035,7 +1072,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); @@ -1072,7 +1109,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; @@ -1093,7 +1130,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); @@ -1109,7 +1146,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); } @@ -1134,7 +1183,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 } }; @@ -1153,19 +1208,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; @@ -1178,11 +1225,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.LIGHT), 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) -> { @@ -1194,6 +1250,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 } } @@ -1294,7 +1351,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; } @@ -1325,7 +1382,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) { @@ -1484,9 +1544,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() { @@ -1571,7 +1629,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 } } @@ -1579,26 +1637,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()); @@ -1622,7 +1665,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() { @@ -1927,6 +1970,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 } @@ -1935,7 +1979,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(); @@ -1951,8 +1995,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); @@ -1967,6 +2012,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); } } @@ -2281,7 +2327,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 @@ -2381,7 +2427,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 @@ -2416,7 +2462,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 1043577580bee20a46ae4b2c9e7cef27d45568ad..3f0168a9edfa67f99e6fe9ce161878034e57c3f4 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -47,6 +47,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; @@ -138,7 +139,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) { @@ -200,9 +201,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) { @@ -232,6 +233,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); @@ -575,6 +736,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); @@ -593,9 +756,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(); @@ -606,12 +772,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)); } @@ -669,6 +843,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(); @@ -678,6 +854,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 @@ -735,6 +912,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(); @@ -750,17 +928,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(); @@ -838,18 +1017,26 @@ 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()) { - LevelChunk chunk = (LevelChunk) optional.get(); + // 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"); + this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings + playerchunk.broadcastChanges(chunk); + this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings + this.level.getProfiler().pop(); + // Tuinity end 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 @@ -857,7 +1044,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) { @@ -866,25 +1057,28 @@ public class ServerChunkCache extends ChunkSource { } // Paper - timings } - this.level.getProfiler().popPush("broadcast"); - this.chunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping - Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); // CraftBukkit - decompile error - - Objects.requireNonNull(playerchunk); - - // Paper start - timings - optional.ifPresent(chunk -> { - this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings - playerchunk.broadcastChanges(chunk); - this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings - }); - // Paper end - }); - this.level.getProfiler().pop(); + // Tuinity - no, iterating just ONCE is expensive enough! Don't do it TWICE! Code moved up 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) { @@ -1031,46 +1225,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 2f3e69ad809199ffc2661d524bb627ec8dbc2e80..0fcd6a9162f5bddb3c4fc42b3a64efde7c7d9a9b 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 baa25df9f446c8edea9666983425df31c32a13ff..f9ed48f5bbde84fd1804e482f2777b516cc3a1ef 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -114,6 +114,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; 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; @@ -161,6 +162,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 Level implements WorldGenLevel { @@ -189,7 +191,9 @@ public class ServerLevel extends Level implements WorldGenLevel { 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; @@ -300,6 +304,172 @@ public class ServerLevel extends Level implements WorldGenLevel { } } // 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) { @@ -347,7 +517,7 @@ public class ServerLevel extends Level implements WorldGenLevel { 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; @@ -400,6 +570,14 @@ public class ServerLevel extends Level implements WorldGenLevel { } 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; @@ -545,7 +723,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } timings.scheduledBlocks.stopTiming(); // Paper - this.getServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic gameprofilerfiller.popPush("raid"); this.timings.raids.startTiming(); // Paper - timings this.raids.tick(); @@ -558,7 +736,7 @@ public class ServerLevel extends Level implements WorldGenLevel { 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 @@ -605,12 +783,12 @@ public class ServerLevel extends Level implements WorldGenLevel { 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(); } @@ -655,6 +833,10 @@ public class ServerLevel extends Level implements WorldGenLevel { 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(); @@ -664,10 +846,10 @@ public class ServerLevel extends Level implements WorldGenLevel { 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 @@ -690,64 +872,78 @@ public class ServerLevel extends Level implements WorldGenLevel { } 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) + 1; + 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 + blockposition.setY(normalY); // Paper Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation(); - if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) { + blockposition.setY(downY); // Paper + if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition)) { // Paper biomebase_precipitation = Biome.Precipitation.SNOW; } - iblockdata.getBlock().handlePrecipitation(iblockdata, (Level) this, blockposition1, biomebase_precipitation); + iblockdata.getBlock().handlePrecipitation(iblockdata, (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(); } @@ -873,7 +1069,27 @@ public class ServerLevel extends Level implements WorldGenLevel { } + // 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"); + this.entityManager.updateNavigatorsInRegion(entity); // Tuinity - optimise notify + 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 @@ -914,7 +1130,13 @@ public class ServerLevel extends Level implements WorldGenLevel { } // } 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) { @@ -1206,9 +1428,13 @@ public class ServerLevel extends Level implements WorldGenLevel { // Spigot Start for (net.minecraft.world.level.block.entity.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 @@ -1305,9 +1531,19 @@ public class ServerLevel extends Level implements WorldGenLevel { VoxelShape voxelshape1 = newState.getCollisionShape(this, pos); if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { - Iterator iterator = this.navigatingMobs.iterator(); + // Tuinity start - optimise notify() + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region region = this.getChunkSource().chunkMap.dataRegionManager.getRegion(pos.getX() >> 4, pos.getZ() >> 4); + if (region == null) { + return; + } + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigatorsFromRegion = ((ChunkMap.DataRegionData)region.regionData).getNavigators(); + if (navigatorsFromRegion == null) { + return; + } + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = navigatorsFromRegion.iterator(); - while (iterator.hasNext()) { + + try { while (iterator.hasNext()) { // Tuinity end - optimise notify() // CraftBukkit start - fix SPIGOT-6362 Mob entityinsentient; try { @@ -1326,6 +1562,11 @@ public class ServerLevel extends Level implements WorldGenLevel { navigationabstract.recomputePath(pos); } } + // Tuinity start - optimise notify() + } finally { + iterator.finishedIterating(); + } + // Tuinity end - optimise notify() } } // Paper @@ -2107,10 +2348,12 @@ public class ServerLevel extends Level implements WorldGenLevel { public void onTickingStart(Entity entity) { ServerLevel.this.entityTickList.add(entity); + ServerLevel.this.entityManager.addNavigatorsIfPathingToRegion(entity); // Tuinity - optimise notify } public void onTickingEnd(Entity entity) { ServerLevel.this.entityTickList.remove(entity); + ServerLevel.this.entityManager.removeNavigatorsFromData(entity); // Tuinity - optimise notify } public void onTrackingStart(Entity entity) { diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index e32da100eabf0d3de12375402e9378c726811358..1c9aec21aa22d0d202a023e9252d1412685ed4b0 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -261,7 +261,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) { @@ -419,7 +419,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; } } @@ -427,7 +427,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()); } } @@ -1558,6 +1558,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 1c83fbc96a074c85a3e349e936ff1f3198596709..e97e3b4de132a452d560c19a1dcecd28f8010c62 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); @@ -226,7 +246,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); @@ -279,12 +299,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()) { @@ -301,12 +321,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)) { @@ -318,7 +344,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 } } @@ -328,7 +354,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 f0df7b2bd618c7be18c4c86f735b303dc73d98ad..73e1efe5bcec0522384118f0ca5e4d4f3e8bce82 100644 --- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java @@ -25,6 +25,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; @@ -168,13 +179,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.playerChunkMap = chunkMap; // Paper 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.queue.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() { } @@ -191,15 +355,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(() -> { @@ -222,17 +387,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); }, () -> { @@ -242,6 +406,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(() -> { @@ -263,6 +428,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(() -> { @@ -273,6 +439,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(); // Paper start //ichunkaccess.b(false); // Don't need to disable this @@ -320,7 +517,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public void tryScheduleUpdate() { - if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper + if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Tuinity - rewrite light engine this.taskMailbox.tell(() -> { this.runUpdate(); this.scheduled.set(false); @@ -337,12 +534,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl if (queue.poll(pre, post)) { pre.forEach(Runnable::run); pre.clear(); - super.runUpdates(Integer.MAX_VALUE, true, true); + this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine post.forEach(Runnable::run); post.clear(); } else { // might have level updates to go still - super.runUpdates(Integer.MAX_VALUE, true, true); + this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine } // Paper end } 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 064aecb28f05fcf572ee7d29f611a31cc7b6e49a..c4cea533f619624976c4d1290312ed1a6b250855 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -536,6 +536,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; @@ -576,12 +582,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(); @@ -595,16 +602,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)); @@ -690,7 +704,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 @@ -1227,7 +1266,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()); } @@ -1266,6 +1305,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) { @@ -1315,11 +1360,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) { @@ -1354,6 +1399,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) { @@ -1373,12 +1419,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 @@ -1465,6 +1522,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, false, true, + 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 61405c2b53e03a4b83e2c70c6e4d3739ca9676cb..1f307f8e3f0f484dad33e9af085dabd93a3509fd 100644 --- a/src/main/java/net/minecraft/server/players/GameProfileCache.java +++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java @@ -62,6 +62,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; @@ -69,6 +74,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()); @@ -83,6 +89,7 @@ public class GameProfileCache { if (uuid != null) { this.profilesByUUID.put(uuid, entry); } + } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency } @@ -119,7 +126,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()); @@ -142,8 +149,9 @@ public class GameProfileCache { } // Paper end - public synchronized Optional get(String name) { // Paper - synchronize + public Optional 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; @@ -157,10 +165,14 @@ public class GameProfileCache { Optional optional; if (usercache_usercacheentry != null) { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency usercache_usercacheentry.setLastAccess(this.getNextOperation()); optional = Optional.of(usercache_usercacheentry.getProfile()); } else { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency + try { this.lookupLock.lock(); // Tuinity - allow better concurrency optional = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players + } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency if (optional.isPresent()) { this.add((GameProfile) optional.get()); flag = false; @@ -172,6 +184,7 @@ public class GameProfileCache { } return optional; + } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency } public void getAsync(String username, Consumer> consumer) { @@ -322,7 +335,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 48045993c8ad4b014cf4a67f7c4db42e014d1c81..936ae5576902e6593bd21af4d3cf3998109347b5 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -177,6 +177,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); @@ -266,7 +267,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())); @@ -730,7 +731,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()); @@ -934,13 +935,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())); @@ -1215,7 +1216,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 896d892237b29eb404398db07264eb6f04786754..2453492429a743677db07e31d575c1473fedf4ad 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) { @@ -3794,7 +3985,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n // Paper end // 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); @@ -3803,7 +3994,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // Paper end - fix MC-4 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 a8e5be1c941755b3e5b335d8211ca70a6c6fc32f..5eb93bacd303ebed0a702221f8ae31631d42f45d 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -789,7 +789,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/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java index 84a0ee595bebcc1947c602c4c06e7437706ce37c..efe66264ad5717bf3aac0fbda07275fb5571acc1 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java @@ -83,7 +83,11 @@ public class AcquirePoi extends Behavior { return true; } }; - Set set = poiManager.findAllClosestFirst(this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet()); + // Tuinity start - optimise POI access + java.util.List poiposes = new java.util.ArrayList<>(); + com.tuinity.tuinity.util.PoiAccess.findNearestPoiPositions(poiManager, this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); + Set set = new java.util.HashSet<>(poiposes); + // Tuinity end - optimise POI access Path path = entity.getNavigation().createPath(set, this.poiType.getValidRange()); if (path != null && path.canReach()) { BlockPos blockPos = path.getTarget(); diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java index 09998d160a6d79fdb5a5041a5d572649a1532e6a..3fe1f9bd4bb670d9a1ddabf2475f4d8f44d7e6fe 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java @@ -30,11 +30,19 @@ public class GateBehavior extends Behavior { @Override protected boolean canStillUse(ServerLevel world, E entity, long time) { - return this.behaviors.stream().filter((behavior) -> { - return behavior.getStatus() == Behavior.Status.RUNNING; - }).anyMatch((behavior) -> { - return behavior.canStillUse(world, entity, time); - }); + // Tuinity start - remove streams + List>> entries = this.behaviors.entries; + for (int i = 0; i < entries.size(); i++) { + ShufflingList.WeightedEntry> entry = entries.get(i); + Behavior behavior = entry.getData(); + if (behavior.getStatus() == Status.RUNNING) { + if (behavior.canStillUse(world, entity, time)) { + return true; + } + } + } + return false; + // Tuinity end - remove streams } @Override @@ -45,25 +53,35 @@ public class GateBehavior extends Behavior { @Override protected void start(ServerLevel world, E entity, long time) { this.orderPolicy.apply(this.behaviors); - this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); + this.runningPolicy.apply(this.behaviors.entries, world, entity, time); // Tuinity - remove streams } @Override protected void tick(ServerLevel world, E entity, long time) { - this.behaviors.stream().filter((behavior) -> { - return behavior.getStatus() == Behavior.Status.RUNNING; - }).forEach((behavior) -> { - behavior.tickOrStop(world, entity, time); - }); + // Tuinity start - remove streams + List>> entries = this.behaviors.entries; + for (int i = 0; i < entries.size(); i++) { + ShufflingList.WeightedEntry> entry = entries.get(i); + Behavior behavior = entry.getData(); + if (behavior.getStatus() == Status.RUNNING) { + behavior.tickOrStop(world, entity, time); + } + } + // Tuinity end - remove streams } @Override protected void stop(ServerLevel world, E entity, long time) { - this.behaviors.stream().filter((behavior) -> { - return behavior.getStatus() == Behavior.Status.RUNNING; - }).forEach((behavior) -> { - behavior.doStop(world, entity, time); - }); + // Tuinity start - remove streams + List>> entries = this.behaviors.entries; + for (int i = 0; i < entries.size(); i++) { + ShufflingList.WeightedEntry> entry = entries.get(i); + Behavior behavior = entry.getData(); + if (behavior.getStatus() == Status.RUNNING) { + behavior.doStop(world, entity, time); + } + } + // Tuinity end - remove streams this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); } @@ -94,25 +112,33 @@ public class GateBehavior extends Behavior { public static enum RunningPolicy { RUN_ONE { @Override - public void apply(Stream> tasks, ServerLevel world, E entity, long time) { - tasks.filter((behavior) -> { - return behavior.getStatus() == Behavior.Status.STOPPED; - }).filter((behavior) -> { - return behavior.tryStart(world, entity, time); - }).findFirst(); + // Tuinity start - remove streams + public void apply(List>> tasks, ServerLevel world, E entity, long time) { + for (int i = 0; i < tasks.size(); i++) { + ShufflingList.WeightedEntry> task = tasks.get(i); + Behavior behavior = task.getData(); + if (behavior.getStatus() == Status.STOPPED && behavior.tryStart(world, entity, time)) { + break; + } + } + // Tuinity end - remove streams } }, TRY_ALL { @Override - public void apply(Stream> tasks, ServerLevel world, E entity, long time) { - tasks.filter((behavior) -> { - return behavior.getStatus() == Behavior.Status.STOPPED; - }).forEach((behavior) -> { - behavior.tryStart(world, entity, time); - }); + // Tuinity start - remove streams + public void apply(List>> tasks, ServerLevel world, E entity, long time) { + for (int i = 0; i < tasks.size(); i++) { + ShufflingList.WeightedEntry> task = tasks.get(i); + Behavior behavior = task.getData(); + if (behavior.getStatus() == Status.STOPPED) { + behavior.tryStart(world, entity, time); + } + } + // Tuinity end - remove streams } }; - public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); + public abstract void apply(List>> tasks, ServerLevel world, E entity, long time); // Tuinity - remove streams } } diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java b/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java index 1f59e790d62f0be8e505e339a6699ca3964aea0d..bb43e47d4b3989610a52c1941598865aee93ac04 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java @@ -34,21 +34,42 @@ public class SetLookAndInteract extends Behavior { @Override public boolean checkExtraStartConditions(ServerLevel world, LivingEntity entity) { - return this.selfFilter.test(entity) && this.getVisibleEntities(entity).stream().anyMatch(this::isMatchingTarget); + // Tuinity start - remove streams + if (!this.selfFilter.test(entity)) { + return false; + } + + List visibleEntities = this.getVisibleEntities(entity); + for (int i = 0; i < visibleEntities.size(); i++) { + LivingEntity livingEntity = visibleEntities.get(i); + if (this.isMatchingTarget(livingEntity)) { + return true; + } + } + return false; + // Tuinity end - remove streams } @Override public void start(ServerLevel world, LivingEntity entity, long time) { super.start(world, entity, time); Brain brain = entity.getBrain(); - brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).ifPresent((list) -> { - list.stream().filter((livingEntity2) -> { - return livingEntity2.distanceToSqr(entity) <= (double)this.interactionRangeSqr; - }).filter(this::isMatchingTarget).findFirst().ifPresent((livingEntity) -> { - brain.setMemory(MemoryModuleType.INTERACTION_TARGET, livingEntity); - brain.setMemory(MemoryModuleType.LOOK_TARGET, new EntityTracker(livingEntity, true)); - }); - }); + // Tuinity start - remove streams + List list = brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).orElse(null); + if (list != null) { + double maxRangeSquared = (double)this.interactionRangeSqr; + for (int i = 0; i < list.size(); i++) { + LivingEntity livingEntity2 = list.get(i); + if (livingEntity2.distanceToSqr(entity) <= maxRangeSquared) { + if (this.isMatchingTarget(livingEntity2)) { + brain.setMemory(MemoryModuleType.INTERACTION_TARGET, livingEntity2); + brain.setMemory(MemoryModuleType.LOOK_TARGET, new EntityTracker(livingEntity2, true)); + break; + } + } + } + } + // Tuinity end - remove streams } private boolean isMatchingTarget(LivingEntity entity) { diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java b/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java index 4fa64b1e2004810906bb0b174436c8e687a75ada..d5a3c6d239abbb31c52ec2dfb9b18b1b705cbc88 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java @@ -12,7 +12,7 @@ import java.util.Random; import java.util.stream.Stream; public class ShufflingList { - protected final List> entries; + public final List> entries; // Tuinity - public private final Random random = new Random(); private final boolean isUnsafe; // Paper diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java index e605daac0c90f5d0b9315d1499938feb0e478d0e..570316cf7831de70086fae35676006ee052851e0 100644 --- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java @@ -27,7 +27,7 @@ import net.minecraft.world.phys.Vec3; public abstract class PathNavigation { private static final int MAX_TIME_RECOMPUTE = 20; - protected final Mob mob; + protected final Mob mob; public final Mob getEntity() { return this.mob; } // Tuinity - public accessor protected final Level level; @Nullable protected Path path; @@ -40,7 +40,7 @@ public abstract class PathNavigation { protected long lastTimeoutCheck; protected double timeoutLimit; protected float maxDistanceToWaypoint = 0.5F; - protected boolean hasDelayedRecomputation; + protected boolean hasDelayedRecomputation; protected final boolean needsPathRecalculation() { return this.hasDelayedRecomputation; } // Tuinity - public accessor protected long timeLastRecompute; protected NodeEvaluator nodeEvaluator; private BlockPos targetPos; @@ -49,6 +49,13 @@ public abstract class PathNavigation { public final PathFinder pathFinder; private boolean isStuck; + // Tuinity start + public boolean isViableForPathRecalculationChecking() { + return !this.needsPathRecalculation() && + (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0); + } + // Tuinity end + public PathNavigation(Mob mob, Level world) { this.mob = mob; this.level = world; @@ -404,7 +411,7 @@ public abstract class PathNavigation { } public void recomputePath(BlockPos pos) { - if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { + if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { // Tuinity - diff on change - needed for isViableForPathRecalculationChecking() Node node = this.path.getEndNode(); Vec3 vec3 = new Vec3(((double)node.x + this.mob.getX()) / 2.0D, ((double)node.y + this.mob.getY()) / 2.0D, ((double)node.z + this.mob.getZ()) / 2.0D); if (pos.closerThan(vec3, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()))) { diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java index e41b2fa1db6fb77a26cdb498904021b430e35be0..f0ba454eea673bf02d1f6d7fe30c4f672643fe0c 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java @@ -49,8 +49,12 @@ public class NearestBedSensor extends Sensor { return true; } }; - Stream stream = poiManager.findAll(PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY); - Path path = entity.getNavigation().createPath(stream, PoiType.HOME.getValidRange()); + // Tuinity start - optimise POI access + java.util.List poiposes = new java.util.ArrayList<>(); + // don't ask me why it's unbounded. ask mojang. + com.tuinity.tuinity.util.PoiAccess.findAnyPoiPositions(poiManager, PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); + Path path = entity.getNavigation().createPath(new java.util.HashSet<>(poiposes), PoiType.HOME.getValidRange()); + // Tuinity end - optimise POI access if (path != null && path.canReach()) { BlockPos blockPos = path.getTarget(); Optional optional = poiManager.getType(blockPos); diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java index 49f3b25d28072b61f5cc97260df61df892a58714..de6b591eb865c6f5c23aaa4b9374bb9bbaaa85f6 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java @@ -25,17 +25,20 @@ public class NearestItemSensor extends Sensor { protected void doTick(ServerLevel world, Mob entity) { Brain brain = entity.getBrain(); List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(8.0D, 4.0D, 8.0D), (itemEntity) -> { - return true; + return itemEntity.closerThan(entity, 9.0D) && entity.wantsToPickUp(itemEntity.getItem()); // Tuinity - move predicate into getEntities }); - list.sort(Comparator.comparingDouble(entity::distanceToSqr)); + list.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); // better to take the sort perf hit than using line of sight more than we need to. + // Tuinity start - remove streams // Paper start - remove streams in favour of lists ItemEntity nearest = null; - for (ItemEntity entityItem : list) { - if (entity.wantsToPickUp(entityItem.getItem()) && entityItem.closerThan(entity, 9.0D) && entity.hasLineOfSight(entityItem)) { + for (int i = 0; i < list.size(); i++) { + ItemEntity entityItem = list.get(i); + if (entity.hasLineOfSight(entityItem)) { nearest = entityItem; break; } } + // Tuinity end - remove streams brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); // Paper end } diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java index ffd83db0a419ab589e89feeddd3fb038d6ed5839..31ef567cf4f331d3329dd176392686db56aead66 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java @@ -18,12 +18,19 @@ public class NearestLivingEntitySensor extends Sensor { List list = world.getEntitiesOfClass(LivingEntity.class, aABB, (livingEntity2) -> { return livingEntity2 != entity && livingEntity2.isAlive(); }); - list.sort(Comparator.comparingDouble(entity::distanceToSqr)); + // Tuinity start - remove streams + list.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); Brain brain = entity.getBrain(); brain.setMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES, list); // Paper start - remove streams in favour of lists - List visibleMobs = new java.util.ArrayList<>(list); - visibleMobs.removeIf(otherEntityLiving -> !Sensor.isEntityTargetable(entity, otherEntityLiving)); + List visibleMobs = new java.util.ArrayList<>(); + for (int i = 0, len = list.size(); i < len; i++) { + LivingEntity nearby = list.get(i); + if (Sensor.isEntityTargetable(entity, nearby)) { + visibleMobs.add(nearby); + } + } + // Tuinity end - remove streams brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, visibleMobs); // Paper end } diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java index 457ea75137b8b02dc32bf1769ae8d57c470da470..3392a8d425d9f5e1417a665fb1514d013bf89337 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java @@ -21,25 +21,31 @@ public class PlayerSensor extends Sensor { @Override protected void doTick(ServerLevel world, LivingEntity entity) { - // Paper start - remove streams in favour of lists - List players = new java.util.ArrayList<>(world.players()); - players.removeIf(player -> !EntitySelector.NO_SPECTATORS.test(player) || !entity.closerThan(player, 16.0D)); // Paper - removeIf only re-allocates once compared to iterator + // Tuinity start - remove streams + List players = (List)world.getNearbyPlayers(entity, entity.getX(), entity.getY(), entity.getZ(), 16.0D, EntitySelector.NO_SPECTATORS); + players.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); Brain brain = entity.getBrain(); brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, players); - Player nearest = null, nearestTargetable = null; - for (Player player : players) { - if (Sensor.isEntityTargetable(entity, player)) { - if (nearest == null) nearest = player; - if (Sensor.isEntityAttackable(entity, player)) { - nearestTargetable = player; - break; // Both variables are assigned, no reason to loop further - } + Player firstTargetable = null; + Player firstAttackable = null; + for (int index = 0, len = players.size(); index < len; ++index) { + Player player = players.get(index); + if (firstTargetable == null && isEntityTargetable(entity, player)) { + firstTargetable = player; + } + if (firstAttackable == null && isEntityAttackable(entity, player)) { + firstAttackable = player; + } + + if (firstAttackable != null && firstTargetable != null) { + break; } } - brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, nearest); - brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, nearestTargetable); - // Paper end + + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, firstTargetable); + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, Optional.ofNullable(firstAttackable)); + // Tuinity end - remove streams } } diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java index 478010bc291fa3276aab0f66ce6283403af710ec..a198538fe4ae560adc66fad5a2f6b80bbd894e4b 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java @@ -22,7 +22,17 @@ public class VillagerBabiesSensor extends Sensor { } private List getNearestVillagerBabies(LivingEntity entities) { - return this.getVisibleEntities(entities).stream().filter(this::isVillagerBaby).collect(Collectors.toList()); + // Tuinity start - remove streams + List list = new java.util.ArrayList<>(); + List visibleEntities = this.getVisibleEntities(entities); + for (int i = 0; i < visibleEntities.size(); i++) { + LivingEntity livingEntity = visibleEntities.get(i); + if (this.isVillagerBaby(livingEntity)) { + list.add(livingEntity); + } + } + return list; + // Tuinity end - remove streams } private boolean isVillagerBaby(LivingEntity entity) { 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..3ba30a2e6f1e3eb82b2b6e8968fd2babbf220ded 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 @@ -37,7 +37,7 @@ public class PoiManager extends SectionStorage { public static final int VILLAGE_SECTION_SIZE = 1; private final PoiManager.DistanceTracker distanceTracker; private final LongSet loadedChunks = new LongOpenHashSet(); - private final net.minecraft.server.level.ServerLevel world; // Paper + public final net.minecraft.server.level.ServerLevel world; // Paper // Tuinity public public PoiManager(File directory, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) { super(directory, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, world); @@ -100,36 +100,55 @@ public class PoiManager extends SectionStorage { } public Optional find(Predicate typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { - return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); + // Tuinity start - re-route to faster logic + BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); + return Optional.ofNullable(ret); + // Tuinity end - re-route to faster logic } public Optional findClosest(Predicate typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { - return this.getInRange(typePredicate, pos, radius, occupationStatus).map(PoiRecord::getPos).min(Comparator.comparingDouble((blockPos2) -> { - return blockPos2.distSqr(pos); - })); + // Tuinity start - re-route to faster logic + BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius*radius, occupationStatus, false); + return Optional.ofNullable(ret); + // Tuinity end - re-route to faster logic } public Optional findClosest(Predicate predicate, Predicate predicate2, BlockPos blockPos, int i, PoiManager.Occupancy occupancy) { - return this.getInRange(predicate, blockPos, i, occupancy).map(PoiRecord::getPos).filter(predicate2).min(Comparator.comparingDouble((blockPos2) -> { - return blockPos2.distSqr(blockPos); - })); + // Tuinity start - re-route to faster logic + BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataPosition(this, predicate, predicate2, blockPos, i, i*i, occupancy, false); + return Optional.ofNullable(ret); + // Tuinity end - re-route to faster logic } public Optional take(Predicate typePredicate, Predicate positionPredicate, BlockPos pos, int radius) { - return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE).filter((poi) -> { - return positionPredicate.test(poi.getPos()); - }).findFirst().map((poi) -> { - poi.acquireTicket(); - return poi.getPos(); - }); + // Tuinity start - re-route to faster logic + PoiRecord ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecord( + this, typePredicate, positionPredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE, false + ); + if (ret == null) { + return Optional.empty(); + } + ret.acquireTicket(); + return Optional.of(ret.getPos()); + // Tuinity end - re-route to faster logic } public Optional getRandom(Predicate typePredicate, Predicate positionPredicate, PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, Random random) { - List list = this.getInRange(typePredicate, pos, radius, occupationStatus).collect(Collectors.toList()); - Collections.shuffle(list, random); - return list.stream().filter((poiRecord) -> { - return positionPredicate.test(poiRecord.getPos()); - }).findFirst().map(PoiRecord::getPos); + // Tuinity start - re-route to faster logic + List list = new java.util.ArrayList<>(); + com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecords( + this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list + ); + + // the old method shuffled the list and then tried to find the first element in it that + // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a + // shuffle entirely, and just pick a random element from list + if (list.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(list.get(random.nextInt(list.size())).getPos()); + // Tuinity end - re-route to faster logic } public boolean release(BlockPos pos) { @@ -183,7 +202,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/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java index 75c1c4671fedb425dea20dc4fb0c6cb2304dee83..fc7b364adc2d0e5db22aa25e029c2e13c84d6096 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java @@ -25,12 +25,12 @@ import org.apache.logging.log4j.Logger; public class PoiSection { private static final Logger LOGGER = LogManager.getLogger(); private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); - private final Map> byType = Maps.newHashMap(); + private final Map> byType = Maps.newHashMap(); public final Map> getData() { return this.byType; } // Tuinity - public accessor private final Runnable setDirty; private boolean isValid; public static Codec codec(Runnable updateListener) { - return RecordCodecBuilder.create((instance) -> { + return RecordCodecBuilder.create((instance) -> { // Tuinity - decompile fix return instance.group(RecordCodecBuilder.point(updateListener), Codec.BOOL.optionalFieldOf("Valid", Boolean.valueOf(false)).forGetter((poiSet) -> { return poiSet.isValid; }), PoiRecord.codec(updateListener).listOf().fieldOf("Records").forGetter((poiSet) -> { 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 19980b2d627eb3cacf8d0c3e6785ad2206910fbc..e7a7de5ad9b64876df77e20465631ca8e5b19a4a 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 6124e3a32325e8c74bf839010a79d7c82c49aaff..56053b158127150fcd1fba4b6970a52e9bb38db6 100644 --- a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java +++ b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java @@ -106,7 +106,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 474078b68f1bf22037495f42bae59b790fd8cb46..61a4dea715689b0ce9247040db5dd2080ee2e167 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -167,6 +167,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; @@ -203,9 +205,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 @@ -279,6 +389,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 @@ -364,6 +475,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 } @@ -552,7 +672,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 } @@ -867,6 +987,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 @@ -996,26 +1117,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; } @@ -1024,26 +1126,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; } @@ -1331,10 +1429,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 88145f04989c71a686aae1b486087ecdf55e268c..c6821bfb28b582733cd977864c28ca5cf0c69872 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 8393950a0b38ec7897d7643803d5accdb1f983f3..c8d88bbf7e765ce69101c0b04b919c9cfb99a4e2 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; @@ -41,6 +42,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess { net.minecraft.world.level.Level getLevel(); // 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 4a40fa5f7c71481ef0b275955bbd81a737889781..a5d7fdd9ec0342e000e467a002846873a10d75fc 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 c4acc7e0035d37167cf5a56f8d90ba17f85cbbe3..2beae8a39987dd9c514e9d369056e65943d7b130 100644 --- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java @@ -12,7 +12,7 @@ public class DataLayer { public static final int SIZE = 2048; 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 final DataLayer EMPTY_NIBBLE_ARRAY = new DataLayer() { @Override @@ -62,6 +62,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 { @@ -76,7 +77,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) { @@ -162,7 +163,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 a84b75a53a0324fab9aeb9b80bf74eb0a84ecd2e..4f6a356e3a27915f1d95132a0a5fddb163735cf6 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()) { @@ -794,6 +926,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; @@ -813,6 +946,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 @@ -848,6 +982,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 7dc3d806a680150c6a2fffa1436fd63bbdc31eb3..f6d05372f592a3b7619ad6989630c140ffd4f03b 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; @@ -72,6 +74,53 @@ public class ProtoChunk implements ChunkAccess { // Paper end private static boolean PRINTED_OUTDATED_CTOR_MSG = false; // 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 + @Deprecated // Paper start - add level public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { // Paper start @@ -100,6 +149,10 @@ public class ProtoChunk implements ChunkAccess { } } 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; @@ -197,7 +250,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 9ee2321150d3de7bf1a11c212951bc69872f4dd8..9815a91040e7cbcbcd171e68b0a935ea26ff8a2a 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/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java index 90f7b06bd2c558be35c4577044fa033e1fb5cc22..8f244db7e46ac1a3d2c8358f001d488900a76926 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java @@ -61,11 +61,11 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl } @Nullable - protected Optional get(long pos) { + public Optional get(long pos) { // Tuinity - public return this.storage.get(pos); } - protected Optional getOrLoad(long pos) { + public Optional getOrLoad(long pos) { // Tuinity - public if (this.outsideStoredRange(pos)) { return Optional.empty(); } else { 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..6ba8b50b59d3f81ec4c974defc319b1bab27c04b 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,42 @@ 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) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Tuinity 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) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Tuinity 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; + com.tuinity.tuinity.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Tuinity + // 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 d1428fe87ec3be070d9a125a1774ea758d4cd74b..a7079aa957646410b43ebce5f0b55dfb05c792b1 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); @@ -52,6 +54,65 @@ public class PersistentEntitySectionManager implements A this.entityGetter = new LevelEntityGetterAdapter<>(this.visibleEntityStorage, this.sectionStorage); } + // Tuinity start - optimise notify() + public final void removeNavigatorsFromData(Entity entity, final int chunkX, final int chunkZ) { + if (!(entity instanceof net.minecraft.world.entity.Mob)) { + return; + } + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(chunkX, chunkZ); + if (section != null) { + net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; + sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); + } + } + + public final void removeNavigatorsFromData(Entity entity) { + if (!(entity instanceof net.minecraft.world.entity.Mob)) { + return; + } + BlockPos entityPos = entity.blockPosition(); + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); + if (section != null) { + net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; + sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); + } + } + + public final void addNavigatorsIfPathingToRegion(Entity entity) { + if (!(entity instanceof net.minecraft.world.entity.Mob)) { + return; + } + BlockPos entityPos = entity.blockPosition(); + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); + if (section != null) { + net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; + if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { + sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); + } + } + } + + public final void updateNavigatorsInRegion(Entity entity) { + if (!(entity instanceof net.minecraft.world.entity.Mob)) { + return; + } + BlockPos entityPos = entity.blockPosition(); + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); + if (section != null) { + net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; + if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { + sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); + } else { + sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); + } + } + } + // Tuinity end - optimise notify() + void removeSectionIfEmpty(long sectionPos, EntitySection section) { if (section.isEmpty()) { this.sectionStorage.remove(sectionPos); @@ -93,6 +154,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); @@ -146,7 +208,9 @@ public class PersistentEntitySectionManager implements A } public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("Asynchronous chunk ticking status update"); // Tuinity Visibility visibility = Visibility.fromFullChunkStatus(levelType); + this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Tuinity this.updateChunkStatus(chunkPos, visibility); } @@ -388,18 +452,38 @@ public class PersistentEntitySectionManager implements A @Override public void onMove() { BlockPos blockPos = this.entity.blockPosition(); - long l = SectionPos.asLong(blockPos); + long l = SectionPos.asLong(blockPos); // Tuinity - diff on change, new position section if (l != this.currentSectionKey) { - Visibility visibility = this.currentSection.getStatus(); + PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Tuinity + // Tuinity start + int shift = PersistentEntitySectionManager.this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.regionChunkShift; + int oldChunkX = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionX(this.currentSectionKey); + int oldChunkZ = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionZ(this.currentSectionKey); + int oldRegionX = oldChunkX >> shift; + int oldRegionZ = oldChunkZ >> shift; + + int newRegionX = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionX(l) >> shift; + int newRegionZ = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionZ(l) >> shift; + + if (oldRegionX != newRegionX || oldRegionZ != newRegionZ) { + PersistentEntitySectionManager.this.removeNavigatorsFromData((Entity)this.entity, oldChunkX, oldChunkZ); + } + // Tuinity end + Visibility visibility = this.currentSection.getStatus(); // Tuinity - diff on change - this should be OLD section visibility if (!this.currentSection.remove(this.entity)) { PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l); } PersistentEntitySectionManager.this.removeSectionIfEmpty(this.currentSectionKey, this.currentSection); - EntitySection entitySection = PersistentEntitySectionManager.this.sectionStorage.getOrCreateSection(l); + EntitySection entitySection = PersistentEntitySectionManager.this.sectionStorage.getOrCreateSection(l); // Tuinity - diff on change, this should be NEW section entitySection.add(this.entity); this.currentSection = entitySection; this.currentSectionKey = l; + // Tuinity start + if ((oldRegionX != newRegionX || oldRegionZ != newRegionZ) && visibility.isTicking() && entitySection.getStatus().isTicking()) { + PersistentEntitySectionManager.this.addNavigatorsIfPathingToRegion((Entity)this.entity); + } + // Tuinity end this.updateStatus(visibility, entitySection.getStatus()); } @@ -433,6 +517,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/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java index d5ba2e679ed1858ea18e18feffce50544ae036c2..78da12a3feb05a5504daf8379be3d568c389a458 100644 --- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java @@ -52,16 +52,37 @@ public class PortalForcer { // int i = flag ? 16 : 128; // CraftBukkit end - villageplace.ensureLoadedAndValid(this.level, blockposition, i); - Optional optional = villageplace.getInSquare((villageplacetype) -> { - return villageplacetype == PoiType.NETHER_PORTAL; - }, blockposition, i, PoiManager.Occupancy.ANY).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error - return villageplacerecord.getPos().distSqr(blockposition); - }).thenComparingInt((villageplacerecord) -> { - return villageplacerecord.getPos().getY(); - })).filter((villageplacerecord) -> { - return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); - }).findFirst(); + // Tuinity start - optimise portals + Optional optional; + java.util.List records = new java.util.ArrayList<>(); + com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataRecords( + villageplace, + (PoiType type) -> { + return type == PoiType.NETHER_PORTAL; + }, + (BlockPos pos) -> { + net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.ChunkStatus.EMPTY); + if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) { + // why would we generate the chunk? + return false; + } + return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); + }, + blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records + ); + + // this gets us most of the way there, but we bias towards lower y values. + PoiRecord lowestYRecord = null; + for (PoiRecord record : records) { + if (lowestYRecord == null) { + lowestYRecord = record; + } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { + lowestYRecord = record; + } + } + // now we're done + optional = Optional.ofNullable(lowestYRecord); + // Tuinity end - optimise portals return optional.map((villageplacerecord) -> { BlockPos blockposition1 = villageplacerecord.getPos(); 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 0a76032b48af4327580b99730e534f628924fe35..c9c668aa5b2ddf21ffcce8b395e3d88b4b8cf822 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 9954e45c32a4b6d80fe912ed9d55cd4fc8c4e98b..1ec307d705087eec9d867f9f8e8858ac388f3846 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -239,7 +239,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"); @@ -884,6 +884,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); @@ -918,6 +919,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 @@ -2451,6 +2453,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 7dc26321e20e26821096e79356a358879306cd78..2e79e2a23a4aec4b526814f7e959232c0a074860 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -290,7 +290,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(); @@ -313,7 +313,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; } @@ -352,13 +352,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 @@ -432,14 +439,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 @@ -487,13 +487,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); } @@ -2672,7 +2675,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()); } @@ -2697,14 +2700,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 @@ -2713,11 +2716,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 ea7df53656766a8dc4ab5fe66de894301db634e1..b153a8c9e7fdf5560148f02ba2f52c37ad3b5ace 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 85ca30aef0703db6859e66c62781ecfd334426e7..8ce49478441e77cedf5148ecb81d78b32660329e 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -519,27 +519,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 9e8918d03b8213e5f6689fc93030138fd704aca9..5038bd2d0920ffc37a33d0c971cf28c330e58e06 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -517,15 +517,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 08634e060b35d653c5677b7c1012cfda266bf002..70ff3b27d3dae162d05edba7987136ee53decf2f 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; @@ -235,4 +236,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..966639cc6ba6684bfb52e91ac047808cf4d003e4 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -205,7 +205,13 @@ public class ActivationRange ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, waterActivationRange ); // Paper end - world.getEntities().get(maxBB, ActivationRange::activateEntity); + // Tuinity start + java.util.List entities = world.getEntities((Entity)null, maxBB, null); + for (int i = 0; i < entities.size(); i++) { + Entity entity = entities.get(i); + ActivationRange.activateEntity(entity); + } + // Tuinity end } 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`