mirror of
https://github.com/PurpurMC/Purpur.git
synced 2026-02-17 16:37:43 +01:00
Upstream has released updates that appear to apply and compile correctly Paper Changes: 02fbcbc Add reobf mappings patch for LevelChunk#level (CraftBukkit changes type) (#6079) 2641b91 Also deobfuscate secondary stacktraces in crash reports (#6078) cc063e1 Fix incorrect variable usage in per-player mob spawning patch (#6077) Tuinity Changes: 4867bfb Use correct y value for snow/ice formation 361be02 Update paper
18250 lines
897 KiB
Diff
18250 lines
897 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
diff --git a/build.gradle.kts b/build.gradle.kts
|
|
index 5540da58e66f83b283863d3158a9b4ab5ba636db..ab8de6c4e3c0bea2b9f498da00adf88e987d2364 100644
|
|
--- a/build.gradle.kts
|
|
+++ b/build.gradle.kts
|
|
@@ -1,10 +1,16 @@
|
|
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
|
|
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
|
|
import io.papermc.paperweight.tasks.BaseTask
|
|
+import io.papermc.paperweight.tasks.GenerateReobfMappings
|
|
+import io.papermc.paperweight.tasks.PatchMappings
|
|
+import io.papermc.paperweight.util.Constants
|
|
import io.papermc.paperweight.util.Git
|
|
+import io.papermc.paperweight.util.cache
|
|
import io.papermc.paperweight.util.defaultOutput
|
|
import io.papermc.paperweight.util.openZip
|
|
import io.papermc.paperweight.util.path
|
|
+import io.papermc.paperweight.util.registering
|
|
+import io.papermc.paperweight.util.set
|
|
import shadow.org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE
|
|
import java.nio.file.Files
|
|
import java.text.SimpleDateFormat
|
|
@@ -30,8 +36,8 @@ repositories {
|
|
}
|
|
|
|
dependencies {
|
|
- implementation(project(":Paper-API"))
|
|
- implementation(project(":Paper-MojangAPI"))
|
|
+ implementation(project(":Tuinity-API")) // Tuinity
|
|
+ implementation("com.destroystokyo.paper:paper-mojangapi:1.16.5-R0.1-SNAPSHOT") // Tuinity
|
|
// Paper start
|
|
implementation("org.jline:jline-terminal-jansi:3.12.1")
|
|
implementation("net.minecrell:terminalconsoleappender:1.2.0")
|
|
@@ -82,7 +88,7 @@ tasks.jar {
|
|
attributes(
|
|
"Main-Class" to "org.bukkit.craftbukkit.Main",
|
|
"Implementation-Title" to "CraftBukkit",
|
|
- "Implementation-Version" to "git-Paper-$implementationVersion",
|
|
+ "Implementation-Version" to "git-Tuinity-$implementationVersion", // Tuinity
|
|
"Implementation-Vendor" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date()), // Paper
|
|
"Specification-Title" to "Bukkit",
|
|
"Specification-Version" to project.version,
|
|
@@ -107,6 +113,22 @@ publishing {
|
|
}
|
|
}
|
|
|
|
+val generateReobfMappings = rootProject.tasks.named<GenerateReobfMappings>("generateReobfMappings")
|
|
+
|
|
+val patchReobfMappings by tasks.registering<PatchMappings> {
|
|
+ inputMappings.set(generateReobfMappings.flatMap { it.reobfMappings })
|
|
+ patch.set(rootProject.layout.cache.resolve("paperweight/upstreams/paper/build-data/reobf-mappings-patch.tiny"))
|
|
+
|
|
+ fromNamespace.set(Constants.DEOBF_NAMESPACE)
|
|
+ toNamespace.set(Constants.SPIGOT_NAMESPACE)
|
|
+
|
|
+ outputMappings.set(layout.cache.resolve("paperweight/mappings/reobf-patched.tiny"))
|
|
+}
|
|
+
|
|
+tasks.reobfJar {
|
|
+ mappingsFile.set(patchReobfMappings.flatMap { it.outputMappings })
|
|
+}
|
|
+
|
|
val generatePom = tasks.named<GenerateMavenPom>("generatePomFileForMavenPublication")
|
|
|
|
tasks.shadowJar {
|
|
@@ -178,7 +200,7 @@ tasks.test {
|
|
fun TaskContainer.registerRunTask(
|
|
name: String, block: JavaExec.() -> Unit
|
|
): TaskProvider<JavaExec> = register<JavaExec>(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<BlockPos> positions) {
|
|
+ for (final BlockPos pos : positions) {
|
|
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
|
|
+ }
|
|
+
|
|
+ this.performLightDecrease(lightAccess);
|
|
+ }
|
|
+
|
|
+ protected Iterator<BlockPos> 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<BlockPos> 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<BlockState> 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<BlockPos> 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<ArrayDeque<byte[]>> 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<BlockPos> 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<BlockPos> 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<BlockPos> 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<ChunkPos> chunks,
|
|
+ final Consumer<ChunkPos> 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<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
|
|
+ final Long2ObjectOpenHashMap<boolean[]> 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<AxisDirection> 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<ChunkPos> 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<SkyStarLightEngine> cachedSkyPropagators;
|
|
+ protected final ArrayDeque<BlockStarLightEngine> 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<Void> 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<Void> 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<ChunkPos> chunks, final Consumer<ChunkPos> 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<BlockPos> 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> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<BlockPos> changedPositions = new HashSet<>();
|
|
+ public Boolean[] changedSectionSet;
|
|
+ public ShortOpenHashSet queuedEdgeChecksSky;
|
|
+ public ShortOpenHashSet queuedEdgeChecksBlock;
|
|
+ public List<Runnable> lightTasks;
|
|
+
|
|
+ public final CompletableFuture<Void> onComplete = new CompletableFuture<>();
|
|
+
|
|
+ public final long chunkCoordinate;
|
|
+
|
|
+ public ChunkTasks(final long chunkCoordinate) {
|
|
+ this.chunkCoordinate = chunkCoordinate;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
|
|
index b9cdbf8acccfd6b207a0116f068168f3b8c8e17d..7404989c37ee1b7aa4e6999a063180d099532f7e 100644
|
|
--- a/src/main/java/co/aikar/timings/MinecraftTimings.java
|
|
+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
|
|
@@ -45,6 +45,8 @@ public final class MinecraftTimings {
|
|
|
|
public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update");
|
|
public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate");
|
|
+ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager
|
|
+ public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search
|
|
|
|
private static final Map<Class<?>, String> taskNameCache = new MapMaker().weakKeys().makeMap();
|
|
|
|
diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
|
|
index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..9d920565ff65a84b1b9a2a4777fd8bc8f07e0153 100644
|
|
--- a/src/main/java/co/aikar/timings/TimingsExport.java
|
|
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
|
|
@@ -153,7 +153,7 @@ public class TimingsExport extends Thread {
|
|
return pair(rule, world.getWorld().getGameRuleValue(rule));
|
|
})),
|
|
pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()),
|
|
- pair("notick-viewdistance", world.getChunkSource().chunkMap.getEffectiveNoTickViewDistance())
|
|
+ pair("notick-viewdistance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()) // Tuinity - replace old player chunk management
|
|
));
|
|
}));
|
|
|
|
@@ -228,7 +228,8 @@ public class TimingsExport extends Thread {
|
|
parent.put("config", createObject(
|
|
pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)),
|
|
pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)),
|
|
- pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
|
|
+ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Tuinity - add config to timings report
|
|
+ pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)) // Tuinity - add config to timings report
|
|
));
|
|
|
|
new TimingsExport(listeners, parent, history).start();
|
|
diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
|
|
index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..3918b24c98faa5232c7ffd733ba8000562132785 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/Metrics.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/Metrics.java
|
|
@@ -593,7 +593,7 @@ public class Metrics {
|
|
boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
|
|
// Only start Metrics, if it's enabled in the config
|
|
if (config.getBoolean("enabled", true)) {
|
|
- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
|
|
+ Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page
|
|
|
|
metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
|
|
String minecraftVersion = Bukkit.getVersion();
|
|
@@ -603,7 +603,7 @@ public class Metrics {
|
|
|
|
metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size()));
|
|
metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline"));
|
|
- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown"));
|
|
+ metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Tuinity - we have our own bstats page
|
|
|
|
metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
|
|
Map<String, Map<String, Integer>> 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<ChunkPos> 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<ChunkPos> 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<ChunkPos> queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius));
|
|
updateLight(sender, world, lightengine, queue);
|
|
diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
|
|
index 580bae0d414d371a07a6bfeefc41fdd989dc0083..d50b61876f15d95b836b3dd81d9c3492c91a8448 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
|
|
@@ -29,8 +29,8 @@ public class PaperVersionFetcher implements VersionFetcher {
|
|
@Nonnull
|
|
@Override
|
|
public Component getVersionMessage(@Nonnull String serverVersion) {
|
|
- String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]");
|
|
- final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]);
|
|
+ String[] parts = serverVersion.substring("git-Tuinity-".length()).split("[-\\s]"); // Tuinity
|
|
+ final Component updateMessage = getUpdateStatusMessage("Spottedleaf/Tuinity", GITHUB_BRANCH_NAME, parts[0]); // Tuinity
|
|
final Component history = getHistory();
|
|
|
|
return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage;
|
|
@@ -54,13 +54,10 @@ public class PaperVersionFetcher implements VersionFetcher {
|
|
|
|
private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) {
|
|
int distance;
|
|
- try {
|
|
- int jenkinsBuild = Integer.parseInt(versionInfo);
|
|
- distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion());
|
|
- } catch (NumberFormatException ignored) {
|
|
+ // Tuinity - we don't have jenkins setup
|
|
versionInfo = versionInfo.replace("\"", "");
|
|
distance = fetchDistanceFromGitHub(repo, branch, versionInfo);
|
|
- }
|
|
+ // Tuinity - we don't have jenkins setup
|
|
|
|
switch (distance) {
|
|
case -1:
|
|
diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
|
index da13ff17609b7bc8076d9297edf8decf01a2ed88..b4c69d39eee19339b1de295151d7ed3bf61635c1 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
|
@@ -312,6 +312,7 @@ public final class PaperTickList<T> extends ServerTickList<T> { // 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<ServerPlayer, PlayerLoaderData> playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f);
|
|
+ protected final ReferenceLinkedOpenHashSet<PlayerLoaderData> chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f);
|
|
+
|
|
+ protected final TreeSet<PlayerLoaderData> 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<PlayerLoaderData> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<PlayerLoaderData> 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<ChunkPriorityHolder> loadQueue = new ArrayDeque<>();
|
|
+ protected final LongOpenHashSet sentChunks = new LongOpenHashSet();
|
|
+
|
|
+ protected final TreeSet<ChunkPriorityHolder> 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<ChunkPriorityHolder> 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<RegionSection> regionsBySection = new Long2ObjectOpenHashMap<>();
|
|
+ protected final ReferenceLinkedOpenHashSet<Region> needsRecalculation = new ReferenceLinkedOpenHashSet<>();
|
|
+ protected final int minSectionRecalcCount;
|
|
+ protected final double maxDeadRegionPercent;
|
|
+ protected final Supplier<RegionData> regionDataSupplier;
|
|
+ protected final Supplier<RegionSectionData> regionSectionDataSupplier;
|
|
+
|
|
+ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount,
|
|
+ final double maxDeadRegionPercent, final int sectionMergeRadius,
|
|
+ final int regionSectionChunkShift,
|
|
+ final String name, final Supplier<RegionData> regionDataSupplier,
|
|
+ final Supplier<RegionSectionData> 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<Region<T>> checked = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ for (RegionSection<T> section : this.regionsBySection.values()) {
|
|
+ if (!checked.add(section.region)) {
|
|
+ section.region.check();
|
|
+ }
|
|
+ }
|
|
+ for (Region<T> 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<Region> 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<RegionSection> 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<RegionSection> 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<RegionSection> 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<RegionSection> sections = new IteratorSafeOrderedReferenceSet<>();
|
|
+ protected final ReferenceOpenHashSet<RegionSection> 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<RegionSection> 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<RegionSection<T>> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
|
|
+ final RegionSection<T> 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<RegionSection> 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<RegionSection> 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<Class<? extends net.minecraft.network.protocol.Packet<?>>, 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<AABB> TEMP_COLLISION_LIST = new UnsafeList<>(1024);
|
|
+ static boolean tempCollisionListInUse;
|
|
+
|
|
+ public static UnsafeList<AABB> getTempCollisionList() {
|
|
+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
|
|
+ return new UnsafeList<>(16);
|
|
+ }
|
|
+ tempCollisionListInUse = true;
|
|
+ return TEMP_COLLISION_LIST;
|
|
+ }
|
|
+
|
|
+ public static void returnTempCollisionList(List<AABB> list) {
|
|
+ if (list != TEMP_COLLISION_LIST) {
|
|
+ return;
|
|
+ }
|
|
+ ((UnsafeList)list).setSize(0);
|
|
+ tempCollisionListInUse = false;
|
|
+ }
|
|
+
|
|
+ static final UnsafeList<Entity> TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024);
|
|
+ static boolean tempGetEntitiesListInUse;
|
|
+
|
|
+ public static UnsafeList<Entity> getTempGetEntitiesList() {
|
|
+ if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) {
|
|
+ return new UnsafeList<>(16);
|
|
+ }
|
|
+ tempGetEntitiesListInUse = true;
|
|
+ return TEMP_GET_ENTITIES_LIST;
|
|
+ }
|
|
+
|
|
+ public static void returnTempGetEntitiesList(List<Entity> 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..089d66ce4913e97c5fc79daee0f2fd932664f28f
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java
|
|
@@ -0,0 +1,600 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+import com.tuinity.tuinity.voxel.AABBVoxelShape;
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.server.level.ServerChunkCache;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.WorldGenRegion;
|
|
+import net.minecraft.util.Mth;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.CollisionGetter;
|
|
+import net.minecraft.world.level.EntityGetter;
|
|
+import net.minecraft.world.level.block.Blocks;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.border.WorldBorder;
|
|
+import net.minecraft.world.level.chunk.ChunkAccess;
|
|
+import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
+import net.minecraft.world.phys.AABB;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
+import net.minecraft.world.phys.shapes.ArrayVoxelShape;
|
|
+import net.minecraft.world.phys.shapes.CollisionContext;
|
|
+import net.minecraft.world.phys.shapes.Shapes;
|
|
+import net.minecraft.world.phys.shapes.VoxelShape;
|
|
+import java.util.List;
|
|
+import java.util.function.BiPredicate;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+public final class CollisionUtil {
|
|
+
|
|
+ public static final double COLLISION_EPSILON = 1.0E-7;
|
|
+
|
|
+ public static boolean isEmpty(final AABB aabb) {
|
|
+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON && (aabb.maxY - aabb.minY) < COLLISION_EPSILON && (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
|
|
+ final double maxX, final double maxY, final double maxZ) {
|
|
+ return (maxX - minX) < COLLISION_EPSILON && (maxY - minY) < COLLISION_EPSILON && (maxZ - minZ) < COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) {
|
|
+ double x = (double)(chunkX << 4);
|
|
+ double z = (double)(chunkZ << 4);
|
|
+ // use a bounding box bigger than the chunk to prevent entities from entering it on move
|
|
+ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
|
|
+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false);
|
|
+ }
|
|
+
|
|
+ /*
|
|
+ A couple of rules for VoxelShape collisions:
|
|
+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
|
|
+ checks.
|
|
+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
|
|
+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
|
|
+ will automatically round it to 0.
|
|
+ */
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
|
|
+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
|
|
+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
|
|
+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
|
|
+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
|
|
+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ,
|
|
+ final double maxX, final double maxY, final double maxZ) {
|
|
+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON &&
|
|
+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON &&
|
|
+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) {
|
|
+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON &&
|
|
+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON &&
|
|
+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static double collideX(final AABB target, final AABB source, final double source_move) {
|
|
+ if (source_move == 0.0) {
|
|
+ return 0.0;
|
|
+ }
|
|
+
|
|
+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON &&
|
|
+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ public static double collideY(final AABB target, final AABB source, final double source_move) {
|
|
+ if (source_move == 0.0) {
|
|
+ return 0.0;
|
|
+ }
|
|
+
|
|
+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
|
|
+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ public static double collideZ(final AABB target, final AABB source, final double source_move) {
|
|
+ if (source_move == 0.0) {
|
|
+ return 0.0;
|
|
+ }
|
|
+
|
|
+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
|
|
+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ public static AABB offsetX(final AABB box, final double dx) {
|
|
+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB offsetY(final AABB box, final double dy) {
|
|
+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB offsetZ(final AABB box, final double dz) {
|
|
+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0
|
|
+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0
|
|
+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0
|
|
+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0
|
|
+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0
|
|
+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0
|
|
+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false);
|
|
+ }
|
|
+
|
|
+ public static double performCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> 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<AABB> 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<AABB> 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<AABB> 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<AABB> 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<AABB> 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<AABB> 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<AABB> 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<AABB> into, final boolean loadChunks, final boolean collidesWithUnloaded,
|
|
+ final boolean checkBorder, final boolean checkOnly, final BiPredicate<BlockState, BlockPos> 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<BlockState> blocks = section.states;
|
|
+
|
|
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
|
|
+ for (int currX = minX; currX <= maxX; ++currX) {
|
|
+ int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8);
|
|
+ int blockX = currX | chunkXGlobalPos;
|
|
+ int blockY = currY;
|
|
+ int blockZ = currZ | chunkZGlobalPos;
|
|
+
|
|
+ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
|
|
+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
|
|
+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0);
|
|
+ if (edgeCount == 3) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ BlockState blockData = blocks.get(localBlockIndex);
|
|
+ if (blockData.isAir()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
|
|
+ mutablePos.set(blockX, blockY, blockZ);
|
|
+ if (collisionShape == null) {
|
|
+ collisionShape = entity == null ? CollisionContext.empty() : CollisionContext.of(entity);
|
|
+ }
|
|
+ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape);
|
|
+ if (voxelshape2 != Shapes.empty()) {
|
|
+ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ);
|
|
+
|
|
+ if (predicate != null && !predicate.test(blockData, mutablePos)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (checkOnly) {
|
|
+ if (voxelshape3.intersects(aabb)) {
|
|
+ return true;
|
|
+ }
|
|
+ } else {
|
|
+ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb,
|
|
+ final List<AABB> into, final boolean checkOnly, final Predicate<Entity> 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<Entity> 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<AABB> into, final boolean loadChunks, final boolean collidesWithUnloadedChunks,
|
|
+ final boolean checkBorder, final boolean checkOnly, final BiPredicate<BlockState, BlockPos> blockPredicate,
|
|
+ final Predicate<Entity> entityPredicate) {
|
|
+ if (checkOnly) {
|
|
+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate)
|
|
+ || getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate);
|
|
+ } else {
|
|
+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate)
|
|
+ | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private CollisionUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f84060952c947d79bf2dffc61c96a300e8d7fac2
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
|
|
@@ -0,0 +1,128 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.core.SectionPos;
|
|
+import net.minecraft.util.Mth;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+
|
|
+public final class CoordinateUtils {
|
|
+
|
|
+ // dx, dz are relative to the target chunk
|
|
+ // dx, dz in [-radius, radius]
|
|
+ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
|
|
+ return (dx + radius) + (2 * radius + 1)*(dz + radius);
|
|
+ }
|
|
+
|
|
+ // the chunk keys are compatible with vanilla
|
|
+
|
|
+ public static long getChunkKey(final BlockPos pos) {
|
|
+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public static long getChunkKey(final Entity entity) {
|
|
+ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public static long getChunkKey(final ChunkPos pos) {
|
|
+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public static long getChunkKey(final SectionPos pos) {
|
|
+ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public static long getChunkKey(final int x, final int z) {
|
|
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public static int getChunkX(final long chunkKey) {
|
|
+ return (int)chunkKey;
|
|
+ }
|
|
+
|
|
+ public static int getChunkZ(final long chunkKey) {
|
|
+ return (int)(chunkKey >>> 32);
|
|
+ }
|
|
+
|
|
+ public static int getChunkCoordinate(final double blockCoordinate) {
|
|
+ return Mth.floor(blockCoordinate) >> 4;
|
|
+ }
|
|
+
|
|
+ // the section keys are compatible with vanilla's
|
|
+
|
|
+ static final int SECTION_X_BITS = 22;
|
|
+ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
|
|
+ static final int SECTION_Y_BITS = 20;
|
|
+ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
|
|
+ static final int SECTION_Z_BITS = 22;
|
|
+ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
|
|
+ // format is y,z,x (in order of LSB to MSB)
|
|
+ static final int SECTION_Y_SHIFT = 0;
|
|
+ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
|
|
+ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
|
|
+ static final int SECTION_TO_BLOCK_SHIFT = 4;
|
|
+
|
|
+ public static long getChunkSectionKey(final int x, final int y, final int z) {
|
|
+ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
|
|
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
|
|
+ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
|
|
+ }
|
|
+
|
|
+ public static long getChunkSectionKey(final SectionPos pos) {
|
|
+ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
|
|
+ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
|
|
+ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
|
|
+ }
|
|
+
|
|
+ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
|
|
+ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
|
|
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
|
|
+ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
|
|
+ }
|
|
+
|
|
+ public static long getChunkSectionKey(final BlockPos pos) {
|
|
+ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
|
|
+ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
|
|
+ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
|
|
+ }
|
|
+
|
|
+ public static long getChunkSectionKey(final Entity entity) {
|
|
+ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
|
|
+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
|
|
+ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
|
|
+ }
|
|
+
|
|
+ public static int getChunkSectionX(final long key) {
|
|
+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
|
|
+ }
|
|
+
|
|
+ public static int getChunkSectionY(final long key) {
|
|
+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
|
|
+ }
|
|
+
|
|
+ public static int getChunkSectionZ(final long key) {
|
|
+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
|
|
+ }
|
|
+
|
|
+ // the block coordinates are not necessarily compatible with vanilla's
|
|
+
|
|
+ public static int getBlockCoordinate(final double blockCoordinate) {
|
|
+ return Mth.floor(blockCoordinate);
|
|
+ }
|
|
+
|
|
+ public static long getBlockKey(final int x, final int y, final int z) {
|
|
+ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
|
|
+ }
|
|
+
|
|
+ public static long getBlockKey(final BlockPos pos) {
|
|
+ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
|
|
+ }
|
|
+
|
|
+ public static long getBlockKey(final Entity entity) {
|
|
+ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
|
|
+ }
|
|
+
|
|
+ private CoordinateUtils() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
|
|
@@ -0,0 +1,226 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+public final class IntegerUtil {
|
|
+
|
|
+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
|
|
+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
|
|
+
|
|
+ public static int ceilLog2(final int value) {
|
|
+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static long ceilLog2(final long value) {
|
|
+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int floorLog2(final int value) {
|
|
+ // xor is optimized subtract for 2^n -1
|
|
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
|
|
+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int floorLog2(final long value) {
|
|
+ // xor is optimized subtract for 2^n -1
|
|
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
|
|
+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int roundCeilLog2(final int value) {
|
|
+ // optimized variant of 1 << (32 - leading(val - 1))
|
|
+ // given
|
|
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
|
|
+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
|
|
+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
|
|
+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
|
|
+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
|
|
+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
|
|
+ }
|
|
+
|
|
+ public static long roundCeilLog2(final long value) {
|
|
+ // see logic documented above
|
|
+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
|
|
+ }
|
|
+
|
|
+ public static int roundFloorLog2(final int value) {
|
|
+ // optimized variant of 1 << (31 - leading(val))
|
|
+ // given
|
|
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
|
|
+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
|
|
+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
|
|
+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
|
|
+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
|
|
+ }
|
|
+
|
|
+ public static long roundFloorLog2(final long value) {
|
|
+ // see logic documented above
|
|
+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
|
|
+ }
|
|
+
|
|
+ public static boolean isPowerOfTwo(final int n) {
|
|
+ // 2^n has one bit
|
|
+ // note: this rets true for 0 still
|
|
+ return IntegerUtil.getTrailingBit(n) == n;
|
|
+ }
|
|
+
|
|
+ public static boolean isPowerOfTwo(final long n) {
|
|
+ // 2^n has one bit
|
|
+ // note: this rets true for 0 still
|
|
+ return IntegerUtil.getTrailingBit(n) == n;
|
|
+ }
|
|
+
|
|
+ public static int getTrailingBit(final int n) {
|
|
+ return -n & n;
|
|
+ }
|
|
+
|
|
+ public static long getTrailingBit(final long n) {
|
|
+ return -n & n;
|
|
+ }
|
|
+
|
|
+ public static int trailingZeros(final int n) {
|
|
+ return Integer.numberOfTrailingZeros(n);
|
|
+ }
|
|
+
|
|
+ public static int trailingZeros(final long n) {
|
|
+ return Long.numberOfTrailingZeros(n);
|
|
+ }
|
|
+
|
|
+ // from hacker's delight (signed division magic value)
|
|
+ public static int getDivisorMultiple(final long numbers) {
|
|
+ return (int)(numbers >>> 32);
|
|
+ }
|
|
+
|
|
+ // from hacker's delight (signed division magic value)
|
|
+ public static int getDivisorShift(final long numbers) {
|
|
+ return (int)numbers;
|
|
+ }
|
|
+
|
|
+ // copied from hacker's delight (signed division magic value)
|
|
+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
|
|
+ public static long getDivisorNumbers(final int d) {
|
|
+ final int ad = IntegerUtil.branchlessAbs(d);
|
|
+
|
|
+ if (ad < 2) {
|
|
+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
|
|
+ }
|
|
+
|
|
+ final int two31 = 0x80000000;
|
|
+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
|
|
+
|
|
+ int p = 31;
|
|
+
|
|
+ // all these variables are UNSIGNED!
|
|
+ int t = two31 + (d >>> 31);
|
|
+ int anc = t - 1 - t%ad;
|
|
+ int q1 = (int)((two31 & mask)/(anc & mask));
|
|
+ int r1 = two31 - q1*anc;
|
|
+ int q2 = (int)((two31 & mask)/(ad & mask));
|
|
+ int r2 = two31 - q2*ad;
|
|
+ int delta;
|
|
+
|
|
+ do {
|
|
+ p = p + 1;
|
|
+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
|
|
+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
|
|
+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
|
|
+ q1 = q1 + 1;
|
|
+ r1 = r1 - anc;
|
|
+ }
|
|
+ q2 = 2*q2; // Update q2 = 2**p/|d|.
|
|
+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
|
|
+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
|
|
+ q2 = q2 + 1;
|
|
+ r2 = r2 - ad;
|
|
+ }
|
|
+ delta = ad - r2;
|
|
+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
|
|
+
|
|
+ int magicNum = q2 + 1;
|
|
+ if (d < 0) {
|
|
+ magicNum = -magicNum;
|
|
+ }
|
|
+ int shift = p - 32;
|
|
+ return ((long)magicNum << 32) | shift;
|
|
+ }
|
|
+
|
|
+ public static int branchlessAbs(final int val) {
|
|
+ // -n = -1 ^ n + 1
|
|
+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
|
|
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
|
|
+ }
|
|
+
|
|
+ public static long branchlessAbs(final long val) {
|
|
+ // -n = -1 ^ n + 1
|
|
+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
|
|
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
|
|
+ }
|
|
+
|
|
+ //https://github.com/skeeto/hash-prospector for hash functions
|
|
+
|
|
+ //score = ~590.47984224483832
|
|
+ public static int hash0(int x) {
|
|
+ x *= 0x36935555;
|
|
+ x ^= x >>> 16;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ //score = ~310.01596637036749
|
|
+ public static int hash1(int x) {
|
|
+ x ^= x >>> 15;
|
|
+ x *= 0x356aaaad;
|
|
+ x ^= x >>> 17;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ public static int hash2(int x) {
|
|
+ x ^= x >>> 16;
|
|
+ x *= 0x7feb352d;
|
|
+ x ^= x >>> 15;
|
|
+ x *= 0x846ca68b;
|
|
+ x ^= x >>> 16;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ public static int hash3(int x) {
|
|
+ x ^= x >>> 17;
|
|
+ x *= 0xed5ad4bb;
|
|
+ x ^= x >>> 11;
|
|
+ x *= 0xac4c1b51;
|
|
+ x ^= x >>> 15;
|
|
+ x *= 0x31848bab;
|
|
+ x ^= x >>> 14;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ //score = ~365.79959673201887
|
|
+ public static long hash1(long x) {
|
|
+ x ^= x >>> 27;
|
|
+ x *= 0xb24924b71d2d354bL;
|
|
+ x ^= x >>> 28;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ //h2 hash
|
|
+ public static long hash2(long x) {
|
|
+ x ^= x >>> 32;
|
|
+ x *= 0xd6e8feb86659fd93L;
|
|
+ x ^= x >>> 32;
|
|
+ x *= 0xd6e8feb86659fd93L;
|
|
+ x ^= x >>> 32;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ public static long hash3(long x) {
|
|
+ x ^= x >>> 45;
|
|
+ x *= 0xc161abe5704b6c79L;
|
|
+ x ^= x >>> 41;
|
|
+ x *= 0xe3e5389aedbc90f7L;
|
|
+ x ^= x >>> 56;
|
|
+ x *= 0x1f9aba75a52db073L;
|
|
+ x ^= x >>> 53;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ private IntegerUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
|
|
@@ -0,0 +1,100 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+public final class IntervalledCounter {
|
|
+
|
|
+ protected long[] times;
|
|
+ protected final long interval;
|
|
+ protected long minTime;
|
|
+ protected int sum;
|
|
+ protected int head; // inclusive
|
|
+ protected int tail; // exclusive
|
|
+
|
|
+ public IntervalledCounter(final long interval) {
|
|
+ this.times = new long[8];
|
|
+ this.interval = interval;
|
|
+ }
|
|
+
|
|
+ public void updateCurrentTime() {
|
|
+ this.updateCurrentTime(System.nanoTime());
|
|
+ }
|
|
+
|
|
+ public void updateCurrentTime(final long currentTime) {
|
|
+ int sum = this.sum;
|
|
+ int head = this.head;
|
|
+ final int tail = this.tail;
|
|
+ final long minTime = currentTime - this.interval;
|
|
+
|
|
+ final int arrayLen = this.times.length;
|
|
+
|
|
+ // guard against overflow by using subtraction
|
|
+ while (head != tail && this.times[head] - minTime < 0) {
|
|
+ head = (head + 1) % arrayLen;
|
|
+ --sum;
|
|
+ }
|
|
+
|
|
+ this.sum = sum;
|
|
+ this.head = head;
|
|
+ this.minTime = minTime;
|
|
+ }
|
|
+
|
|
+ public void addTime(final long currTime) {
|
|
+ // guard against overflow by using subtraction
|
|
+ if (currTime - this.minTime < 0) {
|
|
+ return;
|
|
+ }
|
|
+ int nextTail = (this.tail + 1) % this.times.length;
|
|
+ if (nextTail == this.head) {
|
|
+ this.resize();
|
|
+ nextTail = (this.tail + 1) % this.times.length;
|
|
+ }
|
|
+
|
|
+ this.times[this.tail] = currTime;
|
|
+ this.tail = nextTail;
|
|
+ }
|
|
+
|
|
+ public void updateAndAdd(final int count) {
|
|
+ final long currTime = System.nanoTime();
|
|
+ this.updateCurrentTime(currTime);
|
|
+ for (int i = 0; i < count; ++i) {
|
|
+ this.addTime(currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void updateAndAdd(final int count, final long currTime) {
|
|
+ this.updateCurrentTime(currTime);
|
|
+ for (int i = 0; i < count; ++i) {
|
|
+ this.addTime(currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void resize() {
|
|
+ final long[] oldElements = this.times;
|
|
+ final long[] newElements = new long[this.times.length * 2];
|
|
+ this.times = newElements;
|
|
+
|
|
+ final int head = this.head;
|
|
+ final int tail = this.tail;
|
|
+ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
|
|
+ this.head = 0;
|
|
+ this.tail = size;
|
|
+
|
|
+ if (tail >= head) {
|
|
+ System.arraycopy(oldElements, head, newElements, 0, size);
|
|
+ } else {
|
|
+ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
|
|
+ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // returns in units per second
|
|
+ public double getRate() {
|
|
+ return this.size() / (this.interval * 1.0e-9);
|
|
+ }
|
|
+
|
|
+ public int size() {
|
|
+ final int head = this.head;
|
|
+ final int tail = this.tail;
|
|
+
|
|
+ return tail >= head ? (tail - head) : (tail + (this.times.length - head));
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a08377e4b0d9c2d78cf851e2c72770cf623de51a
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/TickThread.java
|
|
@@ -0,0 +1,41 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import org.bukkit.Bukkit;
|
|
+
|
|
+public final class TickThread extends Thread {
|
|
+
|
|
+ public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("tuinity.strict-thread-checks");
|
|
+
|
|
+ static {
|
|
+ if (STRICT_THREAD_CHECKS) {
|
|
+ MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static void softEnsureTickThread(final String reason) {
|
|
+ if (!STRICT_THREAD_CHECKS) {
|
|
+ return;
|
|
+ }
|
|
+ ensureTickThread(reason);
|
|
+ }
|
|
+
|
|
+
|
|
+ public static void ensureTickThread(final String reason) {
|
|
+ if (!Bukkit.isPrimaryThread()) {
|
|
+ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
|
|
+ throw new IllegalStateException(reason);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
|
|
+
|
|
+ public TickThread(final Runnable run, final String name, final int id) {
|
|
+ super(run, name);
|
|
+ this.id = id;
|
|
+ }
|
|
+
|
|
+ public static TickThread getCurrentTickThread() {
|
|
+ return (TickThread)Thread.currentThread();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..958b3aff3dda64323456d7e0ef0346a72d43f3f1
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
|
|
@@ -0,0 +1,46 @@
|
|
+package com.tuinity.tuinity.util;
|
|
+
|
|
+import net.minecraft.world.level.LevelHeightAccessor;
|
|
+
|
|
+public final class WorldUtil {
|
|
+
|
|
+ // min, max are inclusive
|
|
+
|
|
+ public static int getMaxSection(final LevelHeightAccessor world) {
|
|
+ return world.getMaxSection() - 1; // getMaxSection() is exclusive
|
|
+ }
|
|
+
|
|
+ public static int getMinSection(final LevelHeightAccessor world) {
|
|
+ return world.getMinSection();
|
|
+ }
|
|
+
|
|
+ public static int getMaxLightSection(final LevelHeightAccessor world) {
|
|
+ return getMaxSection(world) + 1;
|
|
+ }
|
|
+
|
|
+ public static int getMinLightSection(final LevelHeightAccessor world) {
|
|
+ return getMinSection(world) - 1;
|
|
+ }
|
|
+
|
|
+
|
|
+
|
|
+ public static int getTotalSections(final LevelHeightAccessor world) {
|
|
+ return getMaxSection(world) - getMinSection(world) + 1;
|
|
+ }
|
|
+
|
|
+ public static int getTotalLightSections(final LevelHeightAccessor world) {
|
|
+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
|
|
+ }
|
|
+
|
|
+ public static int getMinBlockY(final LevelHeightAccessor world) {
|
|
+ return getMinSection(world) << 4;
|
|
+ }
|
|
+
|
|
+ public static int getMaxBlockY(final LevelHeightAccessor world) {
|
|
+ return (getMaxSection(world) << 4) | 15;
|
|
+ }
|
|
+
|
|
+ private WorldUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..98a1be343d81d6431476fea6a68014def8ce923b
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
|
|
@@ -0,0 +1,334 @@
|
|
+package com.tuinity.tuinity.util.maplist;
|
|
+
|
|
+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
|
|
+import org.bukkit.Bukkit;
|
|
+import java.util.Arrays;
|
|
+import java.util.NoSuchElementException;
|
|
+
|
|
+public final class IteratorSafeOrderedReferenceSet<E> {
|
|
+
|
|
+ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
|
|
+
|
|
+ protected final Reference2IntLinkedOpenHashMap<E> 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<E> 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<E> 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<Reference2IntMap.Entry<E>> 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<E>() {
|
|
+ @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<E> 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<E> iterator() {
|
|
+ return this.iterator(0);
|
|
+ }
|
|
+
|
|
+ public IteratorSafeOrderedReferenceSet.Iterator<E> 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<E> unsafeIterator() {
|
|
+ return this.unsafeIterator(0);
|
|
+ }
|
|
+ public java.util.Iterator<E> unsafeIterator(final int flags) {
|
|
+ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
|
|
+ }
|
|
+
|
|
+ public static interface Iterator<E> extends java.util.Iterator<E> {
|
|
+
|
|
+ public void finishedIterating();
|
|
+
|
|
+ }
|
|
+
|
|
+ protected static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
|
|
+
|
|
+ protected final IteratorSafeOrderedReferenceSet<E> 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<E> 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<Ticket> 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<Ticket> empty
|
|
+ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
|
|
+
|
|
+ }
|
|
+
|
|
+ public static void main(final String[] args) {
|
|
+ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
|
|
+ @Override
|
|
+ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> 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<Ticket> 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<Ticket> 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<Property<?>, 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<Property<?>, Comparable<?>> entry : this_map.entrySet()) {
|
|
+ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void loadInTable(final Table<Property<?>, Comparable<?>, StateHolder<?, ?>> table,
|
|
+ final Map<Property<?>, Comparable<?>> this_map) {
|
|
+ final Set<Property<?>> 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<Property<?>, Map<Comparable<?>, StateHolder<?, ?>>> map = table.rowMap();
|
|
+ for (final Property<?> property : map.keySet()) {
|
|
+ final Map<Comparable<?>, 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<Comparable<?>, StateHolder<?, ?>> entry : propertyMap.entrySet()) {
|
|
+ if (entry.getValue() == null) {
|
|
+ // TODO what
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue();
|
|
+ }
|
|
+ }
|
|
+
|
|
+
|
|
+ for (final Map.Entry<Property<?>, 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<Property<?>> 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<AABB> toAabbs() { // getAABBs
|
|
+ List<AABB> 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<Class<? extends Entity>, 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<org.bukkit.entity.Entity> 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<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
|
|
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
|
|
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, 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<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
|
|
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
|
|
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, 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<Entity> into, final Predicate<? super Entity> predicate) {
|
|
+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
|
|
+ }
|
|
+
|
|
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
|
|
+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
|
|
+ }
|
|
+
|
|
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
|
|
+ final Predicate<? super T> predicate) {
|
|
+ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate);
|
|
+ }
|
|
+
|
|
+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
|
|
+ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
|
|
+
|
|
+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
|
|
+ final BasicEntityList<Entity> 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 <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
|
|
+ final Predicate<? super T> 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<E extends Entity> {
|
|
+
|
|
+ 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<Entity>[] 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<Entity> 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<Entity> 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<Entity> into, final Predicate<? super Entity> 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<Entity>[] entitiesBySection = this.entitiesBySection;
|
|
+
|
|
+ for (int section = min; section <= max; ++section) {
|
|
+ final BasicEntityList<Entity> 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<Entity> into,
|
|
+ final Predicate<? super Entity> 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<Entity>[] entitiesBySection = this.entitiesBySection;
|
|
+
|
|
+ for (int section = min; section <= max; ++section) {
|
|
+ final BasicEntityList<Entity> 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<Entity> into,
|
|
+ final Predicate<? super Entity> 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<Entity>[] entitiesBySection = this.entitiesBySection;
|
|
+
|
|
+ for (int section = min; section <= max; ++section) {
|
|
+ final BasicEntityList<Entity> 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 <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
|
|
+ final Predicate<? super T> 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<Entity>[] entitiesBySection = this.entitiesBySection;
|
|
+
|
|
+ for (int section = min; section <= max; ++section) {
|
|
+ final BasicEntityList<Entity> 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<ChunkSlicesRegion> regions = new Long2ObjectOpenHashMap<>(64, 0.7f);
|
|
+
|
|
+ private final int minSection; // inclusive
|
|
+ private final int maxSection; // inclusive
|
|
+
|
|
+ protected final Long2ObjectOpenHashMap<ChunkHolder.FullChunkStatus> 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<Entity> into, final Predicate<? super Entity> 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<Entity> into, final Predicate<? super Entity> 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 <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
|
|
+ final Predicate<? super T> 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 <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
|
|
+ final Predicate<? super T> 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<Vec3i> {
|
|
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<Vec3i> {
|
|
return this.z;
|
|
}
|
|
|
|
- public Vec3i setX(int x) {
|
|
+ protected Vec3i setX(int x) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
|
|
this.x = x;
|
|
return this;
|
|
}
|
|
|
|
- public Vec3i setY(int y) {
|
|
+ protected Vec3i setY(int y) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
|
|
this.y = y;
|
|
return this;
|
|
}
|
|
|
|
- public Vec3i setZ(int z) {
|
|
+ protected Vec3i setZ(int z) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
|
|
this.z = z;
|
|
return this;
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
|
|
index c203a78a28e6457bd25b34b5c5ecaa35e3f9211e..0232fb8123c7dfa735802442f8575c6ce1566847 100644
|
|
--- a/src/main/java/net/minecraft/network/Connection.java
|
|
+++ b/src/main/java/net/minecraft/network/Connection.java
|
|
@@ -49,6 +49,8 @@ import org.apache.logging.log4j.Logger;
|
|
import org.apache.logging.log4j.Marker;
|
|
import org.apache.logging.log4j.MarkerManager;
|
|
|
|
+
|
|
+import io.netty.util.concurrent.AbstractEventExecutor; // Tuinity
|
|
public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
|
|
private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F;
|
|
@@ -93,6 +95,77 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
public boolean queueImmunity = false;
|
|
public ConnectionProtocol protocol;
|
|
// Paper end
|
|
+ // Tuinity start - add pending task queue
|
|
+ private final Queue<Runnable> 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<Class<? extends net.minecraft.network.protocol.Packet<?>>, 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<Packet<?>> {
|
|
|
|
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<Packet<?>> {
|
|
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<Packet<?>> {
|
|
}
|
|
|
|
private void sendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback) {
|
|
+ // Tuinity start - add flush parameter
|
|
+ this.writePacket(packet, callback, Boolean.TRUE);
|
|
+ }
|
|
+ private void writePacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> 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<Packet<?>> {
|
|
}
|
|
|
|
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<? extends Future<? super Void>> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1) {
|
|
+ // Tuinity start - add flush parameter
|
|
+ this.a(packet, genericfuturelistener, enumprotocol, enumprotocol1, true);
|
|
+ }
|
|
+ private void a(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> 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<Packet<?>> {
|
|
|
|
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<Packet<?>> {
|
|
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<PacketHolder> iterator = this.queue.iterator();
|
|
@@ -361,19 +501,31 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
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<Packet<?>> {
|
|
}
|
|
|
|
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<PacketListener> 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<PacketListener> getCurrentPacketProcessors() {
|
|
+ java.util.List<PacketListener> ret = new java.util.ArrayList<>(4);
|
|
+ for (PacketListener listener : packetProcessing) {
|
|
+ ret.add(listener);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Tuinity end - detailed watchdog information
|
|
+
|
|
public PacketUtils() {}
|
|
|
|
public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> 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<ClientGamePacketList
|
|
|
|
@Override
|
|
public void onPacketDispatch(ServerPlayer player) {
|
|
- remainingSends.incrementAndGet();
|
|
+ // Tuinity - rewrite light engine
|
|
}
|
|
|
|
@Override
|
|
public void onPacketDispatchFinish(ServerPlayer player, ChannelFuture future) {
|
|
- if (remainingSends.decrementAndGet() <= 0) {
|
|
- // incase of any race conditions, schedule this delayed
|
|
- MCUtil.scheduleTask(5, () -> {
|
|
- 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<ClientGamePacketList
|
|
this.blockYMask = new BitSet();
|
|
this.emptySkyYMask = new BitSet();
|
|
this.emptyBlockYMask = new BitSet();
|
|
- this.skyUpdates = Lists.newArrayList();this.cleaner1 = MCUtil.registerListCleaner(this, this.skyUpdates, DataLayer::releaseBytes); // Paper
|
|
- this.blockUpdates = Lists.newArrayList();this.cleaner2 = MCUtil.registerListCleaner(this, this.blockUpdates, DataLayer::releaseBytes); // Paper
|
|
+ this.skyUpdates = Lists.newArrayList();// Tuinity - rewrite light engine
|
|
+ this.blockUpdates = Lists.newArrayList();// Tuinity - rewrite light engine
|
|
|
|
for(int i = 0; i < lightProvider.getLightSectionCount(); ++i) {
|
|
if (bitSet == null || bitSet.get(i)) {
|
|
@@ -85,7 +77,7 @@ public class ClientboundLightUpdatePacket implements Packet<ClientGamePacketList
|
|
bitSet2.set(i);
|
|
} else {
|
|
bitSet.set(i);
|
|
- list.add((byte[])dataLayer.getCloneIfSet()); // Paper
|
|
+ list.add((byte[])dataLayer.getDataRaw()); // Paper // Tuinity - rewrite light engine - data is already cloned, don't do it again
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/Eula.java b/src/main/java/net/minecraft/server/Eula.java
|
|
index a1d5c0f8fe2adb2ee56f3217e089211ec7c61eb0..3789df8ef9c0b4150c3baccf84dbaff57c0fb65a 100644
|
|
--- a/src/main/java/net/minecraft/server/Eula.java
|
|
+++ b/src/main/java/net/minecraft/server/Eula.java
|
|
@@ -64,7 +64,7 @@ public class Eula {
|
|
try {
|
|
Properties properties = new Properties();
|
|
properties.setProperty("eula", "false");
|
|
- properties.store(outputStream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\nYou also agree that tacos are tasty, and the best food in the world."); // Paper - fix lag;
|
|
+ properties.store(outputStream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula)."); // Paper - fix lag; // Tuinity - Tacos are disgusting
|
|
} catch (Throwable var5) {
|
|
if (outputStream != null) {
|
|
try {
|
|
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
|
|
index 9ddedd310eb0323a5a09f51a61bfb7b36503be93..f03001793df82a3316021d280f3c8195fa65479d 100644
|
|
--- a/src/main/java/net/minecraft/server/MCUtil.java
|
|
+++ b/src/main/java/net/minecraft/server/MCUtil.java
|
|
@@ -637,7 +637,7 @@ public final class MCUtil {
|
|
|
|
worldData.addProperty("name", world.getWorld().getName());
|
|
worldData.addProperty("view-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance());
|
|
- worldData.addProperty("no-view-distance", world.getChunkSource().chunkMap.getRawNoTickViewDistance());
|
|
+ worldData.addProperty("no-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Tuinity - replace old player chunk management
|
|
worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
|
|
worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange);
|
|
worldData.addProperty("visible-chunk-count", visibleChunks.size());
|
|
@@ -718,4 +718,70 @@ public final class MCUtil {
|
|
public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
|
|
return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
|
|
}
|
|
+
|
|
+ public static <A> com.mojang.serialization.MapCodec<A> fieldWithFallbacks(com.mojang.serialization.Codec<A> 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<A> extends com.mojang.serialization.MapDecoder.Implementation<A> {
|
|
+ protected final String name;
|
|
+ protected final List<String> fallback;
|
|
+ private final com.mojang.serialization.Decoder<A> elementCodec;
|
|
+
|
|
+ public FieldFallbackDecoder(final String name, final List<String> fallback, final com.mojang.serialization.Decoder<A> elementCodec) {
|
|
+ this.name = name;
|
|
+ this.fallback = fallback;
|
|
+ this.elementCodec = elementCodec;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T> com.mojang.serialization.DataResult<A> decode(final com.mojang.serialization.DynamicOps<T> ops, final com.mojang.serialization.MapLike<T> 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 <T> java.util.stream.Stream<T> keys(final com.mojang.serialization.DynamicOps<T> ops) {
|
|
+ return java.util.stream.Stream.of(ops.createString(name));
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean equals(final Object o) {
|
|
+ if (this == o) {
|
|
+ return true;
|
|
+ }
|
|
+ if (o == null || getClass() != o.getClass()) {
|
|
+ return false;
|
|
+ }
|
|
+ final FieldFallbackDecoder<?> that = (FieldFallbackDecoder<?>)o;
|
|
+ return java.util.Objects.equals(name, that.name) && java.util.Objects.equals(elementCodec, that.elementCodec)
|
|
+ && java.util.Objects.equals(fallback, that.fallback);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ return java.util.Objects.hash(name, fallback, elementCodec);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "FieldDecoder[" + name + ": " + elementCodec + ']';
|
|
+ }
|
|
+ }
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
|
|
index cfd43069ee2b6f79afb12e10d223f6bf75100034..75c31ec2553c0959f1ac34b554a39a73144da8bd 100644
|
|
--- a/src/main/java/net/minecraft/server/Main.java
|
|
+++ b/src/main/java/net/minecraft/server/Main.java
|
|
@@ -57,7 +57,7 @@ import org.apache.logging.log4j.Logger;
|
|
// CraftBukkit start
|
|
import net.minecraft.SharedConstants;
|
|
|
|
-public class Main {
|
|
+public class Main { //
|
|
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
index 7682bd72c3932a9b20f14e552711d74f70b969b1..f3bf6270c7735869083559f907c65d95144dbe6f 100644
|
|
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
@@ -297,6 +297,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public org.bukkit.command.RemoteConsoleCommandSender remoteConsole;
|
|
//public ConsoleReader reader; // Paper
|
|
public static int currentTick = 0; // Paper - Further improve tick loop
|
|
+ public static long currentTickLong = 0L; // Tuinity
|
|
public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
|
|
public int autosavePeriod;
|
|
public boolean serverAutoSave = false; // Paper
|
|
@@ -331,6 +332,76 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
return s0;
|
|
}
|
|
|
|
+ // Tuinity start - execute chunk tasks mid tick
|
|
+ static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us
|
|
+ static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
|
|
+
|
|
+ static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
|
|
+
|
|
+ private static long lastMidTickExecute;
|
|
+ private static long lastMidTickExecuteFailure;
|
|
+
|
|
+ private boolean tickMidTickTasks() {
|
|
+ // give all worlds a fair chance at by targetting them all.
|
|
+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
|
|
+ boolean executed = false;
|
|
+ for (ServerLevel world : this.getAllLevels()) {
|
|
+ long currTime = System.nanoTime();
|
|
+ if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
|
|
+ continue;
|
|
+ }
|
|
+ if (!world.getChunkSource().pollTask()) {
|
|
+ // we need to back off if this fails
|
|
+ world.lastMidTickExecuteFailure = currTime;
|
|
+ } else {
|
|
+ executed = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return executed;
|
|
+ }
|
|
+
|
|
+ public final void executeMidTickTasks() {
|
|
+ org.spigotmc.AsyncCatcher.catchOp("mid tick chunk task execution");
|
|
+ long startTime = System.nanoTime();
|
|
+ if ((startTime - lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
|
|
+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
|
|
+ // so, backoff to prevent this
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ co.aikar.timings.MinecraftTimings.midTickChunkTasks.startTiming();
|
|
+ try {
|
|
+ for (;;) {
|
|
+ boolean moreTasks = this.tickMidTickTasks();
|
|
+ long currTime = System.nanoTime();
|
|
+ long diff = currTime - startTime;
|
|
+
|
|
+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
|
|
+ if (!moreTasks) {
|
|
+ lastMidTickExecuteFailure = currTime;
|
|
+ }
|
|
+
|
|
+ // note: negative values reduce the time
|
|
+ long overuse = diff - MAX_CHUNK_EXEC_TIME;
|
|
+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms
|
|
+ // make sure something like a GC or dumb plugin doesn't screw us over...
|
|
+ overuse = 10L * 1000L * 1000L; // 10ms
|
|
+ }
|
|
+
|
|
+ double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
|
|
+ long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
|
|
+
|
|
+ lastMidTickExecute = currTime + extraSleep;
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ } finally {
|
|
+ co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming();
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - execute chunk tasks mid tick
|
|
+
|
|
public MinecraftServer(OptionSet options, DataPackConfig datapackconfiguration, Thread thread, RegistryAccess.RegistryHolder iregistrycustom_dimension, LevelStorageSource.LevelStorageAccess convertable_conversionsession, WorldData savedata, PackRepository resourcepackrepository, Proxy proxy, DataFixer datafixer, ServerResources datapackresources, @Nullable MinecraftSessionService minecraftsessionservice, @Nullable GameProfileRepository gameprofilerepository, @Nullable GameProfileCache usercache, ChunkProgressListenerFactory worldloadlistenerfactory) {
|
|
super("Server");
|
|
SERVER = this; // Paper - better singleton
|
|
@@ -1133,6 +1204,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
LOGGER.info("Done ({})! For help, type \"help\"", doneTime);
|
|
// Paper end
|
|
|
|
+ com.tuinity.tuinity.config.TuinityConfig.createWorldSections = false; // Tuinity - don't let plugin created worlds fill our config
|
|
org.spigotmc.WatchdogThread.tick(); // Paper
|
|
org.spigotmc.WatchdogThread.hasStarted = true; // Paper
|
|
Arrays.fill( recentTps, 20 );
|
|
@@ -1150,6 +1222,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.lastOverloadWarning = this.nextTickTime;
|
|
}
|
|
|
|
+ ++MinecraftServer.currentTickLong; // Tuinity
|
|
if ( ++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0 )
|
|
{
|
|
final long diff = curTime - tickSection;
|
|
@@ -1164,7 +1237,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
tickSection = curTime;
|
|
}
|
|
- midTickChunksTasksRan = 0; // Paper
|
|
+ // Tuinity - replace logic
|
|
// Spigot end
|
|
|
|
if (this.debugCommandProfilerDelayStart) {
|
|
@@ -1275,22 +1348,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
});
|
|
}
|
|
|
|
- // Paper start
|
|
- public int midTickChunksTasksRan = 0;
|
|
- private long midTickLastRan = 0;
|
|
- public void midTickLoadChunks() {
|
|
- if (!isSameThread() || System.nanoTime() - midTickLastRan < 1000000) {
|
|
- // only check once per 0.25ms incase this code is called in a hot method
|
|
- return;
|
|
- }
|
|
- try (co.aikar.timings.Timing ignored = co.aikar.timings.MinecraftTimings.midTickChunkTasks.startTiming()) {
|
|
- for (ServerLevel value : this.getAllLevels()) {
|
|
- value.getChunkSource().mainThreadProcessor.midTickLoadChunks();
|
|
- }
|
|
- midTickLastRan = System.nanoTime();
|
|
- }
|
|
- }
|
|
- // Paper end
|
|
+ // Tuinity - replace logic
|
|
|
|
@Override
|
|
public TickTask wrapRunnable(Runnable runnable) {
|
|
@@ -1317,6 +1375,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
private boolean pollTaskInternal() {
|
|
if (super.pollTask()) {
|
|
+ this.executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick
|
|
return true;
|
|
} else {
|
|
if (this.haveTime()) {
|
|
@@ -1393,7 +1452,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper start - move oversleep into full server tick
|
|
isOversleep = true;MinecraftTimings.serverOversleep.startTiming();
|
|
this.managedBlock(() -> {
|
|
- midTickLoadChunks(); // will only do loads since we are still considered !canSleepForTick
|
|
+ // Tuinity - replace logic
|
|
return !this.canOversleep();
|
|
});
|
|
isOversleep = false;MinecraftTimings.serverOversleep.stopTiming();
|
|
@@ -1457,6 +1516,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
// Paper end
|
|
|
|
+ com.tuinity.tuinity.util.CachedLists.reset(); // Tuinity
|
|
// Paper start
|
|
long endTime = System.nanoTime();
|
|
long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
|
|
@@ -1483,16 +1543,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public void tickChildren(BooleanSupplier shouldKeepTicking) {
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper
|
|
this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit
|
|
MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.profiler.push("commandFunctions");
|
|
MinecraftTimings.commandFunctionsTimer.startTiming(); // Spigot // Paper
|
|
this.getFunctions().tick();
|
|
MinecraftTimings.commandFunctionsTimer.stopTiming(); // Spigot // Paper
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.profiler.popPush("levels");
|
|
Iterator iterator = this.getAllLevels().iterator();
|
|
|
|
@@ -1503,7 +1563,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.processQueue.remove().run();
|
|
}
|
|
MinecraftTimings.processQueueTimer.stopTiming(); // Spigot
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
MinecraftTimings.timeUpdateTimer.startTiming(); // Spigot // Paper
|
|
// Send time updates to everyone, it will get the right time from the world the player is in.
|
|
// Paper start - optimize time updates
|
|
@@ -1546,11 +1606,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.profiler.push("tick");
|
|
|
|
try {
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
worldserver.timings.doTick.startTiming(); // Spigot
|
|
worldserver.tick(shouldKeepTicking);
|
|
+ // Tuinity start
|
|
+ for (final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) {
|
|
+ regionManager.recalculateRegions();
|
|
+ }
|
|
+ // Tuinity end
|
|
worldserver.timings.doTick.stopTiming(); // Spigot
|
|
- midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
} catch (Throwable throwable) {
|
|
// Spigot Start
|
|
CrashReport crashreport;
|
|
@@ -1649,7 +1714,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
@DontObfuscate
|
|
public String getServerModName() {
|
|
- return "Paper"; //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla!
|
|
+ return "Tuinity"; // Tuinity - Tuinity > //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla!
|
|
}
|
|
|
|
public SystemReport fillSystemReport(SystemReport systemreport) {
|
|
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
index 6a1fd7a983724d9e16e8aef06052108ba7ed46f1..9bb0ff063da162f2b5c91d367d9555c1cf1a3ab1 100644
|
|
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
@@ -222,6 +222,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider
|
|
io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.getClass(); // load mappings for stacktrace deobf
|
|
// Paper end
|
|
+ com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config
|
|
|
|
this.setPvpAllowed(dedicatedserverproperties.pvp);
|
|
this.setFlightAllowed(dedicatedserverproperties.allowFlight);
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index 9fe60d058ea1702930981dbd06093dc594e6bf8e..2dd5909a08a8d4bc250b36d297ef9f3f04aed8bf 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -41,6 +41,8 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
|
|
import net.minecraft.server.MinecraftServer;
|
|
// CraftBukkit end
|
|
|
|
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; // Tuinity
|
|
+
|
|
public class ChunkHolder {
|
|
|
|
public static final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> UNLOADED_CHUNK = Either.right(ChunkHolder.ChunkLoadingFailure.UNLOADED);
|
|
@@ -55,7 +57,7 @@ public class ChunkHolder {
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
|
|
- private CompletableFuture<ChunkAccess> chunkToSave;
|
|
+ public CompletableFuture<ChunkAccess> chunkToSave; // Tuinity - public
|
|
@Nullable
|
|
private final DebugBuffer<ChunkHolder.ChunkSaveDebug> 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<ServerPlayer> 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<Void> 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<LevelChunk> left = either.left();
|
|
if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
@@ -712,7 +759,8 @@ public class ChunkHolder {
|
|
this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this);
|
|
this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING);
|
|
// Paper start - cache ticking ready status
|
|
- ensureMain(this.tickingChunkFuture).thenAccept(either -> { // Paper - ensure main
|
|
+ this.tickingChunkFuture.thenAccept(either -> { // Paper - ensure main // Tuinity - always completed on main
|
|
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticking chunk future completion"); // Tuinity
|
|
either.ifLeft(chunk -> {
|
|
// note: Here is a very good place to add callbacks to logic waiting on this.
|
|
ChunkHolder.this.isTickingReady = true;
|
|
@@ -720,6 +768,9 @@ public class ChunkHolder {
|
|
// Paper start - rewrite ticklistserver
|
|
ChunkHolder.this.chunkMap.level.onChunkSetTicking(ChunkHolder.this.pos.x, ChunkHolder.this.pos.z);
|
|
// Paper end - rewrite ticklistserver
|
|
+ // Tuinity start - ticking chunk set
|
|
+ ChunkHolder.this.chunkMap.level.getChunkSource().tickingChunks.add(chunk);
|
|
+ // Tuinity end - ticking chunk set
|
|
});
|
|
});
|
|
// Paper end
|
|
@@ -729,6 +780,12 @@ public class ChunkHolder {
|
|
if (flag4 && !flag5) {
|
|
this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
|
|
this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
|
|
+ // Tuinity start - ticking chunk set
|
|
+ LevelChunk chunkIfCached = this.getFullChunkUnchecked();
|
|
+ if (chunkIfCached != null) {
|
|
+ this.chunkMap.level.getChunkSource().tickingChunks.remove(chunkIfCached);
|
|
+ }
|
|
+ // Tuinity end - ticking chunk set
|
|
}
|
|
|
|
boolean flag6 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING);
|
|
@@ -742,9 +799,13 @@ public class ChunkHolder {
|
|
this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos);
|
|
this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING);
|
|
// Paper start - cache ticking ready status
|
|
- ensureMain(this.entityTickingChunkFuture).thenAccept(either -> { // Paper ensureMain
|
|
+ this.entityTickingChunkFuture.thenAccept(either -> { // Paper ensureMain // Tuinity - always completed on main
|
|
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async entity ticking chunk future completion"); // Tuinity
|
|
either.ifLeft(chunk -> {
|
|
ChunkHolder.this.isEntityTickingReady = true;
|
|
+ // Tuinity start - entity ticking chunk set
|
|
+ ChunkHolder.this.chunkMap.level.getChunkSource().entityTickingChunks.add(chunk);
|
|
+ // Tuinity end - entity ticking chunk set
|
|
});
|
|
});
|
|
// Paper end
|
|
@@ -754,6 +815,12 @@ public class ChunkHolder {
|
|
if (flag6 && !flag7) {
|
|
this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
|
|
this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
|
|
+ // Tuinity start - entity ticking chunk set
|
|
+ LevelChunk chunkIfCached = this.getFullChunkUnchecked();
|
|
+ if (chunkIfCached != null) {
|
|
+ this.chunkMap.level.getChunkSource().entityTickingChunks.remove(chunkIfCached);
|
|
+ }
|
|
+ // Tuinity end - entity ticking chunk set
|
|
}
|
|
|
|
if (!playerchunk_state1.isOrAfter(playerchunk_state)) {
|
|
@@ -783,11 +850,19 @@ public class ChunkHolder {
|
|
// CraftBukkit start
|
|
// ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
|
|
if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
|
|
- this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main
|
|
+ this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main
|
|
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity
|
|
LevelChunk chunk = (LevelChunk)either.left().orElse(null);
|
|
- if (chunk != null) {
|
|
+ if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Tuinity - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33
|
|
+ // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ if (ChunkHolder.this.loadCallbackScheduled) {
|
|
+ return;
|
|
+ }
|
|
+ ChunkHolder.this.loadCallbackScheduled = true;
|
|
+ // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
chunkStorage.callbackExecutor.execute(() -> {
|
|
- chunk.loadCallback();
|
|
+ ChunkHolder.this.loadCallbackScheduled = false; // Tuinity - only schedule once, now the future is no longer completed as RIGHT if unloaded...
|
|
+ if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Tuinity "
|
|
});
|
|
}
|
|
}).exceptionally((throwable) -> {
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
index 6ed95a0cff3e9c874f14bc90283f750e15765c67..c44faf838c0bc7da480afe0f5ccedcaf8d0fea91 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
@@ -103,6 +103,7 @@ import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
import org.bukkit.entity.Player; // CraftBukkit
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Tuinity
|
|
|
|
public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider {
|
|
|
|
@@ -115,8 +116,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public static final int MAX_VIEW_DISTANCE = 33;
|
|
public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance();
|
|
// Paper start - faster copying
|
|
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<>(); // Paper - faster copying
|
|
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap = new ProtectedVisibleChunksMap(); // Paper - faster copying
|
|
+ // Tuinity start - Don't copy
|
|
+ public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<ChunkHolder> updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>();
|
|
+ // Tuinity end - Don't copy
|
|
|
|
private class ProtectedVisibleChunksMap extends com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder> {
|
|
@Override
|
|
@@ -139,8 +141,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
}
|
|
// Paper end
|
|
- public final com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder> pendingVisibleChunks = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder>(); // Paper - this is used if the visible chunks is updated while iterating only
|
|
- public transient com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder> 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<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); // Paper - moved up
|
|
// public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap; // Paper - moved up
|
|
@@ -148,7 +149,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final LongSet entitiesInLevel;
|
|
public final ServerLevel level;
|
|
private final ThreadedLevelLightEngine lightEngine;
|
|
- private final BlockableEventLoop<Runnable> mainThreadExecutor;
|
|
+ public final BlockableEventLoop<Runnable> mainThreadExecutor; // Tuinity - public
|
|
final java.util.concurrent.Executor mainInvokingExecutor; // Paper
|
|
public final ChunkGenerator generator;
|
|
public final Supplier<DimensionDataStorage> overworldDataStorage;
|
|
@@ -181,32 +182,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final CallbackExecutor callbackExecutor = new CallbackExecutor();
|
|
public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
|
|
|
|
- // Paper start - replace impl with recursive safe multi entry queue
|
|
- // it's possible to schedule multiple tasks currently, so it's vital we change this impl
|
|
- // If we recurse into the executor again, we will append to another queue, ensuring task order consistency
|
|
- private java.util.Queue<Runnable> 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<Runnable> queue = this.queue;
|
|
- this.queue = null;
|
|
- // Paper end
|
|
- Runnable task;
|
|
- while ((task = queue.poll()) != null) { // Paper
|
|
+ // Tuinity start - revert paper's change
|
|
+ Runnable task = queued;
|
|
+ queued = null;
|
|
+ if (task != null) {
|
|
task.run();
|
|
+ // Tuinity end - revert paper's change
|
|
}
|
|
}
|
|
};
|
|
@@ -215,22 +213,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
final CallbackExecutor chunkLoadConversionCallbackExecutor = new CallbackExecutor(); // Paper
|
|
// Paper start - distance maps
|
|
private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
|
|
- // Paper start - no-tick view distance
|
|
- int noTickViewDistance;
|
|
- public final int getRawNoTickViewDistance() {
|
|
- return this.noTickViewDistance;
|
|
- }
|
|
- public final int getEffectiveNoTickViewDistance() {
|
|
- return this.noTickViewDistance == -1 ? this.getEffectiveViewDistance() : this.noTickViewDistance;
|
|
- }
|
|
- public final int getLoadViewDistance() {
|
|
- return Math.max(this.getEffectiveViewDistance(), this.getEffectiveNoTickViewDistance());
|
|
- }
|
|
-
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap;
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap;
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap;
|
|
- // Paper end - no-tick view distance
|
|
+ public final com.tuinity.tuinity.chunk.PlayerChunkLoader playerChunkManager = new com.tuinity.tuinity.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Tuinity - replace chunk loader
|
|
// Paper start - use distance map to optimise tracker
|
|
public static boolean isLegacyTrackingEntity(Entity entity) {
|
|
return entity.isLegacyTrackingEntity;
|
|
@@ -239,7 +222,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// inlined EnumMap, TrackingRange.TrackingRangeType
|
|
static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values();
|
|
public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps;
|
|
- final int[] entityTrackerTrackRanges;
|
|
+ final int[] entityTrackerTrackRanges; public int getEntityTrackerRange(final int ordinal) { return this.entityTrackerTrackRanges[ordinal]; } // Tuinity - public read
|
|
|
|
private int convertSpigotRangeToVanilla(final int vanilla) {
|
|
return MinecraftServer.getServer().getScaledTrackingDistance(vanilla);
|
|
@@ -256,6 +239,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick
|
|
public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40;
|
|
+ public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1);
|
|
+ public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE;
|
|
+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap;
|
|
+ // Tuinity end - optimise checkDespawn
|
|
|
|
void addPlayerToDistanceMaps(ServerPlayer player) {
|
|
int chunkX = MCUtil.getChunkCoordinate(player.getX());
|
|
@@ -266,7 +255,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
|
|
int trackRange = this.entityTrackerTrackRanges[i];
|
|
|
|
- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
|
|
+ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances
|
|
}
|
|
// Paper end - use distance map to optimise entity tracker
|
|
// Paper start - optimise PlayerChunkMap#isOutsideRange
|
|
@@ -275,19 +264,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper start - optimise PlayerChunkMap#isOutsideRange
|
|
this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE);
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
- // Paper start - no-tick view distance
|
|
- int effectiveTickViewDistance = this.getEffectiveViewDistance();
|
|
- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance);
|
|
-
|
|
- if (!this.skipPlayer(player)) {
|
|
- this.playerViewDistanceTickMap.add(player, chunkX, chunkZ, effectiveTickViewDistance);
|
|
- this.playerViewDistanceNoTickMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send)
|
|
- }
|
|
-
|
|
- player.needsChunkCenterUpdate = true;
|
|
- this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured
|
|
- player.needsChunkCenterUpdate = false;
|
|
- // Paper end - no-tick view distance
|
|
+ this.playerChunkManager.addPlayer(player); // Tuinity - replace chunk loader
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS);
|
|
+ // Tuinity end - optimise checkDespawn
|
|
}
|
|
|
|
void removePlayerFromDistanceMaps(ServerPlayer player) {
|
|
@@ -300,11 +280,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.playerMobSpawnMap.remove(player);
|
|
this.playerChunkTickRangeMap.remove(player);
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
- // Paper start - no-tick view distance
|
|
- this.playerViewDistanceBroadcastMap.remove(player);
|
|
- this.playerViewDistanceTickMap.remove(player);
|
|
- this.playerViewDistanceNoTickMap.remove(player);
|
|
- // Paper end - no-tick view distance
|
|
+ this.playerChunkManager.removePlayer(player); // Tuinity - replace chunk loader
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ this.playerGeneralAreaMap.remove(player);
|
|
+ // Tuinity end - optimise checkDespawn
|
|
}
|
|
|
|
void updateMaps(ServerPlayer player) {
|
|
@@ -316,27 +295,50 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
|
|
int trackRange = this.entityTrackerTrackRanges[i];
|
|
|
|
- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
|
|
+ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances
|
|
}
|
|
// Paper end - use distance map to optimise entity tracker
|
|
// Paper start - optimise PlayerChunkMap#isOutsideRange
|
|
this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE);
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
- // Paper start - no-tick view distance
|
|
- int effectiveTickViewDistance = this.getEffectiveViewDistance();
|
|
- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance);
|
|
+ this.playerChunkManager.updatePlayer(player); // Tuinity - replace chunk loader
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS);
|
|
+ // Tuinity end - optimise checkDespawn
|
|
+ }
|
|
+ // Paper end
|
|
+ // Tuinity start
|
|
+ public final List<com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager> regionManagers = new java.util.ArrayList<>();
|
|
+ public final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager dataRegionManager;
|
|
+
|
|
+ public static final class DataRegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionData {
|
|
+ }
|
|
+
|
|
+ public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData {
|
|
+
|
|
+ @Override
|
|
+ public void removeFromRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section,
|
|
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region from) {
|
|
+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
|
|
+ final DataRegionData fromData = (DataRegionData)from.regionData;
|
|
+ }
|
|
|
|
- if (!this.skipPlayer(player)) {
|
|
- this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveTickViewDistance);
|
|
- this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send)
|
|
+ @Override
|
|
+ public void addToRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section,
|
|
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region oldRegion,
|
|
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region newRegion) {
|
|
+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
|
|
+ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData;
|
|
+ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData;
|
|
}
|
|
+ }
|
|
|
|
- player.needsChunkCenterUpdate = true;
|
|
- this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured
|
|
- player.needsChunkCenterUpdate = false;
|
|
- // Paper end - no-tick view distance
|
|
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
|
|
+ return this.pendingUnloads.get(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
|
|
}
|
|
- // Paper end
|
|
+ // Tuiniy end
|
|
+
|
|
+ boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks
|
|
|
|
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
|
|
super(new File(session.getDimensionPath(world.dimension()), "region"), dataFixer, dsync);
|
|
@@ -453,53 +455,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
});
|
|
// Paper end - optimise PlayerChunkMap#isOutsideRange
|
|
- // Paper start - no-tick view distance
|
|
- this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance);
|
|
- this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
|
|
+ this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); // Tuinity - replace chunk loading system
|
|
+ // Tuinity start
|
|
+ this.dataRegionManager = new com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
|
|
+ this.regionManagers.add(this.dataRegionManager);
|
|
+ // Tuinity end
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
|
|
(ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
|
|
- checkHighPriorityChunks(player);
|
|
- if (newState.size() != 1) {
|
|
- return;
|
|
- }
|
|
- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ);
|
|
- if (chunk == null || !chunk.areNeighboursLoaded(2)) {
|
|
- return;
|
|
+ LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
|
|
+ if (chunk != null) {
|
|
+ chunk.updateGeneralAreaCache(newState);
|
|
}
|
|
-
|
|
- ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
|
|
- ChunkMap.this.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update
|
|
},
|
|
(ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<ServerPlayer> newState) -> {
|
|
- if (player.needsChunkCenterUpdate) {
|
|
- player.needsChunkCenterUpdate = false;
|
|
- player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ));
|
|
- }
|
|
- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded
|
|
- },
|
|
- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
|
|
- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), null, true, false); // unloaded, loaded
|
|
});
|
|
- // Paper end - no-tick view distance
|
|
+ // Tuinity end - optimise checkDespawn
|
|
}
|
|
|
|
// Paper start - Chunk Prioritization
|
|
@@ -533,6 +510,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
public void checkHighPriorityChunks(ServerPlayer player) {
|
|
+ if (true) return; // Tuinity - replace player chunk loader
|
|
int currentTick = MinecraftServer.currentTick;
|
|
if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players
|
|
return;
|
|
@@ -540,7 +518,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
player.lastHighPriorityChecked = currentTick;
|
|
it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap();
|
|
|
|
- int viewDistance = getEffectiveNoTickViewDistance();
|
|
+ int viewDistance = 10;//int viewDistance = getEffectiveNoTickViewDistance(); // Tuinity - replace player chunk loader
|
|
net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos();
|
|
|
|
// Prioritize circular near
|
|
@@ -606,7 +584,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
private boolean shouldSkipPrioritization(ChunkPos coord) {
|
|
- if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true;
|
|
+ if (true) return true; // Tuinity - replace player chunk loader - unused outside paper player loader logic
|
|
ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong());
|
|
return chunk != null && (chunk.isFullChunkReady());
|
|
}
|
|
@@ -674,7 +652,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
|
|
@Nullable
|
|
public ChunkHolder getUpdatingChunkIfPresent(long pos) {
|
|
- return (ChunkHolder) this.updatingChunkMap.get(pos);
|
|
+ return this.updatingChunks.getUpdating(pos); // Tuinity - Don't copy
|
|
}
|
|
|
|
// Paper start - remove cloning of visible chunks unless accessed as a collection async
|
|
@@ -682,47 +660,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
private boolean isIterating = false;
|
|
private boolean hasPendingVisibleUpdate = false;
|
|
public void forEachVisibleChunk(java.util.function.Consumer<ChunkHolder> 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<ChunkHolder> getVisibleChunks() {
|
|
- if (Thread.currentThread() == this.level.thread) {
|
|
- return this.visibleChunkMap;
|
|
- } else {
|
|
- synchronized (this.visibleChunkMap) {
|
|
- if (DEBUG_ASYNC_VISIBLE_CHUNKS) new Throwable("Async getVisibleChunks").printStackTrace();
|
|
- if (this.visibleChunksClone == null) {
|
|
- this.visibleChunksClone = this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.clone() : ((ProtectedVisibleChunksMap)this.visibleChunkMap).clone();
|
|
- }
|
|
- return this.visibleChunksClone;
|
|
- }
|
|
+ // Tuinity start - Don't copy (except in rare cases)
|
|
+ synchronized (this.updatingChunks) {
|
|
+ return this.updatingChunks.getVisibleMap().clone();
|
|
}
|
|
+ // Tuinity end - Don't copy (except in rare cases)
|
|
}
|
|
// Paper end
|
|
|
|
@Nullable
|
|
public ChunkHolder getVisibleChunkIfPresent(long pos) {
|
|
- // Paper start - mt safe get
|
|
- if (Thread.currentThread() != this.level.thread) {
|
|
- synchronized (this.visibleChunkMap) {
|
|
- return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos));
|
|
- }
|
|
+ // Tuinity start - Don't copy
|
|
+ if (Thread.currentThread() == this.level.thread) {
|
|
+ return this.updatingChunks.getVisible(pos);
|
|
}
|
|
- return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos));
|
|
- // Paper end
|
|
+ return this.updatingChunks.getVisibleAsync(pos);
|
|
+ // Tuinity end - Don't copy
|
|
}
|
|
|
|
protected IntSupplier getChunkQueueLevel(long pos) {
|
|
@@ -844,12 +800,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
|
|
@Nullable
|
|
ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
|
|
+ if (this.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity
|
|
if (k > ChunkMap.MAX_CHUNK_DISTANCE && level > ChunkMap.MAX_CHUNK_DISTANCE) {
|
|
return holder;
|
|
} else {
|
|
if (holder != null) {
|
|
holder.setTicketLevel(level);
|
|
- holder.updateRanges(); // Paper - optimise isOutsideOfRange
|
|
+ // Tuinity - move to correct place
|
|
}
|
|
|
|
if (holder != null) {
|
|
@@ -864,11 +821,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
holder = (ChunkHolder) this.pendingUnloads.remove(pos);
|
|
if (holder != null) {
|
|
holder.setTicketLevel(level);
|
|
+ holder.updateRanges(); // Paper - optimise isOutsideOfRange // Tuinity - move to correct place
|
|
} else {
|
|
holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
|
|
+ // Tuinity start
|
|
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
|
|
+ this.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
|
|
+ }
|
|
+ // Tuinity end
|
|
}
|
|
|
|
- this.updatingChunkMap.put(pos, holder);
|
|
+ this.updatingChunks.queueUpdate(pos, holder); // Tuinity - Don't copy
|
|
this.modified = true;
|
|
}
|
|
|
|
@@ -1023,7 +986,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
while (longiterator.hasNext()) { // Spigot
|
|
long j = longiterator.nextLong();
|
|
longiterator.remove(); // Spigot
|
|
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
|
|
+ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Tuinity - Don't copy
|
|
|
|
if (playerchunk != null) {
|
|
this.pendingUnloads.put(j, playerchunk);
|
|
@@ -1060,7 +1023,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.level, chunkPos.x, chunkPos.z,
|
|
- poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
|
|
+ poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority
|
|
|
|
if (!chunk.isUnsaved()) {
|
|
return;
|
|
@@ -1081,7 +1044,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
asyncSaveData = ChunkSerializer.getAsyncSaveData(this.level, chunk);
|
|
}
|
|
|
|
- this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY,
|
|
+ this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, // Tuinity - use normal priority
|
|
asyncSaveData, chunk);
|
|
|
|
chunk.setUnsaved(false);
|
|
@@ -1097,7 +1060,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (completablefuture1 != completablefuture) {
|
|
this.scheduleUnload(pos, holder);
|
|
} else {
|
|
- if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) {
|
|
+ // Tuinity start - do not allow ticket level changes while unloading chunks
|
|
+ org.spigotmc.AsyncCatcher.catchOp("playerchunk unload");
|
|
+ boolean unloadingBefore = this.unloadingPlayerChunk;
|
|
+ this.unloadingPlayerChunk = true;
|
|
+ try {
|
|
+ // Tuinity end - do not allow ticket level changes while unloading chunks
|
|
+ // Tuinity start
|
|
+ boolean removed;
|
|
+ if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
|
|
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
|
|
+ this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
|
|
+ }
|
|
+ // Tuinity end
|
|
if (ichunkaccess instanceof LevelChunk) {
|
|
((LevelChunk) ichunkaccess).setLoaded(false);
|
|
}
|
|
@@ -1122,7 +1097,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
|
|
this.lightEngine.tryScheduleUpdate();
|
|
this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
|
|
+ } else if (removed) { // Tuinity start
|
|
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
|
|
+ this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
|
|
+ }
|
|
}
|
|
+ // Tuinity end
|
|
+ } finally { this.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes while unloading chunks
|
|
|
|
}
|
|
};
|
|
@@ -1141,19 +1122,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (!this.modified) {
|
|
return false;
|
|
} else {
|
|
- // Paper start - stop cloning visibleChunks
|
|
- synchronized (this.visibleChunkMap) {
|
|
- if (isIterating) {
|
|
- hasPendingVisibleUpdate = true;
|
|
- this.pendingVisibleChunks.copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder>)this.updatingChunkMap);
|
|
- } else {
|
|
- hasPendingVisibleUpdate = false;
|
|
- this.pendingVisibleChunks.clear();
|
|
- ((ProtectedVisibleChunksMap)this.visibleChunkMap).copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<ChunkHolder>)this.updatingChunkMap);
|
|
- this.visibleChunksClone = null;
|
|
- }
|
|
+ // Tuinity start - Don't copy
|
|
+ synchronized (this.updatingChunks) {
|
|
+ this.updatingChunks.performUpdates();
|
|
}
|
|
- // Paper end
|
|
+ // Tuinity end - Don't copy
|
|
|
|
this.modified = false;
|
|
return true;
|
|
@@ -1166,11 +1139,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (requiredStatus == ChunkStatus.EMPTY) {
|
|
return this.scheduleChunkLoad(chunkcoordintpair);
|
|
} else {
|
|
+ // Tuinity start - revert 1.17 chunk system changes
|
|
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this);
|
|
+ return future.thenComposeAsync((either) -> {
|
|
+ Optional<ChunkAccess> optional = either.left();
|
|
+ if (!optional.isPresent()) {
|
|
+ return CompletableFuture.completedFuture(either);
|
|
+ }
|
|
+ // Tuinity end - revert 1.17 chunk system changes
|
|
+
|
|
+ // Tuinity start - revert 1.17 chunk system changes
|
|
if (requiredStatus == ChunkStatus.LIGHT) {
|
|
this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.FEATURES), chunkcoordintpair);
|
|
}
|
|
-
|
|
- Optional<ChunkAccess> 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<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> {
|
|
@@ -1182,6 +1164,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
} else {
|
|
return this.scheduleChunkGeneration(holder, requiredStatus);
|
|
}
|
|
+ }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Tuinity - revert 1.17 chunk system changes
|
|
}
|
|
}
|
|
|
|
@@ -1282,7 +1265,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
});
|
|
Executor executor = (runnable) -> {
|
|
// Paper start - optimize chunk status progression without jumping through thread pool
|
|
- if (holder.canAdvanceStatus()) {
|
|
+ if (false && holder.canAdvanceStatus()) { // Tuinity - these optimisations will be revisited later
|
|
this.mainInvokingExecutor.execute(runnable);
|
|
return;
|
|
}
|
|
@@ -1313,7 +1296,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.releaseLightTicket(chunkcoordintpair);
|
|
return CompletableFuture.completedFuture(Either.right(playerchunk_failure));
|
|
});
|
|
- }, executor);
|
|
+ }, executor).thenComposeAsync((either) -> { // Tuinity start - force competion on the main thread
|
|
+ return CompletableFuture.completedFuture(either);
|
|
+ }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute
|
|
+ // Tuinity end - force competion on the main thread
|
|
}
|
|
|
|
protected void releaseLightTicket(ChunkPos pos) {
|
|
@@ -1472,9 +1458,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
chunk.unpackTicks();
|
|
return chunk;
|
|
});
|
|
- }, (runnable) -> {
|
|
- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(playerchunk, runnable));
|
|
- });
|
|
+ }, this.mainThreadExecutor); // Tuinity - queue to execute immediately so this doesn't delay chunk unloading
|
|
}
|
|
|
|
public int getTickingGenerated() {
|
|
@@ -1559,7 +1543,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
int k = this.viewDistance;
|
|
|
|
this.viewDistance = j;
|
|
- this.setNoTickViewDistance(this.getRawNoTickViewDistance()); //Paper - no-tick view distance - propagate changes to no-tick, which does the actual chunk loading/sending
|
|
+ this.playerChunkManager.setTickDistance(Mth.clamp(viewDistance, 2, 32)); // Tuinity - replace player loader system
|
|
}
|
|
|
|
}
|
|
@@ -1567,26 +1551,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper start - no-tick view distance
|
|
public final void setNoTickViewDistance(int viewDistance) {
|
|
viewDistance = viewDistance == -1 ? -1 : Mth.clamp(viewDistance, 2, 32);
|
|
-
|
|
- this.noTickViewDistance = viewDistance;
|
|
- int loadViewDistance = this.getLoadViewDistance();
|
|
- this.distanceManager.setNoTickViewDistance(loadViewDistance + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2
|
|
-
|
|
- if (this.level != null && this.level.players != null) { // this can be called from constructor, where these aren't set
|
|
- for (ServerPlayer player : this.level.players) {
|
|
- net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection;
|
|
- if (connection != null) {
|
|
- // moved in from PlayerList
|
|
- connection.send(new net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket(loadViewDistance));
|
|
- }
|
|
- this.updateMaps(player);
|
|
- // Paper end - no-tick view distance
|
|
- }
|
|
- }
|
|
+ this.playerChunkManager.setLoadDistance(viewDistance == -1 ? -1 : viewDistance + 1); // Tuinity - replace player loader system - add 1 here, we need an extra one to send to clients for chunks in this viewDistance to render
|
|
|
|
}
|
|
|
|
- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet<?>[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) {
|
|
+ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet<?>[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { // Tuinity - public
|
|
if (player.level == this.level) {
|
|
if (withinViewDistance && !withinMaxWatchDistance) {
|
|
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
|
|
@@ -1610,7 +1579,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
public int size() {
|
|
- return this.visibleChunkMap.size();
|
|
+ return this.updatingChunks.getVisibleMap().size(); // Tuinity - Don't copy
|
|
}
|
|
|
|
protected DistanceManager getDistanceManager() {
|
|
@@ -1915,6 +1884,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
*/ // Paper end - replaced by distance map
|
|
|
|
this.updateMaps(player); // Paper - distance maps
|
|
+ this.playerChunkManager.updatePlayer(player); // Tuinity - respond to movement immediately
|
|
|
|
}
|
|
|
|
@@ -1923,7 +1893,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper start - per player view distance
|
|
// there can be potential desync with player's last mapped section and the view distance map, so use the
|
|
// view distance map here.
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkPos);
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> inRange = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); // Tuinity - replace player chunk loader system
|
|
|
|
if (inRange == null) {
|
|
return Stream.empty();
|
|
@@ -1939,8 +1909,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
continue;
|
|
}
|
|
ServerPlayer player = (ServerPlayer)temp;
|
|
- int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player);
|
|
- long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player);
|
|
+ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management
|
|
+ int viewDistance = this.playerChunkManager.broadcastMap.getLastViewDistance(player); // Tuinity - replace player chunk loader system
|
|
+ long lastPosition = this.playerChunkManager.broadcastMap.getLastCoordinate(player); // Tuinity - replace player chunk loader system
|
|
|
|
int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - chunkPos.x);
|
|
int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkPos.z);
|
|
@@ -1955,6 +1926,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
continue;
|
|
}
|
|
ServerPlayer player = (ServerPlayer)temp;
|
|
+ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management
|
|
players.add(player);
|
|
}
|
|
}
|
|
@@ -2265,7 +2237,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
|
|
final Entity entity;
|
|
private final int range;
|
|
SectionPos lastSectionPos;
|
|
- public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
|
|
+ public final Set<ServerPlayerConnection> seenBy = new ReferenceOpenHashSet<>(); // Tuinity - optimise map impl
|
|
|
|
public TrackedEntity(Entity entity, int i, int j, boolean flag) {
|
|
this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
|
|
@@ -2365,7 +2337,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
|
|
double vec3d_dy = player.getY() - this.entity.getY();
|
|
double vec3d_dz = player.getZ() - this.entity.getZ();
|
|
// Paper end - remove allocation of Vec3D here
|
|
- int i = Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16);
|
|
+ int i = Math.min(this.getEffectiveRange(), player.getBukkitEntity().getViewDistance() * 16); // Tuinity - per player view distance
|
|
boolean flag = vec3d_dx >= (double) (-i) && vec3d_dx <= (double) i && vec3d_dz >= (double) (-i) && vec3d_dz <= (double) i && this.entity.broadcastToPlayer(player); // Paper - remove allocation of Vec3D here
|
|
|
|
// CraftBukkit start - respect vanish API
|
|
@@ -2400,7 +2372,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
|
|
int j = entity.getType().clientTrackingRange() * 16;
|
|
j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper
|
|
|
|
- if (j < i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic
|
|
+ if (j > i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic // Tuinity - not anymore!
|
|
i = j;
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
index 1cc4e0a1f3d8235ef88b48e01ca8b78a263d2676..428d94c60b826ddf3797d6713661dff1ca835ac2 100644
|
|
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
@@ -36,6 +36,7 @@ import net.minecraft.world.level.chunk.LevelChunk;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Tuinity
|
|
public abstract class DistanceManager {
|
|
|
|
static final Logger LOGGER = LogManager.getLogger();
|
|
@@ -44,9 +45,9 @@ public abstract class DistanceManager {
|
|
private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
|
|
final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
|
|
public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> 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<ChunkHolder> pendingChunkUpdates = new java.util.ArrayDeque<ChunkHolder>() {
|
|
@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<Long2IntMap.Entry> 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<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> 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<Ticket<?>> tickets = entry.getValue();
|
|
if (tickets.remove(target)) {
|
|
// copied from removeTicket
|
|
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
|
|
+ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Tuinity - replace ticket level propagator
|
|
|
|
// can't use entry after it's removed
|
|
if (tickets.isEmpty()) {
|
|
@@ -563,6 +637,7 @@ public abstract class DistanceManager {
|
|
}
|
|
}
|
|
|
|
+ /* Tuinity - replace old loader system
|
|
private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
|
|
|
|
protected final Long2ByteMap chunks = new Long2ByteOpenHashMap();
|
|
@@ -858,4 +933,5 @@ public abstract class DistanceManager {
|
|
}
|
|
// Paper end
|
|
}
|
|
+ */ // Tuinity - replace old loader system
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index 82d3cfb2d346a8b929e9469ae09369f6a639f81d..958b7044c196ebd66f60391c33c64ad2ff82d4e8 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
|
|
|
|
public class ServerChunkCache extends ChunkSource {
|
|
+ public static final org.apache.logging.log4j.Logger LOGGER = org.apache.logging.log4j.LogManager.getLogger(); // Tuinity
|
|
|
|
public static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
|
|
private final DistanceManager distanceManager;
|
|
@@ -139,7 +140,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true);
|
|
}
|
|
|
|
- private long chunkFutureAwaitCounter;
|
|
+ long chunkFutureAwaitCounter; // Tuinity - private -> package private
|
|
|
|
public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer<LevelChunk> onLoad) {
|
|
if (Thread.currentThread() != this.mainThread) {
|
|
@@ -201,9 +202,9 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
try {
|
|
if (onLoad != null) {
|
|
- chunkMap.callbackExecutor.execute(() -> {
|
|
+ // Tuinity - revert incorrect use of callback executor
|
|
onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback.
|
|
- });
|
|
+ // Tuinity - revert incorrect use of callback executor
|
|
}
|
|
} catch (Throwable thr) {
|
|
if (thr instanceof ThreadDeath) {
|
|
@@ -227,6 +228,166 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
// Paper end - rewrite ticklistserver
|
|
|
|
+ // Tuinity start
|
|
+ // this will try to avoid chunk neighbours for lighting
|
|
+ public final ChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) {
|
|
+ LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
|
|
+ if (ifLoaded != null) {
|
|
+ return ifLoaded;
|
|
+ }
|
|
+
|
|
+ ChunkAccess empty = this.getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, true);
|
|
+ if (empty != null && empty.getStatus().isOrAfter(ChunkStatus.FULL)) {
|
|
+ return empty;
|
|
+ }
|
|
+ return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
|
|
+ }
|
|
+
|
|
+ public final ChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) {
|
|
+ LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
|
|
+ if (ifLoaded != null) {
|
|
+ return ifLoaded;
|
|
+ }
|
|
+
|
|
+ ChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ);
|
|
+ if (ret != null && ret.getStatus().isOrAfter(ChunkStatus.FULL)) {
|
|
+ return ret;
|
|
+ } else {
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel,
|
|
+ java.util.function.Consumer<ChunkAccess> 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<ChunkHolder, CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> function,
|
|
+ java.util.function.Consumer<ChunkAccess> 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<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> 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<ChunkAccess> 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<ChunkAccess> 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<LevelChunk> tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
|
|
+ final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> 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<DimensionDataStorage> supplier) {
|
|
this.level = world;
|
|
this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world);
|
|
@@ -570,6 +731,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
return completablefuture;
|
|
}
|
|
|
|
+ private long syncLoadCounter; // Tuinity - prevent plugin unloads from removing our ticket
|
|
+
|
|
private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag) {
|
|
// Paper start - add isUrgent - old sig left in place for dirty nms plugins
|
|
return getChunkFutureMainThread(i, j, chunkstatus, flag, false);
|
|
@@ -588,9 +751,12 @@ public class ServerChunkCache extends ChunkSource {
|
|
ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel());
|
|
currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER));
|
|
}
|
|
+ final Long identifier; // Tuinity - prevent plugin unloads from removing our ticket
|
|
if (flag && !currentlyUnloading) {
|
|
// CraftBukkit end
|
|
this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
|
|
+ identifier = Long.valueOf(this.syncLoadCounter++); // Tuinity - prevent plugin unloads from removing our ticket
|
|
+ this.distanceManager.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity - prevent plugin unloads from removing our ticket
|
|
if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority
|
|
if (this.chunkAbsent(playerchunk, l)) {
|
|
ProfilerFiller gameprofilerfiller = this.level.getProfiler();
|
|
@@ -601,12 +767,20 @@ public class ServerChunkCache extends ChunkSource {
|
|
playerchunk = this.getVisibleChunkIfPresent(k);
|
|
gameprofilerfiller.pop();
|
|
if (this.chunkAbsent(playerchunk, l)) {
|
|
+ this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity
|
|
throw (IllegalStateException) Util.pauseInIde((Throwable) (new IllegalStateException("No chunk holder after ticket has been added")));
|
|
}
|
|
}
|
|
- }
|
|
+ } else { identifier = null; } // Tuinity - prevent plugin unloads from removing our ticket
|
|
// Paper start - Chunk priority
|
|
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap);
|
|
+ // Tuinity start - prevent plugin unloads from removing our ticket
|
|
+ if (flag && !currentlyUnloading) {
|
|
+ future.thenAcceptAsync((either) -> {
|
|
+ ServerChunkCache.this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier);
|
|
+ }, ServerChunkCache.this.mainThreadProcessor);
|
|
+ }
|
|
+ // Tuinity end - prevent plugin unloads from removing our ticket
|
|
if (isUrgent) {
|
|
future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair));
|
|
}
|
|
@@ -664,6 +838,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
public boolean runDistanceManagerUpdates() {
|
|
if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority
|
|
+ if (this.chunkMap.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity
|
|
+ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Tuinity - add timings for distance manager
|
|
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
|
|
boolean flag1 = this.chunkMap.promoteChunkMap();
|
|
|
|
@@ -673,6 +849,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
this.clearCache();
|
|
return true;
|
|
}
|
|
+ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Tuinity - add timings for distance manager
|
|
}
|
|
|
|
// Paper start - helper
|
|
@@ -730,6 +907,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
// CraftBukkit start - modelled on below
|
|
public void purgeUnload() {
|
|
+ if (true) return; // Tuinity - tickets will be removed later, this behavior isn't really well accounted for by the chunk system
|
|
this.level.getProfiler().push("purge");
|
|
this.distanceManager.purgeStaleTickets();
|
|
this.runDistanceManagerUpdates();
|
|
@@ -745,17 +923,18 @@ public class ServerChunkCache extends ChunkSource {
|
|
this.level.getProfiler().push("purge");
|
|
this.level.timings.doChunkMap.startTiming(); // Spigot
|
|
this.distanceManager.purgeStaleTickets();
|
|
- this.level.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.runDistanceManagerUpdates();
|
|
this.level.timings.doChunkMap.stopTiming(); // Spigot
|
|
this.level.getProfiler().popPush("chunks");
|
|
this.level.timings.chunks.startTiming(); // Paper - timings
|
|
+ this.chunkMap.playerChunkManager.tick(); // Tuinity - this is mostly is to account for view distance changes
|
|
this.tickChunks();
|
|
this.level.timings.chunks.stopTiming(); // Paper - timings
|
|
this.level.timings.doChunkUnload.startTiming(); // Spigot
|
|
this.level.getProfiler().popPush("unload");
|
|
this.chunkMap.tick(booleansupplier);
|
|
- this.level.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.level.timings.doChunkUnload.stopTiming(); // Spigot
|
|
this.level.getProfiler().pop();
|
|
this.clearCache();
|
|
@@ -833,12 +1012,15 @@ public class ServerChunkCache extends ChunkSource {
|
|
//Collections.shuffle(list); // Paper
|
|
// Paper - moved up
|
|
this.level.timings.chunkTicks.startTiming(); // Paper
|
|
- final int[] chunksTicked = {0}; this.chunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping
|
|
- Optional<LevelChunk> optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left();
|
|
-
|
|
- if (optional.isPresent()) {
|
|
+ // Tuinity start
|
|
+ int chunksTicked = 0;
|
|
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<LevelChunk> iterator = this.entityTickingChunks.iterator();
|
|
+ try { while (iterator.hasNext()) {
|
|
+ LevelChunk chunk = iterator.next();
|
|
+ ChunkHolder playerchunk = chunk.playerChunk;
|
|
+ if (playerchunk != null) {
|
|
this.level.getProfiler().push("broadcast");
|
|
- LevelChunk chunk = (LevelChunk) optional.get();
|
|
+ // Tuinity end
|
|
|
|
this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings
|
|
playerchunk.broadcastChanges(chunk);
|
|
@@ -846,11 +1028,11 @@ public class ServerChunkCache extends ChunkSource {
|
|
this.level.getProfiler().pop();
|
|
ChunkPos chunkcoordintpair = chunk.getPos();
|
|
|
|
- if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange
|
|
+ if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange // Tuinity - we only iterate entity ticking chunks
|
|
chunk.setInhabitedTime(chunk.getInhabitedTime() + j);
|
|
if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange
|
|
NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag2);
|
|
- if (chunksTicked[0]++ % 10 == 0) this.level.getServer().midTickLoadChunks(); // Paper
|
|
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper // Tuinity
|
|
}
|
|
|
|
// this.level.timings.doTickTiles.startTiming(); // Spigot // Paper
|
|
@@ -858,7 +1040,11 @@ public class ServerChunkCache extends ChunkSource {
|
|
// this.level.timings.doTickTiles.stopTiming(); // Spigot // Paper
|
|
}
|
|
}
|
|
- });
|
|
+ } // Tuinity start - optimise chunk tick iteration
|
|
+ } finally {
|
|
+ iterator.finishedIterating();
|
|
+ }
|
|
+ // Tuinity end - optimise chunk tick iteration
|
|
this.level.timings.chunkTicks.stopTiming(); // Paper
|
|
this.level.getProfiler().push("customSpawners");
|
|
if (flag1) {
|
|
@@ -871,7 +1057,24 @@ public class ServerChunkCache extends ChunkSource {
|
|
this.level.getProfiler().pop();
|
|
}
|
|
|
|
+ // Tuinity start - controlled flush for entity tracker packets
|
|
+ List<net.minecraft.network.Connection> 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<LevelChunk> chunkConsumer) {
|
|
@@ -1018,46 +1221,14 @@ public class ServerChunkCache extends ChunkSource {
|
|
super.doRunTask(task);
|
|
}
|
|
|
|
- // Paper start
|
|
- private long lastMidTickChunkTask = 0;
|
|
- public boolean pollChunkLoadTasks() {
|
|
- if (com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask()) {
|
|
- try {
|
|
- ServerChunkCache.this.runDistanceManagerUpdates();
|
|
- } finally {
|
|
- // from below: process pending Chunk loadCallback() and unloadCallback() after each run task
|
|
- chunkMap.callbackExecutor.run();
|
|
- }
|
|
- return true;
|
|
- }
|
|
- return false;
|
|
- }
|
|
- public void midTickLoadChunks() {
|
|
- net.minecraft.server.MinecraftServer server = ServerChunkCache.this.level.getServer();
|
|
- // always try to load chunks, restrain generation/other updates only. don't count these towards tick count
|
|
- //noinspection StatementWithEmptyBody
|
|
- while (pollChunkLoadTasks()) {}
|
|
-
|
|
- if (System.nanoTime() - lastMidTickChunkTask < 200000) {
|
|
- return;
|
|
- }
|
|
-
|
|
- for (;server.midTickChunksTasksRan < com.destroystokyo.paper.PaperConfig.midTickChunkTasks && server.haveTime();) {
|
|
- if (this.pollTask()) {
|
|
- server.midTickChunksTasksRan++;
|
|
- lastMidTickChunkTask = System.nanoTime();
|
|
- } else {
|
|
- break;
|
|
- }
|
|
- }
|
|
- }
|
|
- // Paper end
|
|
+ // Tuinity - replace logic
|
|
|
|
@Override
|
|
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
|
|
public boolean pollTask() {
|
|
try {
|
|
boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper
|
|
+ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Tuinity
|
|
if (ServerChunkCache.this.runDistanceManagerUpdates()) {
|
|
return true;
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
index 44aa0c4ec6f0e4df2541c74fa7de852dae59bda5..a00627e0fa38632449042f59c053b4dac13e58bf 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
@@ -173,7 +173,7 @@ public class ServerEntity {
|
|
// Paper end - remove allocation of Vec3D here
|
|
boolean flag4 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L;
|
|
|
|
- if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround()) {
|
|
+ if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround() && !(com.tuinity.tuinity.config.TuinityConfig.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Tuinity - send full pos for hard colliding entities to prevent collision problems due to desync
|
|
if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) {
|
|
if (flag2) {
|
|
packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.isOnGround());
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index 8154ca39ec7e2e8559cd125d73a59b8d2b00714c..a83b4e9b6b5bd6a57d4bc2f1cdc78ffb7e7d2fdd 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -115,6 +115,7 @@ import net.minecraft.world.level.block.EntityBlock;
|
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
import net.minecraft.world.level.block.entity.TickingBlockEntity;
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.chunk.ChunkAccess;
|
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
|
import net.minecraft.world.level.chunk.LevelChunk;
|
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
@@ -165,6 +166,7 @@ import org.bukkit.event.server.MapInitializeEvent;
|
|
import org.bukkit.event.weather.LightningStrikeEvent;
|
|
import org.bukkit.event.world.TimeSkipEvent;
|
|
// CraftBukkit end
|
|
+import it.unimi.dsi.fastutil.ints.IntArrayList; // Tuinity
|
|
|
|
public class ServerLevel extends net.minecraft.world.level.Level implements WorldGenLevel {
|
|
|
|
@@ -193,7 +195,9 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
final Int2ObjectMap<EnderDragonPart> dragonParts;
|
|
private final StructureFeatureManager structureFeatureManager;
|
|
private final boolean tickTime;
|
|
-
|
|
+ // Tuinity start - execute chunk tasks mid tick
|
|
+ public long lastMidTickExecuteFailure;
|
|
+ // Tuinity end - execute chunk tasks mid tick
|
|
|
|
// CraftBukkit start
|
|
private int tickPosition;
|
|
@@ -304,6 +308,172 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
}
|
|
// Paper end - rewrite ticklistserver
|
|
+ // Tuinity start
|
|
+ public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
|
|
+ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
|
|
+ // ICollisionAccess methods for VoxelShapes)
|
|
+ // be more strict too, add a block (dumb plugins in move events?)
|
|
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
|
|
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
|
|
+
|
|
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
|
|
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
|
|
+
|
|
+ int minChunkX = minBlockX >> 4;
|
|
+ int maxChunkX = maxBlockX >> 4;
|
|
+
|
|
+ int minChunkZ = minBlockZ >> 4;
|
|
+ int maxChunkZ = maxBlockZ >> 4;
|
|
+
|
|
+ ServerChunkCache chunkProvider = this.getChunkSource();
|
|
+
|
|
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
|
|
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
|
|
+ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public final void loadChunksForMoveAsync(AABB axisalignedbb, double toX, double toZ,
|
|
+ java.util.function.Consumer<List<ChunkAccess>> onLoad) {
|
|
+ if (Thread.currentThread() != this.thread) {
|
|
+ this.getChunkSource().mainThreadProcessor.execute(() -> {
|
|
+ this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad);
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ List<ChunkAccess> 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<ChunkAccess> 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<ServerPlayer> 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<ServerPlayer> 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<Player> getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) {
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<Player> 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<net.minecraft.world.level.Level> resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
|
|
@@ -351,7 +521,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
DataFixer datafixer = minecraftserver.getFixerUpper();
|
|
EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(this, new File(convertable_conversionsession.getDimensionPath(resourcekey), "entities"), datafixer, flag2, minecraftserver);
|
|
|
|
- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
|
|
+ this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Tuinity
|
|
StructureManager definedstructuremanager = minecraftserver.getStructureManager();
|
|
int j = this.spigotConfig.viewDistance; // Spigot
|
|
PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
|
|
@@ -386,6 +556,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper
|
|
}
|
|
|
|
+ // Tuinity start - optimise collision
|
|
+
|
|
+ // Tuinity end - optimise collision
|
|
+
|
|
// CraftBukkit start
|
|
@Override
|
|
public BlockEntity getTileEntity(BlockPos pos, boolean validate) {
|
|
@@ -439,6 +613,14 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
|
|
public void tick(BooleanSupplier shouldKeepTicking) {
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ this.playersAffectingSpawning.clear();
|
|
+ for (ServerPlayer player : this.players) {
|
|
+ if (net.minecraft.world.entity.EntitySelector.affectsSpawning.test(player)) {
|
|
+ this.playersAffectingSpawning.add(player);
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - optimise checkDespawn
|
|
ProfilerFiller gameprofilerfiller = this.getProfiler();
|
|
|
|
this.handlingTick = true;
|
|
@@ -588,7 +770,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
timings.scheduledBlocks.stopTiming(); // Paper
|
|
|
|
- this.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
gameprofilerfiller.popPush("raid");
|
|
this.timings.raids.startTiming(); // Paper - timings
|
|
this.raids.tick();
|
|
@@ -597,7 +779,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
timings.doSounds.startTiming(); // Spigot
|
|
this.runBlockEvents();
|
|
timings.doSounds.stopTiming(); // Spigot
|
|
- this.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.handlingTick = false;
|
|
gameprofilerfiller.pop();
|
|
boolean flag3 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players
|
|
@@ -644,12 +826,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
timings.entityTick.stopTiming(); // Spigot
|
|
timings.tickEntities.stopTiming(); // Spigot
|
|
gameprofilerfiller.pop();
|
|
- this.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.tickBlockEntities();
|
|
}
|
|
|
|
gameprofilerfiller.push("entityManagement");
|
|
- this.getServer().midTickLoadChunks(); // Paper
|
|
+ // Tuinity - replace logic
|
|
this.entityManager.tick();
|
|
gameprofilerfiller.pop();
|
|
}
|
|
@@ -694,6 +876,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
entityplayer.stopSleepInBed(false, false);
|
|
});
|
|
}
|
|
+ // Paper start - optimise random block ticking
|
|
+ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos();
|
|
+ private final com.tuinity.tuinity.util.math.ThreadUnsafeRandom randomTickRandom = new com.tuinity.tuinity.util.math.ThreadUnsafeRandom();
|
|
+ // Paper end
|
|
|
|
public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
|
|
ChunkPos chunkcoordintpair = chunk.getPos();
|
|
@@ -703,10 +889,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
ProfilerFiller gameprofilerfiller = this.getProfiler();
|
|
|
|
gameprofilerfiller.push("thunder");
|
|
- BlockPos blockposition;
|
|
+ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change
|
|
|
|
if (!this.paperConfig.disableThunder && flag && this.isThundering() && this.random.nextInt(100000) == 0) { // Paper - Disable thunder
|
|
- blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
|
|
+ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper
|
|
if (this.isRainingAt(blockposition)) {
|
|
DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition);
|
|
boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * paperConfig.skeleHorseSpawnChance && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper
|
|
@@ -729,64 +915,78 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
|
|
gameprofilerfiller.popPush("iceandsnow");
|
|
- if (!this.paperConfig.disableIceAndSnow && this.random.nextInt(16) == 0) { // Paper - Disable ice and snow
|
|
- blockposition = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.getBlockRandomPos(j, 0, k, 15));
|
|
- BlockPos blockposition1 = blockposition.below();
|
|
+ if (!this.paperConfig.disableIceAndSnow && this.randomTickRandom.nextInt(16) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking
|
|
+ // Paper start - optimise chunk ticking
|
|
+ this.getRandomBlockPosition(j, 0, k, 15, blockposition);
|
|
+ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15) + 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, (net.minecraft.world.level.Level) this, blockposition1, biomebase_precipitation);
|
|
+ iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition, biomebase_precipitation); // Paper
|
|
}
|
|
}
|
|
|
|
- gameprofilerfiller.popPush("tickBlocks");
|
|
+ // Paper start - optimise random block ticking
|
|
+ gameprofilerfiller.popPush("randomTick");
|
|
timings.chunkTicksBlocks.startTiming(); // Paper
|
|
if (randomTickSpeed > 0) {
|
|
- LevelChunkSection[] achunksection = chunk.getSections();
|
|
- int l = achunksection.length;
|
|
-
|
|
- for (int i1 = 0; i1 < l; ++i1) {
|
|
- LevelChunkSection chunksection = achunksection[i1];
|
|
-
|
|
- if (chunksection != LevelChunk.EMPTY_SECTION && chunksection.isRandomlyTicking()) {
|
|
- int j1 = chunksection.bottomBlockY();
|
|
-
|
|
- for (int k1 = 0; k1 < randomTickSpeed; ++k1) {
|
|
- BlockPos blockposition2 = this.getBlockRandomPos(j, j1, k, 15);
|
|
-
|
|
- gameprofilerfiller.push("randomTick");
|
|
- BlockState iblockdata1 = chunksection.getBlockState(blockposition2.getX() - j, blockposition2.getY() - j1, blockposition2.getZ() - k);
|
|
+ LevelChunkSection[] sections = chunk.getSections();
|
|
+ int minSection = com.tuinity.tuinity.util.WorldUtil.getMinSection(this);
|
|
+ for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) {
|
|
+ LevelChunkSection section = sections[sectionIndex];
|
|
+ if (section == null || section.tickingList.size() == 0) {
|
|
+ continue;
|
|
+ }
|
|
|
|
- if (iblockdata1.isRandomlyTicking()) {
|
|
- iblockdata1.randomTick(this, blockposition2, this.random);
|
|
- }
|
|
+ int yPos = (sectionIndex + minSection) << 4;
|
|
+ for (int a = 0; a < randomTickSpeed; ++a) {
|
|
+ int tickingBlocks = section.tickingList.size();
|
|
+ int index = this.randomTickRandom.nextInt(16 * 16 * 16);
|
|
+ if (index >= tickingBlocks) {
|
|
+ continue;
|
|
+ }
|
|
|
|
- FluidState fluid = iblockdata1.getFluidState();
|
|
+ long raw = section.tickingList.getRaw(index);
|
|
+ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw);
|
|
+ int randomX = location & 15;
|
|
+ int randomY = ((location >>> (4 + 4)) & 255) | yPos;
|
|
+ int randomZ = (location >>> 4) & 15;
|
|
|
|
- if (fluid.isRandomlyTicking()) {
|
|
- fluid.randomTick(this, blockposition2, this.random);
|
|
- }
|
|
+ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ);
|
|
+ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw);
|
|
|
|
- gameprofilerfiller.pop();
|
|
- }
|
|
+ iblockdata.randomTick(this, blockposition2, this.randomTickRandom);
|
|
+ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock).
|
|
+ // TODO CHECK ON UPDATE
|
|
}
|
|
}
|
|
}
|
|
+ // Paper end - optimise random block ticking
|
|
timings.chunkTicksBlocks.stopTiming(); // Paper
|
|
gameprofilerfiller.pop();
|
|
}
|
|
@@ -912,7 +1112,26 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
|
|
}
|
|
|
|
+ // Tuinity start - log detailed entity tick information
|
|
+ // TODO replace with varhandle
|
|
+ static final java.util.concurrent.atomic.AtomicReference<Entity> currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>();
|
|
+
|
|
+ public static List<Entity> getCurrentlyTickingEntities() {
|
|
+ Entity ticking = currentlyTickingEntity.get();
|
|
+ List<Entity> ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking });
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Tuinity end - log detailed entity tick information
|
|
+
|
|
public void tickNonPassenger(Entity entity) {
|
|
+ // Tuinity start - log detailed entity tick information
|
|
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
|
|
+ try {
|
|
+ if (currentlyTickingEntity.get() == null) {
|
|
+ currentlyTickingEntity.lazySet(entity);
|
|
+ }
|
|
+ // Tuinity end - log detailed entity tick information
|
|
++TimingHistory.entityTicks; // Paper - timings
|
|
// Spigot start
|
|
co.aikar.timings.Timing timer; // Paper
|
|
@@ -953,7 +1172,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
|
|
// } finally { timer.stopTiming(); } // Paper - timings - move up
|
|
-
|
|
+ // Tuinity start - log detailed entity tick information
|
|
+ } finally {
|
|
+ if (currentlyTickingEntity.get() == entity) {
|
|
+ currentlyTickingEntity.lazySet(null);
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - log detailed entity tick information
|
|
}
|
|
|
|
private void tickPassenger(Entity vehicle, Entity passenger) {
|
|
@@ -1245,9 +1470,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
// Spigot Start
|
|
for (BlockEntity tileentity : chunk.getBlockEntities().values()) {
|
|
if (tileentity instanceof net.minecraft.world.Container) {
|
|
+ // Tuinity start - this area looks like it can load chunks, change the behavior
|
|
+ // chests for example can apply physics to the world
|
|
+ // so instead we just change the active container and call the event
|
|
for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((net.minecraft.world.Container) tileentity).getViewers())) {
|
|
- h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper
|
|
+ ((org.bukkit.craftbukkit.entity.CraftHumanEntity)h).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper
|
|
}
|
|
+ // Tuiniy end
|
|
}
|
|
}
|
|
// Spigot End
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
index a0acaac510aa2206a5c58f2b7aafdbc2bdf7a3dd..0da2dbeba93d428a035872e05177ed3fc29acf9b 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
@@ -260,7 +260,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<ServerPlayer> 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) {
|
|
@@ -423,7 +423,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;
|
|
}
|
|
}
|
|
@@ -431,7 +431,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());
|
|
}
|
|
}
|
|
@@ -1562,6 +1562,18 @@ public class ServerPlayer extends Player {
|
|
this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
|
|
this.doCloseContainer();
|
|
}
|
|
+ // Tuinity start - special close for unloaded inventory
|
|
+ @Override
|
|
+ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
|
|
+ // copied from above
|
|
+ CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
|
|
+ // Paper end
|
|
+ // copied from below
|
|
+ this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
|
|
+ this.containerMenu = this.inventoryMenu;
|
|
+ // do not run close logic
|
|
+ }
|
|
+ // Tuinity end - special close for unloaded inventory
|
|
|
|
public void doCloseContainer() {
|
|
this.containerMenu.removed((Player) this);
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
index e572088cad8b9e09b1d64f7971bacac2f10c5b17..b2c8cae1a777cd63a35ed1340caf205b1b3bb0ad 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
@@ -56,14 +56,28 @@ public class ServerPlayerGameMode {
|
|
@Nullable
|
|
private GameType previousGameModeForPlayer;
|
|
private boolean isDestroyingBlock;
|
|
- private int destroyProgressStart;
|
|
+ private int destroyProgressStart; private long lastDigTime; // Tuinity - lag compensate block breaking
|
|
private BlockPos destroyPos;
|
|
private int gameTicks;
|
|
private boolean hasDelayedDestroy;
|
|
private BlockPos delayedDestroyPos;
|
|
- private int delayedTickStart;
|
|
+ private int delayedTickStart; private long hasDestroyedTooFastStartTime; // Tuinity - lag compensate block breaking
|
|
private int lastSentState;
|
|
|
|
+ // Tuinity start - lag compensate block breaking
|
|
+ private int getTimeDiggingLagCompensate() {
|
|
+ int lagCompensated = (int)((System.nanoTime() - this.lastDigTime) / (50L * 1000L * 1000L));
|
|
+ int tickDiff = this.gameTicks - this.destroyProgressStart;
|
|
+ return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to
|
|
+ }
|
|
+
|
|
+ private int getTimeDiggingTooFastLagCompensate() {
|
|
+ int lagCompensated = (int)((System.nanoTime() - this.hasDestroyedTooFastStartTime) / (50L * 1000L * 1000L));
|
|
+ int tickDiff = this.gameTicks - this.delayedTickStart;
|
|
+ return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
public ServerPlayerGameMode(ServerPlayer player) {
|
|
this.gameModeForPlayer = GameType.DEFAULT_MODE;
|
|
this.destroyPos = BlockPos.ZERO;
|
|
@@ -130,7 +144,7 @@ public class ServerPlayerGameMode {
|
|
if (iblockdata == null || iblockdata.isAir()) { // Paper
|
|
this.hasDelayedDestroy = false;
|
|
} else {
|
|
- float f = this.incrementDestroyProgress(iblockdata, this.delayedDestroyPos, this.delayedTickStart);
|
|
+ float f = this.updateBlockBreakAnimation(iblockdata, this.delayedDestroyPos, this.getTimeDiggingTooFastLagCompensate()); // Tuinity - lag compensate destroying blocks
|
|
|
|
if (f >= 1.0F) {
|
|
this.hasDelayedDestroy = false;
|
|
@@ -150,7 +164,7 @@ public class ServerPlayerGameMode {
|
|
this.lastSentState = -1;
|
|
this.isDestroyingBlock = false;
|
|
} else {
|
|
- this.incrementDestroyProgress(iblockdata, this.destroyPos, this.destroyProgressStart);
|
|
+ this.updateBlockBreakAnimation(iblockdata, this.destroyPos, this.getTimeDiggingLagCompensate()); // Tuinity - lag compensate destroying
|
|
}
|
|
}
|
|
|
|
@@ -158,6 +172,12 @@ public class ServerPlayerGameMode {
|
|
|
|
private float incrementDestroyProgress(BlockState state, BlockPos pos, int i) {
|
|
int j = this.gameTicks - i;
|
|
+ // Tuinity start - change i (startTime) to totalTime
|
|
+ return this.updateBlockBreakAnimation(state, pos, j);
|
|
+ }
|
|
+ private float updateBlockBreakAnimation(BlockState state, BlockPos pos, int totalTime) {
|
|
+ int j = totalTime;
|
|
+ // Tuinity end
|
|
float f = state.getDestroyProgress(this.player, this.player.level, pos) * (float) (j + 1);
|
|
int k = (int) (f * 10.0F);
|
|
|
|
@@ -225,7 +245,7 @@ public class ServerPlayerGameMode {
|
|
return;
|
|
}
|
|
|
|
- this.destroyProgressStart = this.gameTicks;
|
|
+ this.destroyProgressStart = this.gameTicks; this.lastDigTime = System.nanoTime(); // Tuinity - lag compensate block breaking
|
|
float f = 1.0F;
|
|
|
|
iblockdata = this.level.getBlockState(pos);
|
|
@@ -278,12 +298,12 @@ public class ServerPlayerGameMode {
|
|
int j = (int) (f * 10.0F);
|
|
|
|
this.level.destroyBlockProgress(this.player.getId(), pos, j);
|
|
- this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying"));
|
|
+ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying"));
|
|
this.lastSentState = j;
|
|
}
|
|
} else if (action == ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK) {
|
|
if (pos.equals(this.destroyPos)) {
|
|
- int k = this.gameTicks - this.destroyProgressStart;
|
|
+ int k = this.getTimeDiggingLagCompensate(); // Tuinity - lag compensate block breaking
|
|
|
|
iblockdata = this.level.getBlockState(pos);
|
|
if (!iblockdata.isAir()) {
|
|
@@ -300,12 +320,18 @@ public class ServerPlayerGameMode {
|
|
this.isDestroyingBlock = false;
|
|
this.hasDelayedDestroy = true;
|
|
this.delayedDestroyPos = pos;
|
|
- this.delayedTickStart = this.destroyProgressStart;
|
|
+ this.delayedTickStart = this.destroyProgressStart; this.hasDestroyedTooFastStartTime = this.lastDigTime; // Tuinity - lag compensate block breaking
|
|
}
|
|
}
|
|
}
|
|
|
|
+ // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block
|
|
+ if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) {
|
|
+ this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
|
|
+ } else {
|
|
this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "stopped destroying"));
|
|
+ }
|
|
+ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block
|
|
} else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) {
|
|
this.isDestroyingBlock = false;
|
|
if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) {
|
|
@@ -317,7 +343,7 @@ public class ServerPlayerGameMode {
|
|
}
|
|
|
|
this.level.destroyBlockProgress(this.player.getId(), pos, -1);
|
|
- this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying"));
|
|
+ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying")); // Tuinity - this can cause clients on a lagging server to think they stopped destroying a block they're currently destroying
|
|
}
|
|
|
|
}
|
|
@@ -327,7 +353,13 @@ public class ServerPlayerGameMode {
|
|
|
|
public void destroyAndAck(BlockPos pos, ServerboundPlayerActionPacket.Action action, String reason) {
|
|
if (this.destroyBlock(pos)) {
|
|
+ // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block
|
|
+ if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) {
|
|
+ this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
|
|
+ } else {
|
|
this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, reason));
|
|
+ }
|
|
+ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block
|
|
} else {
|
|
this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // CraftBukkit - SPIGOT-5196
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
index de228b677810ce49c4e953ca0b4e590413b20e45..580165d0a728a4558031dac11f5edaf2923b5ad0 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
@@ -23,6 +23,17 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
+// Tuinity start
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
+import com.tuinity.tuinity.util.CoordinateUtils;
|
|
+import java.util.function.Supplier;
|
|
+import net.minecraft.world.level.lighting.LayerLightEventListener;
|
|
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.LongArrayList;
|
|
+import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
+// Tuinity end
|
|
+
|
|
public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
private final ProcessorMailbox<Runnable> taskMailbox;
|
|
@@ -32,13 +43,166 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
private volatile int taskPerBatch = 5;
|
|
private final AtomicBoolean scheduled = new AtomicBoolean();
|
|
|
|
+ // Tuinity start - replace light engine impl
|
|
+ protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine;
|
|
+ public final boolean hasBlockLight;
|
|
+ public final boolean hasSkyLight;
|
|
+ // Tuinity end - replace light engine impl
|
|
+
|
|
public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox<Runnable> processor, ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor) {
|
|
- super(chunkProvider, true, hasBlockLight);
|
|
+ super(chunkProvider, false, false); // Tuinity - destroy vanilla light engine state
|
|
this.chunkMap = chunkStorage;
|
|
this.sorterMailbox = executor;
|
|
this.taskMailbox = processor;
|
|
+ // Tuinity start - replace light engine impl
|
|
+ this.hasBlockLight = true;
|
|
+ this.hasSkyLight = hasBlockLight; // Nice variable name.
|
|
+ this.theLightEngine = new ca.spottedleaf.starlight.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this);
|
|
+ // Tuinity end - replace light engine impl
|
|
+ }
|
|
+
|
|
+ // Tuinity start - replace light engine impl
|
|
+ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) {
|
|
+ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ);
|
|
+ }
|
|
+
|
|
+ protected long relightCounter;
|
|
+
|
|
+ public int relight(java.util.Set<ChunkPos> chunks_param,
|
|
+ java.util.function.Consumer<ChunkPos> 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<ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks_param);
|
|
+ // add tickets
|
|
+ java.util.Map<ChunkPos, Long> ticketIds = new java.util.HashMap<>();
|
|
+ int totalChunks = 0;
|
|
+ for (java.util.Iterator<ChunkPos> 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<CompletableFuture<Void>> 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<Void> updateFuture = runnable.get();
|
|
+
|
|
+ if (updateFuture == null) {
|
|
+ // not scheduled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final int references = this.chunksBeingWorkedOn.addTo(key, 1);
|
|
+ if (references == 0) {
|
|
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
+ }
|
|
+
|
|
+ // append future to this chunk and 1 radius neighbours chunk save futures
|
|
+ // this prevents us from saving the world without first waiting for the light engine
|
|
+
|
|
+ for (int dx = -1; dx <= 1; ++dx) {
|
|
+ for (int dz = -1; dz <= 1; ++dz) {
|
|
+ ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ));
|
|
+ if (neighbour != null) {
|
|
+ neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> {
|
|
+ return curr;
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ updateFuture.thenAcceptAsync((final Void ignore) -> {
|
|
+ final int newReferences = this.chunksBeingWorkedOn.get(key);
|
|
+ if (newReferences == 1) {
|
|
+ this.chunksBeingWorkedOn.remove(key);
|
|
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
+ } else {
|
|
+ this.chunksBeingWorkedOn.put(key, newReferences - 1);
|
|
+ }
|
|
+ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> {
|
|
+ if (thr != null) {
|
|
+ LOGGER.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasLightWork() {
|
|
+ // route to new light engine
|
|
+ return this.theLightEngine.hasUpdates() || !this.lightTasks.isEmpty();
|
|
}
|
|
|
|
+ @Override
|
|
+ public LayerLightEventListener getLayerListener(final LightLayer lightType) {
|
|
+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
|
|
+ // need to use new light hooks for this
|
|
+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness;
|
|
+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos);
|
|
+ return Math.max(sky, block);
|
|
+ }
|
|
+ // Tuinity end - replace light engine impl
|
|
+
|
|
@Override
|
|
public void close() {
|
|
}
|
|
@@ -55,15 +219,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void checkBlock(BlockPos pos) {
|
|
- BlockPos blockPos = pos.immutable();
|
|
- this.addTask(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ThreadedLevelLightEngine.TaskType.POST_UPDATE, Util.name(() -> {
|
|
- super.checkBlock(blockPos);
|
|
- }, () -> {
|
|
- return "checkBlock " + blockPos;
|
|
- }));
|
|
+ // Tuinity start - replace light engine impl
|
|
+ final BlockPos posCopy = pos.immutable();
|
|
+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
|
|
+ return this.theLightEngine.blockChange(posCopy);
|
|
+ });
|
|
+ // Tuinity end - replace light engine impl
|
|
}
|
|
|
|
protected void updateChunkStatus(ChunkPos pos) {
|
|
+ if (true) return; // Tuinity - replace light engine impl
|
|
this.addTask(pos.x, pos.z, () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -86,17 +251,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void updateSectionStatus(SectionPos pos, boolean notReady) {
|
|
- this.addTask(pos.x(), pos.z(), () -> {
|
|
- return 0;
|
|
- }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
- super.updateSectionStatus(pos, notReady);
|
|
- }, () -> {
|
|
- return "updateSectionStatus " + pos + " " + notReady;
|
|
- }));
|
|
+ // Tuinity start - replace light engine impl
|
|
+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
|
|
+ return this.theLightEngine.sectionChange(pos, notReady);
|
|
+ });
|
|
+ // Tuinity end - replace light engine impl
|
|
}
|
|
|
|
@Override
|
|
public void enableLightSources(ChunkPos chunkPos, boolean bl) {
|
|
+ if (true) return; // Tuinity - replace light engine impl
|
|
this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
super.enableLightSources(chunkPos, bl);
|
|
}, () -> {
|
|
@@ -106,6 +270,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) {
|
|
+ if (true) return; // Tuinity - replace light engine impl
|
|
this.addTask(pos.x(), pos.z(), () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -131,6 +296,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void retainData(ChunkPos pos, boolean retainData) {
|
|
+ if (true) return; // Tuinity - replace light engine impl
|
|
this.addTask(pos.x, pos.z, () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -141,6 +307,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
|
|
public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
|
|
+ // Tuinity start - replace light engine impl
|
|
+ if (true) {
|
|
+ boolean lit = excludeBlocks;
|
|
+ final ChunkPos chunkPos = chunk.getPos();
|
|
+
|
|
+ return CompletableFuture.supplyAsync(() -> {
|
|
+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk);
|
|
+ if (!lit) {
|
|
+ chunk.setLightCorrect(false);
|
|
+ this.theLightEngine.lightChunk(chunk, emptySections);
|
|
+ chunk.setLightCorrect(true);
|
|
+ } else {
|
|
+ this.theLightEngine.forceLoadInChunk(chunk, emptySections);
|
|
+ // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have
|
|
+ // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should
|
|
+ // catch what we miss here.
|
|
+ this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z);
|
|
+ }
|
|
+
|
|
+ this.chunkMap.releaseLightTicket(chunkPos);
|
|
+ return chunk;
|
|
+ }, (runnable) -> {
|
|
+ this.theLightEngine.scheduleChunkLight(chunkPos, runnable);
|
|
+ this.tryScheduleUpdate();
|
|
+ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> {
|
|
+ if (throwable != null) {
|
|
+ LOGGER.fatal("Failed to light chunk " + chunkPos, throwable);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+ // Tuinity end - replace light engine impl
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
chunk.setLightCorrect(false);
|
|
this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -175,7 +372,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
|
|
public void tryScheduleUpdate() {
|
|
- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
|
|
+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Tuinity - rewrite light engine
|
|
this.taskMailbox.tell(() -> {
|
|
this.runUpdate();
|
|
this.scheduled.set(false);
|
|
@@ -197,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
|
|
objectListIterator.back(j);
|
|
- super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
+ this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine
|
|
|
|
for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) {
|
|
Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
|
|
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
index 3c1698ba0d3bc412ab957777d9b5211dbc555208..c438cbfa1f964a5bea98bca85e688d8019e1c4fb 100644
|
|
--- a/src/main/java/net/minecraft/server/level/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
@@ -31,6 +31,8 @@ public class TicketType<T> {
|
|
public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
|
|
public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
|
|
public static final TicketType<Long> DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper
|
|
+ public static final TicketType<Long> CHUNK_RELIGHT = create("light_update", Long::compareTo); // Tuinity - ensure chunks stay loaded for lighting
|
|
+ public static final TicketType<Long> REQUIRED_LOAD = create("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail
|
|
|
|
public static <T> TicketType<T> create(String name, Comparator<T> 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<String> currentlyGenerating;
|
|
|
|
+ // Tuinity start
|
|
+ // No-op, this class doesn't provide entity access
|
|
+ @Override
|
|
+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
|
|
+ return Collections.emptyList();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
|
|
+
|
|
+ @Override
|
|
+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
|
|
+
|
|
+ @Override
|
|
+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, AABB box, List<? super T> into, Predicate<? super T> predicate) {}
|
|
+ // Tuinity end
|
|
+
|
|
public WorldGenRegion(ServerLevel world, List<ChunkAccess> list, ChunkStatus chunkstatus, int i) {
|
|
this.generatingStatus = chunkstatus;
|
|
this.writeRadiusCutoff = i;
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
index 35fa416a8ce332e823ed5077a8fd3492683d7ad0..f78119970da27ef66a9d9093e2e42ce129d4cf31 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
@@ -537,6 +537,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
|
|
// Paper end - fix large move vectors killing the server
|
|
|
|
+ // Tuinity start - fix large move vectors killing the server
|
|
+ double otherFieldX = d3 - this.vehicleLastGoodX;
|
|
+ double otherFieldY = d4 - this.vehicleLastGoodY - 1.0E-6D;
|
|
+ double otherFieldZ = d5 - this.vehicleLastGoodZ;
|
|
+ d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
|
|
+ // Tuinity end - fix large move vectors killing the server
|
|
|
|
// CraftBukkit start - handle custom speeds and skipped ticks
|
|
this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
|
|
@@ -577,12 +583,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
return;
|
|
}
|
|
|
|
- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
|
|
+ AABB oldBox = entity.getBoundingBox(); // Tuinity - copy from player movement packet
|
|
|
|
- d6 = d3 - this.vehicleLastGoodX;
|
|
- d7 = d4 - this.vehicleLastGoodY - 1.0E-6D;
|
|
- d8 = d5 - this.vehicleLastGoodZ;
|
|
+ d6 = d3 - this.vehicleLastGoodX; // Tuinity - diff on change, used for checking large move vectors above
|
|
+ d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Tuinity - diff on change, used for checking large move vectors above
|
|
+ d8 = d5 - this.vehicleLastGoodZ; // Tuinity - diff on change, used for checking large move vectors above
|
|
entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
|
|
+ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
|
|
double d11 = d7;
|
|
|
|
d6 = d3 - entity.getX();
|
|
@@ -596,16 +603,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
boolean flag1 = false;
|
|
|
|
if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
|
|
- flag1 = true;
|
|
+ flag1 = true; // Tuinity - diff on change, this should be moved wrongly
|
|
ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10));
|
|
}
|
|
Location curPos = this.getCraftPlayer().getLocation(); // Spigot
|
|
|
|
entity.absMoveTo(d3, d4, d5, f, f1);
|
|
this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
|
|
- boolean flag2 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
|
|
-
|
|
- if (flag && (flag1 || !flag2)) {
|
|
+ // Tuinity start - optimise out extra getCubes
|
|
+ boolean teleportBack = flag1; // violating this is always a fail
|
|
+ if (!teleportBack) {
|
|
+ // note: only call after setLocation, or else getBoundingBox is wrong
|
|
+ AABB newBox = entity.getBoundingBox();
|
|
+ if (didCollide || !oldBox.equals(newBox)) {
|
|
+ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox);
|
|
+ } // else: no collision at all detected, why do we care?
|
|
+ }
|
|
+ if (teleportBack) { // Tuinity end - optimise out extra getCubes
|
|
entity.absMoveTo(d0, d1, d2, f, f1);
|
|
this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
|
|
this.connection.send(new ClientboundMoveVehiclePacket(entity));
|
|
@@ -691,7 +705,32 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
}
|
|
|
|
private boolean noBlocksAround(Entity entity) {
|
|
- return entity.level.getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir);
|
|
+ // Tuinity start - stop using streams, this is already a known fixed problem in Entity#move
|
|
+ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D);
|
|
+ int minX = Mth.floor(box.minX);
|
|
+ int minY = Mth.floor(box.minY);
|
|
+ int minZ = Mth.floor(box.minZ);
|
|
+ int maxX = Mth.floor(box.maxX);
|
|
+ int maxY = Mth.floor(box.maxY);
|
|
+ int maxZ = Mth.floor(box.maxZ);
|
|
+
|
|
+ Level world = entity.level;
|
|
+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
|
+
|
|
+ for (int y = minY; y <= maxY; ++y) {
|
|
+ for (int z = minZ; z <= maxZ; ++z) {
|
|
+ for (int x = minX; x <= maxX; ++x) {
|
|
+ pos.set(x, y, z);
|
|
+ BlockState type = world.getTypeIfLoaded(pos);
|
|
+ if (type != null && !type.isAir()) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ // Tuinity end - stop using streams, this is already a known fixed problem in Entity#move
|
|
}
|
|
|
|
@Override
|
|
@@ -1247,7 +1286,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
}
|
|
|
|
if (this.awaitingPositionFromClient != null) {
|
|
- if (this.tickCount - this.awaitingTeleportTime > 20) {
|
|
+ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Tuinity - this will greatly screw with clients with > 1000ms RTT
|
|
this.awaitingTeleportTime = this.tickCount;
|
|
this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
|
|
}
|
|
@@ -1286,6 +1325,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
double currDeltaZ = toZ - prevZ;
|
|
double d11 = Math.max(d7 * d7 + d8 * d8 + d9 * d9, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
|
|
// Paper end - fix large move vectors killing the server
|
|
+ // Tuinity start - fix large move vectors killing the server
|
|
+ double otherFieldX = d0 - this.lastGoodX;
|
|
+ double otherFieldY = d1 - this.lastGoodY;
|
|
+ double otherFieldZ = d2 - this.lastGoodZ;
|
|
+ d11 = Math.max(d11, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
|
|
+ // Tuinity end - fix large move vectors killing the server
|
|
|
|
if (this.player.isSleeping()) {
|
|
if (d11 > 1.0D) {
|
|
@@ -1335,11 +1380,11 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
}
|
|
}
|
|
|
|
- AABB axisalignedbb = this.player.getBoundingBox();
|
|
+ AABB axisalignedbb = this.player.getBoundingBox(); // Tuinity - diff on change, should be old AABB
|
|
|
|
- d7 = d0 - this.lastGoodX;
|
|
- d8 = d1 - this.lastGoodY;
|
|
- d9 = d2 - this.lastGoodZ;
|
|
+ d7 = d0 - this.lastGoodX; // Tuinity - diff on change, used for checking large move vectors above
|
|
+ d8 = d1 - this.lastGoodY; // Tuinity - diff on change, used for checking large move vectors above
|
|
+ d9 = d2 - this.lastGoodZ; // Tuinity - diff on change, used for checking large move vectors above
|
|
boolean flag = d8 > 0.0D;
|
|
|
|
if (this.player.isOnGround() && !packet.isOnGround() && flag) {
|
|
@@ -1374,6 +1419,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
}
|
|
|
|
this.player.move(MoverType.PLAYER, new Vec3(d7, d8, d9));
|
|
+ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
|
|
this.player.setOnGround(packet.isOnGround()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move
|
|
// Paper start - prevent position desync
|
|
if (this.awaitingPositionFromClient != null) {
|
|
@@ -1393,12 +1439,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
boolean flag1 = false;
|
|
|
|
if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot
|
|
- flag1 = true;
|
|
+ flag1 = true; // Tuinity - diff on change, this should be moved wrongly
|
|
ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
|
|
}
|
|
|
|
this.player.absMoveTo(d0, d1, d2, f, f1);
|
|
- if (!this.player.noPhysics && !this.player.isSleeping() && (flag1 && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew((LevelReader) worldserver, axisalignedbb))) {
|
|
+ // Tuinity start - optimise out extra getCubes
|
|
+ // Original for reference:
|
|
+ // boolean teleportBack = flag1 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb));
|
|
+ boolean teleportBack = flag1; // violating this is always a fail
|
|
+ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) {
|
|
+ AABB newBox = this.player.getBoundingBox();
|
|
+ if (didCollide || !axisalignedbb.equals(newBox)) {
|
|
+ // note: only call after setLocation, or else getBoundingBox is wrong
|
|
+ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox);
|
|
+ } // else: no collision at all detected, why do we care?
|
|
+ }
|
|
+ if (!this.player.noPhysics && !this.player.isSleeping() && teleportBack) { // Tuinity end - optimise out extra getCubes
|
|
this.teleport(d3, d4, d5, f, f1);
|
|
} else {
|
|
// CraftBukkit start - fire PlayerMoveEvent
|
|
@@ -1485,6 +1542,27 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
|
|
}
|
|
}
|
|
|
|
+ // Tuinity start - optimise out extra getCubes
|
|
+ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) {
|
|
+ final List<AABB> collisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList();
|
|
+ try {
|
|
+ com.tuinity.tuinity.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, true, false,
|
|
+ true, false, null, null);
|
|
+
|
|
+ for (int i = 0, len = collisions.size(); i < len; ++i) {
|
|
+ final AABB box = collisions.get(i);
|
|
+ if (!com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ } finally {
|
|
+ com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(collisions);
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - optimise out extra getCubes
|
|
+
|
|
private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) {
|
|
Stream<VoxelShape> stream = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D), (entity) -> {
|
|
return true;
|
|
diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java
|
|
index 9a428e166561b4bc028732ec563d3b2e99f81a8e..771c575ffe46db94d9c91f3fd0440d4deb460de7 100644
|
|
--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java
|
|
+++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java
|
|
@@ -60,6 +60,11 @@ public class GameProfileCache {
|
|
@Nullable
|
|
private Executor executor;
|
|
|
|
+ // Tuinity start
|
|
+ protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock();
|
|
+ protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock();
|
|
+ // Tuinity end
|
|
+
|
|
public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) {
|
|
this.profileRepository = profileRepository;
|
|
this.file = cacheFile;
|
|
@@ -67,6 +72,7 @@ public class GameProfileCache {
|
|
}
|
|
|
|
private void safeAdd(GameProfileCache.GameProfileInfo entry) {
|
|
+ try { this.stateLock.lock(); // Tuinity - allow better concurrency
|
|
GameProfile gameprofile = entry.getProfile();
|
|
|
|
entry.setLastAccess(this.getNextOperation());
|
|
@@ -81,6 +87,7 @@ public class GameProfileCache {
|
|
if (uuid != null) {
|
|
this.profilesByUUID.put(uuid, entry);
|
|
}
|
|
+ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency
|
|
|
|
}
|
|
|
|
@@ -118,7 +125,7 @@ public class GameProfileCache {
|
|
return com.destroystokyo.paper.PaperConfig.isProxyOnlineMode(); // Paper
|
|
}
|
|
|
|
- public synchronized void add(GameProfile profile) { // Paper - synchronize
|
|
+ public void add(GameProfile profile) { // Paper - synchronize // Tuinity - allow better concurrency
|
|
Calendar calendar = Calendar.getInstance();
|
|
|
|
calendar.setTime(new Date());
|
|
@@ -135,8 +142,9 @@ public class GameProfileCache {
|
|
}
|
|
|
|
@Nullable
|
|
- public synchronized GameProfile get(String name) { // Paper - synchronize
|
|
+ public GameProfile get(String name) { // Paper - synchronize // Tuinity start - allow better concurrency
|
|
String s1 = name.toLowerCase(Locale.ROOT);
|
|
+ boolean stateLocked = true; try { this.stateLock.lock(); // Tuinity - allow better concurrency
|
|
GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1);
|
|
boolean flag = false;
|
|
|
|
@@ -150,10 +158,14 @@ public class GameProfileCache {
|
|
GameProfile gameprofile;
|
|
|
|
if (usercache_usercacheentry != null) {
|
|
+ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency
|
|
usercache_usercacheentry.setLastAccess(this.getNextOperation());
|
|
gameprofile = usercache_usercacheentry.getProfile();
|
|
} else {
|
|
+ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency
|
|
+ try { this.lookupLock.lock(); // Tuinity - allow better concurrency
|
|
gameprofile = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players
|
|
+ } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency
|
|
if (gameprofile != null) {
|
|
this.add(gameprofile);
|
|
flag = false;
|
|
@@ -165,6 +177,7 @@ public class GameProfileCache {
|
|
}
|
|
|
|
return gameprofile;
|
|
+ } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency
|
|
}
|
|
|
|
public void getAsync(String s, Consumer<GameProfile> consumer) {
|
|
@@ -326,7 +339,9 @@ public class GameProfileCache {
|
|
}
|
|
|
|
private Stream<GameProfileCache.GameProfileInfo> getTopMRUProfiles(int limit) {
|
|
+ try { this.stateLock.lock(); // Tuinity - allow better concurrency
|
|
return ImmutableList.copyOf(this.profilesByUUID.values()).stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit((long) limit);
|
|
+ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency
|
|
}
|
|
|
|
private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) {
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index b46e64a386d256d30eac330c463c71396452563d..570cea8ee6a442b2dc3c6ef849294ef0c02027ca 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -175,6 +175,7 @@ public abstract class PlayerList {
|
|
abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor
|
|
|
|
public void placeNewPlayer(Connection connection, ServerPlayer player) {
|
|
+ player.isRealPlayer = true; // Paper // Tuinity - this is a better place to write this that works and isn't overriden by plugins
|
|
ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper
|
|
if (prev != null) {
|
|
disconnectPendingPlayer(prev);
|
|
@@ -264,7 +265,7 @@ public abstract class PlayerList {
|
|
boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
|
|
|
|
// Spigot - view distance
|
|
- playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.getLoadViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance
|
|
+ playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance // Tuinity - replace old player chunk management
|
|
player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
|
|
playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName())));
|
|
playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
|
|
@@ -723,7 +724,7 @@ public abstract class PlayerList {
|
|
SocketAddress socketaddress = loginlistener.connection.getRemoteAddress();
|
|
|
|
ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameprofile);
|
|
- entity.isRealPlayer = true; // Paper - Chunk priority
|
|
+ // Tuinity - some plugins (namely protocolsupport) bypass this logic completely! So this needs to be moved.
|
|
Player player = entity.getBukkitEntity();
|
|
PlayerLoginEvent event = new PlayerLoginEvent(player, hostname, ((java.net.InetSocketAddress) socketaddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.getRawAddress()).getAddress());
|
|
|
|
@@ -927,13 +928,13 @@ public abstract class PlayerList {
|
|
|
|
worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
|
|
entityplayer1.forceCheckHighPriority(); // Player - Chunk priority
|
|
- while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) {
|
|
+ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), null, true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Tuinity - make sure this loads chunks, we default to NOT loading now
|
|
entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ());
|
|
}
|
|
// CraftBukkit start
|
|
LevelData worlddata = worldserver1.getLevelData();
|
|
entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag));
|
|
- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.getLoadViewDistance())); // Spigot // Paper - no-tick view distance
|
|
+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance())); // Spigot // Paper - no-tick view distance// Tuinity - replace old player chunk management
|
|
entityplayer1.setLevel(worldserver1);
|
|
entityplayer1.unsetRemoved();
|
|
entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot()));
|
|
@@ -1208,7 +1209,7 @@ public abstract class PlayerList {
|
|
// Really shouldn't happen...
|
|
backingSet = world != null ? world.players.toArray() : players.toArray();
|
|
} else {
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> nearbyPlayers = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4);
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<Either<Integer, IntProvider>> CONSTANT_OR_DISPATCH_CODEC = Codec.either(Codec.INT, Registry.INT_PROVIDER_TYPES.dispatch(IntProvider::getType, IntProviderType::codec));
|
|
- public static final Codec<IntProvider> CODEC = CONSTANT_OR_DISPATCH_CODEC.xmap((either) -> {
|
|
+ public static final Codec<IntProvider> 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<IntProvider> CODEC = new Codec<>() {
|
|
+ @Override
|
|
+ public <T> DataResult<com.mojang.datafixers.util.Pair<IntProvider, T>> decode(com.mojang.serialization.DynamicOps<T> 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 <T> DataResult<T> encode(IntProvider input, com.mojang.serialization.DynamicOps<T> ops, T prefix) {
|
|
+ // forward to real codec
|
|
+ return CODEC_REAL.encode(input, ops, prefix);
|
|
+ }
|
|
+ };
|
|
+ // Tuinity end
|
|
public static final Codec<IntProvider> NON_NEGATIVE_CODEC = codec(0, Integer.MAX_VALUE);
|
|
public static final Codec<IntProvider> POSITIVE_CODEC = codec(1, Integer.MAX_VALUE);
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
index 4fd030ef9537d9b31c6167d73349f4c4a6b33a15..ca7718053a6a2eb715ea3671bd4bc15304ede420 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Entity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
@@ -356,8 +356,27 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
}
|
|
|
|
public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<Entity> 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<Class<? extends Entity>, 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<VoxelShape> stream = !this.level.getWorldBorder().isWithinBounds(axisalignedbb) ? Stream.empty() : Stream.of(voxelshape); // Paper
|
|
- Stream<VoxelShape> stream1 = this.level.getEntityCollisions(this, axisalignedbb.expandTowards(movement), (entity) -> {
|
|
- return true;
|
|
- });
|
|
- RewindableStream<VoxelShape> 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<AABB> 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<VoxelShape> collisions) {
|
|
@@ -2244,9 +2430,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
float f = this.dimensions.width * 0.8F;
|
|
AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
|
|
|
|
- return this.level.getBlockCollisions(this, axisalignedbb, (iblockdata, blockposition) -> {
|
|
- return iblockdata.isSuffocating(this.level, blockposition);
|
|
- }).findAny().isPresent();
|
|
+ // Tuinity start
|
|
+ return com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this.level, this, axisalignedbb, null,
|
|
+ false, false, false, true, (iblockdata, blockposition) -> {
|
|
+ return iblockdata.isSuffocating(this.level, blockposition);
|
|
+ });
|
|
+ // Tuinity end
|
|
}
|
|
}
|
|
|
|
@@ -2254,11 +2443,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
return InteractionResult.PASS;
|
|
}
|
|
|
|
- public boolean canCollideWith(Entity other) {
|
|
+ public boolean canCollideWith(Entity other) { // Tuinity - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override
|
|
return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other);
|
|
}
|
|
|
|
- public boolean canBeCollidedWith() {
|
|
+ public boolean canBeCollidedWith() { // Tuinity - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override
|
|
return false;
|
|
}
|
|
|
|
@@ -3727,7 +3916,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
}
|
|
|
|
public void setDeltaMovement(Vec3 velocity) {
|
|
+ synchronized (this.posLock) { // Tuinity
|
|
this.deltaMovement = velocity;
|
|
+ } // Tuinity
|
|
}
|
|
|
|
public void setDeltaMovement(double x, double y, double z) {
|
|
@@ -3789,7 +3980,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
public final void setPosRaw(double x, double y, double z) {
|
|
// Paper start - fix MC-4
|
|
if (this instanceof ItemEntity) {
|
|
- if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) {
|
|
+ if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert
|
|
// encode/decode from PacketPlayOutEntity
|
|
x = Mth.lfloor(x * 4096.0D) * (1 / 4096.0D);
|
|
y = Mth.lfloor(y * 4096.0D) * (1 / 4096.0D);
|
|
@@ -3804,7 +3995,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
|
|
}
|
|
// Paper end
|
|
if (this.position.x != x || this.position.y != y || this.position.z != z) {
|
|
+ synchronized (this.posLock) { // Tuinity
|
|
this.position = new Vec3(x, y, z);
|
|
+ } // Tuinity
|
|
int i = Mth.floor(x);
|
|
int j = Mth.floor(y);
|
|
int k = Mth.floor(z);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
|
|
index c4c5c35e37b793f3b74349ff03c0829f4913b91c..154b3c767d079f72643c826b962892c1029b0a1b 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Mob.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Mob.java
|
|
@@ -784,7 +784,12 @@ public abstract class Mob extends LivingEntity {
|
|
if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
|
|
this.discard();
|
|
} else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
|
|
- Player entityhuman = this.level.findNearbyPlayer(this, -1.0D, EntitySelector.affectsSpawning); // Paper
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig.hardDespawnDistance + 1, EntitySelector.affectsSpawning); // Paper
|
|
+ if (entityhuman == null) {
|
|
+ entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0);
|
|
+ }
|
|
+ // Tuinity end - optimise checkDespawn
|
|
|
|
if (entityhuman != null) {
|
|
double d0 = entityhuman.distanceToSqr((Entity) this); // CraftBukkit - decompile error
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
index 6c3455823f996e0421975b7f4a00f4e333e9f514..7db31cc24e68aab55a9ba735165da23059bdc626 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
@@ -183,7 +183,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
|
|
data = this.getData(chunkcoordintpair);
|
|
}
|
|
com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
|
|
- chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
|
|
+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority
|
|
}
|
|
// Paper end
|
|
this.distanceTracker.runAllUpdates();
|
|
diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
index 925f16d5eb092518ef774f69a8d99689feb0f5d7..01d8af06f19427354cac95d691e65d31253fef94 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
@@ -91,7 +91,7 @@ public class Turtle extends Animal {
|
|
}
|
|
|
|
public void setHomePos(BlockPos pos) {
|
|
- this.entityData.set(Turtle.HOME_POS, pos);
|
|
+ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos...
|
|
}
|
|
|
|
public BlockPos getHomePos() { // Paper - public
|
|
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
index 1bccd932851045c374e3092d33dc77fab680d0db..069f658003d96a05aac0b30af1d89f15ea554475 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
@@ -498,6 +498,11 @@ public abstract class Player extends LivingEntity {
|
|
this.containerMenu = this.inventoryMenu;
|
|
}
|
|
// Paper end
|
|
+ // Tuinity start - special close for unloaded inventory
|
|
+ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
|
|
+ this.containerMenu = this.inventoryMenu;
|
|
+ }
|
|
+ // Tuinity end - special close for unloaded inventory
|
|
|
|
public void closeContainer() {
|
|
this.containerMenu = this.inventoryMenu;
|
|
diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
|
|
index 7ccfe737fdf7f07b731ea0ff82e897564350705c..abcc3dac7c7369a3f37e85ddeecbe272833298c9 100644
|
|
--- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java
|
|
+++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
|
|
@@ -60,9 +60,10 @@ public class EnderEyeItem extends Item {
|
|
|
|
// CraftBukkit start - Use relative location for far away sounds
|
|
// world.b(1038, blockposition1.c(1, 0, 1), 0);
|
|
- int viewDistance = world.getCraftServer().getViewDistance() * 16;
|
|
+ //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Tuinity - apply view distance patch
|
|
BlockPos soundPos = blockposition1.offset(1, 0, 1);
|
|
for (ServerPlayer player : world.getServer().getPlayerList().players) {
|
|
+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch
|
|
double deltaX = soundPos.getX() - player.getX();
|
|
double deltaZ = soundPos.getZ() - player.getZ();
|
|
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
|
|
diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java
|
|
index fe4dba491b586757a16aa36e62682f364daa2602..ec781ab232d12cedb5f0236860377c4917c576d7 100644
|
|
--- a/src/main/java/net/minecraft/world/level/BlockGetter.java
|
|
+++ b/src/main/java/net/minecraft/world/level/BlockGetter.java
|
|
@@ -84,7 +84,8 @@ public interface BlockGetter extends LevelHeightAccessor {
|
|
return BlockHitResult.miss(raytrace1.getTo(), Direction.getNearest(vec3d.x, vec3d.y, vec3d.z), new BlockPos(raytrace1.getTo()));
|
|
}
|
|
// Paper end
|
|
- FluidState fluid = this.getFluidState(blockposition);
|
|
+ if (iblockdata.isAir()) return null; // Tuinity - optimise air cases
|
|
+ FluidState fluid = iblockdata.getFluidState(); // Tuinity - don't need to go to world state again
|
|
Vec3 vec3d = raytrace1.getFrom();
|
|
Vec3 vec3d1 = raytrace1.getTo();
|
|
VoxelShape voxelshape = raytrace1.getBlockShape(iblockdata, this, blockposition);
|
|
diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
index 2a784a8342e708e0813c7076a2ca8e429446ffd3..b909bd7bf10adc9165df49a210df0d73912cd626 100644
|
|
--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
@@ -36,28 +36,40 @@ public interface CollisionGetter extends BlockGetter {
|
|
return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox()));
|
|
}
|
|
|
|
+ // Tuinity start - optimise collisions
|
|
+ default boolean noCollision(Entity entity, AABB box, Predicate<Entity> 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<Entity> 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<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box, Predicate<Entity> predicate);
|
|
diff --git a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java
|
|
index e420c98d9ccc45d570984dc30fdb928883edec9f..ac83704692cf60c34b579ed11689863ef191cad3 100644
|
|
--- a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java
|
|
+++ b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java
|
|
@@ -99,7 +99,7 @@ public class CollisionSpliterator extends AbstractSpliterator<VoxelShape> {
|
|
|
|
VoxelShape 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<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate);
|
|
+
|
|
+ void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into);
|
|
+
|
|
+ void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into);
|
|
+
|
|
+ <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, final AABB box, List<? super T> into,
|
|
+ Predicate<? super T> predicate);
|
|
+ // Tuinity end
|
|
+
|
|
List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
|
|
|
|
<T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> 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<Entity> hardCollides = (entityx) -> { // Tuinity - optimise entity hard collisions
|
|
+ if (true || entityx.getBoundingBox().intersects(aABB)) { // Tuinity - always true
|
|
if (entity == null) {
|
|
if (entityx.canBeCollidedWith()) {
|
|
return true;
|
|
@@ -67,7 +79,11 @@ public interface EntityGetter {
|
|
}
|
|
|
|
return false;
|
|
- })).stream().map(Entity::getBoundingBox).map(Shapes::create);
|
|
+ }; // Tuinity start - optimise entity hard collisions
|
|
+ predicate = predicate == null ? hardCollides : hardCollides.and(predicate);
|
|
+ return (entity != null && entity.hardCollides() ? this.getEntities(entity, aABB, predicate) : this.getHardCollidingEntities(entity, aABB, predicate))
|
|
+ .stream().map(Entity::getBoundingBox).map(Shapes::create);
|
|
+ // Tuinity end - optimise entity hard collisions
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
|
|
index 17281575ff83bbf1e720335619a78a6d0a0e5077..38753e10b1597a2f3bd2cde208c6e30b26a03b43 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Level.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
|
@@ -166,6 +166,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper
|
|
public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
|
|
|
|
+ public final com.tuinity.tuinity.config.TuinityConfig.WorldConfig tuinityConfig; // Tuinity - Server Config
|
|
+
|
|
public final co.aikar.timings.WorldTimingsHandler timings; // Paper
|
|
public static BlockPos lastPhysicsProblem; // Spigot
|
|
private org.spigotmc.TickLimiter entityLimiter;
|
|
@@ -202,9 +204,117 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
return this.typeKey;
|
|
}
|
|
|
|
+ // Tuinity start
|
|
+ protected final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager;
|
|
+
|
|
+ // Tuinity start - optimise CraftChunk#getEntities
|
|
+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
|
|
+ com.tuinity.tuinity.world.ChunkEntitySlices slices = this.entitySliceManager.getChunk(chunkX, chunkZ);
|
|
+ if (slices == null) {
|
|
+ return new org.bukkit.entity.Entity[0];
|
|
+ }
|
|
+ return slices.getChunkEntities();
|
|
+ }
|
|
+ // Tuinity end - optimise CraftChunk#getEntities
|
|
+
|
|
+ @Override
|
|
+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
|
|
+ List<Entity> ret = new java.util.ArrayList<>();
|
|
+ this.entitySliceManager.getEntities(except, box, ret, predicate);
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
|
|
+ this.entitySliceManager.getEntities(except, box, into, predicate);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
|
|
+ this.entitySliceManager.getHardCollidingEntities(except, box, into, predicate);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, final AABB box, List<? super T> into,
|
|
+ Predicate<? super T> predicate) {
|
|
+ this.entitySliceManager.getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box, Predicate<? super T> predicate) {
|
|
+ List<T> ret = new java.util.ArrayList<>();
|
|
+ this.entitySliceManager.getEntities(entityClass, null, box, ret, predicate);
|
|
+ return ret;
|
|
+ }
|
|
+ // Tuinity end
|
|
+ // Tuinity start - optimise checkDespawn
|
|
+ public final List<net.minecraft.server.level.ServerPlayer> getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY,
|
|
+ double sourceZ, double maxRange, @Nullable Predicate<Entity> 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<net.minecraft.server.level.ServerPlayer> ret = new java.util.ArrayList<>();
|
|
+ chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret);
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ private List<net.minecraft.server.level.ServerPlayer> getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY,
|
|
+ double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
|
|
+ List<net.minecraft.server.level.ServerPlayer> ret = new java.util.ArrayList<>();
|
|
+ double maxRangeSquared = maxRange * maxRange;
|
|
+
|
|
+ for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)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<Entity> 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<net.minecraft.server.level.ServerPlayer>)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<Entity> 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<Entity> predicate) {
|
|
+ return this.getNearestPlayer(null, d0, d1, d2, d3, predicate);
|
|
+ }
|
|
+ // Tuinity end - optimise checkDespawn
|
|
+
|
|
protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, final DimensionType dimensionmanager, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor
|
|
this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
|
|
this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper
|
|
+ this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData)worlddatamutable).getLevelName()); // Tuinity - Server Config
|
|
this.generator = gen;
|
|
this.world = new CraftWorld((ServerLevel) this, gen, env);
|
|
this.ticksPerAnimalSpawns = this.getCraftServer().getTicksPerAnimalSpawns(); // CraftBukkit
|
|
@@ -278,6 +388,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
this.chunkPacketBlockController = this.paperConfig.antiXray ?
|
|
new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor)
|
|
: com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
|
|
+ this.entitySliceManager = new com.tuinity.tuinity.world.EntitySliceManager((ServerLevel)this); // Tuinity
|
|
}
|
|
|
|
// Paper start
|
|
@@ -363,6 +474,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
|
|
+ // Tuinity start - make sure loaded chunks get the inlined variant of this function
|
|
+ net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource();
|
|
+ if (cps.mainThread == Thread.currentThread()) {
|
|
+ LevelChunk ifLoaded = cps.getChunkAtIfLoadedMainThread(chunkX, chunkZ);
|
|
+ if (ifLoaded != null) {
|
|
+ return ifLoaded;
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - make sure loaded chunks get the inlined variant of this function
|
|
return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
|
|
}
|
|
|
|
@@ -551,7 +671,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
|
|
// Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance
|
|
// if copied from above
|
|
- } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) {
|
|
+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Tuinity - replace old player chunk management
|
|
((ServerLevel)this).getChunkSource().blockChanged(blockposition);
|
|
// Paper end - per player view distance
|
|
}
|
|
@@ -862,6 +982,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
|
|
try {
|
|
tickConsumer.accept(entity);
|
|
+ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick
|
|
} catch (Throwable throwable) {
|
|
if (throwable instanceof ThreadDeath) throw throwable; // Paper
|
|
// Paper start - Prevent tile entity and entity crashes
|
|
@@ -991,26 +1112,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
|
|
this.getProfiler().incrementCounter("getEntities");
|
|
List<Entity> list = Lists.newArrayList();
|
|
-
|
|
- this.getEntities().get(box, (entity1) -> {
|
|
- if (entity1 != except && predicate.test(entity1)) {
|
|
- list.add(entity1);
|
|
- }
|
|
-
|
|
- if (entity1 instanceof EnderDragon) {
|
|
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
|
|
- int i = aentitycomplexpart.length;
|
|
-
|
|
- for (int j = 0; j < i; ++j) {
|
|
- EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
|
|
-
|
|
- if (entity1 != except && predicate.test(entitycomplexpart)) {
|
|
- list.add(entitycomplexpart);
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper
|
|
+ this.entitySliceManager.getEntities(except, box, list, predicate); // Tuinity - optimise this call
|
|
return list;
|
|
}
|
|
|
|
@@ -1019,26 +1121,22 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
this.getProfiler().incrementCounter("getEntities");
|
|
List<T> 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<? super T> test = (obj) -> {
|
|
+ return filter.tryCast(obj) != null;
|
|
+ };
|
|
+ predicate = predicate == null ? test : test.and((Predicate)predicate);
|
|
+ Class base;
|
|
+ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) {
|
|
+ this.entitySliceManager.getEntities((Entity) null, box, (List)list, (Predicate)predicate);
|
|
+ } else {
|
|
+ this.entitySliceManager.getEntities(base, null, box, (List)list, (Predicate)predicate); // Tuinity - optimise this call
|
|
}
|
|
-
|
|
- });
|
|
+ }
|
|
+ // Tuinity end - optimise this call
|
|
return list;
|
|
}
|
|
|
|
@@ -1326,10 +1424,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public abstract TagContainer getTagManager();
|
|
|
|
public BlockPos getBlockRandomPos(int x, int y, int z, int l) {
|
|
+ // Paper start - allow use of mutable pos
|
|
+ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos();
|
|
+ this.getRandomBlockPosition(x, y, z, l, ret);
|
|
+ return ret.immutable();
|
|
+ }
|
|
+ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) {
|
|
+ // Paper end
|
|
this.randValue = this.randValue * 3 + 1013904223;
|
|
int i1 = this.randValue >> 2;
|
|
|
|
- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15));
|
|
+ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call
|
|
+ return out; // Paper
|
|
}
|
|
|
|
public boolean noSave() {
|
|
diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
index 31fbcf6a35b902ce80c0a5a23dabb8ec3d8cbdfc..0059f0488acc22ebddc2faf4c5879f9f0c24fd14 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<Block, BlockState> {
|
|
|
|
- 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<O, S> {
|
|
private final ImmutableMap<Property<?>, Comparable<?>> values;
|
|
private Table<Property<?>, Comparable<?>, S> neighbours;
|
|
protected final MapCodec<S> propertiesCodec;
|
|
+ protected final com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Tuinity - optimise state lookup
|
|
|
|
protected StateHolder(O owner, ImmutableMap<Property<?>, Comparable<?>> entries, MapCodec<S> 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 <T extends Comparable<T>> S cycle(Property<T> property) {
|
|
@@ -85,11 +87,11 @@ public abstract class StateHolder<O, S> {
|
|
}
|
|
|
|
public <T extends Comparable<T>> boolean hasProperty(Property<T> property) {
|
|
- return this.values.containsKey(property);
|
|
+ return this.optimisedTable.get(property) != null; // Tuinity - optimise state lookup
|
|
}
|
|
|
|
public <T extends Comparable<T>> T getValue(Property<T> 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<O, S> {
|
|
}
|
|
|
|
public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> 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 <T extends Comparable<T>, V extends T> S setValue(Property<T> 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<Map<Property<?>, Comparable<?>>, S> states) {
|
|
@@ -134,7 +130,7 @@ public abstract class StateHolder<O, S> {
|
|
}
|
|
}
|
|
|
|
- this.neighbours = (Table<Property<?>, Comparable<?>, S>)(table.isEmpty() ? table : ArrayTable.create(table));
|
|
+ this.neighbours = (Table<Property<?>, 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<Boolean> {
|
|
private final ImmutableSet<Boolean> 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<T extends Enum<T> & StringRepresentable> extends Prope
|
|
private final ImmutableSet<T> values;
|
|
private final Map<String, T> 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<T> type, Collection<T> values) {
|
|
super(name, type);
|
|
this.values = ImmutableSet.copyOf(values);
|
|
@@ -31,6 +40,14 @@ public class EnumProperty<T extends Enum<T> & 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<Integer> {
|
|
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<T extends Comparable<T>> {
|
|
}, this::getName);
|
|
private final Codec<Property.Value<T>> 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<T> type) {
|
|
this.clazz = type;
|
|
this.name = name;
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
|
|
index 63203172a127d812fd59cea0546b67e855ce3ad5..498988b70617f086f047d8d293e525377971e66e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
|
|
@@ -1,5 +1,6 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
|
import it.unimi.dsi.fastutil.shorts.ShortList;
|
|
import java.util.Collection;
|
|
@@ -42,6 +43,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Tuinity start
|
|
+ default SWMRNibbleArray[] getBlockNibbles() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ default void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ default SWMRNibbleArray[] getSkyNibbles() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ default void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ public default boolean[] getSkyEmptinessMap() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ public default void setSkyEmptinessMap(final boolean[] emptinessMap) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ public default boolean[] getBlockEmptinessMap() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ public default void setBlockEmptinessMap(final boolean[] emptinessMap) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
BlockState getType(final int x, final int y, final int z); // Paper
|
|
@Nullable
|
|
BlockState setBlockState(BlockPos pos, BlockState state, boolean moved);
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
index c2b0b1adcff5baf169901710d492317d44b93846..c7636191fa2ba92db95a7f779d0e5a1bd45198aa 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
@@ -196,7 +196,7 @@ public abstract class ChunkGenerator {
|
|
// Get origin location (re)defined by event call.
|
|
center = new BlockPos(event.getOrigin().getBlockX(), event.getOrigin().getBlockY(), event.getOrigin().getBlockZ());
|
|
// Get world (re)defined by event call.
|
|
- world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle();
|
|
+ //world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle(); // Tuinity - callers and this function don't expect this to change
|
|
// Get radius and whether to find unexplored structures (re)defined by event call.
|
|
radius = event.getRadius();
|
|
skipExistingChunks = event.shouldFindUnexplored();
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
index 25570730f376665ca6477263d3b3f94d725ecd21..21d85b2f70e5ffe46220905b27715579d7fcdc59 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
@@ -11,7 +11,7 @@ public class DataLayer {
|
|
public static final int LAYER_SIZE = 128;
|
|
private static final int NIBBLE_SIZE = 4;
|
|
@Nullable
|
|
- protected byte[] data;
|
|
+ protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Tuinity - provide accessor
|
|
// Paper start
|
|
public static byte[] EMPTY_NIBBLE = new byte[2048];
|
|
private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072);
|
|
@@ -54,6 +54,7 @@ public class DataLayer {
|
|
boolean poolSafe = false;
|
|
public java.lang.Runnable cleaner;
|
|
private void registerCleaner() {
|
|
+ if (true) return; // Tuinity - purge cleaner usage
|
|
if (!poolSafe) {
|
|
cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes);
|
|
} else {
|
|
@@ -68,7 +69,7 @@ public class DataLayer {
|
|
}
|
|
public DataLayer(byte[] bytes, boolean isSafe) {
|
|
this.data = bytes;
|
|
- if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety
|
|
+ // Tuinity - purge cleaner usage
|
|
registerCleaner();
|
|
// Paper end
|
|
if (bytes.length != 2048) {
|
|
@@ -153,7 +154,7 @@ public class DataLayer {
|
|
}
|
|
// Paper end
|
|
public DataLayer copy() {
|
|
- return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor
|
|
+ return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Tuinity - no longer clone in constructor
|
|
}
|
|
|
|
public String toString() {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
|
|
index 8245c5834ec69beb8e3b95fb3900601009a9273f..88f30cd8e57ccb69da633daac49f8bc9e44111da 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
|
|
@@ -1,5 +1,6 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
|
import java.util.BitSet;
|
|
import java.util.Map;
|
|
@@ -29,6 +30,48 @@ public class ImposterProtoChunk extends ProtoChunk {
|
|
this.wrapped = wrapped;
|
|
}
|
|
|
|
+ // Tuinity start - rewrite light engine
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getBlockNibbles() {
|
|
+ return this.getWrapped().getBlockNibbles();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.getWrapped().setBlockNibbles(nibbles);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getSkyNibbles() {
|
|
+ return this.getWrapped().getSkyNibbles();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.getWrapped().setSkyNibbles(nibbles);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getSkyEmptinessMap() {
|
|
+ return this.getWrapped().getSkyEmptinessMap();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.getWrapped().setSkyEmptinessMap(emptinessMap);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getBlockEmptinessMap() {
|
|
+ return this.getWrapped().getBlockEmptinessMap();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.getWrapped().setBlockEmptinessMap(emptinessMap);
|
|
+ }
|
|
+ // Tuinity end - rewrite light engine
|
|
+
|
|
@Nullable
|
|
@Override
|
|
public BlockEntity getBlockEntity(BlockPos pos) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
index cc02b577453fa251f0f1b508281ddea2513138a1..54e23d303aad286ab46c3e5f9b17a5f9922e2942 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
@@ -1,5 +1,7 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.destroystokyo.paper.exception.ServerInternalException;
|
|
import com.google.common.collect.Maps;
|
|
@@ -17,7 +19,6 @@ import java.util.Collections;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
-import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Supplier;
|
|
@@ -28,7 +29,6 @@ import net.minecraft.CrashReport;
|
|
import net.minecraft.CrashReportCategory;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.core.BlockPos;
|
|
-import net.minecraft.core.DefaultedRegistry;
|
|
import net.minecraft.core.Registry;
|
|
import net.minecraft.core.SectionPos;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
@@ -125,11 +125,62 @@ public class LevelChunk implements ChunkAccess {
|
|
private volatile boolean isLightCorrect;
|
|
private final Int2ObjectMap<GameEventDispatcher> 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<Block> blockTickScheduler, TickList<Fluid> fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer<LevelChunk> 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<net.minecraft.server.level.ServerPlayer> 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<net.minecraft.server.level.ServerPlayer> playerGeneralAreaCache;
|
|
+
|
|
+ public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<net.minecraft.server.level.ServerPlayer> 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<Entity> predicate) {
|
|
+ if (!this.playerGeneralAreaCacheSet) {
|
|
+ this.updateGeneralAreaCache();
|
|
+ }
|
|
+
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<Entity> predicate,
|
|
+ double range, java.util.List<net.minecraft.server.level.ServerPlayer> ret) {
|
|
+ if (!this.playerGeneralAreaCacheSet) {
|
|
+ this.updateGeneralAreaCache();
|
|
+ }
|
|
+
|
|
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<LevelChunk> consumer) {
|
|
this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer);
|
|
+ // Tuinity start - copy over protochunk light
|
|
+ this.setBlockNibbles(protoChunk.getBlockNibbles());
|
|
+ this.setSkyNibbles(protoChunk.getSkyNibbles());
|
|
+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap());
|
|
+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap());
|
|
+ // Tuinity end - copy over protochunk light
|
|
Iterator iterator = protoChunk.getBlockEntities().values().iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -788,6 +920,7 @@ public class LevelChunk implements ChunkAccess {
|
|
|
|
// CraftBukkit start
|
|
public void loadCallback() {
|
|
+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Tuinity
|
|
// Paper start - neighbour cache
|
|
int chunkX = this.chunkPos.x;
|
|
int chunkZ = this.chunkPos.z;
|
|
@@ -807,6 +940,7 @@ public class LevelChunk implements ChunkAccess {
|
|
// Paper end - neighbour cache
|
|
org.bukkit.Server server = this.level.getCraftServer();
|
|
this.level.getChunkSource().addLoadedChunk(this); // Paper
|
|
+ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Tuinity - rewrite player chunk management
|
|
if (server != null) {
|
|
/*
|
|
* If it's a new world, the first few chunks are generated inside
|
|
@@ -842,6 +976,7 @@ public class LevelChunk implements ChunkAccess {
|
|
}
|
|
|
|
public void unloadCallback() {
|
|
+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Tuinity
|
|
org.bukkit.Server server = this.level.getCraftServer();
|
|
org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved());
|
|
server.getPluginManager().callEvent(unloadEvent);
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
index cdac1f7b30e4c043dcb12ac9e29af926df8170bd..5d2f76eeb4aef0a5ee8c202c1c682171d4d5b2ea 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
@@ -18,7 +18,8 @@ public class LevelChunkSection {
|
|
short nonEmptyBlockCount; // Paper - package-private
|
|
private short tickingBlockCount;
|
|
private short tickingFluidCount;
|
|
- final PalettedContainer<BlockState> states; // Paper - package-private
|
|
+ public final PalettedContainer<BlockState> 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<T> implements PaletteResize<T> {
|
|
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<T> implements PaletteResize<T> {
|
|
public interface CountConsumer<T> {
|
|
void accept(T object, int count);
|
|
}
|
|
+
|
|
+ // Paper start
|
|
+ public void forEachLocation(PalettedContainer.CountConsumer<T> datapaletteblock_a) {
|
|
+ this.storage.forEach((int location, int data) -> {
|
|
+ datapaletteblock_a.accept(this.palette.valueFor(data), location);
|
|
+ });
|
|
+ }
|
|
+ // Paper end
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
|
|
index 78bd3274866fed3d627a3eda7b96b92716507d38..ccdadf5d7c07d74f5bea94fc21784114b6d520da 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
|
|
@@ -1,5 +1,7 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Sets;
|
|
@@ -65,6 +67,53 @@ public class ProtoChunk implements ChunkAccess {
|
|
private volatile boolean isLightCorrect;
|
|
final net.minecraft.world.level.Level level; // Paper - Add level
|
|
|
|
+ // Tuinity start - rewrite light engine
|
|
+ protected volatile SWMRNibbleArray[] blockNibbles;
|
|
+ protected volatile SWMRNibbleArray[] skyNibbles;
|
|
+ protected volatile boolean[] skyEmptinessMap;
|
|
+ protected volatile boolean[] blockEmptinessMap;
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getBlockNibbles() {
|
|
+ return this.blockNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.blockNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getSkyNibbles() {
|
|
+ return this.skyNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.skyNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getSkyEmptinessMap() {
|
|
+ return this.skyEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.skyEmptinessMap = emptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getBlockEmptinessMap() {
|
|
+ return this.blockEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.blockEmptinessMap = emptinessMap;
|
|
+ }
|
|
+ // Tuinity end - rewrite light engine
|
|
+
|
|
// Paper start - add level
|
|
@Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { this(pos, upgradeData, world, null); }
|
|
public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) {
|
|
@@ -79,6 +128,10 @@ public class ProtoChunk implements ChunkAccess {
|
|
// Paper start - add level
|
|
@Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList<Block> blockTickScheduler, ProtoTickList<Fluid> fluidTickScheduler, LevelHeightAccessor world) { this(pos, upgradeData, levelChunkSections, blockTickScheduler, fluidTickScheduler, world, null); }
|
|
public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList<Block> blockTickScheduler, ProtoTickList<Fluid> fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) {
|
|
+ // Tuinity start
|
|
+ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ // Tuinity end
|
|
this.level = level;
|
|
// Paper end
|
|
this.chunkPos = pos;
|
|
@@ -176,7 +229,7 @@ public class ProtoChunk implements ChunkAccess {
|
|
|
|
LevelChunkSection levelChunkSection = this.getOrCreateSection(l);
|
|
BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state);
|
|
- if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) {
|
|
+ if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Tuinity - move block updates to only happen after lighting occurs (or during, thanks chunk system)
|
|
this.lightEngine.checkBlock(pos);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
index 18432dc1e5067d65cda709b7b3bcc2dd37b77d02..917fa5a3106259c01d6a01acf770890dbdf50f1a 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
@@ -65,6 +65,21 @@ import org.apache.logging.log4j.Logger;
|
|
|
|
public class ChunkSerializer {
|
|
|
|
+ // Tuinity start - replace light engine impl
|
|
+ private static final int STARLIGHT_LIGHT_VERSION = 5;
|
|
+
|
|
+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
|
|
+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
|
|
+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
|
|
+ // Tuinity end - replace light engine impl
|
|
+ // Tuinity start
|
|
+ // TODO: Check on update
|
|
+ public static long getLastWorldSaveTime(CompoundTag chunkData) {
|
|
+ CompoundTag levelData = chunkData.getCompound("Level");
|
|
+ return levelData.getLong("LastUpdate");
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
public static final String TAG_UPGRADE_DATA = "UpgradeData";
|
|
|
|
@@ -118,7 +133,7 @@ public class ChunkSerializer {
|
|
}
|
|
// Paper end
|
|
BiomeSource worldchunkmanager = chunkgenerator.getBiomeSource();
|
|
- CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate
|
|
+ CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate // Tuinity - diff on change
|
|
ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos")); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate
|
|
|
|
if (!Objects.equals(pos, chunkcoordintpair1)) {
|
|
@@ -133,13 +148,20 @@ public class ChunkSerializer {
|
|
ProtoTickList<Fluid> 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<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>();
|
|
+ public static final Int2ObjectMap<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>(); // Tuinity - public
|
|
public static final RegionFileVersion VERSION_GZIP = register(new RegionFileVersion(1, GZIPInputStream::new, GZIPOutputStream::new));
|
|
public static final RegionFileVersion VERSION_DEFLATE = register(new RegionFileVersion(2, InflaterInputStream::new, DeflaterOutputStream::new));
|
|
public static final RegionFileVersion VERSION_NONE = register(new RegionFileVersion(3, (inputStream) -> {
|
|
diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
|
|
index f01182a0ac8a14bcd5b1deb778306e7bf1bf70ed..2cfc54a577d0a63a504e24bc54fd763fe51083e5 100644
|
|
--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
|
|
+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
|
|
@@ -9,54 +9,40 @@ import javax.annotation.Nullable;
|
|
import net.minecraft.world.entity.Entity;
|
|
|
|
public class EntityTickList {
|
|
- private Int2ObjectMap<Entity> active = new Int2ObjectLinkedOpenHashMap<>();
|
|
- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>();
|
|
- @Nullable
|
|
- private Int2ObjectMap<Entity> iterated;
|
|
+ private final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Entity> 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<Entity> entry : Int2ObjectMaps.fastIterable(this.active)) {
|
|
- this.passive.put(entry.getIntKey(), entry.getValue());
|
|
- }
|
|
-
|
|
- Int2ObjectMap<Entity> int2ObjectMap = this.active;
|
|
- this.active = this.passive;
|
|
- this.passive = int2ObjectMap;
|
|
- }
|
|
+ // Tuinity - replace with better logic, do not delay removals
|
|
|
|
}
|
|
|
|
public void add(Entity entity) {
|
|
this.ensureActiveIsNotIterated();
|
|
- this.active.put(entity.getId(), entity);
|
|
+ this.entities.add(entity); // Tuinity - replace with better logic, do not delay removals/additions
|
|
}
|
|
|
|
public void remove(Entity entity) {
|
|
this.ensureActiveIsNotIterated();
|
|
- this.active.remove(entity.getId());
|
|
+ this.entities.remove(entity); // Tuinity - replace with better logic, do not delay removals/additions
|
|
}
|
|
|
|
public boolean contains(Entity entity) {
|
|
- return this.active.containsKey(entity.getId());
|
|
+ return this.entities.contains(entity); // Tuinity - replace with better logic, do not delay removals/additions
|
|
}
|
|
|
|
public void forEach(Consumer<Entity> action) {
|
|
- if (this.iterated != null) {
|
|
- throw new UnsupportedOperationException("Only one concurrent iteration supported");
|
|
- } else {
|
|
- this.iterated = this.active;
|
|
-
|
|
- try {
|
|
- for(Entity entity : this.active.values()) {
|
|
- action.accept(entity);
|
|
- }
|
|
- } finally {
|
|
- this.iterated = null;
|
|
+ // Tuinity start - replace with better logic, do not delay removals/additions
|
|
+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
|
|
+ // (by dfl iterator() is configured to not iterate over new entries)
|
|
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
|
|
+ try {
|
|
+ while (iterator.hasNext()) {
|
|
+ action.accept(iterator.next());
|
|
}
|
|
-
|
|
+ } finally {
|
|
+ iterator.finishedIterating();
|
|
}
|
|
+
|
|
+ // Tuinity end - replace with better logic, do not delay removals/additions
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
index 79e733b3ea2e6589d60f3b322244479d2b3b9f86..a99d0a00bbdb90588b87a3f85c62bdc1468b5e5a 100644
|
|
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
@@ -41,8 +41,10 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
|
|
private final Long2ObjectMap<PersistentEntitySectionManager.ChunkLoadStatus> chunkLoadStatuses = new Long2ObjectOpenHashMap<>();
|
|
private final LongSet chunksToUnload = new LongOpenHashSet();
|
|
private final Queue<ChunkEntities<T>> loadingInbox = Queues.newConcurrentLinkedQueue();
|
|
+ public final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager; // Tuinity
|
|
|
|
- public PersistentEntitySectionManager(Class<T> entityClass, LevelCallback<T> handler, EntityPersistentStorage<T> dataAccess) {
|
|
+ public PersistentEntitySectionManager(Class<T> entityClass, LevelCallback<T> handler, EntityPersistentStorage<T> dataAccess, com.tuinity.tuinity.world.EntitySliceManager entitySliceManager) { // Tuinity
|
|
+ this.entitySliceManager = entitySliceManager; // Tuinity
|
|
this.visibleEntityStorage = new EntityLookup<>();
|
|
this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility);
|
|
this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN);
|
|
@@ -93,6 +95,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
|
|
long l = SectionPos.asLong(entity.blockPosition());
|
|
EntitySection<T> entitySection = this.sectionStorage.getOrCreateSection(l);
|
|
entitySection.add(entity);
|
|
+ this.entitySliceManager.addEntity((Entity)entity); // Tuinity
|
|
entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, l, entitySection));
|
|
if (!existing) {
|
|
this.callbacks.onCreated(entity);
|
|
@@ -147,6 +150,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
|
|
|
|
public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) {
|
|
Visibility visibility = Visibility.fromFullChunkStatus(levelType);
|
|
+ this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Tuinity
|
|
this.updateChunkStatus(chunkPos, visibility);
|
|
}
|
|
|
|
@@ -383,6 +387,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
|
|
BlockPos blockPos = this.entity.blockPosition();
|
|
long l = SectionPos.asLong(blockPos);
|
|
if (l != this.currentSectionKey) {
|
|
+ PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Tuinity
|
|
Visibility visibility = this.currentSection.getStatus();
|
|
if (!this.currentSection.remove(this.entity)) {
|
|
PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l);
|
|
@@ -426,6 +431,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> 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<ColumnPlacer> 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<IntProvider> size, java.util.Optional<Integer> minSize, java.util.Optional<Integer> extraSize) {
|
|
+ if (size.isPresent()) {
|
|
+ this.size = size.get();
|
|
+ } else {
|
|
+ this.size = net.minecraft.util.valueproviders.BiasedToBottomInt.of(minSize.get().intValue(), minSize.get().intValue() + extraSize.get().intValue());
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end
|
|
private final IntProvider size;
|
|
|
|
public ColumnPlacer(IntProvider size) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java
|
|
index 5da68897148192905c2747676c1ee2ee649f923f..b990099cf274f8cb0d96c139345cf0bf328affd6 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java
|
|
@@ -18,13 +18,13 @@ public class TreeConfiguration implements FeatureConfiguration {
|
|
return treeConfiguration.trunkProvider;
|
|
}), TrunkPlacer.CODEC.fieldOf("trunk_placer").forGetter((treeConfiguration) -> {
|
|
return treeConfiguration.trunkPlacer;
|
|
- }), BlockStateProvider.CODEC.fieldOf("foliage_provider").forGetter((treeConfiguration) -> {
|
|
+ }), net.minecraft.server.MCUtil.fieldWithFallbacks(BlockStateProvider.CODEC, "foliage_provider", "leaves_provider").forGetter((treeConfiguration) -> { // Paper - provide fallback for rename
|
|
return treeConfiguration.foliageProvider;
|
|
- }), BlockStateProvider.CODEC.fieldOf("sapling_provider").forGetter((treeConfiguration) -> {
|
|
+ }), BlockStateProvider.CODEC.optionalFieldOf("sapling_provider", new SimpleStateProvider(Blocks.OAK_SAPLING.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide default - it looks like for now this is OK because it's just used to check canSurvive. Same check happens in 1.16.5 for the default we provide - so it should retain behavior...
|
|
return treeConfiguration.saplingProvider;
|
|
}), FoliagePlacer.CODEC.fieldOf("foliage_placer").forGetter((treeConfiguration) -> {
|
|
return treeConfiguration.foliagePlacer;
|
|
- }), BlockStateProvider.CODEC.fieldOf("dirt_provider").forGetter((treeConfiguration) -> {
|
|
+ }), BlockStateProvider.CODEC.optionalFieldOf("dirt_provider", new SimpleStateProvider(Blocks.DIRT.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide defaults, old data DOES NOT have this key (thankfully ALL OLD DATA used DIRT)
|
|
return treeConfiguration.dirtProvider;
|
|
}), FeatureSize.CODEC.fieldOf("minimum_size").forGetter((treeConfiguration) -> {
|
|
return treeConfiguration.minimumSize;
|
|
diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
|
|
index 120498a39b7ca7aee9763084507508d4a1c425aa..6f7e6429c35eea346517cbf08cf223fc6d838a8c 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/AABB.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/AABB.java
|
|
@@ -25,6 +25,17 @@ public class AABB {
|
|
this.maxZ = Math.max(z1, z2);
|
|
}
|
|
|
|
+ // Tuinity start
|
|
+ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) {
|
|
+ this.minX = minX;
|
|
+ this.minY = minY;
|
|
+ this.minZ = minZ;
|
|
+ this.maxX = maxX;
|
|
+ this.maxY = maxY;
|
|
+ this.maxZ = maxZ;
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
public AABB(BlockPos pos) {
|
|
this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1));
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
index 99427b6130895ddecee8bcf77db72d809c24c375..af1ef430e81cb9bdd749aa235577c63fa381f4c5 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
@@ -6,6 +6,9 @@ import java.util.Arrays;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.core.Direction;
|
|
|
|
+// Tuinity start
|
|
+import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
|
|
+// Tuinity end
|
|
public class ArrayVoxelShape extends VoxelShape {
|
|
private final DoubleList xs;
|
|
private final DoubleList ys;
|
|
@@ -16,6 +19,11 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
}
|
|
|
|
ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
|
|
+ // Tuinity start - optimise multi-aabb shapes
|
|
+ this(shape, xPoints, yPoints, zPoints, null, 0.0, 0.0, 0.0);
|
|
+ }
|
|
+ ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints, net.minecraft.world.phys.AABB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) {
|
|
+ // Tuinity end - optimise multi-aabb shapes
|
|
super(shape);
|
|
int i = shape.getXSize() + 1;
|
|
int j = shape.getYSize() + 1;
|
|
@@ -27,6 +35,12 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
} else {
|
|
throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape."));
|
|
}
|
|
+ // Tuinity start - optimise multi-aabb shapes
|
|
+ this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.toAabbs().toArray(EMPTY) : boundingBoxesRepresentation;
|
|
+ this.offsetX = offsetX;
|
|
+ this.offsetY = offsetY;
|
|
+ this.offsetZ = offsetZ;
|
|
+ // Tuinity end - optimise multi-aabb shapes
|
|
}
|
|
|
|
@Override
|
|
@@ -42,4 +56,152 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
}
|
|
+
|
|
+ // Tuinity start
|
|
+ public static final class DoubleListOffsetExposed extends AbstractDoubleList {
|
|
+
|
|
+ public final DoubleArrayList list;
|
|
+ public final double offset;
|
|
+
|
|
+ public DoubleListOffsetExposed(final DoubleArrayList list, final double offset) {
|
|
+ this.list = list;
|
|
+ this.offset = offset;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public double getDouble(final int index) {
|
|
+ return this.list.getDouble(index) + this.offset;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int size() {
|
|
+ return this.list.size();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ static final net.minecraft.world.phys.AABB[] EMPTY = new net.minecraft.world.phys.AABB[0];
|
|
+ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation;
|
|
+
|
|
+ final double offsetX;
|
|
+ final double offsetY;
|
|
+ final double offsetZ;
|
|
+
|
|
+ public final net.minecraft.world.phys.AABB[] getBoundingBoxesRepresentation() {
|
|
+ return this.boundingBoxesRepresentation;
|
|
+ }
|
|
+
|
|
+ public final double getOffsetX() {
|
|
+ return this.offsetX;
|
|
+ }
|
|
+
|
|
+ public final double getOffsetY() {
|
|
+ return this.offsetY;
|
|
+ }
|
|
+
|
|
+ public final double getOffsetZ() {
|
|
+ return this.offsetZ;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public java.util.List<net.minecraft.world.phys.AABB> toAabbs() {
|
|
+ if (this.boundingBoxesRepresentation == null) {
|
|
+ return super.toAabbs();
|
|
+ }
|
|
+ java.util.List<net.minecraft.world.phys.AABB> ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length);
|
|
+
|
|
+ double offX = this.offsetX;
|
|
+ double offY = this.offsetY;
|
|
+ double offZ = this.offsetZ;
|
|
+
|
|
+ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) {
|
|
+ ret.add(boundingBox.move(offX, offY, offZ));
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ protected static DoubleArrayList getList(DoubleList from) {
|
|
+ if (from instanceof DoubleArrayList) {
|
|
+ return (DoubleArrayList)from;
|
|
+ } else {
|
|
+ return DoubleArrayList.wrap(from.toDoubleArray());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public VoxelShape move(double x, double y, double z) {
|
|
+ if (x == 0.0 && y == 0.0 && z == 0.0) {
|
|
+ return this;
|
|
+ }
|
|
+ DoubleListOffsetExposed xPoints, yPoints, zPoints;
|
|
+ double offsetX, offsetY, offsetZ;
|
|
+
|
|
+ if (this.xs instanceof DoubleListOffsetExposed) {
|
|
+ xPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.xs).list, offsetX = this.offsetX + x);
|
|
+ yPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.ys).list, offsetY = this.offsetY + y);
|
|
+ zPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.zs).list, offsetZ = this.offsetZ + z);
|
|
+ } else {
|
|
+ xPoints = new DoubleListOffsetExposed(getList(this.xs), offsetX = x);
|
|
+ yPoints = new DoubleListOffsetExposed(getList(this.ys), offsetY = y);
|
|
+ zPoints = new DoubleListOffsetExposed(getList(this.zs), offsetZ = z);
|
|
+ }
|
|
+
|
|
+ return new ArrayVoxelShape(this.shape, xPoints, yPoints, zPoints, this.boundingBoxesRepresentation, offsetX, offsetY, offsetZ);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean intersects(net.minecraft.world.phys.AABB axisalingedbb) {
|
|
+ // this can be optimised by checking an "overall shape" first, but not needed
|
|
+ double offX = this.offsetX;
|
|
+ double offY = this.offsetY;
|
|
+ double offZ = this.offsetZ;
|
|
+
|
|
+ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) {
|
|
+ if (com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(axisalingedbb, boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ,
|
|
+ boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forAllBoxes(Shapes.DoubleLineConsumer doubleLineConsumer) {
|
|
+ if (this.boundingBoxesRepresentation == null) {
|
|
+ super.forAllBoxes(doubleLineConsumer);
|
|
+ return;
|
|
+ }
|
|
+ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) {
|
|
+ doubleLineConsumer.consume(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ,
|
|
+ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public VoxelShape optimize() {
|
|
+ if (this == Shapes.empty() || this.boundingBoxesRepresentation.length == 0) {
|
|
+ return this;
|
|
+ }
|
|
+
|
|
+ VoxelShape simplified = Shapes.empty();
|
|
+ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) {
|
|
+ simplified = Shapes.joinUnoptimized(simplified, Shapes.box(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ,
|
|
+ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ), BooleanOp.OR);
|
|
+ }
|
|
+
|
|
+ if (!(simplified instanceof ArrayVoxelShape)) {
|
|
+ return simplified;
|
|
+ }
|
|
+
|
|
+ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation = ((ArrayVoxelShape)simplified).getBoundingBoxesRepresentation();
|
|
+
|
|
+ if (boundingBoxesRepresentation.length == 1) {
|
|
+ return new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBoxesRepresentation[0]).optimize();
|
|
+ }
|
|
+
|
|
+ return simplified;
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
index 16bc18cacbf7a23fb744c8a12e7fd8da699b2fea..472c47a585da7d95b3f4774d3caef1d864b6337a 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
@@ -26,16 +26,17 @@ public final class Shapes {
|
|
DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1);
|
|
discreteVoxelShape.fill(0, 0, 0);
|
|
return new CubeVoxelShape(discreteVoxelShape);
|
|
- });
|
|
+ }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Tuinity - OBFHELPER
|
|
public static final VoxelShape INFINITY = box(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
|
|
private static final VoxelShape EMPTY = new ArrayVoxelShape(new BitSetDiscreteVoxelShape(0, 0, 0), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})));
|
|
+ public static final com.tuinity.tuinity.voxel.AABBVoxelShape BLOCK_OPTIMISED = new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); // Tuinity
|
|
|
|
public static VoxelShape empty() {
|
|
return EMPTY;
|
|
}
|
|
|
|
public static VoxelShape block() {
|
|
- return BLOCK;
|
|
+ return BLOCK_OPTIMISED; // Tuinity
|
|
}
|
|
|
|
public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
|
|
@@ -47,30 +48,11 @@ public final class Shapes {
|
|
}
|
|
|
|
public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
|
|
- if (!(maxX - minX < 1.0E-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) {
|
|
- int i = findBits(minX, maxX);
|
|
- int j = findBits(minY, maxY);
|
|
- int k = findBits(minZ, maxZ);
|
|
- if (i >= 0 && j >= 0 && k >= 0) {
|
|
- if (i == 0 && j == 0 && k == 0) {
|
|
- return block();
|
|
- } else {
|
|
- int l = 1 << i;
|
|
- int m = 1 << j;
|
|
- int n = 1 << k;
|
|
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(l, m, n, (int)Math.round(minX * (double)l), (int)Math.round(minY * (double)m), (int)Math.round(minZ * (double)n), (int)Math.round(maxX * (double)l), (int)Math.round(maxY * (double)m), (int)Math.round(maxZ * (double)n));
|
|
- return new CubeVoxelShape(bitSetDiscreteVoxelShape);
|
|
- }
|
|
- } else {
|
|
- return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ}));
|
|
- }
|
|
- } else {
|
|
- return empty();
|
|
- }
|
|
+ return new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Tuinity
|
|
}
|
|
|
|
public static VoxelShape create(AABB box) {
|
|
- return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ);
|
|
+ return new com.tuinity.tuinity.voxel.AABBVoxelShape(box); // Tuinity
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@@ -132,6 +114,20 @@ public final class Shapes {
|
|
}
|
|
|
|
public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
|
|
+ // Tuinity start - optimise voxelshape
|
|
+ if (predicate == BooleanOp.AND) {
|
|
+ if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) {
|
|
+ return com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb, ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb);
|
|
+ } else if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof ArrayVoxelShape) {
|
|
+ return ((ArrayVoxelShape)shape2).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb);
|
|
+ } else if (shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape1 instanceof ArrayVoxelShape) {
|
|
+ return ((ArrayVoxelShape)shape1).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb);
|
|
+ }
|
|
+ }
|
|
+ return joinIsNotEmptyVanilla(shape1, shape2, predicate);
|
|
+ }
|
|
+ public static boolean joinIsNotEmptyVanilla(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
|
|
+ // Tuinity end - optimise voxelshape
|
|
if (predicate.apply(false, false)) {
|
|
throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
|
|
} else {
|
|
@@ -285,6 +281,43 @@ public final class Shapes {
|
|
}
|
|
|
|
public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) {
|
|
+ // Tuinity start - optimise shape creation here for lighting, as this shape is going to be used
|
|
+ // for transparency checks
|
|
+ if (shape == BLOCK || shape == BLOCK_OPTIMISED) {
|
|
+ return BLOCK_OPTIMISED;
|
|
+ } else if (shape == empty()) {
|
|
+ return empty();
|
|
+ }
|
|
+
|
|
+ if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) {
|
|
+ final AABB box = ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape).aabb;
|
|
+ switch (direction) {
|
|
+ case WEST: // -X
|
|
+ case EAST: { // +X
|
|
+ final boolean useEmpty = direction == Direction.EAST ? !DoubleMath.fuzzyEquals(box.maxX, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) :
|
|
+ !DoubleMath.fuzzyEquals(box.minX, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON);
|
|
+ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, box.minY, box.minZ, 1.0, box.maxY, box.maxZ)).optimize();
|
|
+ }
|
|
+ case DOWN: // -Y
|
|
+ case UP: { // +Y
|
|
+ final boolean useEmpty = direction == Direction.UP ? !DoubleMath.fuzzyEquals(box.maxY, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) :
|
|
+ !DoubleMath.fuzzyEquals(box.minY, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON);
|
|
+ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, 0.0, box.minZ, box.maxX, 1.0, box.maxZ)).optimize();
|
|
+ }
|
|
+ case NORTH: // -Z
|
|
+ case SOUTH: { // +Z
|
|
+ final boolean useEmpty = direction == Direction.SOUTH ? !DoubleMath.fuzzyEquals(box.maxZ, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) :
|
|
+ !DoubleMath.fuzzyEquals(box.minZ,0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON);
|
|
+ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, box.minY, 0.0, box.maxX, box.maxY, 1.0)).optimize();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // fall back to vanilla
|
|
+ return getFaceShapeVanilla(shape, direction);
|
|
+ }
|
|
+ public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) {
|
|
+ // Tuinity end
|
|
if (shape == block()) {
|
|
return block();
|
|
} else {
|
|
@@ -299,7 +332,7 @@ public final class Shapes {
|
|
i = 0;
|
|
}
|
|
|
|
- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i));
|
|
+ return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Tuinity - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape
|
|
}
|
|
}
|
|
|
|
@@ -324,6 +357,53 @@ public final class Shapes {
|
|
}
|
|
|
|
public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
|
|
+ // Tuinity start - try to optimise for the case where the shapes do _not_ occlude
|
|
+ // which is _most_ of the time in lighting
|
|
+ if (one == getFullUnoptimisedCube() || one == BLOCK_OPTIMISED
|
|
+ || two == getFullUnoptimisedCube() || two == BLOCK_OPTIMISED) {
|
|
+ return true;
|
|
+ }
|
|
+ boolean v1Empty = one == empty();
|
|
+ boolean v2Empty = two == empty();
|
|
+ if (v1Empty && v2Empty) {
|
|
+ return false;
|
|
+ }
|
|
+ if ((one instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v1Empty)
|
|
+ && (two instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v2Empty)) {
|
|
+ if (!v1Empty && !v2Empty && (one != two)) {
|
|
+ AABB boundingBox1 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb;
|
|
+ AABB boundingBox2 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb;
|
|
+ // can call it here in some cases
|
|
+
|
|
+ // check overall bounding box
|
|
+ double minY = Math.min(boundingBox1.minY, boundingBox2.minY);
|
|
+ double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY);
|
|
+ if (minY > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxY < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) {
|
|
+ return false;
|
|
+ }
|
|
+ double minX = Math.min(boundingBox1.minX, boundingBox2.minX);
|
|
+ double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX);
|
|
+ if (minX > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxX < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) {
|
|
+ return false;
|
|
+ }
|
|
+ double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ);
|
|
+ double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ);
|
|
+ if (minZ > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxZ < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) {
|
|
+ return false;
|
|
+ }
|
|
+ // fall through to full merge check
|
|
+ } else {
|
|
+ AABB boundingBox = v1Empty ? ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb : ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb;
|
|
+ // check if the bounding box encloses the full cube
|
|
+ return (boundingBox.minY <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (boundingBox.minX <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (boundingBox.minZ <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON));
|
|
+ }
|
|
+ }
|
|
+ return faceShapeOccludesVanilla(one, two);
|
|
+ }
|
|
+ public static boolean faceShapeOccludesVanilla(VoxelShape one, VoxelShape two) {
|
|
+ // Tuinity end
|
|
if (one != block() && two != block()) {
|
|
if (one.isEmpty() && two.isEmpty()) {
|
|
return false;
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
index f325d76c79d63629200262a77eab7cdcc9beedfa..ad23eafd6d9e7901f726977ad8404fa34dc0874e 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
@@ -16,11 +16,17 @@ import net.minecraft.world.phys.BlockHitResult;
|
|
import net.minecraft.world.phys.Vec3;
|
|
|
|
public abstract class VoxelShape {
|
|
- protected final DiscreteVoxelShape shape;
|
|
+ public final DiscreteVoxelShape shape; // Tuinity - public
|
|
@Nullable
|
|
private VoxelShape[] faces;
|
|
|
|
- VoxelShape(DiscreteVoxelShape voxels) {
|
|
+ // Tuinity start
|
|
+ public boolean intersects(AABB shape) {
|
|
+ return Shapes.joinIsNotEmpty(this, new com.tuinity.tuinity.voxel.AABBVoxelShape(shape), BooleanOp.AND);
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
+ protected VoxelShape(DiscreteVoxelShape voxels) { // Tuinity - protected
|
|
this.shape = voxels;
|
|
}
|
|
|
|
@@ -163,7 +169,7 @@ public abstract class VoxelShape {
|
|
}
|
|
}
|
|
|
|
- private VoxelShape calculateFace(Direction direction) {
|
|
+ protected VoxelShape calculateFace(Direction direction) { // Tuinity
|
|
Direction.Axis axis = direction.getAxis();
|
|
DoubleList doubleList = this.getCoords(axis);
|
|
if (doubleList.size() == 2 && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0D, 1.0E-7D)) {
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
|
|
index 40d6dfe30e8f388fb2014ba81f9ea4a986354b88..9de4b1c9402e78c661b4d2dc7d70439e75768bc8 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
|
|
@@ -110,13 +110,7 @@ public class CraftChunk implements Chunk {
|
|
this.getWorld().getChunkAt(x, z); // Transient load for this tick
|
|
}
|
|
|
|
- // Paper start - improve CraftChunk#getEntities
|
|
- return this.worldServer.entityManager.sectionStorage.getExistingSectionsInChunk(ChunkPos.asLong(this.x, this.z))
|
|
- .flatMap(net.minecraft.world.level.entity.EntitySection::getEntities)
|
|
- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
|
|
- .filter(entity -> entity != null && entity.isValid())
|
|
- .toArray(Entity[]::new);
|
|
- // Paper end
|
|
+ return ((CraftWorld)this.getWorld()).getHandle().getChunkEntities(this.x, this.z); // Tuinity - optimise this better than paper :)
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
index 2ba3b01092cef23bcc958244992ef44103bc7e74..130a088e694b85f7d56620352f044161ea56caf3 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
@@ -230,7 +230,7 @@ import javax.annotation.Nullable; // Paper
|
|
import javax.annotation.Nonnull; // Paper
|
|
|
|
public final class CraftServer implements Server {
|
|
- private final String serverName = "Paper"; // Paper
|
|
+ private final String serverName = "Tuinity"; // Tuinity // Paper
|
|
private final String serverVersion;
|
|
private final String bukkitVersion = Versioning.getBukkitVersion();
|
|
private final Logger logger = Logger.getLogger("Minecraft");
|
|
@@ -875,6 +875,7 @@ public final class CraftServer implements Server {
|
|
|
|
org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot
|
|
com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper
|
|
+ com.tuinity.tuinity.config.TuinityConfig.init((File) console.options.valueOf("tuinity-settings")); // Tuinity - Server Config
|
|
for (ServerLevel world : this.console.getAllLevels()) {
|
|
world.serverLevelData.setDifficulty(config.difficulty);
|
|
world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals);
|
|
@@ -909,6 +910,7 @@ public final class CraftServer implements Server {
|
|
}
|
|
world.spigotConfig.init(); // Spigot
|
|
world.paperConfig.init(); // Paper
|
|
+ world.tuinityConfig.init(); // Tuinity - Server Config
|
|
}
|
|
|
|
Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper
|
|
@@ -2374,6 +2376,14 @@ public final class CraftServer implements Server {
|
|
return com.destroystokyo.paper.PaperConfig.config;
|
|
}
|
|
|
|
+ // Tuinity start - add config to timings report
|
|
+ @Override
|
|
+ public YamlConfiguration getTuinityConfig()
|
|
+ {
|
|
+ return com.tuinity.tuinity.config.TuinityConfig.config;
|
|
+ }
|
|
+ // Tuinity end - add config to timings report
|
|
+
|
|
@Override
|
|
public void restart() {
|
|
org.spigotmc.RestartCommand.restart();
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
index 3403b75c8311f1e52a0533363c5f0307442f8a15..92cb1fd2419eb3a3e64ebc0c5e699a79483f8c44 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
@@ -289,7 +289,7 @@ public class CraftWorld implements World {
|
|
public int getTileEntityCount() {
|
|
return net.minecraft.server.MCUtil.ensureMain(() -> {
|
|
// We don't use the full world tile entity list, so we must iterate chunks
|
|
- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.visibleChunkMap;
|
|
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map
|
|
int size = 0;
|
|
for (ChunkHolder playerchunk : chunks.values()) {
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
|
|
@@ -312,7 +312,7 @@ public class CraftWorld implements World {
|
|
return net.minecraft.server.MCUtil.ensureMain(() -> {
|
|
int ret = 0;
|
|
|
|
- for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) {
|
|
+ for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().values()) { // Tuinity - change updating chunks map
|
|
if (chunkHolder.getTickingChunk() != null) {
|
|
++ret;
|
|
}
|
|
@@ -351,13 +351,20 @@ public class CraftWorld implements World {
|
|
this.generator = gen;
|
|
|
|
this.environment = env;
|
|
+ // Tuinity start - per world spawn limits
|
|
+ this.monsterSpawn = world.tuinityConfig.spawnLimitMonsters;
|
|
+ this.animalSpawn = world.tuinityConfig.spawnLimitAnimals;
|
|
+ this.waterAmbientSpawn = world.tuinityConfig.spawnLimitWaterAmbient;
|
|
+ this.waterAnimalSpawn = world.tuinityConfig.spawnLimitWaterAnimals;
|
|
+ this.ambientSpawn = world.tuinityConfig.spawnLimitAmbient;
|
|
// Paper start - per world spawn limits
|
|
- this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters;
|
|
- this.animalSpawn = this.world.paperConfig.spawnLimitAnimals;
|
|
- this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals;
|
|
- this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient;
|
|
- this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient;
|
|
+ if (this.monsterSpawn == -1) this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters;
|
|
+ if (this.animalSpawn == -1) this.animalSpawn = this.world.paperConfig.spawnLimitAnimals;
|
|
+ if (this.waterAnimalSpawn == -1) this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals;
|
|
+ if (this.waterAmbientSpawn == -1) this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient;
|
|
+ if (this.ambientSpawn == -1) this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient;
|
|
// Paper end
|
|
+ // Tuinity end - per world spawn limits
|
|
}
|
|
|
|
@Override
|
|
@@ -431,14 +438,7 @@ public class CraftWorld implements World {
|
|
|
|
@Override
|
|
public Chunk getChunkAt(int x, int z) {
|
|
- // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it
|
|
- net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
|
|
- if (chunk == null) {
|
|
- addTicket(x, z);
|
|
- chunk = this.world.getChunkSource().getChunk(x, z, true);
|
|
- }
|
|
- return chunk.bukkitChunk;
|
|
- // Paper end
|
|
+ return this.world.getChunkSource().getChunk(x, z, true).bukkitChunk; // Tuinity - revert paper diff
|
|
}
|
|
|
|
// Paper start
|
|
@@ -486,13 +486,16 @@ public class CraftWorld implements World {
|
|
public Chunk[] getLoadedChunks() {
|
|
// Paper start
|
|
if (Thread.currentThread() != world.getLevel().thread) {
|
|
- synchronized (world.getChunkSource().chunkMap.visibleChunkMap) {
|
|
- Long2ObjectLinkedOpenHashMap<ChunkHolder> 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<ChunkHolder> 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<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
|
|
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map
|
|
return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
|
|
}
|
|
|
|
@@ -2671,7 +2674,7 @@ public class CraftWorld implements World {
|
|
// Paper end
|
|
return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
|
|
- if (chunk != null) addTicket(x, z); // Paper
|
|
+ if (false && chunk != null) addTicket(x, z); // Paper // Tuinity - revert
|
|
return java.util.concurrent.CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
|
|
}, net.minecraft.server.MinecraftServer.getServer());
|
|
}
|
|
@@ -2696,14 +2699,14 @@ public class CraftWorld implements World {
|
|
throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
|
|
}
|
|
net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap;
|
|
- if (viewDistance != chunkMap.getEffectiveViewDistance()) {
|
|
+ if (true) { // Tuinity - replace old player chunk management
|
|
chunkMap.setViewDistance(viewDistance);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getNoTickViewDistance() {
|
|
- return getHandle().getChunkSource().chunkMap.getEffectiveNoTickViewDistance();
|
|
+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Tuinity - replace old player chunk management
|
|
}
|
|
|
|
@Override
|
|
@@ -2712,11 +2715,22 @@ public class CraftWorld implements World {
|
|
throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
|
|
}
|
|
net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap;
|
|
- if (viewDistance != chunkMap.getRawNoTickViewDistance()) {
|
|
+ if (true) { // Tuinity - replace old player chunk management
|
|
chunkMap.setNoTickViewDistance(viewDistance);
|
|
}
|
|
}
|
|
// Paper end - per player view distance
|
|
+ // Tuinity start - add view distances
|
|
+ @Override
|
|
+ public int getSendViewDistance() {
|
|
+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSendViewDistance(int viewDistance) {
|
|
+ getHandle().getChunkSource().chunkMap.playerChunkManager.setTargetSendDistance(viewDistance);
|
|
+ }
|
|
+ // Tuinity end - add view distances
|
|
|
|
// Spigot start
|
|
private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot()
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
|
|
index c3c7b34ceb1b8f0ed042b29924c633fa7519dc30..c59deadcfbfd5afbf951a167979a4eceb0c63579 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
|
|
@@ -146,6 +146,13 @@ public class Main {
|
|
.defaultsTo(new File("paper.yml"))
|
|
.describedAs("Yml file");
|
|
// Paper end
|
|
+ // Tuinity start - Server Config
|
|
+ acceptsAll(asList("tuinity", "tuinity-settings"), "File for tuinity settings")
|
|
+ .withRequiredArg()
|
|
+ .ofType(File.class)
|
|
+ .defaultsTo(new File("tuinity.yml"))
|
|
+ .describedAs("Yml file");
|
|
+ // Tuinity end - Server Config
|
|
|
|
// Paper start
|
|
acceptsAll(asList("server-name"), "Name of the server")
|
|
@@ -269,7 +276,7 @@ public class Main {
|
|
if (buildDate.before(deadline.getTime())) {
|
|
// Paper start - This is some stupid bullshit
|
|
System.err.println("*** Warning, you've not updated in a while! ***");
|
|
- System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads ***"); // Paper
|
|
+ System.err.println("*** Please download a new build ***"); // Paper // Tuinity
|
|
//System.err.println("*** Server will start in 20 seconds ***");
|
|
//Thread.sleep(TimeUnit.SECONDS.toMillis(20));
|
|
// Paper End
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
index 8246ad7ebecdfc0b7519fe4412fef7b07407e850..c0a508295d2e68d92ec8d24e14f9b7626911f548 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
@@ -517,27 +517,36 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
this.entity.setYHeadRot(yaw);
|
|
}
|
|
|
|
- @Override// Paper start
|
|
- public java.util.concurrent.CompletableFuture<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> ret = new java.util.concurrent.CompletableFuture<>();
|
|
+
|
|
+ world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), location.getX(), location.getZ(), (list) -> {
|
|
+ net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource();
|
|
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) {
|
|
+ chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId());
|
|
}
|
|
- }).thenAccept((chunk) -> future.complete(teleport(loc, cause))).exceptionally(ex -> {
|
|
- future.completeExceptionally(ex);
|
|
- return null;
|
|
+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
|
|
+ try {
|
|
+ ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE);
|
|
+ } catch (Throwable throwable) {
|
|
+ if (throwable instanceof ThreadDeath) {
|
|
+ throw (ThreadDeath)throwable;
|
|
+ }
|
|
+ ret.completeExceptionally(throwable);
|
|
+ }
|
|
+ });
|
|
});
|
|
- return future;
|
|
+
|
|
+ return ret;
|
|
}
|
|
- // Paper end
|
|
+ // Tuinity end - implement teleportAsync better
|
|
|
|
@Override
|
|
public boolean teleport(Location location) {
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
index 4e95bf2eb6434d8ca44d478262329c56b0b0a079..1da5b6f73e78a697031f7662e68c546543fb9d1a 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
@@ -516,15 +516,70 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
}
|
|
}
|
|
|
|
+ // Tuinity start - implement view distances
|
|
+ @Override
|
|
+ public int getSendViewDistance() {
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ return chunkMap.playerChunkManager.getTargetSendDistance();
|
|
+ }
|
|
+ return data.getTargetSendViewDistance();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSendViewDistance(int viewDistance) {
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ throw new IllegalStateException("Player is not attached to world");
|
|
+ }
|
|
+
|
|
+ data.setTargetSendViewDistance(viewDistance);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int getNoTickViewDistance() {
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ return chunkMap.playerChunkManager.getTargetNoTickViewDistance();
|
|
+ }
|
|
+ return data.getTargetNoTickViewDistance();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setNoTickViewDistance(int viewDistance) {
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ throw new IllegalStateException("Player is not attached to world");
|
|
+ }
|
|
+
|
|
+ data.setTargetNoTickViewDistance(viewDistance);
|
|
+ }
|
|
+
|
|
@Override
|
|
public int getViewDistance() {
|
|
- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ return chunkMap.playerChunkManager.getTargetViewDistance();
|
|
+ }
|
|
+ return data.getTargetTickViewDistance();
|
|
}
|
|
|
|
@Override
|
|
public void setViewDistance(int viewDistance) {
|
|
- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
|
|
+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
|
|
+ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
|
|
+ if (data == null) {
|
|
+ throw new IllegalStateException("Player is not attached to world");
|
|
+ }
|
|
+
|
|
+ data.setTargetTickViewDistance(viewDistance);
|
|
}
|
|
+ // Tuinity end - implement view distances
|
|
|
|
@Override
|
|
public <T> T getClientOption(com.destroystokyo.paper.ClientOption<T> 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<BukkitWorker> 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<Score> consumer) {
|
|
+ // Tuinity start - add timings for scoreboard search
|
|
+ // plugins leaking scoreboards will make this very expensive, let server owners debug it easily
|
|
+ co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync();
|
|
+ try {
|
|
+ // Tuinity end - add timings for scoreboard search
|
|
for (CraftScoreboard scoreboard : this.scoreboards) {
|
|
Scoreboard board = scoreboard.board;
|
|
board.forAllObjectives(criteria, name, (score) -> consumer.accept(score));
|
|
}
|
|
+ } finally { // Tuinity start - add timings for scoreboard search
|
|
+ co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync();
|
|
+ }
|
|
+ // Tuinity end - add timings for scoreboard search
|
|
}
|
|
}
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
|
|
index a430506c31d9ce7a5c90d726a68f097498629545..e8c5109c36d437287e3eec23a5d1031f197a6162 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
|
|
@@ -1,5 +1,6 @@
|
|
package org.bukkit.craftbukkit.util;
|
|
|
|
+import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
import java.util.function.Predicate;
|
|
@@ -234,4 +235,20 @@ public class DummyGeneratorAccess implements LevelAccessor {
|
|
public boolean destroyBlock(BlockPos pos, boolean drop, Entity breakingEntity, int maxUpdateDepth) {
|
|
return false; // SPIGOT-6515
|
|
}
|
|
+
|
|
+ // Tuinity start
|
|
+ @Override
|
|
+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
|
|
+ return Collections.emptyList();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
|
|
+
|
|
+ @Override
|
|
+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
|
|
+
|
|
+ @Override
|
|
+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, AABB box, List<? super T> into, Predicate<? super T> 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<E> extends AbstractList<E> implements List<E>, RandomAcc
|
|
return this.indexOf(o) >= 0;
|
|
}
|
|
|
|
+ // Tuinity start
|
|
+ protected transient int maxSize;
|
|
+ public void setSize(int size) {
|
|
+ if (this.maxSize < this.size) {
|
|
+ this.maxSize = this.size;
|
|
+ }
|
|
+ this.size = size;
|
|
+ }
|
|
+
|
|
+ public void completeReset() {
|
|
+ if (this.data != null) {
|
|
+ Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null);
|
|
+ }
|
|
+ this.size = 0;
|
|
+ this.maxSize = 0;
|
|
+ if (this.iterPool != null) {
|
|
+ for (Iterator temp : this.iterPool) {
|
|
+ if (temp == null) {
|
|
+ continue;
|
|
+ }
|
|
+ ((Itr)temp).valid = false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end
|
|
+
|
|
@Override
|
|
public void clear() {
|
|
// Create new array to reset memory usage to initial capacity
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
|
|
index 774556a62eb240da42e84db4502e2ed43495be17..001b1e5197eaa51bfff9031aa6c69876c9a47960 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
|
|
@@ -11,7 +11,7 @@ public final class Versioning {
|
|
public static String getBukkitVersion() {
|
|
String result = "Unknown-Version";
|
|
|
|
- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties");
|
|
+ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.tuinity/tuinity-api/pom.properties"); // Tuinity
|
|
Properties properties = new Properties();
|
|
|
|
if (stream != null) {
|
|
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
|
|
index a08583863f9fa08016bdfc7949a273eaa4429927..7361bf04de16d0526dc4cdbd0f564713df041d90 100644
|
|
--- a/src/main/java/org/spigotmc/ActivationRange.java
|
|
+++ b/src/main/java/org/spigotmc/ActivationRange.java
|
|
@@ -205,7 +205,7 @@ public class ActivationRange
|
|
ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, waterActivationRange );
|
|
// Paper end
|
|
|
|
- world.getEntities().get(maxBB, ActivationRange::activateEntity);
|
|
+ world.getEntities(null, maxBB).forEach(ActivationRange::activateEntity); // Tuinity
|
|
}
|
|
MinecraftTimings.entityActivationCheckTimer.stopTiming();
|
|
}
|
|
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
|
|
index 7a23b56752f6733ee626a8b1e4c3b78591855c4e..17151d9dc8c7030b54f1ba0956424d9d704c81ab 100644
|
|
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
|
|
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
|
|
@@ -10,8 +10,9 @@ public class AsyncCatcher
|
|
|
|
public static void catchOp(String reason)
|
|
{
|
|
- if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
|
|
+ if ( (AsyncCatcher.enabled || com.tuinity.tuinity.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Tuinity
|
|
{
|
|
+ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed thread check for reason: Asynchronous " + reason, new Throwable()); // Tuinity - not all exceptions are printed
|
|
throw new IllegalStateException( "Asynchronous " + reason + "!" );
|
|
}
|
|
}
|
|
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
|
|
index dcfbe77bdb25d9c58ffb7b75c48bdb580bc0de47..fcd3917e0af080ae4726e7f511abf586df4dc7de 100644
|
|
--- a/src/main/java/org/spigotmc/WatchdogThread.java
|
|
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
|
|
@@ -23,6 +23,78 @@ public class WatchdogThread extends Thread
|
|
private volatile long lastTick;
|
|
private volatile boolean stopping;
|
|
|
|
+ // Tuinity start - log detailed tick information
|
|
+ private void dumpEntity(net.minecraft.world.entity.Entity entity) {
|
|
+ Logger log = Bukkit.getServer().getLogger();
|
|
+ double posX, posY, posZ;
|
|
+ net.minecraft.world.phys.Vec3 mot;
|
|
+ double moveStartX, moveStartY, moveStartZ;
|
|
+ net.minecraft.world.phys.Vec3 moveVec;
|
|
+ synchronized (entity.posLock) {
|
|
+ posX = entity.getX();
|
|
+ posY = entity.getY();
|
|
+ posZ = entity.getZ();
|
|
+ mot = entity.getDeltaMovement();
|
|
+ moveStartX = entity.getMoveStartX();
|
|
+ moveStartY = entity.getMoveStartY();
|
|
+ moveStartZ = entity.getMoveStartZ();
|
|
+ moveVec = entity.getMoveVector();
|
|
+ }
|
|
+
|
|
+ String entityType = entity.getMinecraftKey().toString();
|
|
+ java.util.UUID entityUUID = entity.getUUID();
|
|
+ net.minecraft.world.level.Level world = entity.level;
|
|
+
|
|
+ log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName());
|
|
+ log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger());
|
|
+ log.log(Level.SEVERE, "Entity UUID: " + entityUUID);
|
|
+ log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")");
|
|
+ log.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)");
|
|
+ log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox());
|
|
+ if (moveVec != null) {
|
|
+ log.log(Level.SEVERE, "Move call information: ");
|
|
+ log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")");
|
|
+ log.log(Level.SEVERE, "Move vector: " + moveVec.toString());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void dumpTickingInfo() {
|
|
+ Logger log = Bukkit.getServer().getLogger();
|
|
+
|
|
+ // ticking entities
|
|
+ for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) {
|
|
+ this.dumpEntity(entity);
|
|
+ net.minecraft.world.entity.Entity vehicle = entity.getVehicle();
|
|
+ if (vehicle != null) {
|
|
+ log.log(Level.SEVERE, "Detailing vehicle for above entity:");
|
|
+ this.dumpEntity(vehicle);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // packet processors
|
|
+ for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) {
|
|
+ if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) {
|
|
+ net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player;
|
|
+ long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets();
|
|
+ if (player == null) {
|
|
+ log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener);
|
|
+ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets);
|
|
+ } else {
|
|
+ this.dumpEntity(player);
|
|
+ net.minecraft.world.entity.Entity vehicle = player.getVehicle();
|
|
+ if (vehicle != null) {
|
|
+ log.log(Level.SEVERE, "Detailing vehicle for above entity:");
|
|
+ this.dumpEntity(vehicle);
|
|
+ }
|
|
+ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets);
|
|
+ }
|
|
+ } else {
|
|
+ log.log(Level.SEVERE, "Handling packet for connection: " + packetListener);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Tuinity end - log detailed tick information
|
|
+
|
|
private WatchdogThread(long timeoutTime, boolean restart)
|
|
{
|
|
super( "Paper Watchdog Thread" );
|
|
@@ -121,6 +193,7 @@ public class WatchdogThread extends Thread
|
|
log.log( Level.SEVERE, "------------------------------" );
|
|
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
|
|
com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
|
|
+ this.dumpTickingInfo(); // Tuinity - log detailed tick information
|
|
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log );
|
|
log.log( Level.SEVERE, "------------------------------" );
|
|
//
|
|
diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png
|
|
index a7d785f60c884ee4ee487cc364402d66c3dc2ecc..1e6ee83b1a207eca59d82b25c06895ce894e8173 100644
|
|
GIT binary patch
|
|
literal 23418
|
|
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
|
|
|
|
literal 14310
|
|
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%0ABLt+m?son?j
|
|
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
|
|
WX&3$!%>|s_1IbA#OV)^+1pg1OmmZn`
|
|
|