Files
Purpur/patches/server/0001-Tuinity-Server-Changes.patch
BillyGalbreath a81e2e7c99 Updated Upstream (Paper & Tuinity)
Upstream has released updates that appear to apply and compile correctly

Paper Changes:
3dbb8926e Fix PotionSplashEvent for water splash potions (#5697)
b759d006e Adds per-world spawn limits (#4837)
81de619d3 Updated Upstream (CraftBukkit) (#5786)
5e3604f1b Better fix for invulnerable crystals and improve Origin API (#5761)
d5fe9c817 Updated Upstream (CraftBukkit) (#5784)

Tuinity Changes:
df76b3cc5 Update Upstream (Paper)
2021-06-08 07:29:43 -05:00

21844 lines
1.1 MiB

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 14 Dec 2018 21:53:58 -0800
Subject: [PATCH] Tuinity Server Changes
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
diff --git a/pom.xml b/pom.xml
index 1a9204c869dd36e80932b1366352db15ebd70723..642ee96ead8176f5c5a811946b050f4fa5dab6e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,11 +1,11 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
- <artifactId>paper</artifactId>
+ <artifactId>tuinity</artifactId>
<packaging>jar</packaging>
<version>1.16.5-R0.1-SNAPSHOT</version>
- <name>Paper</name>
- <url>https://papermc.io</url>
+ <name>Tuinity-Server</name>
+ <url>https://github.com/Spottedleaf/Tuinity</url>
<properties>
<!-- <skipTests>true</skipTests> Paper - This [was] not going to end well -->
@@ -19,8 +19,8 @@
</properties>
<parent>
- <groupId>com.destroystokyo.paper</groupId>
- <artifactId>paper-parent</artifactId>
+ <groupId>com.tuinity</groupId>
+ <artifactId>tuinity-parent</artifactId>
<version>dev-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -39,8 +39,8 @@
<dependencies>
<dependency>
- <groupId>com.destroystokyo.paper</groupId>
- <artifactId>paper-api</artifactId>
+ <groupId>com.tuinity</groupId>
+ <artifactId>tuinity-api</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
@@ -50,6 +50,13 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
+ <!-- Tuinity start - fix compile issue (cannot see new api) by moving netty include BEFORE server jar -->
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-all</artifactId>
+ <version>4.1.50.Final</version>
+ </dependency>
+ <!-- Tuinity end - fix compile issue (cannot see new api) by moving netty include BEFORE server jar -->
<dependency>
<groupId>io.papermc</groupId>
<artifactId>minecraft-server</artifactId>
@@ -107,11 +114,7 @@
<artifactId>cleaner</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-all</artifactId>
- <version>4.1.50.Final</version>
- </dependency>
+ <!-- Tuinity - fix compile issue (cannot see new api) by moving netty include BEFORE server jar -->
<!-- deprecated API depend -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
@@ -183,19 +186,26 @@
<version>4.8.47</version>
<scope>test</scope>
</dependency>
+ <!-- for optimized protocol handling -->
+ <dependency>
+ <groupId>com.velocitypowered</groupId>
+ <artifactId>velocity-native</artifactId>
+ <version>1.1.0-SNAPSHOT</version>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
<!-- This builds a completely 'ready to start' jar with all dependencies inside -->
<build>
- <finalName>paper-${minecraft.version}</finalName>
- <defaultGoal>clean install</defaultGoal> <!-- Paper -->
+ <finalName>tuinity-${minecraft.version}</finalName>
+ <defaultGoal>install</defaultGoal> <!-- Paper -->
<plugins>
<plugin>
<groupId>com.lukegb.mojo</groupId>
<artifactId>gitdescribe-maven-plugin</artifactId>
<version>1.3</version>
<configuration>
- <outputPrefix>git-Paper-</outputPrefix>
+ <outputPrefix>git-Tuinity-</outputPrefix> <!-- Tuinity -->
<scmDirectory>..</scmDirectory>
</configuration>
<executions>
diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
index b4d43ceed368552e703886213327a0c0bb5ccb92..67980e1dc186c0b458eca9f00acfea7d2b26d575 100644
--- a/src/main/java/co/aikar/timings/MinecraftTimings.java
+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
@@ -46,6 +46,9 @@ 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 scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search
+ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager
+
private static final Map<Class<?>, String> taskNameCache = new MapMaker().weakKeys().makeMap();
private MinecraftTimings() {}
diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
index e33e889c291d37a821a4fbd40d9aac7bb079de0d..35810f42d7a0cd50a4cbe90e8d698fe57914c889 100644
--- a/src/main/java/co/aikar/timings/TimingsExport.java
+++ b/src/main/java/co/aikar/timings/TimingsExport.java
@@ -154,7 +154,7 @@ public class TimingsExport extends Thread {
return pair(rule, world.getWorld().getGameRuleValue(rule));
})),
pair("ticking-distance", world.getChunkProvider().playerChunkMap.getEffectiveViewDistance()),
- pair("notick-viewdistance", world.getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance())
+ pair("notick-viewdistance", world.getChunkProvider().playerChunkMap.playerChunkManager.getTargetNoTickViewDistance()) // Tuinity - replace old player chunk management
));
}));
@@ -229,7 +229,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 dee00aac05f1acf050f05d4db557a08dd0f301c8..52c0ab1ce46e1f3233ef746d9bc699356fa9fae4 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() || PaperConfig.isProxyOnlineMode() ? "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 12313a37ceeb6a0b6a539c38fdba67e5e43d7413..ec2b9995f1bf0f6cf029df7bfac526c2672acf3a 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -219,6 +219,44 @@ public class PaperCommand extends Command {
}
}
+ private void starlightFixLight(EntityPlayer sender, WorldServer world, LightEngineThreaded lightengine, int radius) {
+ long start = System.nanoTime();
+ java.util.LinkedHashSet<ChunkCoordIntPair> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.getChunkCoordinates(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
+
+ int[] pending = new int[1];
+ for (java.util.Iterator<ChunkCoordIntPair> iterator = chunks.iterator(); iterator.hasNext();) {
+ final ChunkCoordIntPair chunkPos = iterator.next();
+
+ final net.minecraft.world.level.chunk.IChunkAccess chunk = world.getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z);
+ if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ continue;
+ }
+
+ ++pending[0];
+ }
+
+ int[] relitChunks = new int[1];
+ lightengine.relight(chunks,
+ (ChunkCoordIntPair 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");
+ }
+
private void doFixLight(CommandSender sender, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("Only players can use this command");
@@ -227,7 +265,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;
@@ -240,6 +278,13 @@ public class PaperCommand extends Command {
WorldServer world = (WorldServer) handle.world;
LightEngineThreaded lightengine = world.getChunkProvider().getLightEngine();
+ // Tuinity start - rewrite light engine
+ if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) {
+ this.starlightFixLight(handle, world, lightengine, radius);
+ return;
+ }
+ // Tuinity end - rewrite light engine
+
BlockPosition center = MCUtil.toBlockPosition(player.getLocation());
Deque<ChunkCoordIntPair> queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius));
updateLight(sender, world, lightengine, queue);
@@ -362,7 +407,7 @@ public class PaperCommand extends Command {
int ticking = 0;
int entityTicking = 0;
- for (PlayerChunk chunk : world.getChunkProvider().playerChunkMap.updatingChunks.values()) {
+ for (PlayerChunk chunk : world.getChunkProvider().playerChunkMap.updatingChunks.getUpdatingMap().values()) { // Tuinity - change updating chunks map
if (chunk.getFullChunkIfCached() == null) {
continue;
}
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 8918bad880d6eeed30db39b6326b2f65e24edf45..b870cca05f0ba354e6976a70511235636093d13c 100644
--- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
@@ -186,6 +186,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
}
public void onChunkSetTicking(final int chunkX, final int chunkZ) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list chunk ticking update"); // Tuinity - soft async catcher
final ArrayList<NextTickListEntry<T>> pending = this.pendingChunkTickLoad.remove(MCUtil.getCoordinateKey(chunkX, chunkZ));
if (pending == null) {
return;
@@ -268,6 +269,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public void nextTick() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list tick"); // Tuinity - soft async catcher
++this.currentTick;
if (this.currentTick != this.world.getTime()) {
if (!this.warnedAboutDesync) {
@@ -280,6 +282,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public void tick() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list tick"); // Tuinity - soft async catcher
final ChunkProviderServer chunkProvider = this.world.getChunkProvider();
this.world.getMethodProfiler().enter("cleaning");
@@ -307,6 +310,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
if (toTick.tickState == STATE_TICKING) {
toTick.tickState = STATE_TICKED;
} // else it's STATE_CANCELLED_TICK
+ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick
} else {
// re-schedule eventually
toTick.tickState = STATE_SCHEDULED;
@@ -424,6 +428,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
}
public void schedule(final BlockPosition pos, final T data, final long targetTick, final TickListPriority priority) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list schedule"); // Tuinity - soft async catcher
final NextTickListEntry<T> entry = new NextTickListEntry<>(pos, data, targetTick, priority);
if (this.excludeFromScheduling.test(entry.getData())) {
return;
@@ -479,6 +484,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public List<NextTickListEntry<T>> getEntriesInBoundingBox(final StructureBoundingBox structureboundingbox, final boolean removeReturned, final boolean excludeTicked) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get in bounding box"); // Tuinity - soft async catcher
if (structureboundingbox.getMinX() == structureboundingbox.getMaxX() || structureboundingbox.getMinZ() == structureboundingbox.getMaxZ()) {
return Collections.emptyList(); // vanilla behaviour, check isBlockInSortof above
}
@@ -535,6 +541,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public void copy(StructureBoundingBox structureboundingbox, BlockPosition blockposition) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list copy"); // Tuinity - soft async catcher
// start copy from TickListServer // TODO check on update
List<NextTickListEntry<T>> list = this.getEntriesInBoundingBox(structureboundingbox, false, false);
Iterator<NextTickListEntry<T>> iterator = list.iterator();
@@ -554,6 +561,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public List<NextTickListEntry<T>> getEntriesInChunk(ChunkCoordIntPair chunkPos, boolean removeReturned, boolean excludeTicked) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get"); // Tuinity - soft async catcher
// Vanilla DOES get the entries 2 blocks out of the chunk too, but that doesn't matter since we ignore chunks
// not at ticking status, and ticking status requires neighbours loaded
// so with this method we will reduce scheduler churning
@@ -585,6 +593,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public NBTTagList serialize(ChunkCoordIntPair chunkcoordintpair) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list serialize"); // Tuinity - soft async catcher
// start copy from TickListServer // TODO check on update
List<NextTickListEntry<T>> list = this.getEntriesInChunk(chunkcoordintpair, false, true);
@@ -594,6 +603,7 @@ public final class PaperTickList<T> extends TickListServer<T> { // extend to avo
@Override
public int getTotalScheduledEntries() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get size"); // Tuinity - soft async catcher
// good thing this is only used in debug reports // TODO check on update
int ret = 0;
diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java
index 103576715ef6ae4df4b216ae9ae31b6fb1086bd5..e8fdbe7b8d8192a3247d98534e78ede7a7314a91 100644
--- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java
+++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java
@@ -590,10 +590,11 @@ public class CommandDispatcher<S> {
final String truncatedInput = fullInput.substring(0, cursor);
@SuppressWarnings("unchecked") final CompletableFuture<Suggestions>[] futures = new CompletableFuture[parent.getChildren().size()];
int i = 0;
+ final String remainingLower = truncatedInput.substring(start).toLowerCase(); // Tuinity
for (final CommandNode<S> node : parent.getChildren()) {
CompletableFuture<Suggestions> future = Suggestions.empty();
try {
- future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start));
+ future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start, remainingLower)); // Tuinity
} catch (final CommandSyntaxException ignored) {
}
futures[i++] = future;
diff --git a/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java b/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java
index cb993ca102402d9c19ea9fa04e5db09c21205896..849686f7b2a8b0044f7cd14c8c2e401e80966462 100644
--- a/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java
+++ b/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java
@@ -34,10 +34,10 @@ public class BoolArgumentType implements ArgumentType<Boolean> {
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) {
- if ("true".startsWith(builder.getRemaining().toLowerCase())) {
+ if ("true".startsWith(builder.getRemainingLowercase())) { // Tuinity
builder.suggest("true");
}
- if ("false".startsWith(builder.getRemaining().toLowerCase())) {
+ if ("false".startsWith(builder.getRemainingLowercase())) { // Tuinity
builder.suggest("false");
}
return builder.buildFuture();
diff --git a/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java b/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java
index bc0024adb804ac055a4f8afb7f85d00ec13931e9..0343f6663c450c3f0d9c57d817eef9c979055939 100644
--- a/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java
+++ b/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java
@@ -14,9 +14,16 @@ public class SuggestionsBuilder {
private final String input;
private final int start;
private final String remaining;
+ private String remainingLowercase; public final String getRemainingLowercase() { return this.remainingLowercase == null ? this.remainingLowercase = this.remaining.toLowerCase() : this.remainingLowercase; } // Tuinity
private final List<Suggestion> result = new ArrayList<>();
public SuggestionsBuilder(final String input, final int start) {
+ // Tuinity start
+ this(input, start, null);
+ }
+ public SuggestionsBuilder(final String input, final int start, final String remainingLowercase) {
+ this.remainingLowercase = remainingLowercase;
+ // Tuinity end
this.input = input;
this.start = start;
this.remaining = input.substring(start);
diff --git a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java
index 7720578796e28d28e8c0c9aa40155cd205c17d54..e5db29d4cadb5702c7d06b0b6e2d05586a652ec8 100644
--- a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java
+++ b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java
@@ -20,11 +20,11 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
public class LiteralCommandNode<S> extends CommandNode<S> {
- private final String literal;
+ private final String literal; private final String literalLower; // Tuinity
public LiteralCommandNode(final String literal, final Command<S> command, final Predicate<S> requirement, final CommandNode<S> redirect, final RedirectModifier<S> modifier, final boolean forks) {
super(command, requirement, redirect, modifier, forks);
- this.literal = literal;
+ this.literal = literal; this.literalLower = this.literal.toLowerCase(); // Tuinity
}
public String getLiteral() {
@@ -66,7 +66,7 @@ public class LiteralCommandNode<S> extends CommandNode<S> {
@Override
public CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) {
- if (literal.toLowerCase().startsWith(builder.getRemaining().toLowerCase())) {
+ if (literalLower.startsWith(builder.getRemainingLowercase())) { // Tuinity
return builder.suggest(literal).buildFuture();
} else {
return Suggestions.empty();
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..0d577aa1c7868ce89c3902535adcb554b1f47551
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java
@@ -0,0 +1,964 @@
+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.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.game.PacketPlayOutViewCentre;
+import net.minecraft.network.protocol.game.PacketPlayOutViewDistance;
+import net.minecraft.server.level.EntityPlayer;
+import net.minecraft.world.level.ChunkCoordIntPair;
+import net.minecraft.world.level.chunk.Chunk;
+import net.minecraft.server.MCUtil;
+import net.minecraft.util.MathHelper;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.server.level.PlayerChunk;
+import net.minecraft.server.level.PlayerChunkMap;
+import net.minecraft.server.level.TicketType;
+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 PlayerChunkMap chunkMap;
+ protected final Reference2ObjectLinkedOpenHashMap<EntityPlayer, 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 PlayerChunkMap chunkMap, final PooledLinkedHashSets<EntityPlayer> pooledHashSets) {
+ this.chunkMap = chunkMap;
+ this.broadcastMap = new PlayerAreaMap(pooledHashSets,
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ if (player.needsChunkCenterUpdate) {
+ player.needsChunkCenterUpdate = false;
+ player.playerConnection.sendPacket(new PacketPlayOutViewCentre(currPosX, currPosZ));
+ }
+ PlayerChunkLoader.this.onChunkEnter(player, rangeX, rangeZ);
+ },
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ);
+ });
+ this.loadMap = new PlayerAreaMap(pooledHashSets,
+ null,
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ if (newState != null) {
+ return;
+ }
+ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(MCUtil.getCoordinateKey(rangeX, rangeZ));
+ });
+ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets,
+ null,
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ if (newState != null) {
+ return;
+ }
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
+ PlayerChunkLoader.this.chunkMap.world.getChunkProvider().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
+ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.pair())) {
+ --PlayerChunkLoader.this.concurrentChunkLoads;
+ }
+ });
+ this.tickMap = new PlayerAreaMap(pooledHashSets,
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ if (newState.size() != 1) {
+ return;
+ }
+ Chunk chunk = PlayerChunkLoader.this.chunkMap.world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ);
+ if (chunk == null || !chunk.areNeighboursLoaded(2)) {
+ return;
+ }
+
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
+ PlayerChunkLoader.this.chunkMap.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
+ },
+ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+ if (newState != null) {
+ return;
+ }
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
+ PlayerChunkLoader.this.chunkMap.world.getChunkProvider().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 = MCUtil.getCoordinateKey(chunkX, chunkZ);
+ final PlayerChunk chunk = this.chunkMap.getVisibleChunk(key);
+
+ if (chunk == null) {
+ return false;
+ }
+
+ return chunk.getSendingChunk() != null && this.isTargetedForPlayerLoad.contains(key);
+ }
+
+ public boolean isChunkSent(final EntityPlayer 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.world.getPlayers().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 ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(chunkX, chunkZ);
+ this.chunkMap.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
+ }
+
+ public void onChunkSendReady(final int chunkX, final int chunkZ) {
+ final long chunkKey = MCUtil.getCoordinateKey(chunkX, chunkZ);
+
+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> 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 EntityPlayer)) {
+ continue;
+ }
+ this.onChunkEnter((EntityPlayer)raw, chunkX, chunkZ);
+ }
+
+ // now let's try and queue mid tick logic again
+ }
+
+ public void onChunkEnter(final EntityPlayer 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 = MCUtil.getCoordinateX(playerPos);
+ final int playerChunkZ = MCUtil.getCoordinateZ(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(MCUtil.getCoordinateKey(chunkX, chunkZ))) {
+ --this.concurrentChunkLoads;
+ }
+ }
+
+ public void onChunkLeave(final EntityPlayer 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 EntityPlayer 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 EntityPlayer 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 EntityPlayer 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 EntityPlayer 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 = MCUtil.getCoordinateKey(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.world.getChunkProvider().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 ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(offX, offZ);
+
+ this.chunkMap.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
+ if (this.chunkMap.world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) {
+ continue;
+ }
+
+ if (priority > 0.0 && this.chunkTicketTracker.add(MCUtil.getCoordinateKey(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 EntityPlayer 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 EntityPlayer 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(MCUtil.getCoordinateKey(chunkX, chunkZ));
+ }
+
+ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) {
+ if (this.sentChunks.add(MCUtil.getCoordinateKey(chunkX, chunkZ))) {
+ this.player.getWorldServer().getChunkProvider().playerChunkMap.sendChunk(this.player,
+ new ChunkCoordIntPair(chunkX, chunkZ), new Packet[2], false, true); // unloaded, loaded
+ this.player.playerConnection.networkManager.execute(onChunkSend);
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+
+ public void unloadChunk(final int chunkX, final int chunkZ) {
+ if (this.sentChunks.remove(MCUtil.getCoordinateKey(chunkX, chunkZ))) {
+ this.player.getWorldServer().getChunkProvider().playerChunkMap.sendChunk(this.player,
+ new ChunkCoordIntPair(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.locX();
+ final double posZ = this.player.locZ();
+ final float yaw = MCUtil.normalizeYaw(this.player.yaw + 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.getMot().magnitudeXZSquared() > LOOK_PRIORITY_SPEED_THRESHOLD ||
+ this.player.abilities.isFlying);
+
+ // 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)?
+ MathHelper.floor(this.lastLocX) == MathHelper.floor(posX)
+ && MathHelper.floor(this.lastLocZ) == MathHelper.floor(posZ)
+ ) : (
+ // has our chunk stayed the same
+ (MathHelper.floor(this.lastLocX) >> 4) == (MathHelper.floor(posX) >> 4)
+ && (MathHelper.floor(this.lastLocZ) >> 4) == (MathHelper.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 = MathHelper.floor(posX) >> 4;
+ final int centerChunkZ = MathHelper.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.playerConnection.sendPacket(new PacketPlayOutViewDistance(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..5ea5b3933725d80dd193e815ac507ee51ee17630
--- /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.WorldServer;
+import net.minecraft.world.level.ChunkCoordIntPair;
+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 WorldServer 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 WorldServer 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 ChunkCoordIntPair(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 ChunkCoordIntPair(chunkX, chunkZ).toString());
+ }
+ if (--this.chunkCount != 0) {
+ return;
+ }
+ this.region.markSectionDead(this);
+ }
+
+ @Override
+ public String toString() {
+ return "RegionSection{" +
+ "regionCoordinate=" + new ChunkCoordIntPair(this.regionCoordinate).toString() + "," +
+ "chunkCount=" + this.chunkCount + "," +
+ "chunksBitset=" + toString(this.chunksBitset) + "," +
+ "hash=" + this.hashCode() +
+ "}";
+ }
+
+ public String toStringWithRegion() {
+ return "RegionSection{" +
+ "regionCoordinate=" + new ChunkCoordIntPair(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);
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..f722f9838424f345b69aef11510c194c6629e439
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java
@@ -0,0 +1,289 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.core.BlockPosition;
+import net.minecraft.server.level.WorldServer;
+import net.minecraft.world.level.chunk.Chunk;
+import net.minecraft.world.level.chunk.ChunkSection;
+import net.minecraft.world.level.chunk.ChunkStatus;
+import net.minecraft.world.level.chunk.DataPaletteBlock;
+import net.minecraft.world.level.block.state.IBlockData;
+import net.minecraft.world.level.chunk.IChunkAccess;
+import net.minecraft.world.level.chunk.ILightAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.ProtoChunkExtension;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.VoxelShapes;
+import net.minecraft.world.level.World;
+import java.util.ArrayList;
+import java.util.HashSet;
+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 World world) {
+ super(false, world);
+ }
+
+ @Override
+ protected boolean[] getEmptinessMap(final IChunkAccess chunk) {
+ return chunk.getBlockEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) {
+ chunk.setBlockEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) {
+ return chunk.getBlockNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) {
+ chunk.setBlockNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final IChunkAccess chunk) {
+ return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLit());
+ }
+
+ @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, new SWMRNibbleArray());
+ }
+ } else {
+ nibble.setNonNull();
+ }
+ }
+
+ @Override
+ protected final void checkBlock(final ILightAccess 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 VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+ final IBlockData blockState = this.getBlockState(worldX, worldY, worldZ);
+ final int emittedLevel = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, worldX, worldY, worldZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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 BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition();
+ protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition();
+
+ @Override
+ protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect, final VariableBlockLightHandler customBlockLight) {
+ final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ);
+ int level = centerState.getEmittedLight() & 0xFF;
+ if (customBlockLight != null) {
+ level = this.getCustomLightLevel(customBlockLight, worldX, worldY, worldZ, level);
+ }
+
+ if (level >= (15 - 1) || level > expect) {
+ return level;
+ }
+
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final IBlockData conditionallyOpaqueState;
+ int opacity = centerState.getOpacityIfCached();
+
+ if (opacity == -1) {
+ this.recalcCenterPos.setValues(worldX, worldY, worldZ);
+ opacity = centerState.getOpacity(lightAccess.getWorld(), 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 IBlockData 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.setValues(offX, offY, offZ);
+ final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms);
+ final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms);
+ if (VoxelShapes.combinationOccludes(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 ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions) {
+ for (final BlockPosition pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ protected Iterator<BlockPosition> getSources(final ILightAccess lightAccess, final IChunkAccess chunk) {
+ if (chunk instanceof ProtoChunkExtension || chunk instanceof Chunk) {
+ // 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<BlockPosition> sources = new ArrayList<>();
+
+ int offX = chunk.getPos().x << 4;
+ int offZ = chunk.getPos().z << 4;
+
+ final ChunkSection[] sections = chunk.getSections();
+ for (int sectionY = 0; sectionY <= 15; ++sectionY) {
+ if (sections[sectionY] == null || sections[sectionY].isFullOfAir()) {
+ // no sources in empty sections
+ continue;
+ }
+ final DataPaletteBlock<IBlockData> section = sections[sectionY].blockIds;
+ final int offY = sectionY << 4;
+
+ for (int index = 0; index < (16 * 16 * 16); ++index) {
+ final IBlockData state = section.rawGet(index);
+ if (state.getEmittedLight() <= 0) {
+ continue;
+ }
+
+ // index = x | (z << 4) | (y << 8)
+ sources.add(new BlockPosition(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
+ }
+ }
+
+ final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+ if (customBlockHandler == null) {
+ return sources.iterator();
+ }
+
+ final Set<BlockPosition> ret = new HashSet<>(sources);
+ ret.addAll(customBlockHandler.getCustomLightPositions(chunk.getPos().x, chunk.getPos().z));
+
+ return ret.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.getLightSources().collect(Collectors.toList()).iterator();
+ } catch (final Exception cme) {
+ continue;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) {
+ // setup sources
+ final int emittedMask = this.emittedLightMask;
+ final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+ for (final Iterator<BlockPosition> positions = this.getSources(lightAccess, chunk); positions.hasNext();) {
+ final BlockPosition pos = positions.next();
+ final IBlockData blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
+ final int emittedLight = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, pos.getX(), pos.getY(), pos.getZ(), blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java
new file mode 100644
index 0000000000000000000000000000000000000000..81963ada0eafea91947f4437b22bcad47e4709ed
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java
@@ -0,0 +1,325 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.world.level.chunk.NibbleArray;
+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
+
+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
+ protected static final byte[] FULL_LIT = new byte[ARRAY_SIZE];
+ static {
+ Arrays.fill(FULL_LIT, (byte)-1);
+ }
+ // 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);
+ }
+
+ 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();
+ }
+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
+ this.storageUpdating = this.storageVisible = bytes;
+ }
+
+ // operation type: visible
+ public boolean isAllZero() {
+ final int state = this.stateVisible;
+
+ if (state == INIT_STATE_NULL) {
+ return false;
+ } else if (state == INIT_STATE_UNINIT) {
+ return true;
+ }
+
+ synchronized (this) {
+ final byte[] bytes = this.storageVisible;
+
+ if (bytes == null) {
+ return this.stateVisible == INIT_STATE_UNINIT;
+ }
+
+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
+ byte whole = bytes[i << 4];
+
+ for (int k = 1; k < (1 << 4); ++k) {
+ whole |= bytes[(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() {
+ 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() {
+ 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_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 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
+ 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);
+ }
+
+ 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 {
+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
+ }
+
+ freeBytes(this.storageUpdating);
+ this.storageUpdating = this.storageVisible;
+ }
+ this.updatingDirty = false;
+ this.stateVisible = this.stateUpdating;
+ }
+
+ return true;
+ }
+
+ // operation type: visible
+ public NibbleArray toVanillaNibble() {
+ synchronized (this) {
+ switch (this.stateVisible) {
+ case INIT_STATE_NULL:
+ return null;
+ case INIT_STATE_UNINIT:
+ return new NibbleArray();
+ case INIT_STATE_INIT:
+ return new NibbleArray(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));
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..86a880d0f13f0fee70b09626c394c9e25551e672
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java
@@ -0,0 +1,706 @@
+package com.tuinity.tuinity.chunk.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.BlockPosition;
+import net.minecraft.world.level.ChunkCoordIntPair;
+import net.minecraft.world.level.chunk.ChunkSection;
+import net.minecraft.world.level.chunk.ChunkStatus;
+import net.minecraft.world.level.IBlockAccess;
+import net.minecraft.world.level.block.state.IBlockData;
+import net.minecraft.world.level.chunk.IChunkAccess;
+import net.minecraft.world.level.chunk.ILightAccess;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.VoxelShapes;
+import net.minecraft.world.level.World;
+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 World 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);
+ }
+
+ 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 ChunkSection 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
+ this.getNibbleFromCache(chunkX, chunkY, chunkZ).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.extrudeLower(nibble);
+ break;
+ }
+ }
+ } else {
+ currNibble.setNonNull();
+ }
+ }
+
+ protected final void rewriteNibbleCacheForSkylight(final IChunkAccess 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 IChunkAccess chunk) {
+ return chunk.getSkyEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) {
+ chunk.setSkyEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) {
+ return chunk.getSkyNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) {
+ chunk.setSkyNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final IChunkAccess chunk) {
+ // can only use chunks for sky stuff if their sections have been init'd
+ return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide ? true : chunk.isLit());
+ }
+
+ @Override
+ protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess 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 ILightAccess lightAccess, final IChunkAccess 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);
+ }
+
+
+ protected final BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition();
+ protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition();
+
+ @Override
+ protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect, final VariableBlockLightHandler customBlockLight) {
+ if (expect == 15) {
+ return expect;
+ }
+
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ);
+ int opacity = centerState.getOpacityIfCached();
+
+
+ final IBlockData conditionallyOpaqueState;
+ if (opacity < 0) {
+ this.recalcCenterPos.setValues(worldX, worldY, worldZ);
+ opacity = Math.max(1, centerState.getOpacity(lightAccess.getWorld(), 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 IBlockData 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.setValues(offX, offY, offZ);
+ final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms);
+ final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms);
+ if (VoxelShapes.combinationOccludes(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 checkBlock(final ILightAccess 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))
+ );
+ }
+
+ @Override
+ protected void propagateBlockChanges(final ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions) {
+ this.rewriteNibbleCacheForSkylight(atChunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final IBlockAccess world = lightAccess.getWorld();
+ 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 BlockPosition 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 BlockPosition pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ @Override
+ protected void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) {
+ this.rewriteNibbleCacheForSkylight(chunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final IBlockAccess world = lightAccess.getWorld();
+ final ChunkCoordIntPair chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ final ChunkSection[] sections = chunk.getSections();
+
+ int highestNonEmptySection = this.maxSection;
+ while (highestNonEmptySection == (this.minSection - 1) ||
+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isFullOfAir()) {
+ 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 IBlockAccess world, final int worldX, int startY, final int worldZ,
+ final boolean extrudeInitialised, final boolean delayLightSet) {
+ final BlockPosition.MutableBlockPosition 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);
+
+ IBlockData 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);
+ }
+ IBlockData current = this.getBlockState(worldX, startY, worldZ);
+ if (current == null) {
+ current = AIR_BLOCK_STATE;
+ }
+
+ final VoxelShape fromShape;
+ if (above.isConditionallyFullOpaque()) {
+ this.mutablePos2.setValues(worldX, startY + 1, worldZ);
+ fromShape = above.getCullingFace(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
+ if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), fromShape)) {
+ // above wont let us propagate
+ break;
+ }
+ } else {
+ fromShape = VoxelShapes.getEmptyShape();
+ }
+
+ 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.setValues(worldX, startY, worldZ);
+ long flags = 0L;
+ if (current.isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = current.getCullingFace(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
+
+ if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+ // can't propagate here, we're done on this column.
+ break;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = current.getOpacity(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/com/tuinity/tuinity/chunk/light/StarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b0455b7475a75bba010fd71798eaa204bcf9562
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java
@@ -0,0 +1,1603 @@
+package com.tuinity.tuinity.chunk.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.BlockPosition;
+import net.minecraft.server.level.WorldServer;
+import net.minecraft.world.level.ChunkCoordIntPair;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.chunk.ChunkSection;
+import net.minecraft.core.EnumDirection;
+import net.minecraft.world.level.EnumSkyBlock;
+import net.minecraft.world.level.IBlockAccess;
+import net.minecraft.world.level.block.state.IBlockData;
+import net.minecraft.world.level.chunk.IChunkAccess;
+import net.minecraft.world.level.chunk.ILightAccess;
+import net.minecraft.core.SectionPosition;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.VoxelShapes;
+import net.minecraft.world.level.World;
+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 IBlockData AIR_BLOCK_STATE = Blocks.AIR.getBlockData();
+
+ protected static final ChunkSection EMPTY_CHUNK_SECTION = new ChunkSection(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 EnumDirection 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 = EnumDirection.from(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 ChunkSection[] 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. no index is null.
+ // index = x + (z * 5)
+ protected final IChunkAccess[] chunkCache = new IChunkAccess[5 * 5];
+
+ // index = x + (z * 5)
+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
+
+ protected final BlockPosition.MutableBlockPosition mutablePos1 = new BlockPosition.MutableBlockPosition();
+ protected final BlockPosition.MutableBlockPosition mutablePos2 = new BlockPosition.MutableBlockPosition();
+ protected final BlockPosition.MutableBlockPosition mutablePos3 = new BlockPosition.MutableBlockPosition();
+
+ 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 static final boolean isClientSide = false;
+
+ protected final World world;
+ protected final int minLightSection;
+ protected final int maxLightSection;
+ protected final int minSection;
+ protected final int maxSection;
+
+ protected StarLightEngine(final boolean skylightPropagator, final World world) {
+ this.skylightPropagator = skylightPropagator;
+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
+ //this.isClientSide = 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 ChunkSection[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 ILightAccess 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 IChunkAccess chunk = (IChunkAccess)chunkProvider.getFeaturesReadyChunk(cx, cz); // mappings are awful here, this is the "get chunk at if at least features"
+
+ 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 IChunkAccess 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 IChunkAccess chunk) {
+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
+ }
+
+ protected final ChunkSection 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 ChunkSection section) {
+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
+ }
+
+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final ChunkSection[] 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].isFullOfAir() ? 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 ILightAccess 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.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(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 IBlockData getBlockState(final int worldX, final int worldY, final int worldZ) {
+ final ChunkSection 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.getType(worldX & 15, worldY & 15, worldZ & 15);
+ }
+
+ return null;
+ }
+
+ protected final IBlockData getBlockState(final int sectionIndex, final int localIndex) {
+ final ChunkSection section = this.sectionCache[sectionIndex];
+
+ if (section != null) {
+ return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.blockIds.rawGet(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 int getCustomLightLevel(final VariableBlockLightHandler customBlockHandler, final int worldX, final int worldY,
+ final int worldZ, final int dfl) {
+ final int ret = customBlockHandler.getLightLevel(worldX, worldY, worldZ);
+ return ret == -1 ? dfl : ret;
+ }
+
+ // :(
+
+ 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();
+ }
+
+ /**
+ * @deprecated To be removed in 1.17 due to variable section count
+ */
+ @Deprecated
+ public static SWMRNibbleArray[] getFilledEmptyLight() {
+ return getFilledEmptyLight(16 - (-1) + 1);
+ }
+
+ public static SWMRNibbleArray[] getFilledEmptyLight(final World 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 IChunkAccess chunk);
+
+ protected abstract void setEmptinessMap(final IChunkAccess chunk, final boolean[] to);
+
+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk);
+
+ protected abstract void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to);
+
+ protected abstract boolean canUseChunk(final IChunkAccess chunk);
+
+ // This reference is explicitly stored here so that a heap dump will show what blocks were being processed.
+ // This is to debug a rather nasty unbounded queue problem.
+ protected Set<BlockPosition> changedBlocksSet;
+ public final void blocksChangedInChunk(final ILightAccess lightAccess, final int chunkX, final int chunkZ,
+ final Set<BlockPosition> positions, final Boolean[] changedSections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ this.changedBlocksSet = positions;
+ final IChunkAccess 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.changedBlocksSet = null;
+ 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 ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions);
+
+ protected abstract void checkBlock(final ILightAccess 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 ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect, final VariableBlockLightHandler customBlockLight);
+
+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
+
+ protected void checkChunkEdge(final ILightAccess lightAccess, final IChunkAccess 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;
+ }
+
+ final VariableBlockLightHandler customLightHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+ 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, customLightHandler) != currentLevel) {
+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
+ }
+
+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel, customLightHandler) != 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 ILightAccess lightAccess, final IChunkAccess chunk, final ShortCollection sections) {
+ final ChunkCoordIntPair 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 ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkCoordIntPair 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 ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkCoordIntPair 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 IChunkAccess chunk) {
+ final ChunkSection[] sections = chunk.getSections();
+ final Boolean[] ret = new Boolean[sections.length];
+
+ for (int i = 0; i < sections.length; ++i) {
+ if (sections[i] == null || sections[i].isFullOfAir()) {
+ ret[i] = Boolean.TRUE;
+ } else {
+ ret[i] = Boolean.FALSE;
+ }
+ }
+
+ return ret;
+ }
+
+ public final void forceHandleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess 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 ILightAccess lightAccess, final int chunkX, final int chunkZ,
+ final Boolean[] emptinessChanges) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ final IChunkAccess 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);
+
+ // 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 ILightAccess lightAccess, final IChunkAccess chunk,
+ final Boolean[] emptinessChanges, final boolean unlit) {
+ final World world = (World)lightAccess.getWorld();
+ 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) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, sectionY, dz + chunkZ);
+
+ // 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 ChunkSection 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
+ if (nibble != null) {
+ nibble.setNull();
+ }
+ } 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 ILightAccess lightAccess, final int chunkX, final int chunkZ) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final IChunkAccess 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 ILightAccess lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final IChunkAccess 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 ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks);
+
+ public final void light(final ILightAccess lightAccess, final IChunkAccess 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); // TODO
+ this.setNibbles(chunk, nibbles);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void relightChunks(final ILightAccess lightAccess, final Set<ChunkCoordIntPair> chunks,
+ final Consumer<ChunkCoordIntPair> 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 ChunkCoordIntPair chunkPos : chunks) {
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+ final IChunkAccess chunk = (IChunkAccess) lightAccess.getFeaturesReadyChunk(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 IChunkAccess neighbour = (IChunkAccess) lightAccess.getFeaturesReadyChunk(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 IChunkAccess neighbour2 = (IChunkAccess)lightAccess.getFeaturesReadyChunk(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 ChunkCoordIntPair(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.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(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 ILightAccess lightAccess) {
+ final IBlockAccess world = lightAccess.getWorld();
+ 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 IBlockData 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.setValues(offX, offY, offZ);
+ long flags = 0;
+ if (blockState.isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getOpacity(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 IBlockData fromBlock = this.getBlockState(posX, posY, posZ);
+ this.mutablePos2.setValues(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.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape();
+
+ if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), 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 IBlockData 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.setValues(offX, offY, offZ);
+ long flags = 0;
+ if (blockState.isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getOpacity(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 ILightAccess lightAccess) {
+ final IBlockAccess world = lightAccess.getWorld();
+ 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;
+ final VariableBlockLightHandler customLightHandler = this.skylightPropagator ? null : ((WorldServer)world).customBlockLightHandlers;
+
+ 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 IBlockData 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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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.setValues(offX, offY, offZ);
+ long flags = 0;
+ if (blockState.isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getOpacity(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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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 IBlockData fromBlock = this.getBlockState(posX, posY, posZ);
+ this.mutablePos2.setValues(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.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape();
+
+ if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), 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 IBlockData 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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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.setValues(offX, offY, offZ);
+ long flags = 0;
+ if (blockState.isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getOpacity(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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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/com/tuinity/tuinity/chunk/light/StarLightInterface.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..df686b97460796004cad1477760647a98741d751
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java
@@ -0,0 +1,490 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import com.tuinity.tuinity.util.WorldUtil;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import net.minecraft.core.BlockPosition;
+import net.minecraft.server.level.WorldServer;
+import net.minecraft.world.level.ChunkCoordIntPair;
+import net.minecraft.world.level.chunk.ChunkStatus;
+import net.minecraft.world.level.chunk.IChunkAccess;
+import net.minecraft.world.level.chunk.ILightAccess;
+import net.minecraft.world.level.lighting.LightEngineLayerEventListener;
+import net.minecraft.world.level.chunk.NibbleArray;
+import net.minecraft.core.SectionPosition;
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public final class StarLightInterface {
+
+
+ /**
+ * Can be {@code null}, indicating the light is all empty.
+ */
+ protected final WorldServer world;
+ protected final ILightAccess lightAccess;
+
+ protected final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
+ protected final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
+
+ protected final Long2ObjectOpenHashMap<ChunkChanges> changedBlocks = new Long2ObjectOpenHashMap<>();
+
+ protected final LightEngineLayerEventListener skyReader;
+ protected final LightEngineLayerEventListener blockReader;
+ protected static final boolean isClientSide = false;
+
+ protected final int minSection;
+ protected final int maxSection;
+ protected final int minLightSection;
+ protected final int maxLightSection;
+
+ public StarLightInterface(final ILightAccess lightAccess, final boolean hasSkyLight, final boolean hasBlockLight) {
+ this.lightAccess = lightAccess;
+ this.world = lightAccess == null ? null : (WorldServer)lightAccess.getWorld();
+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
+ 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.skyReader = !hasSkyLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() {
+ @Override
+ public NibbleArray a(final SectionPosition pos) {
+ final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(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 b(final BlockPosition blockPos) {
+ final int x = blockPos.getX();
+ int y = blockPos.getY();
+ final int z = blockPos.getZ();
+
+ final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4);
+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(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 a(final SectionPosition pos, final boolean notReady) {
+ StarLightInterface.this.sectionChange(pos, notReady);
+ }
+ };
+ this.blockReader = !hasBlockLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() {
+ @Override
+ public NibbleArray a(final SectionPosition pos) {
+ final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+
+ if (pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
+ return null;
+ }
+
+ return chunk != null ? chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble() : null;
+ }
+
+ @Override
+ public int b(final BlockPosition 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 IChunkAccess 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 a(final SectionPosition pos, final boolean notReady) {
+ return; // block engine doesn't care
+ }
+ };
+ }
+
+ public LightEngineLayerEventListener getSkyReader() {
+ return this.skyReader;
+ }
+
+ public LightEngineLayerEventListener getBlockReader() {
+ return this.blockReader;
+ }
+
+ public boolean isClientSide() {
+ return this.isClientSide;
+ }
+
+ public IChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
+ if (this.world == null) {
+ // empty world
+ return null;
+ }
+ return this.world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ);
+ }
+
+ public boolean hasUpdates() {
+ synchronized (this) {
+ return !this.changedBlocks.isEmpty();
+ }
+ }
+
+ public WorldServer getWorld() {
+ return this.world;
+ }
+
+ public ILightAccess 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 void blockChange(BlockPosition pos) {
+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
+ return;
+ }
+
+ pos = pos.immutableCopy();
+ synchronized (this.changedBlocks) {
+ this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
+ return new ChunkChanges();
+ }).changedPositions.add(pos);
+ }
+ }
+
+ public void sectionChange(final SectionPosition pos, final boolean newEmptyValue) {
+ if (this.world == null) { // empty world
+ return;
+ }
+
+ synchronized (this.changedBlocks) {
+ final ChunkChanges changes = this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
+ return new ChunkChanges();
+ });
+ if (changes.changedSectionSet == null) {
+ changes.changedSectionSet = new Boolean[this.maxSection - this.minSection + 1];
+ }
+ changes.changedSectionSet[pos.getY() - this.minSection] = Boolean.valueOf(newEmptyValue);
+ }
+ }
+
+ public void forceLoadInChunk(final IChunkAccess 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 IChunkAccess 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<ChunkCoordIntPair> chunks, final Consumer<ChunkCoordIntPair> 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 propagateChanges() {
+ synchronized (this.changedBlocks) {
+ if (this.changedBlocks.isEmpty()) {
+ return;
+ }
+ }
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ // TODO be smarter about this in the future
+ final Long2ObjectOpenHashMap<ChunkChanges> changedBlocks;
+ synchronized (this.changedBlocks) {
+ changedBlocks = this.changedBlocks.clone();
+ this.changedBlocks.clear();
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<ChunkChanges>> iterator = changedBlocks.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Long2ObjectMap.Entry<ChunkChanges> entry = iterator.next();
+ final long coordinate = entry.getLongKey();
+ final ChunkChanges changes = entry.getValue();
+ final Set<BlockPosition> positions = changes.changedPositions;
+ final Boolean[] sectionChanges = changes.changedSectionSet;
+
+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
+
+ if (skyEngine != null) {
+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
+ }
+ if (blockEngine != null) {
+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
+ }
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ protected static final class ChunkChanges {
+
+ // note: on the main thread, empty section changes are queued before block changes. This means we don't need
+ // to worry about cases where a block change is called inside an empty chunk section, according to the "emptiness" map per chunk,
+ // for example.
+ public final Set<BlockPosition> changedPositions = new HashSet<>();
+
+ public Boolean[] changedSectionSet;
+
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..20309334d81011f18fbb67be209a1eec25447b5c
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java
@@ -0,0 +1,33 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.core.BlockPosition;
+import java.util.Collection;
+
+/**
+ * Recommended implementation is {@link VariableBlockLightHandlerImpl}, but you can implement this interface yourself
+ * if you want.
+ *
+ * @deprecated To be removed in 1.17 due to Mojang adding a custom light block.
+ */
+@Deprecated
+public interface VariableBlockLightHandler {
+
+ /**
+ * Returns the custom light level for the specified position. Must return {@code -1} if there is custom level.
+ * @param x Block x world coordinate
+ * @param y Block y world coordinate
+ * @param z Block z world coordinate
+ * @return Custom light level for the specified position
+ */
+ public int getLightLevel(final int x, final int y, final int z);
+
+ /**
+ * Returns a collection of all the custom light positions inside the specified chunk. This must be fast,
+ * as it is used during chunk lighting.
+ * @param chunkX Chunk's x coordinate.
+ * @param chunkZ Chunk's z coordinate.
+ * @return Collection of all the custom light positions in the specified chunk.
+ */
+ public Collection<BlockPosition> getCustomLightPositions(final int chunkX, final int chunkZ);
+
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e4442a94559346b19a536d35ce5def612074838
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java
@@ -0,0 +1,112 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.core.BlockPosition;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.locks.StampedLock;
+
+public class VariableBlockLightHandlerImpl implements VariableBlockLightHandler {
+
+ protected final Long2ObjectOpenHashMap<Set<BlockPosition>> positionsByChunk = new Long2ObjectOpenHashMap<>();
+ protected final Long2IntOpenHashMap lightValuesByPosition = new Long2IntOpenHashMap();
+ protected final StampedLock seqlock = new StampedLock();
+ {
+ this.lightValuesByPosition.defaultReturnValue(-1);
+ this.positionsByChunk.defaultReturnValue(Collections.emptySet());
+ }
+
+ @Override
+ public int getLightLevel(final int x, final int y, final int z) {
+ final long key = CoordinateUtils.getBlockKey(x, y, z);
+ try {
+ final long attempt = this.seqlock.tryOptimisticRead();
+ if (attempt != 0L) {
+ final int ret = this.lightValuesByPosition.get(key);
+
+ if (this.seqlock.validate(attempt)) {
+ return ret;
+ }
+ }
+ } catch (final Error error) {
+ throw error;
+ } catch (final Throwable thr) {
+ // ignore
+ }
+
+ this.seqlock.readLock();
+ try {
+ return this.lightValuesByPosition.get(key);
+ } finally {
+ this.seqlock.tryUnlockRead();
+ }
+ }
+
+ @Override
+ public Collection<BlockPosition> getCustomLightPositions(final int chunkX, final int chunkZ) {
+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ try {
+ final long attempt = this.seqlock.tryOptimisticRead();
+ if (attempt != 0L) {
+ final Set<BlockPosition> ret = new HashSet<>(this.positionsByChunk.get(key));
+
+ if (this.seqlock.validate(attempt)) {
+ return ret;
+ }
+ }
+ } catch (final Error error) {
+ throw error;
+ } catch (final Throwable thr) {
+ // ignore
+ }
+
+ this.seqlock.readLock();
+ try {
+ return new HashSet<>(this.positionsByChunk.get(key));
+ } finally {
+ this.seqlock.tryUnlockRead();
+ }
+ }
+
+ public void setSource(final int x, final int y, final int z, final int to) {
+ if (to < 0 || to > 15) {
+ throw new IllegalArgumentException();
+ }
+ this.seqlock.writeLock();
+ try {
+ if (this.lightValuesByPosition.put(CoordinateUtils.getBlockKey(x, y, z), to) == -1) {
+ this.positionsByChunk.computeIfAbsent(CoordinateUtils.getChunkKey(x >> 4, z >> 4), (final long keyInMap) -> {
+ return new HashSet<>();
+ }).add(new BlockPosition(x, y, z));
+ }
+ } finally {
+ this.seqlock.tryUnlockWrite();
+ }
+ }
+
+ public int removeSource(final int x, final int y, final int z) {
+ this.seqlock.writeLock();
+ try {
+ final int ret = this.lightValuesByPosition.remove(CoordinateUtils.getBlockKey(x, y, z));
+
+ if (ret != -1) {
+ final long chunkKey = CoordinateUtils.getChunkKey(x >> 4, z >> 4);
+
+ final Set<BlockPosition> positions = this.positionsByChunk.get(chunkKey);
+ positions.remove(new BlockPosition(x, y, z));
+
+ if (positions.isEmpty()) {
+ this.positionsByChunk.remove(chunkKey);
+ }
+ }
+
+ return ret;
+ } finally {
+ this.seqlock.tryUnlockWrite();
+ }
+ }
+}
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..414c8de2bcc10165e2d328a5746899ac087698b4
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
@@ -0,0 +1,398 @@
+package com.tuinity.tuinity.config;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.TicketType;
+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 boolean tickWorldsInParallel;
+
+ /**
+ * if tickWorldsInParallel == true, then this value is used as a default only for worlds
+ */
+ public static int tickThreads;
+
+ /*
+ private static void worldticking() {
+ tickWorldsInParallel = TuinityConfig.getBoolean("tick-worlds-in-parallel", false);
+ tickThreads = TuinityConfig.getInt("server-tick-threads", 1); // will be 4 in the future
+ }*/
+
+ public static int delayChunkUnloadsBy;
+
+ private static void delayChunkUnloadsBy() {
+ delayChunkUnloadsBy = TuinityConfig.getInt("delay-chunkunloads-by", 5) * 20;
+ if (delayChunkUnloadsBy >= 0) {
+ TicketType.DELAYED_UNLOAD.loadPeriod = delayChunkUnloadsBy;
+ }
+ }
+
+ public static boolean lagCompensateBlockBreaking;
+
+ private static void lagCompensateBlockBreaking() {
+ lagCompensateBlockBreaking = TuinityConfig.getBoolean("lag-compensate-block-breaking", true);
+ }
+
+ 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." +
+ net.minecraft.network.protocol.game.PacketPlayInAutoRecipe.class.getSimpleName() + ".interval", 4.0);
+ TuinityConfig.getDouble("packet-limiter.limits." +
+ net.minecraft.network.protocol.game.PacketPlayInAutoRecipe.class.getSimpleName() + ".max-packet-rate", 5.0);
+ TuinityConfig.getString("packet-limiter.limits." +
+ net.minecraft.network.protocol.game.PacketPlayInAutoRecipe.class.getSimpleName() + ".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 useNewLightEngine;
+
+ private static void useNewLightEngine() {
+ useNewLightEngine = TuinityConfig.getBoolean("use-new-light-engine", true);
+ }
+
+ public static boolean sendFullPosForHardCollidingEntities;
+
+ private static void sendFullPosForHardCollidingEntities() {
+ sendFullPosForHardCollidingEntities = TuinityConfig.getBoolean("send-full-pos-for-hard-colliding-entities", true);
+ }
+
+ 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 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));
+ }
+
+ 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));
+ }
+
+ 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));
+ }
+
+ 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));
+ }
+
+ 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));
+ }
+
+ /** ignored if {@link TuinityConfig#tickWorldsInParallel} == false */
+ public int threads;
+
+ /*
+ private void worldthreading() {
+ final int threads = this.getInt("tick-threads", -1);
+ this.threads = threads == -1 ? TuinityConfig.tickThreads : threads;
+ }*/
+ }
+
+}
\ No newline at end of file
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..e0a2f10f92430fd59519e57dee3286035c287bf3
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
@@ -0,0 +1,74 @@
+package com.tuinity.tuinity.util;
+
+import net.minecraft.world.phys.AxisAlignedBB;
+import net.minecraft.world.level.chunk.Chunk;
+import net.minecraft.world.entity.Entity;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.util.UnsafeList;
+import java.util.List;
+
+public class CachedLists {
+
+ static final UnsafeList<AxisAlignedBB> TEMP_COLLISION_LIST = new UnsafeList<>(1024);
+ static boolean tempCollisionListInUse;
+
+ public static UnsafeList<AxisAlignedBB> getTempCollisionList() {
+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
+ return new UnsafeList<>(16);
+ }
+ tempCollisionListInUse = true;
+ return TEMP_COLLISION_LIST;
+ }
+
+ public static void returnTempCollisionList(List<AxisAlignedBB> 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;
+ }
+
+ static final UnsafeList<Chunk> TEMP_GET_CHUNKS_LIST = new UnsafeList<>(1024);
+ static boolean tempGetChunksListInUse;
+
+ public static UnsafeList<Chunk> getTempGetChunksList() {
+ if (!Bukkit.isPrimaryThread() || tempGetChunksListInUse) {
+ return new UnsafeList<>();
+ }
+ tempGetChunksListInUse = true;
+ return TEMP_GET_CHUNKS_LIST;
+ }
+
+ public static void returnTempGetChunksList(List<Chunk> list) {
+ if (list != TEMP_GET_CHUNKS_LIST) {
+ return;
+ }
+ ((UnsafeList)list).setSize(0);
+ tempGetChunksListInUse = false;
+ }
+
+ public static void reset() {
+ TEMP_COLLISION_LIST.completeReset();
+ TEMP_GET_ENTITIES_LIST.completeReset();
+ TEMP_GET_CHUNKS_LIST.completeReset();
+ }
+}
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..17cece8ee25ad6145bc0bdf7d15c2ea988c85f87
--- /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.BlockPosition;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.util.MathHelper;
+import net.minecraft.core.SectionPosition;
+import net.minecraft.world.level.ChunkCoordIntPair;
+
+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 BlockPosition pos) {
+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final Entity entity) {
+ return ((long)(MathHelper.floor(entity.locZ()) >> 4) << 32) | ((MathHelper.floor(entity.locX()) >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final ChunkCoordIntPair pos) {
+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final SectionPosition 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 MathHelper.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 SectionPosition 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 ChunkCoordIntPair 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 BlockPosition 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 ((MathHelper.floorLong(entity.locX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+ ((MathHelper.floorLong(entity.locY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+ ((MathHelper.floorLong(entity.locZ()) << (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 MathHelper.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 BlockPosition 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.locX() & 0x7FFFFFF) | (((long)entity.locZ() & 0x7FFFFFF) << 27) | ((long)entity.locY() << 54);
+ }
+
+ private CoordinateUtils() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
@@ -0,0 +1,226 @@
+package com.tuinity.tuinity.util;
+
+public final class IntegerUtil {
+
+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
+
+ public static int ceilLog2(final int value) {
+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+ }
+
+ public static long ceilLog2(final long value) {
+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+ }
+
+ public static int floorLog2(final int value) {
+ // xor is optimized subtract for 2^n -1
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+ }
+
+ public static int floorLog2(final long value) {
+ // xor is optimized subtract for 2^n -1
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+ }
+
+ public static int roundCeilLog2(final int value) {
+ // optimized variant of 1 << (32 - leading(val - 1))
+ // given
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
+ }
+
+ public static long roundCeilLog2(final long value) {
+ // see logic documented above
+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
+ }
+
+ public static int roundFloorLog2(final int value) {
+ // optimized variant of 1 << (31 - leading(val))
+ // given
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
+ }
+
+ public static long roundFloorLog2(final long value) {
+ // see logic documented above
+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
+ }
+
+ public static boolean isPowerOfTwo(final int n) {
+ // 2^n has one bit
+ // note: this rets true for 0 still
+ return IntegerUtil.getTrailingBit(n) == n;
+ }
+
+ public static boolean isPowerOfTwo(final long n) {
+ // 2^n has one bit
+ // note: this rets true for 0 still
+ return IntegerUtil.getTrailingBit(n) == n;
+ }
+
+ public static int getTrailingBit(final int n) {
+ return -n & n;
+ }
+
+ public static long getTrailingBit(final long n) {
+ return -n & n;
+ }
+
+ public static int trailingZeros(final int n) {
+ return Integer.numberOfTrailingZeros(n);
+ }
+
+ public static int trailingZeros(final long n) {
+ return Long.numberOfTrailingZeros(n);
+ }
+
+ // from hacker's delight (signed division magic value)
+ public static int getDivisorMultiple(final long numbers) {
+ return (int)(numbers >>> 32);
+ }
+
+ // from hacker's delight (signed division magic value)
+ public static int getDivisorShift(final long numbers) {
+ return (int)numbers;
+ }
+
+ // copied from hacker's delight (signed division magic value)
+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
+ public static long getDivisorNumbers(final int d) {
+ final int ad = IntegerUtil.branchlessAbs(d);
+
+ if (ad < 2) {
+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
+ }
+
+ final int two31 = 0x80000000;
+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
+
+ int p = 31;
+
+ // all these variables are UNSIGNED!
+ int t = two31 + (d >>> 31);
+ int anc = t - 1 - t%ad;
+ int q1 = (int)((two31 & mask)/(anc & mask));
+ int r1 = two31 - q1*anc;
+ int q2 = (int)((two31 & mask)/(ad & mask));
+ int r2 = two31 - q2*ad;
+ int delta;
+
+ do {
+ p = p + 1;
+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
+ q1 = q1 + 1;
+ r1 = r1 - anc;
+ }
+ q2 = 2*q2; // Update q2 = 2**p/|d|.
+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
+ q2 = q2 + 1;
+ r2 = r2 - ad;
+ }
+ delta = ad - r2;
+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
+
+ int magicNum = q2 + 1;
+ if (d < 0) {
+ magicNum = -magicNum;
+ }
+ int shift = p - 32;
+ return ((long)magicNum << 32) | shift;
+ }
+
+ public static int branchlessAbs(final int val) {
+ // -n = -1 ^ n + 1
+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+ }
+
+ public static long branchlessAbs(final long val) {
+ // -n = -1 ^ n + 1
+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+ }
+
+ //https://github.com/skeeto/hash-prospector for hash functions
+
+ //score = ~590.47984224483832
+ public static int hash0(int x) {
+ x *= 0x36935555;
+ x ^= x >>> 16;
+ return x;
+ }
+
+ //score = ~310.01596637036749
+ public static int hash1(int x) {
+ x ^= x >>> 15;
+ x *= 0x356aaaad;
+ x ^= x >>> 17;
+ return x;
+ }
+
+ public static int hash2(int x) {
+ x ^= x >>> 16;
+ x *= 0x7feb352d;
+ x ^= x >>> 15;
+ x *= 0x846ca68b;
+ x ^= x >>> 16;
+ return x;
+ }
+
+ public static int hash3(int x) {
+ x ^= x >>> 17;
+ x *= 0xed5ad4bb;
+ x ^= x >>> 11;
+ x *= 0xac4c1b51;
+ x ^= x >>> 15;
+ x *= 0x31848bab;
+ x ^= x >>> 14;
+ return x;
+ }
+
+ //score = ~365.79959673201887
+ public static long hash1(long x) {
+ x ^= x >>> 27;
+ x *= 0xb24924b71d2d354bL;
+ x ^= x >>> 28;
+ return x;
+ }
+
+ //h2 hash
+ public static long hash2(long x) {
+ x ^= x >>> 32;
+ x *= 0xd6e8feb86659fd93L;
+ x ^= x >>> 32;
+ x *= 0xd6e8feb86659fd93L;
+ x ^= x >>> 32;
+ return x;
+ }
+
+ public static long hash3(long x) {
+ x ^= x >>> 45;
+ x *= 0xc161abe5704b6c79L;
+ x ^= x >>> 41;
+ x *= 0xe3e5389aedbc90f7L;
+ x ^= x >>> 56;
+ x *= 0x1f9aba75a52db073L;
+ x ^= x >>> 53;
+ return x;
+ }
+
+ private IntegerUtil() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
@@ -0,0 +1,100 @@
+package com.tuinity.tuinity.util;
+
+public final class IntervalledCounter {
+
+ protected long[] times;
+ protected final long interval;
+ protected long minTime;
+ protected int sum;
+ protected int head; // inclusive
+ protected int tail; // exclusive
+
+ public IntervalledCounter(final long interval) {
+ this.times = new long[8];
+ this.interval = interval;
+ }
+
+ public void updateCurrentTime() {
+ this.updateCurrentTime(System.nanoTime());
+ }
+
+ public void updateCurrentTime(final long currentTime) {
+ int sum = this.sum;
+ int head = this.head;
+ final int tail = this.tail;
+ final long minTime = currentTime - this.interval;
+
+ final int arrayLen = this.times.length;
+
+ // guard against overflow by using subtraction
+ while (head != tail && this.times[head] - minTime < 0) {
+ head = (head + 1) % arrayLen;
+ --sum;
+ }
+
+ this.sum = sum;
+ this.head = head;
+ this.minTime = minTime;
+ }
+
+ public void addTime(final long currTime) {
+ // guard against overflow by using subtraction
+ if (currTime - this.minTime < 0) {
+ return;
+ }
+ int nextTail = (this.tail + 1) % this.times.length;
+ if (nextTail == this.head) {
+ this.resize();
+ nextTail = (this.tail + 1) % this.times.length;
+ }
+
+ this.times[this.tail] = currTime;
+ this.tail = nextTail;
+ }
+
+ public void updateAndAdd(final int count) {
+ final long currTime = System.nanoTime();
+ this.updateCurrentTime(currTime);
+ for (int i = 0; i < count; ++i) {
+ this.addTime(currTime);
+ }
+ }
+
+ public void updateAndAdd(final int count, final long currTime) {
+ this.updateCurrentTime(currTime);
+ for (int i = 0; i < count; ++i) {
+ this.addTime(currTime);
+ }
+ }
+
+ private void resize() {
+ final long[] oldElements = this.times;
+ final long[] newElements = new long[this.times.length * 2];
+ this.times = newElements;
+
+ final int head = this.head;
+ final int tail = this.tail;
+ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
+ this.head = 0;
+ this.tail = size;
+
+ if (tail >= head) {
+ System.arraycopy(oldElements, head, newElements, 0, size);
+ } else {
+ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
+ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
+ }
+ }
+
+ // returns in units per second
+ public double getRate() {
+ return this.size() / (this.interval * 1.0e-9);
+ }
+
+ public int size() {
+ final int head = this.head;
+ final int tail = this.tail;
+
+ return tail >= head ? (tail - head) : (tail + (this.times.length - head));
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/util/PoiAccess.java b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
new file mode 100644
index 0000000000000000000000000000000000000000..5018a3378680d98605e0e4a79bf112e032c98925
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
@@ -0,0 +1,812 @@
+package com.tuinity.tuinity.util;
+
+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.core.BlockPosition;
+import net.minecraft.util.MathHelper;
+import net.minecraft.core.SectionPosition;
+import net.minecraft.world.entity.ai.village.poi.VillagePlace;
+import net.minecraft.world.entity.ai.village.poi.VillagePlaceRecord;
+import net.minecraft.world.entity.ai.village.poi.VillagePlaceSection;
+import net.minecraft.world.entity.ai.village.poi.VillagePlaceType;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Provides optimised access to POI data. All returned values will be identical to vanilla.
+ */
+public final class PoiAccess {
+
+ public static final class SortedChunkSectionPos {
+
+ public final int relativeX;
+ public final int absoluteY;
+ public final int relativeZ;
+
+ public final double smallestDistanceToSection;
+
+ public SortedChunkSectionPos(int relativeX, int absoluteY, int relativeZ, double smallestDistanceToSection) {
+ this.relativeX = relativeX;
+ this.absoluteY = absoluteY;
+ this.relativeZ = relativeZ;
+ this.smallestDistanceToSection = smallestDistanceToSection;
+ }
+
+ protected double smallestDistanceAhead;
+
+ }
+
+ protected static double clamp(final double val, final double min, final double max) {
+ return (val < min ? min : (val > max ? max : val));
+ }
+
+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ,
+ final double boxMaxX, final double boxMaxY, final double boxMaxZ,
+
+ final double circleX, final double circleY, final double circleZ) {
+ // is the circle center inside the box?
+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) {
+ return 0.0;
+ }
+
+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0;
+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0;
+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0;
+
+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0;
+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0;
+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0;
+
+ double centerDiffX = circleX - boxCenterX;
+ double centerDiffY = circleY - boxCenterY;
+ double centerDiffZ = circleZ - boxCenterZ;
+
+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX);
+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY);
+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ);
+
+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ);
+ }
+
+
+ // key is:
+ // upper 32 bits:
+ // upper 16 bits: max y section
+ // lower 16 bits: min y section
+ // lower 32 bits:
+ // upper 16 bits: section
+ // lower 16 bits: radius
+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) {
+ return (
+ (maxSection & 0xFFFFL) << (64 - 16)
+ | (minSection & 0xFFFFL) << (64 - 32)
+ | (section & 0xFFFFL) << (64 - 48)
+ | (radius & 0xFFFFL) << (64 - 64)
+ );
+ }
+
+ protected static final Long2ObjectOpenHashMap<List<SortedChunkSectionPos>> cachedSortedPositionsAddHalf =
+ new Long2ObjectOpenHashMap<>();
+ protected static final Long2ObjectOpenHashMap<List<SortedChunkSectionPos>> cachedSortedPositionsNoHalf =
+ new Long2ObjectOpenHashMap<>();
+
+ public static List<SortedChunkSectionPos> getSortedPositions(final BlockPosition center, final int radius, final boolean addHalfToSection) {
+ final Long2ObjectOpenHashMap<List<SortedChunkSectionPos>> map = addHalfToSection ? cachedSortedPositionsAddHalf : cachedSortedPositionsNoHalf;
+
+ final int section = center.getY() >> 4;
+ if (section < -16) {
+ return calculateSortedPositions(0, 15, section, radius, addHalfToSection);
+ } else if (section > (16 + 16)) {
+ return calculateSortedPositions(0, 15, section, radius, addHalfToSection);
+ }
+
+ final long key = getKey(0, 15, center.getY() >> 4, radius);
+ synchronized (map) {
+ List<SortedChunkSectionPos> existing = map.get(key);
+ if (existing != null) {
+ return existing;
+ }
+ }
+
+ final List<SortedChunkSectionPos> calculated = calculateSortedPositions(0, 15, section, radius, addHalfToSection);
+
+ synchronized (map) {
+ map.putIfAbsent(key, calculated);
+ }
+
+ return calculated;
+ }
+
+ protected static List<SortedChunkSectionPos> calculateSortedPositions(final int minSection, final int maxSection, final int section, final int radius, final boolean addHalfToSection) {
+ final List<SortedChunkSectionPos> ret = new ArrayList<>();
+
+ final double sourceX = 8.0;
+ final double sourceY = section * 16.0 + 8.0;
+ final double sourceZ = 8.0;
+ final double minSectionAdd = addHalfToSection ? 0.5 : 0.0;
+ final double maxSectionAdd = addHalfToSection ? 15.5 : 16.0;
+ for (int y = minSection; y <= maxSection; ++y) {
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ double distanceSquared = getSmallestDistanceSquared(
+ (double)(dx << 4) + minSectionAdd,
+ (double)(y << 4) + minSectionAdd,
+ (double)(dz << 4) + minSectionAdd,
+ (double)(dx << 4) + maxSectionAdd,
+ (double)(y << 4) + maxSectionAdd,
+ (double)(dz << 4) + maxSectionAdd,
+
+ sourceX, sourceY, sourceZ
+ );
+ // max additional distance (due to varying center point)
+ final double a = (Math.sqrt(2*2*2))*(addHalfToSection ? 7.5 : 8.0);
+ distanceSquared = Math.sqrt(distanceSquared) - a;
+ distanceSquared *= distanceSquared;
+
+ ret.add(new SortedChunkSectionPos(dx, y, dz, distanceSquared - 1.0e-3)); // account for floating point errors
+ }
+ }
+ }
+
+ final BlockPosition relativeCenter = new BlockPosition(8, (section << 4) | 8, 8);
+ ret.sort((s1, s2) -> {
+ return Double.compare(relativeCenter.distanceSquared((s1.relativeX << 4) + 8, (s1.absoluteY << 4) + 8, (s1.relativeZ << 4) + 8, false),
+ relativeCenter.distanceSquared((s2.relativeX << 4) + 8, (s2.absoluteY << 4) + 8, (s2.relativeZ << 4) + 8, false));
+ });
+
+ double smallest = Double.MAX_VALUE;
+ for (int i = ret.size() - 1; i >= 0; --i) {
+ final SortedChunkSectionPos chunkPos = ret.get(i);
+ chunkPos.smallestDistanceAhead = smallest;
+ if (chunkPos.smallestDistanceToSection < smallest) {
+ smallest = chunkPos.smallestDistanceToSection;
+ }
+ }
+
+ // we set smallestDistanceAhead so that callers iterating over the list do not need to search any more sections
+ // than they need to (the intersects check for a sphere and a 3d box is seriously expensive, especially with
+ // how many sections there are).
+
+ return ret;
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance.
+ public static BlockPosition findClosestPoiDataPosition(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final VillagePlaceRecord ret = findClosestPoiDataRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
+ );
+
+ return ret == null ? null : ret.getPosition();
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
+ public static void findClosestPoiDataPositions(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final Set<BlockPosition> ret) {
+ final Set<BlockPosition> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPosition> newPredicate = (final BlockPosition pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutableCopy());
+ };
+
+ final List<VillagePlaceRecord> toConvert = new ArrayList<>();
+ findClosestPoiDataRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, toConvert
+ );
+
+ for (final VillagePlaceRecord record : toConvert) {
+ ret.add(record.getPosition());
+ }
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance.
+ public static VillagePlaceRecord findClosestPoiDataRecord(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final List<VillagePlaceRecord> ret = new ArrayList<>();
+ findClosestPoiDataRecords(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, ret
+ );
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ // only includes x/z axis
+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
+ public static void findClosestPoiDataRecords(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final List<VillagePlaceRecord> ret) {
+ final Predicate<? super VillagePlaceRecord> occupancyFilter = occupancy.getPredicate();
+
+ final List<VillagePlaceRecord> closestRecords = new ArrayList<>();
+ double closestDistanceSquared = maxDistance * maxDistance;
+
+ final int lowerX = MathHelper.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerZ = MathHelper.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = MathHelper.floor(sourcePosition.getX() + range) >> 4;
+ final int upperZ = MathHelper.floor(sourcePosition.getZ() + range) >> 4;
+
+ final List<SortedChunkSectionPos> chunks = getSortedPositions(sourcePosition, (range + 15) >> 4, true);
+
+ final int centerX = sourcePosition.getX() >> 4;
+ final int centerY = sourcePosition.getY() >> 4;
+ final int centerZ = sourcePosition.getZ() >> 4;
+
+ for (final SortedChunkSectionPos relativeSection : chunks) {
+ if (relativeSection.smallestDistanceToSection > closestDistanceSquared) {
+ if (relativeSection.smallestDistanceAhead > closestDistanceSquared) {
+ break;
+ }
+ continue;
+ }
+ final int sectionX = relativeSection.relativeX + centerX;
+ final int sectionY = relativeSection.absoluteY;
+ final int sectionZ = relativeSection.relativeZ + centerZ;
+
+ if (sectionX < lowerX || sectionX > upperX || sectionZ < lowerZ || sectionZ > upperZ) {
+ // out of bound chunk
+ continue;
+ }
+
+ final double sectionDistanceSquared = getSmallestDistanceSquared(
+ (sectionX << 4) + 0.5,
+ (sectionY << 4) + 0.5,
+ (sectionZ << 4) + 0.5,
+ (sectionX << 4) + 15.5,
+ (sectionY << 4) + 15.5,
+ (sectionZ << 4) + 15.5,
+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ()
+ );
+ if (sectionDistanceSquared > closestDistanceSquared) {
+ continue;
+ }
+
+ final Optional<VillagePlaceSection> poiSectionOptional = load ? poiStorage.getOrLoad(SectionPosition.asLong(sectionX, sectionY, sectionZ))
+ : poiStorage.getIfLoaded(SectionPosition.asLong(sectionX, sectionY, sectionZ));
+
+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
+ continue;
+ }
+
+ final VillagePlaceSection poiSection = poiSectionOptional.orElse(null);
+
+ final Map<VillagePlaceType, Set<VillagePlaceRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<VillagePlaceType, Set<VillagePlaceRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final VillagePlaceRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPosition poiPosition = poiData.getPosition();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ // it's important that it's poiPosition.distanceSquared(source) : the value actually is different IF the values are swapped!
+ final double dataRange = poiPosition.distanceSquared(sourcePosition);
+
+ if (dataRange > closestDistanceSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ if (dataRange < closestDistanceSquared) {
+ closestRecords.clear();
+ closestDistanceSquared = dataRange;
+ }
+ closestRecords.add(poiData);
+ }
+ }
+ }
+
+ // uh oh! we might have multiple records that match the distance sorting!
+ // we need to re-order our results by the way vanilla would have iterated over them.
+ closestRecords.sort((record1, record2) -> {
+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
+ // is fine and should be preserved (this sort is stable so we're good there)
+ // but they iterate sections by x then by z (like the following)
+ // for (int x = -dx; x <= dx; ++x)
+ // for (int z = -dz; z <= dz; ++z)
+ // ....
+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
+ final BlockPosition pos1 = record1.getPosition();
+ final BlockPosition pos2 = record2.getPosition();
+
+ final int cx1 = pos1.getX() >> 4;
+ final int cz1 = pos1.getZ() >> 4;
+
+ final int cx2 = pos2.getX() >> 4;
+ final int cz2 = pos2.getZ() >> 4;
+
+ if (cz2 != cz1) {
+ // want smaller z
+ return Integer.compare(cz1, cz2);
+ }
+
+ if (cx2 != cx1) {
+ // want smaller x
+ return Integer.compare(cx1, cx2);
+ }
+
+ // same chunk
+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
+ // so now we just compare section y, wanting smaller y
+
+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
+ });
+
+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
+ ret.addAll(closestRecords);
+ }
+
+ // finds the closest poi entry pos.
+ public static BlockPosition findNearestPoiPosition(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final VillagePlaceRecord ret = findNearestPoiRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
+ );
+ return ret == null ? null : ret.getPosition();
+ }
+
+ // finds the closest `max` poi entry positions.
+ public static void findNearestPoiPositions(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<BlockPosition> ret) {
+ final Set<BlockPosition> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPosition> newPredicate = (final BlockPosition pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutableCopy());
+ };
+
+ final List<VillagePlaceRecord> toConvert = new ArrayList<>();
+ findNearestPoiRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, max, toConvert
+ );
+
+ for (final VillagePlaceRecord record : toConvert) {
+ ret.add(record.getPosition());
+ }
+ }
+
+ // finds the closest poi entry.
+ public static VillagePlaceRecord findNearestPoiRecord(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final List<VillagePlaceRecord> ret = new ArrayList<>();
+ findNearestPoiRecords(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load,
+ 1, ret
+ );
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ // finds the closest `max` poi entries.
+ public static void findNearestPoiRecords(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ // position predicate must not modify chunk POI
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final double maxDistance,
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<VillagePlaceRecord> ret) {
+ final Predicate<? super VillagePlaceRecord> occupancyFilter = occupancy.getPredicate();
+
+ final double maxDistanceSquared = maxDistance * maxDistance;
+ final Double2ObjectRBTreeMap<List<VillagePlaceRecord>> closestRecords = new Double2ObjectRBTreeMap<>();
+ int totalRecords = 0;
+ double furthestDistanceSquared = maxDistanceSquared;
+
+ final int lowerX = MathHelper.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerZ = MathHelper.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = MathHelper.floor(sourcePosition.getX() + range) >> 4;
+ final int upperZ = MathHelper.floor(sourcePosition.getZ() + range) >> 4;
+
+ final List<SortedChunkSectionPos> chunks = getSortedPositions(sourcePosition, (range + 15) >> 4, true);
+
+ final int centerX = sourcePosition.getX() >> 4;
+ final int centerY = sourcePosition.getY() >> 4;
+ final int centerZ = sourcePosition.getZ() >> 4;
+
+ for (final SortedChunkSectionPos relativeSection : chunks) {
+ if (relativeSection.smallestDistanceToSection > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
+ if (relativeSection.smallestDistanceAhead > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
+ break;
+ }
+ continue;
+ }
+ final int sectionX = relativeSection.relativeX + centerX;
+ final int sectionY = relativeSection.absoluteY;
+ final int sectionZ = relativeSection.relativeZ + centerZ;
+
+ if (sectionX < lowerX || sectionX > upperX || sectionZ < lowerZ || sectionZ > upperZ) {
+ // out of bound chunk
+ continue;
+ }
+
+ final double sectionDistanceSquared = getSmallestDistanceSquared(
+ (sectionX << 4) + 0.5,
+ (sectionY << 4) + 0.5,
+ (sectionZ << 4) + 0.5,
+ (sectionX << 4) + 15.5,
+ (sectionY << 4) + 15.5,
+ (sectionZ << 4) + 15.5,
+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ()
+ );
+
+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
+ continue;
+ }
+
+ final Optional<VillagePlaceSection> poiSectionOptional = load ? poiStorage.getOrLoad(SectionPosition.asLong(sectionX, sectionY, sectionZ))
+ : poiStorage.getIfLoaded(SectionPosition.asLong(sectionX, sectionY, sectionZ));
+
+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
+ continue;
+ }
+
+ final VillagePlaceSection poiSection = poiSectionOptional.orElse(null);
+
+ final Map<VillagePlaceType, Set<VillagePlaceRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<VillagePlaceType, Set<VillagePlaceRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final VillagePlaceRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPosition poiPosition = poiData.getPosition();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ // it's important that it's poiPosition.distanceSquared(source) : the value actually is different IF the values are swapped!
+ final double dataRange = poiPosition.distanceSquared(sourcePosition);
+
+ if (dataRange > maxDistanceSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (dataRange > furthestDistanceSquared && totalRecords >= max) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ if (dataRange > furthestDistanceSquared) {
+ // we know totalRecords < max, so this entry is now our furthest
+ furthestDistanceSquared = dataRange;
+ }
+
+ closestRecords.computeIfAbsent(dataRange, (final double key) -> {
+ return new ArrayList<>();
+ }).add(poiData);
+
+ if (++totalRecords >= max) {
+ if (closestRecords.size() >= 2) {
+ int entriesInClosest = 0;
+ final Iterator<Double2ObjectMap.Entry<List<VillagePlaceRecord>>> iterator = closestRecords.double2ObjectEntrySet().iterator();
+ double nextFurthestDistanceSquared = 0.0;
+
+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) {
+ final Double2ObjectMap.Entry<List<VillagePlaceRecord>> recordEntry = iterator.next();
+ entriesInClosest += recordEntry.getValue().size();
+ nextFurthestDistanceSquared = recordEntry.getDoubleKey();
+ }
+
+ if (entriesInClosest >= max) {
+ // the last set of entries at range wont even be considered for sure... nuke em
+ final Double2ObjectMap.Entry<List<VillagePlaceRecord>> recordEntry = iterator.next();
+ totalRecords -= recordEntry.getValue().size();
+ iterator.remove();
+
+ furthestDistanceSquared = nextFurthestDistanceSquared;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ final List<VillagePlaceRecord> closestRecordsUnsorted = new ArrayList<>();
+
+ // we're done here, so now just flatten the map and sort it.
+
+ for (final List<VillagePlaceRecord> records : closestRecords.values()) {
+ closestRecordsUnsorted.addAll(records);
+ }
+
+ // uh oh! we might have multiple records that match the distance sorting!
+ // we need to re-order our results by the way vanilla would have iterated over them.
+ closestRecordsUnsorted.sort((record1, record2) -> {
+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
+ // is fine and should be preserved (this sort is stable so we're good there)
+ // but they iterate sections by x then by z (like the following)
+ // for (int x = -dx; x <= dx; ++x)
+ // for (int z = -dz; z <= dz; ++z)
+ // ....
+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
+ final BlockPosition pos1 = record1.getPosition();
+ final BlockPosition pos2 = record2.getPosition();
+
+ final int cx1 = pos1.getX() >> 4;
+ final int cz1 = pos1.getZ() >> 4;
+
+ final int cx2 = pos2.getX() >> 4;
+ final int cz2 = pos2.getZ() >> 4;
+
+ if (cz2 != cz1) {
+ // want smaller z
+ return Integer.compare(cz1, cz2);
+ }
+
+ if (cx2 != cx1) {
+ // want smaller x
+ return Integer.compare(cx1, cx2);
+ }
+
+ // same chunk
+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
+ // so now we just compare section y, wanting smaller section y
+
+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
+ });
+
+ // trim out any entries exceeding our maximum
+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) {
+ closestRecordsUnsorted.remove(i);
+ }
+
+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
+ ret.addAll(closestRecordsUnsorted);
+ }
+
+ public static BlockPosition findAnyPoiPosition(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final VillagePlaceRecord ret = findAnyPoiRecord(
+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load
+ );
+
+ return ret == null ? null : ret.getPosition();
+ }
+
+ public static void findAnyPoiPositions(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<BlockPosition> ret) {
+ final Set<BlockPosition> positions = new HashSet<>();
+ // pos predicate is last thing that runs before adding to ret.
+ final Predicate<BlockPosition> newPredicate = (final BlockPosition pos) -> {
+ if (positionPredicate != null && !positionPredicate.test(pos)) {
+ return false;
+ }
+ return positions.add(pos.immutableCopy());
+ };
+
+ final List<VillagePlaceRecord> toConvert = new ArrayList<>();
+ findAnyPoiRecords(
+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert
+ );
+
+ for (final VillagePlaceRecord record : toConvert) {
+ ret.add(record.getPosition());
+ }
+ }
+
+ public static VillagePlaceRecord findAnyPoiRecord(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final VillagePlace.Occupancy occupancy,
+ final boolean load) {
+ final List<VillagePlaceRecord> ret = new ArrayList<>();
+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret);
+ return ret.isEmpty() ? null : ret.get(0);
+ }
+
+ public static void findAnyPoiRecords(final VillagePlace poiStorage,
+ final Predicate<VillagePlaceType> villagePlaceType,
+ final Predicate<BlockPosition> positionPredicate,
+ final BlockPosition sourcePosition,
+ final int range, // distance on x y z axis
+ final VillagePlace.Occupancy occupancy,
+ final boolean load,
+ final int max,
+ final List<VillagePlaceRecord> ret) {
+ // the biggest issue with the original mojang implementation is that they chain so many streams together
+ // the amount of streams chained just rolls performance, even if nothing is iterated over
+ final Predicate<? super VillagePlaceRecord> occupancyFilter = occupancy.getPredicate();
+ final double rangeSquared = range * range;
+
+ int added = 0;
+
+ // First up, we need to iterate the chunks
+ // all the values here are in chunk sections
+ final int lowerX = MathHelper.floor(sourcePosition.getX() - range) >> 4;
+ final int lowerY = Math.max(0, MathHelper.floor(sourcePosition.getY() - range) >> 4);
+ final int lowerZ = MathHelper.floor(sourcePosition.getZ() - range) >> 4;
+ final int upperX = MathHelper.floor(sourcePosition.getX() + range) >> 4;
+ final int upperY = Math.min(15, MathHelper.floor(sourcePosition.getY() + range) >> 4);
+ final int upperZ = MathHelper.floor(sourcePosition.getZ() + range) >> 4;
+
+ // Vanilla iterates by x until max is reached then increases z
+ // vanilla also searches by increasing Y section value
+ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) {
+ for (int currX = lowerX; currX <= upperX; ++currX) {
+ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need
+ final Optional<VillagePlaceSection> poiSectionOptional = load ? poiStorage.getOrLoad(SectionPosition.asLong(currX, currY, currZ)) :
+ poiStorage.getIfLoaded(SectionPosition.asLong(currX, currY, currZ));
+ final VillagePlaceSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null);
+ if (poiSection == null) {
+ continue;
+ }
+
+ final Map<VillagePlaceType, Set<VillagePlaceRecord>> sectionData = poiSection.getData();
+ if (sectionData.isEmpty()) {
+ continue;
+ }
+
+ // now we search the section data
+ for (final Map.Entry<VillagePlaceType, Set<VillagePlaceRecord>> entry : sectionData.entrySet()) {
+ if (!villagePlaceType.test(entry.getKey())) {
+ // filter out by poi type
+ continue;
+ }
+
+ // now we can look at the poi data
+ for (final VillagePlaceRecord poiData : entry.getValue()) {
+ if (!occupancyFilter.test(poiData)) {
+ // filter by occupancy
+ continue;
+ }
+
+ final BlockPosition poiPosition = poiData.getPosition();
+
+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
+ // out of range for square radius
+ continue;
+ }
+
+ if (poiPosition.distanceSquared(sourcePosition) > rangeSquared) {
+ // out of range for distance check
+ continue;
+ }
+
+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
+ // filter by position
+ continue;
+ }
+
+ // found one!
+ ret.add(poiData);
+ if (++added >= max) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private PoiAccess() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..08ed243259f052165c6f75aed1d1d65a14219715
--- /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();
+ }
+}
\ No newline at end of file
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..6dca98c5c43126c7b2dea2987b757e3de822c17c
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
@@ -0,0 +1,48 @@
+package com.tuinity.tuinity.util;
+
+import net.minecraft.world.level.World;
+
+public final class WorldUtil {
+
+ // min, max are inclusive
+ // TODO update these for 1.17
+
+ public static int getMaxSection(final World world) {
+ return 15;
+ }
+
+ public static int getMinSection(final World world) {
+ return 0;
+ }
+
+ public static int getMaxLightSection(final World world) {
+ return getMaxSection(world) + 1;
+ }
+
+ public static int getMinLightSection(final World world) {
+ return getMinSection(world) - 1;
+ }
+
+
+
+ public static int getTotalSections(final World world) {
+ return getMaxSection(world) - getMinSection(world) + 1;
+ }
+
+ public static int getTotalLightSections(final World world) {
+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
+ }
+
+ public static int getMinBlockY(final World world) {
+ return getMinSection(world) << 4;
+ }
+
+ public static int getMaxBlockY(final World 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..be408aebbccbda46e8aa82ef337574137cfa0096
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
@@ -0,0 +1,335 @@
+package com.tuinity.tuinity.util.maplist;
+
+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+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/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cc49e8e4ad841df2b38dc37ec761bf360f5a357
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
@@ -0,0 +1,300 @@
+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 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(MCUtil.getSectionKey(x, y, z));
+ }
+
+ public void setSource(final int x, final int y, final int z, final int level) {
+ this.setSource(MCUtil.getSectionKey(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(MCUtil.getSectionKey(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 = MCUtil.getSectionX(coordinate);
+ final int y = MCUtil.getSectionY(coordinate);
+ final int z = MCUtil.getSectionZ(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 = MCUtil.getSectionKey(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 = MCUtil.getSectionX(coordinate);
+ final int y = MCUtil.getSectionY(coordinate);
+ final int z = MCUtil.getSectionZ(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 = MCUtil.getSectionKey(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..6985facdb2d1689a71472d3ac282e4f5f3cb9869
--- /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.IBlockDataHolder;
+import net.minecraft.world.level.block.state.properties.IBlockState;
+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 IBlockDataHolder<?, ?> this_state;
+
+ protected long[] index_table;
+ protected IBlockDataHolder<?, ?>[][] value_table;
+
+ public ZeroCollidingReferenceStateTable(final IBlockDataHolder<?, ?> state, final Map<IBlockState<?>, Comparable<?>> this_map) {
+ this.this_state = state;
+ this.this_index_table = this.create_table(this_map.keySet());
+
+ int max_id = -1;
+ for (final IBlockState<?> 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<IBlockState<?>, Comparable<?>> entry : this_map.entrySet()) {
+ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue();
+ }
+ }
+
+ public void loadInTable(final Table<IBlockState<?>, Comparable<?>, IBlockDataHolder<?, ?>> table,
+ final Map<IBlockState<?>, Comparable<?>> this_map) {
+ final Set<IBlockState<?>> combined = new HashSet<>(table.rowKeySet());
+ combined.addAll(this_map.keySet());
+
+ this.index_table = this.create_table(combined);
+
+ int max_id = -1;
+ for (final IBlockState<?> property : combined) {
+ final int id = lookup_vindex(property, this.index_table);
+ if (id > max_id) {
+ max_id = id;
+ }
+ }
+
+ this.value_table = new IBlockDataHolder[max_id + 1][];
+
+ final Map<IBlockState<?>, Map<Comparable<?>, IBlockDataHolder<?, ?>>> map = table.rowMap();
+ for (final IBlockState<?> property : map.keySet()) {
+ final Map<Comparable<?>, IBlockDataHolder<?, ?>> propertyMap = map.get(property);
+
+ final int id = lookup_vindex(property, this.index_table);
+ final IBlockDataHolder<?, ?>[] states = this.value_table[id] = new IBlockDataHolder[property.getValues().size()];
+
+ for (final Map.Entry<Comparable<?>, IBlockDataHolder<?, ?>> entry : propertyMap.entrySet()) {
+ if (entry.getValue() == null) {
+ // TODO what
+ continue;
+ }
+
+ states[((IBlockState)property).getIdFor(entry.getKey())] = entry.getValue();
+ }
+ }
+
+
+ for (final Map.Entry<IBlockState<?>, Comparable<?>> entry : this_map.entrySet()) {
+ final IBlockState<?> property = entry.getKey();
+ final int index = lookup_vindex(property, this.index_table);
+
+ if (this.value_table[index] == null) {
+ this.value_table[index] = new IBlockDataHolder[property.getValues().size()];
+ }
+
+ this.value_table[index][((IBlockState)property).getIdFor(entry.getValue())] = this.this_state;
+ }
+ }
+
+
+ protected long[] create_table(final Collection<IBlockState<?>> collection) {
+ int max_id = -1;
+ for (final IBlockState<?> 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 IBlockState<?> 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 IBlockState<?> 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 IBlockDataHolder<?, ?> get(final IBlockState<?> property, final Comparable<?> with) {
+ final int withId = ((IBlockState)property).getIdFor(with);
+ if (withId < 0) {
+ return null;
+ }
+
+ final int index = lookup_vindex(property, this.index_table);
+ final IBlockDataHolder<?, ?>[][] table = this.value_table;
+ if (index < 0 || index >= table.length) {
+ return null;
+ }
+
+ final IBlockDataHolder<?, ?>[] values = table[index];
+
+ if (withId >= values.length) {
+ return null;
+ }
+
+ return values[withId];
+ }
+
+ protected static int lookup_vindex(final IBlockState<?> 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..ccc6935f45f91eac17cf09d044060b5bb4c2c935
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java
@@ -0,0 +1,165 @@
+package com.tuinity.tuinity.voxel;
+
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import it.unimi.dsi.fastutil.doubles.DoubleList;
+import net.minecraft.world.phys.AxisAlignedBB;
+import net.minecraft.core.EnumDirection;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.VoxelShapes;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class AABBVoxelShape extends VoxelShape {
+
+ public final AxisAlignedBB aabb;
+
+ public AABBVoxelShape(AxisAlignedBB aabb) {
+ super(VoxelShapes.getFullUnoptimisedCube().getShape());
+ this.aabb = aabb;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.aabb.isEmpty();
+ }
+
+ @Override
+ public double b(EnumDirection.EnumAxis enumdirection_enumaxis) { // getMin
+ 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 c(EnumDirection.EnumAxis enumdirection_enumaxis) { //getMax
+ 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 AxisAlignedBB getBoundingBox() { // rets bounding box enclosing this entire shape
+ return this.aabb;
+ }
+
+ // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis.
+ @Override
+ protected double a(EnumDirection.EnumAxis enumdirection_enumaxis, int i) { // getPointFromIndex
+ 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 a(EnumDirection.EnumAxis enumdirection_enumaxis) { // getPoints
+ 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 a(double d0, double d1, double d2) { // createOffset
+ return new AABBVoxelShape(this.aabb.offset(d0, d1, d2));
+ }
+
+ @Override
+ public VoxelShape c() { // simplify
+ return this;
+ }
+
+ @Override
+ public void b(VoxelShapes.a voxelshapes_a) { // forEachAABB
+ voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ);
+ }
+
+ @Override
+ public List<AxisAlignedBB> d() { // getAABBs
+ List<AxisAlignedBB> ret = new ArrayList<>(1);
+ ret.add(this.aabb);
+ return ret;
+ }
+
+ @Override
+ protected int a(EnumDirection.EnumAxis 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 boolean b(double d0, double d1, double d2) { // containsPoint
+ return this.aabb.contains(d0, d1, d2);
+ }
+
+ @Override
+ public VoxelShape a(EnumDirection enumdirection) { // unknown
+ return super.a(enumdirection);
+ }
+
+ @Override
+ public double a(EnumDirection.EnumAxis enumdirection_enumaxis, AxisAlignedBB axisalignedbb, double d0) { // collide
+ if (this.aabb.isEmpty() || axisalignedbb.isEmpty()) {
+ return d0;
+ }
+ switch (enumdirection_enumaxis.ordinal()) {
+ case 0:
+ return AxisAlignedBB.collideX(this.aabb, axisalignedbb, d0);
+ case 1:
+ return AxisAlignedBB.collideY(this.aabb, axisalignedbb, d0);
+ case 2:
+ return AxisAlignedBB.collideZ(this.aabb, axisalignedbb, d0);
+ default:
+ throw new IllegalStateException("Unknown axis requested");
+ }
+ }
+
+ @Override
+ public boolean intersects(AxisAlignedBB axisalingedbb) {
+ return this.aabb.voxelShapeIntersect(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..57359791192a90b025e5fea38f3ce37e524aed82
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
@@ -0,0 +1,399 @@
+package com.tuinity.tuinity.world;
+
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import net.minecraft.world.entity.boss.EntityComplexPart;
+import net.minecraft.world.entity.boss.enderdragon.EntityEnderDragon;
+import net.minecraft.world.phys.AxisAlignedBB;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.util.MathHelper;
+import net.minecraft.world.level.World;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+
+public final class ChunkEntitySlices {
+
+ private static final int RTREE_THRESHOLD = 20;
+
+ protected final int minSection;
+ protected final int maxSection;
+ protected final int chunkX;
+ protected final int chunkZ;
+ protected final World world;
+
+ protected final EntityCollectionBySection allEntities;
+ protected final EntityCollectionBySection hardCollidingEntities;
+ protected final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
+
+ public ChunkEntitySlices(final World world, final int chunkX, final int chunkZ,
+ 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<>();
+ }
+
+ // synchronized is used in this class for write protection, thank you dumbass mods for doing dumb
+ // shit async.
+
+ public synchronized void addEntity(final Entity entity, final int chunkSection) {
+ 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) {
+ 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 AxisAlignedBB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
+ }
+
+ public void getEntities(final Entity except, final AxisAlignedBB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
+ }
+
+ public <T extends Entity> void getEntities(final EntityTypes<?> type, final AxisAlignedBB 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 AxisAlignedBB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+ if (collection != null) {
+ collection.getEntities(except, box, (List)into, (Predicate)predicate);
+ } else {
+ synchronized (this) {
+ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz));
+ }
+ collection.getEntities(except, 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 AxisAlignedBB 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 = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = MathHelper.clamp(MathHelper.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 AxisAlignedBB 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 = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = MathHelper.clamp(MathHelper.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 EntityEnderDragon) {
+ for (final EntityComplexPart part : ((EntityEnderDragon)entity).children) {
+ if (part == except || !part.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final EntityTypes<?> type, final AxisAlignedBB 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 = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = MathHelper.clamp(MathHelper.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.getEntityType() != 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..3ec78275494f0f0b7ad3c53467ec6c6395cb257d
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
@@ -0,0 +1,115 @@
+package com.tuinity.tuinity.world;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.server.level.WorldServer;
+import java.util.concurrent.locks.StampedLock;
+
+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 WorldServer world;
+
+ private final StampedLock stateLock = new StampedLock();
+ protected final Long2ObjectOpenHashMap<ChunkSlicesRegion> regions = new Long2ObjectOpenHashMap<>(32, 0.7f);
+
+ public EntitySliceManager(final WorldServer world) {
+ this.world = world;
+ }
+
+ 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/BaseBlockPosition.java b/src/main/java/net/minecraft/core/BaseBlockPosition.java
index a44bcdb053877a6281e566ffe03ef72ffd50ca08..e72c852517f9fdc7b7a3a5bc5d3aa067815ff944 100644
--- a/src/main/java/net/minecraft/core/BaseBlockPosition.java
+++ b/src/main/java/net/minecraft/core/BaseBlockPosition.java
@@ -18,9 +18,9 @@ public class BaseBlockPosition implements Comparable<BaseBlockPosition> {
return IntStream.of(new int[]{baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ()});
});
public static final BaseBlockPosition ZERO = new BaseBlockPosition(0, 0, 0);
- private int a;public final void setX(final int x) { this.a = x; } // Paper - OBFHELPER
- private int b;public final void setY(final int y) { this.b = y; } // Paper - OBFHELPER
- private int e;public final void setZ(final int z) { this.e = z; } // Paper - OBFHELPER
+ protected int a; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the x coordinate - Also revert the decision to expose set on an immutable type
+ protected int b; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the y coordinate - Also revert the decision to expose set on an immutable type
+ protected int e; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the z coordinate - Also revert the decision to expose set on an immutable type
// Paper start
public boolean isValidLocation() {
@@ -73,15 +73,15 @@ public class BaseBlockPosition implements Comparable<BaseBlockPosition> {
return this.e;
}
- public void o(int i) { // Paper - protected -> public
+ protected void o_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type
this.a = i;
}
- public void p(int i) { // Paper - protected -> public
+ protected void p_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type
this.b = i;
}
- public void q(int i) { // Paper - protected -> public
+ protected void q_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type
this.e = i;
}
diff --git a/src/main/java/net/minecraft/core/BlockPosition.java b/src/main/java/net/minecraft/core/BlockPosition.java
index 4abc87b7e737bc652e84f76a508ab85501d1556f..6a6381e85fef2ae2b9b5e6dff0b7917b92fa01e5 100644
--- a/src/main/java/net/minecraft/core/BlockPosition.java
+++ b/src/main/java/net/minecraft/core/BlockPosition.java
@@ -455,10 +455,10 @@ public class BlockPosition extends BaseBlockPosition {
}
public final BlockPosition.MutableBlockPosition setValues(int i, int j, int k) { return d(i, j, k);} // Paper - OBFHELPER
- public BlockPosition.MutableBlockPosition d(int i, int j, int k) {
- this.o(i);
- this.p(j);
- this.q(k);
+ public final BlockPosition.MutableBlockPosition d(int i, int j, int k) { // Tuinity
+ ((BaseBlockPosition)this).a = i; // Tuinity - force inline
+ ((BaseBlockPosition)this).b = j; // Tuinity - force inline
+ ((BaseBlockPosition)this).e = k; // Tuinity - force inline
return this;
}
@@ -468,12 +468,18 @@ public class BlockPosition extends BaseBlockPosition {
}
public final BlockPosition.MutableBlockPosition setValues(final BaseBlockPosition baseblockposition) { return this.g(baseblockposition); } // Paper - OBFHELPER
- public BlockPosition.MutableBlockPosition g(BaseBlockPosition baseblockposition) {
- return this.d(baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ());
+ public final BlockPosition.MutableBlockPosition g(BaseBlockPosition baseblockposition) { // Tuinity
+ ((BaseBlockPosition)this).a = baseblockposition.a; // Tuinity - force inline
+ ((BaseBlockPosition)this).b = baseblockposition.b; // Tuinity - force inline
+ ((BaseBlockPosition)this).e = baseblockposition.e; // Tuinity - force inline
+ return this;
}
- public BlockPosition.MutableBlockPosition g(long i) {
- return this.d(b(i), c(i), d(i));
+ public final BlockPosition.MutableBlockPosition g(long i) { // Tuinity
+ ((BaseBlockPosition)this).a = (int)(i >> 38); // Tuinity - force inline
+ ((BaseBlockPosition)this).b = (int)((i << 52) >> 52); // Tuinity - force inline
+ ((BaseBlockPosition)this).e = (int)((i << 26) >> 38); // Tuinity - force inline
+ return this;
}
public BlockPosition.MutableBlockPosition a(EnumAxisCycle enumaxiscycle, int i, int j, int k) {
@@ -488,8 +494,11 @@ public class BlockPosition extends BaseBlockPosition {
return this.d(baseblockposition.getX() + i, baseblockposition.getY() + j, baseblockposition.getZ() + k);
}
- public BlockPosition.MutableBlockPosition c(EnumDirection enumdirection) {
- return this.c(enumdirection, 1);
+ public final BlockPosition.MutableBlockPosition c(EnumDirection enumdirection) { // Tuinity
+ ((BaseBlockPosition)this).a += enumdirection.getAdjacentX(); // Tuinity - force inline
+ ((BaseBlockPosition)this).b += enumdirection.getAdjacentY(); // Tuinity - force inline
+ ((BaseBlockPosition)this).e += enumdirection.getAdjacentZ(); // Tuinity - force inline
+ return this;
}
public BlockPosition.MutableBlockPosition c(EnumDirection enumdirection, int i) {
@@ -518,21 +527,30 @@ public class BlockPosition extends BaseBlockPosition {
}
}
- /* // Paper start - comment out useless overrides @Override
- @Override
- public void o(int i) {
- super.o(i);
+ // Tuinity start
+ // only expose set on the mutable blockpos
+ public final void setX(int value) {
+ ((BaseBlockPosition)this).a = value;
+ }
+ public final void setY(int value) {
+ ((BaseBlockPosition)this).b = value;
+ }
+ public final void setZ(int value) {
+ ((BaseBlockPosition)this).e = value;
}
- @Override
- public void p(int i) {
- super.p(i);
+ public final void o(int i) {
+ ((BaseBlockPosition)this).a = i; // need cast thanks to name conflict
+ }
+
+ public final void p(int i) {
+ ((BaseBlockPosition)this).b = i;
}
- public void q(int i) {
- super.q(i);
+ public final void q(int i) {
+ ((BaseBlockPosition)this).e = i;
}
- */ // Paper end
+ // Tuinity end
@Override
public BlockPosition immutableCopy() {
diff --git a/src/main/java/net/minecraft/core/EnumDirection.java b/src/main/java/net/minecraft/core/EnumDirection.java
index 703bdefeb615ef8d15b428a893b5e4939d726f13..7918d830a4aef09c9f517284e83a9376299116ad 100644
--- a/src/main/java/net/minecraft/core/EnumDirection.java
+++ b/src/main/java/net/minecraft/core/EnumDirection.java
@@ -174,8 +174,8 @@ public enum EnumDirection implements INamable {
return EnumDirection.q[MathHelper.a(i % EnumDirection.q.length)];
}
- @Nullable
- public static EnumDirection a(int i, int j, int k) {
+ @Nullable public static EnumDirection from(int i, int j, int k) { return a(i, j, k); } // Tuinity - OBFHELPER
+ @Nullable public static EnumDirection a(int i, int j, int k) {
return (EnumDirection) EnumDirection.r.get(BlockPosition.a(i, j, k));
}
diff --git a/src/main/java/net/minecraft/core/SectionPosition.java b/src/main/java/net/minecraft/core/SectionPosition.java
index 7d9a16eb81288b74425319c60525f57c98ad3b69..427413c668865e1660f1d81daf6a3385f08a0e38 100644
--- a/src/main/java/net/minecraft/core/SectionPosition.java
+++ b/src/main/java/net/minecraft/core/SectionPosition.java
@@ -10,7 +10,7 @@ import net.minecraft.world.level.ChunkCoordIntPair;
public class SectionPosition extends BaseBlockPosition {
- private SectionPosition(int i, int j, int k) {
+ public SectionPosition(int i, int j, int k) { // Tuinity - private -> public
super(i, j, k);
}
diff --git a/src/main/java/net/minecraft/network/NetworkManager.java b/src/main/java/net/minecraft/network/NetworkManager.java
index bf0c01eaf593972bbb18c22cfdb3abd658ec6498..b6c0ef0df93f0350fa70e857e06d79ae34d7d4b1 100644
--- a/src/main/java/net/minecraft/network/NetworkManager.java
+++ b/src/main/java/net/minecraft/network/NetworkManager.java
@@ -45,6 +45,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 NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
private static final Logger LOGGER = LogManager.getLogger();
@@ -89,6 +91,61 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
public EnumProtocol protocol;
// Paper end
+ // 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 - 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.packetQueue.isEmpty();
+ if (!queue) {
+ this.channel.eventLoop().execute(run);
+ } else {
+ this.pendingTasks.add(run);
+ if (this.packetQueue.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
+
public NetworkManager(EnumProtocolDirection enumprotocoldirection) {
this.h = enumprotocoldirection;
}
@@ -163,8 +220,64 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot
}
+ // 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 PacketPlayOutKickDisconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]), (future) -> {
+ this.close(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]);
+ });
+ this.stopReading();
+ this.stopReadingPackets = true;
+ }
+ // Tuinity end - packet limiter
+
protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) throws Exception {
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 {
a(packet, this.packetListener);
} catch (CancelledPacketHandleException cancelledpackethandleexception) {
@@ -240,7 +353,7 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
MCUtil.isMainThread() && packet.isReady() && this.packetQueue.isEmpty() &&
(packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
))) {
- this.dispatchPacket(packet, genericfuturelistener);
+ this.writePacket(packet, genericfuturelistener, null); // Tuinity
return;
}
// write the packets to the queue, then flush - antixray hooks there already
@@ -266,6 +379,14 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
private void dispatchPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericFutureListener) { this.b(packet, genericFutureListener); } // Paper - OBFHELPER
private void b(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericfuturelistener) {
+ // Tuinity start - add flush parameter
+ this.writePacket(packet, genericfuturelistener, Boolean.TRUE);
+ }
+ private void writePacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericfuturelistener, Boolean flushConditional) {
+ this.packetWrites.getAndIncrement(); // must be befeore using canFlush
+ boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue();
+ final boolean flush = effectiveFlush || packet instanceof PacketPlayOutKeepAlive || packet instanceof PacketPlayOutKickDisconnect; // no delay for certain packets
+ // Tuinity end - add flush parameter
EnumProtocol enumprotocol = EnumProtocol.a(packet);
EnumProtocol enumprotocol1 = (EnumProtocol) this.channel.attr(NetworkManager.c).get();
@@ -288,7 +409,7 @@ public class NetworkManager 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);
@@ -308,39 +429,83 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
}
// Paper end
} else {
- this.channel.eventLoop().execute(() -> {
- if (enumprotocol != enumprotocol1) {
- this.setProtocol(enumprotocol);
- }
+ // Tuinity start - optimise packets that are not flushed
+ Runnable choice1 = null;
+ AbstractEventExecutor.LazyRunnable choice2 = null;
+ // 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) {
+ choice1 = () -> {
+ if (enumprotocol != enumprotocol1) {
+ this.setProtocol(enumprotocol);
+ }
- // Paper start
- if (!isConnected()) {
- packet.onPacketDispatchFinish(player, null);
- return;
- }
- try {
+ // Paper start
+ if (!isConnected()) {
+ packet.onPacketDispatchFinish(player, null);
+ return;
+ }
+ try {
+ // Paper end
+ ChannelFuture channelfuture1 = (flush) ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter
+
+
+ if (genericfuturelistener != null) {
+ channelfuture1.addListener(genericfuturelistener);
+ }
+ // Paper start
+ if (packet.hasFinishListener()) {
+ channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
+ }
+ // Paper end
+
+ channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+ // Paper start
+ } catch (Exception e) {
+ LOGGER.error("NetworkException: " + player, e);
+ close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));;
+ packet.onPacketDispatchFinish(player, null);
+ }
// Paper end
- ChannelFuture channelfuture1 = this.channel.writeAndFlush(packet);
-
-
- if (genericfuturelistener != null) {
- channelfuture1.addListener(genericfuturelistener);
- }
- // Paper start
- if (packet.hasFinishListener()) {
- channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
- }
- // Paper end
+ };
+ } else {
+ // explicitly declare a variable to make the lambda use the type
+ choice2 = () -> {
+ if (enumprotocol != enumprotocol1) {
+ this.setProtocol(enumprotocol);
+ }
- channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
- // Paper start
- } catch (Exception e) {
- LOGGER.error("NetworkException: " + player, e);
- close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));;
- packet.onPacketDispatchFinish(player, null);
- }
- // Paper end
- });
+ // Paper start
+ if (!isConnected()) {
+ packet.onPacketDispatchFinish(player, null);
+ return;
+ }
+ try {
+ // Paper end
+ ChannelFuture channelfuture1 = (flush) ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter
+
+
+ if (genericfuturelistener != null) {
+ channelfuture1.addListener(genericfuturelistener);
+ }
+ // Paper start
+ if (packet.hasFinishListener()) {
+ channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
+ }
+ // Paper end
+
+ channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+ // Paper start
+ } catch (Exception e) {
+ LOGGER.error("NetworkException: " + player, e);
+ close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));;
+ packet.onPacketDispatchFinish(player, null);
+ }
+ // Paper end
+ };
+ }
+ this.channel.eventLoop().execute(choice1 != null ? choice1 : choice2);
+ // Tuinity end - optimise packets that are not flushed
}
}
@@ -362,7 +527,10 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
return false;
}
private boolean processQueue() {
+ try { // Tuinity - add pending task queue
if (this.packetQueue.isEmpty()) return true;
+ final boolean needsFlush = this.canFlush; // Tuinity - make only one flush call per sendPacketQueue() call
+ boolean hasWrotePacket = false;
// 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<QueuedPacket> iterator = this.packetQueue.iterator();
@@ -370,19 +538,31 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
NetworkManager.QueuedPacket 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.getPacket();
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.dispatchPacket(packet, queued.getGenericFutureListener());
+ this.writePacket(packet, queued.getGenericFutureListener(), (!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
@@ -405,7 +585,14 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
}
if (this.packetListener instanceof PlayerConnection) {
+ // Tuinity start - detailed watchdog information
+ net.minecraft.network.protocol.PlayerConnectionUtils.packetProcessing.push(this.packetListener);
+ try {
+ // Tuinity end - detailed watchdog information
((PlayerConnection) this.packetListener).tick();
+ } finally { // Tuinity start - detailed watchdog information
+ net.minecraft.network.protocol.PlayerConnectionUtils.packetProcessing.pop();
+ } // Tuinity start - detailed watchdog information
}
if (this.channel != null) {
@@ -456,10 +643,16 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
return this.channel instanceof LocalChannel || this.channel instanceof LocalServerChannel;
}
- public void a(Cipher cipher, Cipher cipher1) {
+ public void a(javax.crypto.SecretKey secretkey) { // Tuinity
this.n = true;
- this.channel.pipeline().addBefore("splitter", "decrypt", new PacketDecrypter(cipher));
- this.channel.pipeline().addBefore("prepender", "encrypt", new PacketEncrypter(cipher1));
+ // Tuinity start
+ try {
+ this.channel.pipeline().addBefore("splitter", "decrypt", new PacketDecrypter(/*MinecraftEncryption.a(2, secretkey)*/ secretkey));
+ this.channel.pipeline().addBefore("prepender", "encrypt", new PacketEncrypter(/*MinecraftEncryption.a(1, secretkey)*/ secretkey));
+ } catch (java.security.GeneralSecurityException e) {
+ throw new RuntimeException("Couldn't enable encryption", e);
+ }
+ // Tuinity end
}
public boolean isConnected() {
diff --git a/src/main/java/net/minecraft/network/PacketCompressor.java b/src/main/java/net/minecraft/network/PacketCompressor.java
index 45b9d3d3c84d11e7f27f699506a1036dff9fdc53..43ea54479ba70a5c3f4a0f8e4e5a0d03e93162c1 100644
--- a/src/main/java/net/minecraft/network/PacketCompressor.java
+++ b/src/main/java/net/minecraft/network/PacketCompressor.java
@@ -7,14 +7,18 @@ import java.util.zip.Deflater;
public class PacketCompressor extends MessageToByteEncoder<ByteBuf> {
- private final byte[] a = new byte[8192];
- private final Deflater b;
+ // Tuinity start - use Velocity natives
+// private final byte[] a = new byte[8192];
+// private final Deflater b;
private int c;
+ private final com.velocitypowered.natives.compression.VelocityCompressor compressor;
public PacketCompressor(int i) {
this.c = i;
- this.b = new Deflater();
+// this.b = new Deflater();
+ this.compressor = com.velocitypowered.natives.util.Natives.compress.get().create(-1);
}
+ // Tuinity end
protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception {
int i = bytebuf.readableBytes();
@@ -24,24 +28,46 @@ public class PacketCompressor extends MessageToByteEncoder<ByteBuf> {
packetdataserializer.d(0);
packetdataserializer.writeBytes(bytebuf);
} else {
- byte[] abyte = new byte[i];
-
- bytebuf.readBytes(abyte);
- packetdataserializer.d(abyte.length);
- this.b.setInput(abyte, 0, i);
- this.b.finish();
-
- while (!this.b.finished()) {
- int j = this.b.deflate(this.a);
-
- packetdataserializer.writeBytes(this.a, 0, j);
+ // Tuinity start - delegate to Velocity natives
+// byte[] abyte = new byte[i];
+//
+// bytebuf.readBytes(abyte);
+// packetdataserializer.d(abyte.length);
+// this.b.setInput(abyte, 0, i);
+// this.b.finish();
+//
+// while (!this.b.finished()) {
+// int j = this.b.deflate(this.a);
+//
+// packetdataserializer.writeBytes(this.a, 0, j);
+// }
+//
+// this.b.reset();
+ packetdataserializer.d(i);
+ ByteBuf source = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(),
+ this.compressor, bytebuf);
+ try {
+ this.compressor.deflate(source, bytebuf1);
+ } finally {
+ source.release();
}
-
- this.b.reset();
+ // Tuinity end
}
}
+ // Tuinity start
+ @Override
+ protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
+ return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, msg.readableBytes() + 1);
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+ this.compressor.close();
+ }
+ // Tuinity end
+
public void a(int i) {
this.c = i;
}
diff --git a/src/main/java/net/minecraft/network/PacketDecompressor.java b/src/main/java/net/minecraft/network/PacketDecompressor.java
index 1932ca55dad37ca773f215eaec23164533d509d3..ce7d7b4e8c1e5de935c83d7db5186ad8a0849e60 100644
--- a/src/main/java/net/minecraft/network/PacketDecompressor.java
+++ b/src/main/java/net/minecraft/network/PacketDecompressor.java
@@ -10,13 +10,17 @@ import java.util.zip.Inflater;
public class PacketDecompressor extends ByteToMessageDecoder {
- private final Inflater a;
+ // Tuinity start - use Velocity natives
+ //private final Inflater a;
+ private final com.velocitypowered.natives.compression.VelocityCompressor compressor;
private int b;
public PacketDecompressor(int i) {
this.b = i;
- this.a = new Inflater();
+ //this.a = new Inflater();
+ this.compressor = com.velocitypowered.natives.util.Natives.compress.get().create(-1);
}
+ // Tuinity end
protected void decode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, List<Object> list) throws Exception {
if (bytebuf.readableBytes() != 0) {
@@ -34,20 +38,41 @@ public class PacketDecompressor extends ByteToMessageDecoder {
throw new DecoderException("Badly compressed packet - size of " + i + " is larger than protocol maximum of " + 2097152);
}
- byte[] abyte = new byte[packetdataserializer.readableBytes()];
-
- packetdataserializer.readBytes(abyte);
- this.a.setInput(abyte);
- byte[] abyte1 = new byte[i];
-
- this.a.inflate(abyte1);
- list.add(Unpooled.wrappedBuffer(abyte1));
- this.a.reset();
+ // Tuinity start
+ ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(), compressor, bytebuf);
+ ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(channelhandlercontext.alloc(), compressor, i);
+ try {
+ compressor.inflate(compatibleIn, uncompressed, i);
+ list.add(uncompressed);
+ bytebuf.clear();
+ } catch (Exception e) {
+ uncompressed.release();
+ throw e;
+ } finally {
+ compatibleIn.release();
+ }
+// byte[] abyte = new byte[packetdataserializer.readableBytes()];
+//
+// packetdataserializer.readBytes(abyte);
+// this.a.setInput(abyte);
+// byte[] abyte1 = new byte[i];
+//
+// this.a.inflate(abyte1);
+// list.add(Unpooled.wrappedBuffer(abyte1));
+// this.a.reset();
+ // Tuinity end
}
}
}
+ // Tuinity start
+ @Override
+ public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
+ this.compressor.close();
+ }
+ // Tuinity end
+
public void a(int i) {
this.b = i;
}
diff --git a/src/main/java/net/minecraft/network/PacketDecrypter.java b/src/main/java/net/minecraft/network/PacketDecrypter.java
index c8aa02b288d67efe1f9e20e038248a4f032f92dd..4bd7c7db965818054929c9222a0968f6d127cc5c 100644
--- a/src/main/java/net/minecraft/network/PacketDecrypter.java
+++ b/src/main/java/net/minecraft/network/PacketDecrypter.java
@@ -8,13 +8,24 @@ import javax.crypto.Cipher;
public class PacketDecrypter extends MessageToMessageDecoder<ByteBuf> {
- private final PacketEncryptionHandler a;
+ // Tuinity start
+ private final com.velocitypowered.natives.encryption.VelocityCipher cipher;
+ //private final PacketEncryptionHandler a;
- public PacketDecrypter(Cipher cipher) {
- this.a = new PacketEncryptionHandler(cipher);
+ public PacketDecrypter(javax.crypto.SecretKey key /* Cipher cipher */) throws java.security.GeneralSecurityException {
+ //this.a = new PacketEncryptionHandler(cipher);
+ this.cipher = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key);
}
protected void decode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, List<Object> list) throws Exception {
- list.add(this.a.a(channelhandlercontext, bytebuf));
+ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(), cipher, bytebuf).slice();
+ try {
+ cipher.process(compatible);
+ list.add(compatible);
+ } catch (Exception e) {
+ compatible.release(); // compatible will never be used if we throw an exception
+ throw e;
+ }
}
+ // Tuinity end
}
diff --git a/src/main/java/net/minecraft/network/PacketEncrypter.java b/src/main/java/net/minecraft/network/PacketEncrypter.java
index 5759f91d5e9dc52b16c8955b8d318da2b53c7af4..0960f415fd3c6c764cf3dd273bb9e9c59d80ab31 100644
--- a/src/main/java/net/minecraft/network/PacketEncrypter.java
+++ b/src/main/java/net/minecraft/network/PacketEncrypter.java
@@ -5,15 +5,38 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import javax.crypto.Cipher;
-public class PacketEncrypter extends MessageToByteEncoder<ByteBuf> {
+// Tuinity start
+// We rewrite this class as the Velocity natives support in-place encryption
+import io.netty.handler.codec.MessageToMessageEncoder; // An unfortunate import, but this is required to fix a compiler error
+public class PacketEncrypter extends MessageToMessageEncoder<ByteBuf> {
- private final PacketEncryptionHandler a;
+ private final com.velocitypowered.natives.encryption.VelocityCipher cipher;
+ //private final PacketEncryptionHandler a;
- public PacketEncrypter(Cipher cipher) {
- this.a = new PacketEncryptionHandler(cipher);
+ public PacketEncrypter(javax.crypto.SecretKey key /* Cipher cipher */) throws java.security.GeneralSecurityException {
+ // this.a = new PacketEncryptionHandler(cipher);
+ this.cipher = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key);
}
- protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception {
- this.a.a(bytebuf, bytebuf1);
+// protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception {
+// this.a.a(bytebuf, bytebuf1);
+// }
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, java.util.List<Object> out) throws Exception {
+ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(ctx.alloc(), this.cipher, msg);
+ try {
+ this.cipher.process(compatible);
+ out.add(compatible);
+ } catch (Exception e) {
+ compatible.release(); // compatible will never be used if we throw an exception
+ throw e;
+ }
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+ cipher.close();
}
}
+// Tuinity end
diff --git a/src/main/java/net/minecraft/network/protocol/PlayerConnectionUtils.java b/src/main/java/net/minecraft/network/protocol/PlayerConnectionUtils.java
index 67d8fe8ad036a9252c774bb6a914c8ec79981876..71a000edfab27c9965d1929af78582821d5af97a 100644
--- a/src/main/java/net/minecraft/network/protocol/PlayerConnectionUtils.java
+++ b/src/main/java/net/minecraft/network/protocol/PlayerConnectionUtils.java
@@ -25,10 +25,30 @@ public class PlayerConnectionUtils {
ensureMainThread(packet, t0, (IAsyncTaskHandler) worldserver.getMinecraftServer());
}
+ // 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 static <T extends PacketListener> void ensureMainThread(Packet<T> packet, T t0, IAsyncTaskHandler<?> iasynctaskhandler) throws CancelledPacketHandleException {
if (!iasynctaskhandler.isMainThread()) {
Timing timing = MinecraftTimings.getPacketTiming(packet); // Paper - timings
iasynctaskhandler.execute(() -> {
+ packetProcessing.push(t0); // Tuinity - detailed watchdog information
+ try { // Tuinity - detailed watchdog information
if (MinecraftServer.getServer().hasStopped() || (t0 instanceof PlayerConnection && ((PlayerConnection) t0).processedDisconnect)) return; // CraftBukkit, MC-142590
if (t0.a().isConnected()) {
try (Timing ignored = timing.startTiming()) { // Paper - timings
@@ -52,6 +72,12 @@ public class PlayerConnectionUtils {
} else {
PlayerConnectionUtils.LOGGER.debug("Ignoring packet due to disconnection: " + packet);
}
+ // Tuinity start - detailed watchdog information
+ } finally {
+ totalMainThreadPacketsProcessed.getAndIncrement();
+ packetProcessing.pop();
+ }
+ // Tuinity end - detailed watchdog information
});
throw CancelledPacketHandleException.INSTANCE;
diff --git a/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutLightUpdate.java b/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutLightUpdate.java
index 9050ff7180f63c1f5756570446c4d0a8cc767779..9dd0f34aaede93ce4fdff9b41f76166edeb95e93 100644
--- a/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutLightUpdate.java
+++ b/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutLightUpdate.java
@@ -35,12 +35,12 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
@Override
public void onPacketDispatch(EntityPlayer player) {
- remainingSends.incrementAndGet();
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) remainingSends.incrementAndGet();
}
@Override
public void onPacketDispatchFinish(EntityPlayer player, ChannelFuture future) {
- if (remainingSends.decrementAndGet() <= 0) {
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && remainingSends.decrementAndGet() <= 0) {
// incase of any race conditions, schedule this delayed
MCUtil.scheduleTask(5, () -> {
if (remainingSends.get() == 0) {
@@ -53,7 +53,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
@Override
public boolean hasFinishListener() {
- return true;
+ return !com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine; // Tuinity - replace light impl
}
// Paper end
@@ -63,8 +63,8 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
this.a = chunkcoordintpair.x;
this.b = chunkcoordintpair.z;
this.i = flag;
- this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper
- this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper
+ this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
+ this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
for (int i = 0; i < 18; ++i) {
NibbleArray nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + i));
@@ -75,7 +75,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
this.e |= 1 << i;
} else {
this.c |= 1 << i;
- this.g.add(nibblearray.getCloneIfSet()); // Paper
+ this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
}
}
@@ -84,7 +84,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
this.f |= 1 << i;
} else {
this.d |= 1 << i;
- this.h.add(nibblearray1.getCloneIfSet()); // Paper
+ this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.getCloneIfSet()); // Paper // Tuinity - don't clone again
}
}
}
@@ -97,8 +97,8 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
this.i = flag;
this.c = i;
this.d = j;
- this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper
- this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper
+ this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
+ this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
for (int k = 0; k < 18; ++k) {
NibbleArray nibblearray;
@@ -106,7 +106,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
if ((this.c & 1 << k) != 0) {
nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + k));
if (nibblearray != null && !nibblearray.c()) {
- this.g.add(nibblearray.getCloneIfSet()); // Paper
+ this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
} else {
this.c &= ~(1 << k);
if (nibblearray != null) {
@@ -118,7 +118,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
if ((this.d & 1 << k) != 0) {
nibblearray = lightengine.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, -1 + k));
if (nibblearray != null && !nibblearray.c()) {
- this.h.add(nibblearray.getCloneIfSet()); // Paper
+ this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
} else {
this.d &= ~(1 << k);
if (nibblearray != null) {
diff --git a/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutMapChunk.java b/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutMapChunk.java
index 3047cf8c4ec1b664d6b790f18d2b1657e4b00435..1295c391ae48f0864c243ca9046f31480eb768fe 100644
--- a/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutMapChunk.java
+++ b/src/main/java/net/minecraft/network/protocol/game/PacketPlayOutMapChunk.java
@@ -32,7 +32,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
@Nullable
private int[] e;
private byte[] f; private byte[] getData() { return this.f; } // Paper - OBFHELPER
- private List<NBTTagCompound> g;
+ private List<NBTTagCompound> g; private List<NBTTagCompound> getTileEntityData() { return this.g; } // Tuinity - OBFHELPER
private boolean h;
// Paper start - Async-Anti-Xray - Set the ready flag to true
@@ -44,7 +44,9 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
// Paper start
private final java.util.List<Packet> extraPackets = new java.util.ArrayList<>();
- private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750);
+ private static final int TE_LIMIT = Integer.getInteger("tuinity.excessive-te-limit", 750); // Tuinity - handle oversized chunk data packets more robustly
+ private static final int TE_SPLIT_LIMIT = Math.max(4096 + 1, Integer.getInteger("tuinity.te-split-limit", 15_000)); // Tuinity - handle oversized chunk data packets more robustly
+ private boolean mustSplit; // Tuinity - handle oversized chunk data packets more robustly
@Override
public java.util.List<Packet> getExtraPackets() {
@@ -53,7 +55,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
// Paper end
// Paper start - Anti-Xray - Add chunk packet info
@Deprecated public PacketPlayOutMapChunk(Chunk chunk, int i) { this(chunk, i, true); } // Notice for updates: Please make sure this constructor isn't used anywhere
- public PacketPlayOutMapChunk(Chunk chunk, int i, boolean modifyBlocks) {
+ public PacketPlayOutMapChunk(Chunk chunk, int i, boolean modifyBlocks) { final int chunkSectionBitSet = i; // Tuinity - handle oversized chunk data packets more robustly
ChunkPacketInfo<IBlockData> chunkPacketInfo = modifyBlocks ? chunk.world.chunkPacketBlockController.getChunkPacketInfo(this, chunk, i) : null;
// Paper end
ChunkCoordIntPair chunkcoordintpair = chunk.getPos();
@@ -62,27 +64,12 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
this.b = chunkcoordintpair.z;
this.h = i == 65535;
this.d = new NBTTagCompound();
- Iterator iterator = chunk.f().iterator();
-
- Entry entry;
-
- while (iterator.hasNext()) {
- entry = (Entry) iterator.next();
- if (((HeightMap.Type) entry.getKey()).c()) {
- this.d.set(((HeightMap.Type) entry.getKey()).b(), new NBTTagLongArray(((HeightMap) entry.getValue()).a()));
- }
- }
-
- if (this.h) {
- this.e = chunk.getBiomeIndex().a();
- }
-
- this.f = new byte[this.a(chunk, i)];
- // Paper start - Anti-Xray - Add chunk packet info
- if (chunkPacketInfo != null) {
- chunkPacketInfo.setData(this.getData());
- }
- this.c = this.writeChunk(new PacketDataSerializer(this.j()), chunk, i, chunkPacketInfo);
+ // Tuinity - move this after the tile entity logic, we need to determine whether we're going to split
+ // Tuinity - before writing chunk block data
+ // Tuinity - note: for future maintenance, git will prefer the smallest diff, so if moving the TE code is
+ // Tuinity - a smaller diff, do that, else move the chunk writing - this makes sure the start/end is correct
+ Iterator iterator; // Tuinity - move declaration up
+ Entry entry; // Tuinity - move delcaration up
// Paper end
this.g = Lists.newArrayList();
iterator = chunk.getTileEntities().entrySet().iterator();
@@ -95,8 +82,16 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
int j = blockposition.getY() >> 4;
if (this.f() || (i & 1 << j) != 0) {
+ // Tuinity start - improve oversized chunk data packet handling
+ ++totalTileEntities;
+ if (totalTileEntities > TE_SPLIT_LIMIT) {
+ this.mustSplit = true;
+ this.getTileEntityData().clear();
+ this.extraPackets.clear();
+ break;
+ }
// Paper start - improve oversized chunk data packet handling
- if (++totalTileEntities > TE_LIMIT) {
+ if (totalTileEntities > TE_LIMIT) { // Tuinity end - improve oversized chunk data packet handling
PacketPlayOutTileEntityData updatePacket = tileentity.getUpdatePacket();
if (updatePacket != null) {
this.extraPackets.add(updatePacket);
@@ -110,7 +105,42 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
this.g.add(nbttagcompound);
}
}
+ // Tuinity start - moved after tile entity gathering
+ iterator = chunk.f().iterator(); // Declared earlier
+
+ while (iterator.hasNext()) {
+ entry = (Entry) iterator.next();
+ if (((HeightMap.Type) entry.getKey()).c()) {
+ this.d.set(((HeightMap.Type) entry.getKey()).b(), new NBTTagLongArray(((HeightMap) entry.getValue()).a()));
+ }
+ }
+
+ if (this.h) {
+ this.e = chunk.getBiomeIndex().a();
+ }
+
+ this.f = new byte[this.a(chunk, i)];
+ // Paper start - Anti-Xray - Add chunk packet info
+ if (chunkPacketInfo != null) {
+ chunkPacketInfo.setData(this.getData());
+ }
+ this.c = this.writeChunk(new PacketDataSerializer(this.j()), chunk, i, chunkPacketInfo);
+ // Tuinity end - moved after tile entity gathering
chunk.world.chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
+ // Tuinity start - improve oversized chunk data packet handling
+ if (this.mustSplit) {
+ int chunkSectionBitSetCopy = chunkSectionBitSet;
+ for (int a = 0, len = Integer.bitCount(chunkSectionBitSet); a < len; ++a) {
+ int trailingBit = com.destroystokyo.paper.util.math.IntegerUtil.getTrailingBit(chunkSectionBitSetCopy);
+ int sectionIndex = Integer.numberOfTrailingZeros(trailingBit);
+ chunkSectionBitSetCopy ^= trailingBit; // move on to the next
+
+ if (chunk.getSections()[sectionIndex] != null) {
+ this.extraPackets.add(new PacketPlayOutMapChunk(chunk, trailingBit));
+ }
+ }
+ }
+ // Tuinity end - improve oversized chunk data packet handling
}
// Paper start - Async-Anti-Xray - Getter and Setter for the ready flag
@@ -201,7 +231,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
for (int l = achunksection.length; k < l; ++k) {
ChunkSection chunksection = achunksection[k];
- if (chunksection != Chunk.a && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) {
+ if ((!this.mustSplit && chunksection != Chunk.a) && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) { // Tuinity - improve oversized chunk data packet handling
j |= 1 << k;
chunksection.writeChunkSection(packetdataserializer, chunkPacketInfo); // Paper - Anti-Xray - Add chunk packet info
}
@@ -218,7 +248,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
for (int l = achunksection.length; k < l; ++k) {
ChunkSection chunksection = achunksection[k];
- if (chunksection != Chunk.a && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) {
+ if ((!this.mustSplit && chunksection != Chunk.a) && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) {
j += chunksection.j();
}
}
diff --git a/src/main/java/net/minecraft/server/EULA.java b/src/main/java/net/minecraft/server/EULA.java
index a0f53c9eff04a40780b3ba568dbfc5bbe9bd8504..3bc5cd1e53dd7c94b948e7f57f0dc8e073e349b0 100644
--- a/src/main/java/net/minecraft/server/EULA.java
+++ b/src/main/java/net/minecraft/server/EULA.java
@@ -72,7 +72,7 @@ public class EULA {
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 throwable1) {
throwable = throwable1;
throw throwable1;
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index 1d72af9cace7aa8f1d20c7c1c5be621f533e2dad..b7399d17dd64ca8b1f1fab405cb0ac914dc53b98 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -55,6 +55,7 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
public final class MCUtil {
+ public static final double COLLISION_EPSILON = 1.0E-7; // Tuinity - Just in case mojang changes this...
public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
0, 2, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
@@ -238,6 +239,63 @@ public final class MCUtil {
return getBlockKey(getBlockCoordinate(entity.locX()), getBlockCoordinate(entity.locY()), getBlockCoordinate(entity.locZ()));
}
+ // Tuinity start
+
+ 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 getSectionKey(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 getSectionKey(final net.minecraft.core.SectionPosition 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 getSectionKey(final ChunkCoordIntPair 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 getSectionKey(final BlockPosition 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 getSectionKey(final Entity entity) {
+ return ((MCUtil.fastFloor(entity.locX()) & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((MCUtil.fastFloor(entity.locY()) & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((MCUtil.fastFloor(entity.locZ()) & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static int getSectionX(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
+ }
+
+ public static int getSectionY(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
+ }
+
+ public static int getSectionZ(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
+ }
+ // Tuinity end
+
// assumes the sets have the same comparator, and if this comparator is null then assume T is Comparable
public static <T> void mergeSortedSets(final java.util.function.Consumer<T> consumer, final java.util.Comparator<? super T> comparator, final java.util.SortedSet<T>...sets) {
final ObjectRBTreeSet<T> all = new ObjectRBTreeSet<>(comparator);
@@ -639,7 +697,7 @@ public final class MCUtil {
worldData.addProperty("name", world.getWorld().getName());
worldData.addProperty("view-distance", world.getChunkProvider().playerChunkMap.getEffectiveViewDistance());
- worldData.addProperty("no-view-distance", world.getChunkProvider().playerChunkMap.getRawNoTickViewDistance());
+ worldData.addProperty("no-view-distance", world.getChunkProvider().playerChunkMap.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());
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index bf80e870e6a2a6fe1d4ae1bea355bcd7a0735d3b..6f90a4182e008b49a4b3328e569311382e4bec0d 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -268,6 +268,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
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
@@ -881,10 +882,11 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
org.spigotmc.WatchdogThread.doStop(); // Paper
if (!isMainThread()) {
MinecraftServer.LOGGER.info("Stopping main thread (Ignore any thread death message you see! - DO NOT REPORT THREAD DEATH TO PAPER)");
+ long start = System.nanoTime(); // Tuinity - really try hard to kill the main thread
while (this.getThread().isAlive()) {
this.getThread().stop();
try {
- Thread.sleep(1);
+ if ((System.nanoTime() - start) <= (15L * 1000L * 1000L * 1000L)) Thread.sleep(1); // Tuinity - really try hard to kill the main thread - if we're past 15s we're probably in a terrible loop, spam it to really kill it
} catch (InterruptedException e) {}
}
// We've just obliterated the main thread, this will prevent stop from dying when removing players
@@ -1085,6 +1087,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
// Paper end
PaperJvmChecker.checkJvm(); // Paper jvm version nag
+ 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 );
@@ -1102,6 +1105,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
this.lastOverloadTime = this.nextTick;
}
+ ++MinecraftServer.currentTickLong; // Tuinity
if ( ++MinecraftServer.currentTick % SAMPLE_INTERVAL == 0 )
{
final long diff = curTime - tickSection;
@@ -1116,7 +1120,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
// Paper end
tickSection = curTime;
}
- midTickChunksTasksRan = 0; // Paper
+ // Tuinity - replace logic
// Spigot end
//MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
@@ -1209,6 +1213,76 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
}
// Paper end
+ // 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 (WorldServer world : this.getWorlds()) {
+ long currTime = System.nanoTime();
+ if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ continue;
+ }
+ if (!world.getChunkProvider().runTasks()) {
+ // 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
+
private void executeModerately() {
this.executeAll();
java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
@@ -1222,22 +1296,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
});
}
- // Paper start
- public int midTickChunksTasksRan = 0;
- private long midTickLastRan = 0;
- public void midTickLoadChunks() {
- if (!isMainThread() || 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 (WorldServer value : this.getWorlds()) {
- value.getChunkProvider().serverThreadQueue.midTickLoadChunks();
- }
- midTickLastRan = System.nanoTime();
- }
- }
- // Paper end
+ // Tuinity - replace logic
@Override
public TickTask postToMainThread(Runnable runnable) {
@@ -1264,6 +1323,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
private boolean bb() {
if (super.executeNext()) {
+ this.executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick
return true;
} else {
if (this.canSleepForTick()) {
@@ -1331,7 +1391,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
// Paper start - move oversleep into full server tick
isOversleep = true;MinecraftTimings.serverOversleep.startTiming();
this.awaitTasks(() -> {
- midTickLoadChunks(); // will only do loads since we are still considered !canSleepForTick
+ // Tuinity - replace logic
return !this.canOversleep();
});
isOversleep = false;MinecraftTimings.serverOversleep.stopTiming();
@@ -1396,6 +1456,8 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
}
// Paper end
+ com.tuinity.tuinity.util.CachedLists.reset(); // Tuinity
+
// Paper start
long endTime = System.nanoTime();
long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
@@ -1422,16 +1484,16 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
}
protected void b(BooleanSupplier booleansupplier) {
- midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper
this.server.getScheduler().mainThreadHeartbeat(this.ticks); // CraftBukkit
MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper
- midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.methodProfiler.enter("commandFunctions");
MinecraftTimings.commandFunctionsTimer.startTiming(); // Spigot // Paper
this.getFunctionData().tick();
MinecraftTimings.commandFunctionsTimer.stopTiming(); // Spigot // Paper
- midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.methodProfiler.exitEnter("levels");
Iterator iterator = this.getWorlds().iterator();
@@ -1442,7 +1504,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
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
@@ -1485,11 +1547,16 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
this.methodProfiler.enter("tick");
try {
- midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
worldserver.timings.doTick.startTiming(); // Spigot
worldserver.doTick(booleansupplier);
+ // Tuinity start
+ for (final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkProvider().playerChunkMap.regionManagers) {
+ regionManager.recalculateRegions();
+ }
+ // Tuinity end
worldserver.timings.doTick.stopTiming(); // Spigot
- midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
} catch (Throwable throwable) {
// Spigot Start
CrashReport crashreport;
@@ -1583,7 +1650,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
}
public String getServerModName() {
- return "Paper"; //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla!
+ return "Tuinity"; // Tuinity //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla!
}
public CrashReport b(CrashReport crashreport) {
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 1929b992be3aeaa37c7b04c458f0ee41a60c692e..3713a110a64fa686e785b9789c33dd09cacc2f48 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -215,6 +215,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now
io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider
// Paper end
+ com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config
this.setPVP(dedicatedserverproperties.pvp);
this.setAllowFlight(dedicatedserverproperties.allowFlight);
@@ -416,7 +417,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
}
if (this.q != null) {
- this.q.b();
+ //this.q.b(); // Tuinity - do not wait for AWT, causes deadlock with sigint handler (AWT shutdown will properly clear our resources anyways)
}
if (this.remoteControlListener != null) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMapDistance.java b/src/main/java/net/minecraft/server/level/ChunkMapDistance.java
index 3644e8b24b082e17752ef52934625416130aaa08..58e14c174cdf76cdea861fd3d4d1195fa27f888a 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMapDistance.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMapDistance.java
@@ -8,6 +8,7 @@ import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntMaps;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Tuinity
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -40,9 +41,9 @@ public abstract class ChunkMapDistance {
private static final int b = 33 + ChunkStatus.a(ChunkStatus.FULL) - 2;
private final Long2ObjectMap<ObjectSet<EntityPlayer>> c = new Long2ObjectOpenHashMap();
public final Long2ObjectOpenHashMap<ArraySetSorted<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
- private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a();
+ //private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a(); final ChunkMapDistance.a getTicketTracker() { return this.ticketLevelTracker; } // Tuinity - OBFHELPER // 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 ChunkMapDistance.c g = new ChunkMapDistance.c(33);
+ //private final ChunkMapDistance.c g = new ChunkMapDistance.c(33); // Tuinity - no longer used
// Paper start use a queue, but still keep unique requirement
public final java.util.Queue<PlayerChunk> pendingChunkUpdates = new java.util.ArrayDeque<PlayerChunk>() {
@Override
@@ -62,6 +63,86 @@ public abstract class ChunkMapDistance {
PlayerChunkMap chunkMap; // Paper
+ // Tuinity start - delay chunk unloads
+ private long nextUnloadId; // delay chunk unloads
+ private final Long2ObjectOpenHashMap<Ticket<Long>> delayedChunks = new Long2ObjectOpenHashMap<>();
+ public final void removeTickets(long chunk, TicketType<?> type) {
+ ArraySetSorted<Ticket<?>> tickets = this.tickets.get(chunk);
+ if (tickets == null) {
+ return;
+ }
+ if (type == TicketType.DELAYED_UNLOAD) {
+ this.delayedChunks.remove(chunk);
+ }
+ boolean changed = tickets.removeIf((Ticket<?> ticket) -> {
+ return ticket.getTicketType() == type;
+ });
+ if (changed) {
+ this.updateTicketLevel(chunk, getLowestTicketLevel(tickets)); // Tuinity - replace ticket level propagator
+ }
+ }
+
+ private final java.util.function.LongFunction<Ticket<Long>> computeFuntion = (long key) -> {
+ Ticket<Long> ret = new Ticket<>(TicketType.DELAYED_UNLOAD, -1, ++ChunkMapDistance.this.nextUnloadId);
+ ret.isCached = true;
+ return ret;
+ };
+
+ private void computeDelayedTicketFor(long chunk, int removedLevel, ArraySetSorted<Ticket<?>> tickets) {
+ int lowestLevel = getLowestTicketLevel(tickets);
+ if (removedLevel > lowestLevel) {
+ return;
+ }
+ final Ticket<Long> ticket = this.delayedChunks.computeIfAbsent(chunk, this.computeFuntion);
+ if (ticket.getTicketLevel() != -1) {
+ // since we modify data used in sorting, we need to remove before
+ tickets.remove(ticket);
+ }
+ ticket.setCreationTick(this.currentTick);
+ ticket.setTicketLevel(removedLevel);
+ tickets.add(ticket); // re-add with new expire time and ticket level
+ }
+ // Tuinity end - delay chunk unloads
+ // 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) -> {
+ ChunkMapDistance.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 PlayerChunkMap.GOLDEN_TICKET - 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 > PlayerChunkMap.GOLDEN_TICKET) {
+ this.ticketLevelPropagator.removeSource(coordinate);
+ } else {
+ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel));
+ }
+ }
+ // Tuinity end - replace ticket level propagator
+
protected ChunkMapDistance(Executor executor, Executor executor1) {
executor1.getClass();
Mailbox<Runnable> mailbox = Mailbox.a("player ticket throttler", executor1::execute);
@@ -74,21 +155,45 @@ public abstract class ChunkMapDistance {
}
protected void purgeTickets() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async purge tickets"); // Tuinity
++this.currentTick;
ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
+ // Tuinity start - delay chunk unloads
+ int[] tempLevel = new int[] { PlayerChunkMap.GOLDEN_TICKET + 1 };
+ Entry<ArraySetSorted<Ticket<?>>>[] entryPass = new Entry[1];
+ java.util.function.Predicate<Ticket<?>> isExpired = (ticket) -> { // CraftBukkit - decompile error
+ // Tuinity start - delay chunk unloads
+ boolean ret = ticket.isExpired(this.currentTick);
+ if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy <= 0) {
+ return ret;
+ }
+ if (ret && ticket.getTicketType().delayUnloadViable && ticket.getTicketLevel() < tempLevel[0]) {
+ tempLevel[0] = ticket.getTicketLevel();
+ }
+ if (ret && ticket.getTicketType() == TicketType.DELAYED_UNLOAD && ticket.isCached) {
+ this.delayedChunks.remove(entryPass[0].getLongKey(), ticket); // clean up ticket...
+ }
+ return ret;
+ };
+ // Tuinity end - delay chunk unloads
while (objectiterator.hasNext()) {
- Entry<ArraySetSorted<Ticket<?>>> entry = (Entry) objectiterator.next();
+ Entry<ArraySetSorted<Ticket<?>>> entry = (Entry) objectiterator.next(); entryPass[0] = entry; // Tuinity - only allocate lambda once
- if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error
- return ticket.b(this.currentTick);
- })) {
- this.ticketLevelTracker.update(entry.getLongKey(), getLowestTicketLevel((ArraySetSorted) entry.getValue()), false);
+ if ((entry.getValue()).removeIf(isExpired)) { // Tuinity - move above - only allocate once
+ // Tuinity start - delay chunk unloads
+ if (tempLevel[0] < (PlayerChunkMap.GOLDEN_TICKET + 1)) {
+ this.computeDelayedTicketFor(entry.getLongKey(), tempLevel[0], entry.getValue());
+ }
+ // Tuinity end - delay chunk unloads
+ this.updateTicketLevel(entry.getLongKey(), getLowestTicketLevel((ArraySetSorted) entry.getValue())); // Tuinity - replace ticket level propagator
}
if (((ArraySetSorted) entry.getValue()).isEmpty()) {
objectiterator.remove();
}
+
+ tempLevel[0] = PlayerChunkMap.GOLDEN_TICKET + 1; // Tuinity - reset
}
}
@@ -101,65 +206,99 @@ public abstract class ChunkMapDistance {
protected abstract boolean a(long i);
@Nullable
- protected abstract PlayerChunk b(long i);
+ protected abstract PlayerChunk b(long i); protected final PlayerChunk getUpdatingChunk(long i) { return this.b(i); } // Tuinity - OBFHELPER
@Nullable
- protected abstract PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k);
+ protected abstract PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k); protected final PlayerChunk updateTicketLevel(long coord, int newLevel, @Nullable PlayerChunk playerchunk, int oldLevel) { return this.a(coord, newLevel, playerchunk, oldLevel); } // Tuinity - OBFHELPER
+ protected long ticketLevelUpdateCount; // Tuinity - replace ticket level propagator
public boolean a(PlayerChunkMap playerchunkmap) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot tick ChunkMapDistance off of the main-thread");// Tuinity
//this.f.a(); // Paper - no longer used
AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
- this.g.a();
- int i = Integer.MAX_VALUE - this.ticketLevelTracker.a(Integer.MAX_VALUE);
- boolean flag = i != 0;
+ //this.g.a(); // 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 {
- while(!this.pendingChunkUpdates.isEmpty()) {
- PlayerChunk remove = this.pendingChunkUpdates.remove();
- remove.isUpdateQueued = false;
- remove.a(playerchunkmap);
- }
- } finally { this.pollingPendingChunkUpdates = false; }
- // Paper end
- return true;
- } else {
- if (!this.l.isEmpty()) {
- LongIterator longiterator = this.l.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();
+ PlayerChunk chunk = this.getUpdatingChunk(key);
+
+ if (chunk == null && newLevel > PlayerChunkMap.GOLDEN_TICKET) {
+ // not loaded and it shouldn't be loaded!
+ continue;
+ }
+
+ int currentLevel = chunk == null ? PlayerChunkMap.GOLDEN_TICKET + 1 : chunk.getTicketLevel();
- if (this.e(j).stream().anyMatch((ticket) -> {
- return ticket.getTicketType() == TicketType.PLAYER;
- })) {
- PlayerChunk playerchunk = playerchunkmap.getUpdatingChunk(j);
+ if (currentLevel == newLevel) {
+ // nothing to do
+ continue;
+ }
+
+ this.updateTicketLevel(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();
+ PlayerChunk chunk = this.getUpdatingChunk(key);
+
+ if (chunk == null) {
+ if (newLevel <= PlayerChunkMap.GOLDEN_TICKET) {
+ throw new IllegalStateException("Expected chunk holder to be created");
}
+ // not loaded and it shouldn't be loaded!
+ continue;
+ }
- CompletableFuture<Either<Chunk, PlayerChunk.Failure>> completablefuture = playerchunk.b();
+ int currentLevel = chunk.oldTicketLevel;
- completablefuture.thenAccept((either) -> {
- this.m.execute(() -> {
- this.k.a(ChunkTaskQueueSorter.a(() -> {
- }, j, false));
- });
- });
+ if (currentLevel == newLevel) {
+ // nothing to do
+ continue;
+ }
+
+ chunk.handleLevelUpdate(playerchunkmap);
+ 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.l.clear();
- }
+ for (;;) {
+ if (recursiveCheck != this.ticketLevelUpdateCount) {
+ continue ticket_update_loop;
+ }
+ PlayerChunk pendingUpdate = this.pendingChunkUpdates.poll();
+ if (pendingUpdate == null) {
+ break;
+ }
- return flag;
+ pendingUpdate.handleLevelUpdate(playerchunkmap);
+ }
+ } finally {
+ this.pollingPendingChunkUpdates = oldPolling;
+ }
}
+
+ return flag;
+ // Tuinity end - replace level propagator
}
boolean pollingPendingChunkUpdates = false; // Paper
@@ -171,7 +310,7 @@ public abstract class ChunkMapDistance {
ticket1.a(this.currentTick);
if (ticket.b() < j) {
- this.ticketLevelTracker.update(i, ticket.b(), true);
+ this.updateTicketLevel(i, ticket.b()); // Tuinity - replace ticket level propagator
}
return ticket == ticket1; // CraftBukkit
@@ -185,27 +324,11 @@ public abstract class ChunkMapDistance {
boolean removed = false; // CraftBukkit
if (arraysetsorted.remove(ticket)) {
removed = true; // CraftBukkit
- // Paper start - delay chunk unloads for player tickets
- long delayChunkUnloadsBy = chunkMap.world.paperConfig.delayChunkUnloadsBy;
- if (ticket.getTicketType() == TicketType.PLAYER && delayChunkUnloadsBy > 0) {
- boolean hasPlayer = false;
- for (Ticket<?> ticket1 : arraysetsorted) {
- if (ticket1.getTicketType() == TicketType.PLAYER) {
- hasPlayer = true;
- break;
- }
- }
- PlayerChunk playerChunk = chunkMap.getUpdatingChunk(i);
- if (!hasPlayer && playerChunk != null && playerChunk.isFullChunkReady()) {
- Ticket<Long> delayUnload = new Ticket<Long>(TicketType.DELAY_UNLOAD, 33, i);
- delayUnload.delayUnloadBy = delayChunkUnloadsBy;
- delayUnload.setCurrentTick(this.currentTick);
- arraysetsorted.remove(delayUnload);
- // refresh ticket
- arraysetsorted.add(delayUnload);
- }
+ // Tuinity start - delay chunk unloads
+ if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy > 0 && ticket.getTicketType().delayUnloadViable) {
+ this.computeDelayedTicketFor(i, ticket.getTicketLevel(), arraysetsorted);
}
- // Paper end
+ // Tuinity end - delay chunk unloads
}
if (arraysetsorted.isEmpty()) {
@@ -213,7 +336,7 @@ public abstract class ChunkMapDistance {
}
int newLevel = getLowestTicketLevel(arraysetsorted); // Paper
- if (newLevel > oldLevel) this.ticketLevelTracker.update(i, newLevel, false); // Paper
+ if (newLevel > oldLevel) this.updateTicketLevel(i, newLevel); // Paper // Tuinity - replace ticket level propagator
return removed; // CraftBukkit
}
@@ -272,7 +395,7 @@ public abstract class ChunkMapDistance {
AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket");
long pair = coords.pair();
PlayerChunk chunk = chunkMap.getUpdatingChunk(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);
@@ -379,6 +502,7 @@ public abstract class ChunkMapDistance {
}
private ArraySetSorted<Ticket<?>> e(long i) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async tickets compute"); // Tuinity
return (ArraySetSorted) this.tickets.computeIfAbsent(i, (j) -> {
return ArraySetSorted.a(4);
});
@@ -396,16 +520,18 @@ public abstract class ChunkMapDistance {
}
public void a(SectionPosition sectionposition, EntityPlayer entityplayer) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async player add"); // Tuinity
long i = sectionposition.r().pair();
((ObjectSet) this.c.computeIfAbsent(i, (j) -> {
return new ObjectOpenHashSet();
})).add(entityplayer);
//this.f.update(i, 0, true); // Paper - no longer used
- this.g.update(i, 0, true);
+ //this.g.update(i, 0, true); // Tuinity - no longer used
}
public void b(SectionPosition sectionposition, EntityPlayer entityplayer) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async player remove"); // Tuinity
long i = sectionposition.r().pair();
ObjectSet<EntityPlayer> objectset = (ObjectSet) this.c.get(i);
if (objectset == null) return; // CraftBukkit - SPIGOT-6208
@@ -414,7 +540,7 @@ public abstract class ChunkMapDistance {
if (objectset == null || objectset.isEmpty()) { // Paper
this.c.remove(i);
//this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
- this.g.update(i, Integer.MAX_VALUE, false);
+ //this.g.update(i, Integer.MAX_VALUE, false); // Tuinity - no longer used
}
}
@@ -433,7 +559,7 @@ public abstract class ChunkMapDistance {
}
protected void setNoTickViewDistance(int i) { // Paper - force abi breakage on usage change
- this.g.a(i);
+ throw new UnsupportedOperationException(); // Tuinity - no longer relevant
}
public int b() {
@@ -456,6 +582,7 @@ public abstract class ChunkMapDistance {
// CraftBukkit start
public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async ticket remove"); // Tuinity
Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
for (java.util.Iterator<Entry<ArraySetSorted<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
@@ -463,7 +590,7 @@ public abstract class ChunkMapDistance {
ArraySetSorted<Ticket<?>> tickets = entry.getValue();
if (tickets.remove(target)) {
// copied from removeTicket
- this.ticketLevelTracker.update(entry.getLongKey(), getLowestTicketLevel(tickets), false);
+ this.updateTicketLevel(entry.getLongKey(), getLowestTicketLevel(tickets)); // Tuinity - replace ticket level propagator
// can't use entry after it's removed
if (tickets.isEmpty()) {
@@ -519,6 +646,7 @@ public abstract class ChunkMapDistance {
}
}
+ /* Tuinity - replace old loader system
class c extends ChunkMapDistance.b {
private int e = 0; private int getViewDistance() { return e; } private void setViewDistance(int value) { this.e = value; } // Paper - OBFHELPER
@@ -737,6 +865,7 @@ public abstract class ChunkMapDistance {
return i <= this.e - 2;
}
}
+ */ // Tuinity - replace old loader system
class b extends ChunkMap {
diff --git a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
index 4a71f9a8a57ed123b31c725a8bd5f94ab0e2174d..1b86c032fde3409cb89b849b9ba094af82cd9269 100644
--- a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
@@ -47,6 +47,12 @@ import net.minecraft.world.level.storage.WorldData;
import net.minecraft.world.level.storage.WorldPersistentData;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
+// Tuinity start
+import it.unimi.dsi.fastutil.objects.Object2BooleanLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
+import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+// Tuinity end
+
public class ChunkProviderServer extends IChunkProvider {
private static final List<ChunkStatus> b = ChunkStatus.a(); public static final List<ChunkStatus> getPossibleChunkStatuses() { return ChunkProviderServer.b; } // Paper - OBFHELPER
@@ -137,7 +143,7 @@ public class ChunkProviderServer extends IChunkProvider {
return (Chunk)this.getChunkAt(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<Chunk> onLoad) {
if (Thread.currentThread() != this.serverThread) {
@@ -199,9 +205,9 @@ public class ChunkProviderServer extends IChunkProvider {
try {
if (onLoad != null) {
- playerChunkMap.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) {
@@ -226,6 +232,165 @@ public class ChunkProviderServer extends IChunkProvider {
}
// Paper end - rewrite ticklistserver
+ // Tuinity start
+ // this will try to avoid chunk neighbours for lighting
+ public final IChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) {
+ Chunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
+ if (ifLoaded != null) {
+ return ifLoaded;
+ }
+
+ IChunkAccess empty = this.getChunkAt(chunkX, chunkZ, ChunkStatus.EMPTY, true);
+ if (empty != null && empty.getChunkStatus() == ChunkStatus.FULL) {
+ return empty;
+ }
+ return this.getChunkAt(chunkX, chunkZ, ChunkStatus.FULL, true);
+ }
+
+ public final IChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) {
+ Chunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
+ if (ifLoaded != null) {
+ return ifLoaded;
+ }
+
+ IChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ);
+ if (ret != null && ret.getChunkStatus() == ChunkStatus.FULL) {
+ return ret;
+ } else {
+ return null;
+ }
+ }
+
+ void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel,
+ java.util.function.Consumer<IChunkAccess> consumer) {
+ this.getChunkAtAsynchronously(chunkX, chunkZ, ticketLevel, (PlayerChunk playerChunk) -> {
+ if (ticketLevel <= 33) {
+ return (CompletableFuture)playerChunk.getFullChunkFuture();
+ } else {
+ return playerChunk.getOrCreateFuture(PlayerChunk.getChunkStatus(ticketLevel), ChunkProviderServer.this.playerChunkMap);
+ }
+ }, consumer);
+ }
+
+ void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel,
+ java.util.function.Function<PlayerChunk, CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>> function,
+ java.util.function.Consumer<IChunkAccess> consumer) {
+ if (Thread.currentThread() != this.serverThread) {
+ throw new IllegalStateException();
+ }
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(chunkX, chunkZ);
+ Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++);
+ this.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
+ this.tickDistanceManager();
+
+ PlayerChunk chunk = this.playerChunkMap.getUpdatingChunk(chunkPos.pair());
+
+ if (chunk == null) {
+ throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.world.getWorld().getName() + "'");
+ }
+
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = function.apply(chunk);
+
+ future.whenCompleteAsync((either, throwable) -> {
+ try {
+ if (throwable != null) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", throwable);
+ } else if (either.right().isPresent()) {
+ MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.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;
+ }
+ MinecraftServer.LOGGER.fatal("Load callback for future await failed " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", thr);
+ return;
+ }
+ } finally {
+ // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback.
+ ChunkProviderServer.this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
+ ChunkProviderServer.this.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
+ }
+ }, this.serverThreadQueue);
+ }
+
+ void chunkLoadAccept(int chunkX, int chunkZ, IChunkAccess chunk, java.util.function.Consumer<IChunkAccess> consumer) {
+ try {
+ consumer.accept(chunk);
+ } catch (Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ MinecraftServer.LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.world.getWorld().getName() + "' threw an exception", throwable);
+ }
+ }
+
+ public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer<IChunkAccess> onLoad) {
+ // try to fire sync
+ int chunkStatusTicketLevel = 33 + ChunkStatus.getTicketLevelOffset(status);
+ PlayerChunk playerChunk = this.playerChunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(chunkX, chunkZ));
+ if (playerChunk != null) {
+ ChunkStatus holderStatus = playerChunk.getChunkHolderStatus();
+ IChunkAccess immediate = playerChunk.getAvailableChunkNow();
+ if (immediate != null) {
+ if (allowSubTicketLevel ? immediate.getChunkStatus().isAtLeastStatus(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isAtLeastStatus(status))) {
+ this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad);
+ return;
+ } else {
+ if (gen || (!allowSubTicketLevel && immediate.getChunkStatus().isAtLeastStatus(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, MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (IChunkAccess chunk) -> {
+ if (chunk == null) {
+ throw new IllegalStateException("Chunk cannot be null");
+ }
+
+ if (!chunk.getChunkStatus().isAtLeastStatus(status)) {
+ if (gen) {
+ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
+ return;
+ } else {
+ ChunkProviderServer.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad);
+ return;
+ }
+ } else {
+ if (allowSubTicketLevel) {
+ ChunkProviderServer.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad);
+ return;
+ } else {
+ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
+ return;
+ }
+ }
+ });
+ }
+
+ final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Chunk> tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Chunk> entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ // Tuinity end
+
public ChunkProviderServer(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, ChunkGenerator chunkgenerator, int i, boolean flag, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier) {
this.world = worldserver;
this.serverThreadQueue = new ChunkProviderServer.a(worldserver);
@@ -569,6 +734,8 @@ public class ChunkProviderServer extends IChunkProvider {
Arrays.fill(this.cacheChunk, (Object) null);
}
+ private long syncLoadCounter; // Tuinity - prevent plugin unloads from removing our ticket
+
private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> 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);
@@ -587,9 +754,12 @@ public class ChunkProviderServer extends IChunkProvider {
PlayerChunk.State currentChunkState = PlayerChunk.getChunkState(playerchunk.getTicketLevel());
currentlyUnloading = (oldChunkState.isAtLeast(PlayerChunk.State.BORDER) && !currentChunkState.isAtLeast(PlayerChunk.State.BORDER));
}
+ final Long identifier; // Tuinity - prevent plugin unloads from removing our ticket
if (flag && !currentlyUnloading) {
// CraftBukkit end
this.chunkMapDistance.a(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
+ identifier = Long.valueOf(this.syncLoadCounter++); // Tuinity - prevent plugin unloads from removing our ticket
+ this.chunkMapDistance.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity - prevent plugin unloads from removing our ticket
if (isUrgent) this.chunkMapDistance.markUrgent(chunkcoordintpair); // Paper
if (this.a(playerchunk, l)) {
GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
@@ -600,12 +770,20 @@ public class ChunkProviderServer extends IChunkProvider {
playerchunk = this.getChunk(k);
gameprofilerfiller.exit();
if (this.a(playerchunk, l)) {
+ this.chunkMapDistance.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity
throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("No chunk holder after ticket has been added")));
}
}
- }
+ } else { identifier = null; } // Tuinity - prevent plugin unloads from removing our ticket
// Paper start
CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = this.a(playerchunk, l) ? PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE : playerchunk.a(chunkstatus, this.playerChunkMap);
+ // Tuinity start - prevent plugin unloads from removing our ticket
+ if (flag && !currentlyUnloading) {
+ future.thenAcceptAsync((either) -> {
+ ChunkProviderServer.this.chunkMapDistance.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier);
+ }, ChunkProviderServer.this.serverThreadQueue);
+ }
+ // Tuinity end - prevent plugin unloads from removing our ticket
if (isUrgent) {
future.thenAccept(either -> this.chunkMapDistance.clearUrgent(chunkcoordintpair));
}
@@ -624,8 +802,8 @@ public class ChunkProviderServer extends IChunkProvider {
return !this.a(playerchunk, k);
}
- @Override
- public IBlockAccess c(int i, int j) {
+ public final IBlockAccess getFeaturesReadyChunk(int x, int z) { return this.c(x, z); } // Tuinity - OBFHELPER
+ @Override public IBlockAccess c(int i, int j) { // Tuinity - OBFHELPER
long k = ChunkCoordIntPair.pair(i, j);
PlayerChunk playerchunk = this.getChunk(k);
@@ -662,6 +840,8 @@ public class ChunkProviderServer extends IChunkProvider {
public boolean tickDistanceManager() { // Paper - private -> public
if (chunkMapDistance.delayDistanceManagerTick) return false; // Paper
+ if (this.playerChunkMap.unloadingPlayerChunk) { MinecraftServer.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.chunkMapDistance.a(this.playerChunkMap);
boolean flag1 = this.playerChunkMap.b();
@@ -671,6 +851,7 @@ public class ChunkProviderServer extends IChunkProvider {
this.clearCache();
return true;
}
+ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Tuinity - add timings for distance manager
}
public final boolean isInEntityTickingChunk(Entity entity) { return this.a(entity); } // Paper - OBFHELPER
@@ -745,6 +926,7 @@ public class ChunkProviderServer extends IChunkProvider {
// 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.world.getMethodProfiler().enter("purge");
this.chunkMapDistance.purgeTickets();
this.tickDistanceManager();
@@ -759,17 +941,18 @@ public class ChunkProviderServer extends IChunkProvider {
this.world.getMethodProfiler().enter("purge");
this.world.timings.doChunkMap.startTiming(); // Spigot
this.chunkMapDistance.purgeTickets();
- this.world.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.tickDistanceManager();
this.world.timings.doChunkMap.stopTiming(); // Spigot
this.world.getMethodProfiler().exitEnter("chunks");
this.world.timings.chunks.startTiming(); // Paper - timings
+ this.playerChunkMap.playerChunkManager.tick(); // Tuinity - this is mostly is to account for view distance changes
this.tickChunks();
this.world.timings.chunks.stopTiming(); // Paper - timings
this.world.timings.doChunkUnload.startTiming(); // Spigot
this.world.getMethodProfiler().exitEnter("unload");
this.playerChunkMap.unloadChunks(booleansupplier);
- this.world.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.world.timings.doChunkUnload.stopTiming(); // Spigot
this.world.getMethodProfiler().exit();
this.clearCache();
@@ -834,32 +1017,38 @@ public class ChunkProviderServer extends IChunkProvider {
for (EntityPlayer player : this.world.players) {
Arrays.fill(player.mobCounts, 0);
}
- spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, true);
+ spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, true, this); // Tuinity
} else {
- spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, false);
+ spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, false, this); // Tuinity
}
// Paper end
this.world.timings.countNaturalMobs.stopTiming(); // Paper - timings
+ int ticked = 0; // Tuinity - exec chunk tasks during world tick
+
this.p = spawnercreature_d;
this.world.getMethodProfiler().exit();
//List<PlayerChunk> list = Lists.newArrayList(this.playerChunkMap.f()); // Paper
//Collections.shuffle(list); // Paper
// Paper - moved up
this.world.timings.chunkTicks.startTiming(); // Paper
- final int[] chunksTicked = {0}; this.playerChunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping
- Optional<Chunk> optional = ((Either) playerchunk.a().getNow(PlayerChunk.UNLOADED_CHUNK)).left();
-
- if (optional.isPresent()) {
+ // Tuinity start - optimise chunk tick iteration
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Chunk> iterator = this.entityTickingChunks.iterator();
+ try {
+ while (iterator.hasNext()) {
+ Chunk chunk = iterator.next();
+ PlayerChunk playerchunk = chunk.playerChunk;
+ if (playerchunk != null) { // make sure load event has been called along with the load logic we put there
+ // Tuinity end - optimise chunk tick iteration
this.world.getMethodProfiler().enter("broadcast");
this.world.timings.broadcastChunkUpdates.startTiming(); // Paper - timings
- playerchunk.a((Chunk) optional.get());
+ playerchunk.a(chunk); // Tuinity
this.world.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings
this.world.getMethodProfiler().exit();
- Optional<Chunk> optional1 = ((Either) playerchunk.b().getNow(PlayerChunk.UNLOADED_CHUNK)).left();
+ // Tuinity
- if (optional1.isPresent()) {
- Chunk chunk = (Chunk) optional1.get();
+ if (true) { // Tuinity
+ // Tuinity
ChunkCoordIntPair chunkcoordintpair = playerchunk.i();
if (!this.playerChunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange
@@ -871,11 +1060,15 @@ public class ChunkProviderServer extends IChunkProvider {
//this.world.timings.chunkTicks.startTiming(); // Spigot // Paper
this.world.a(chunk, k);
//this.world.timings.chunkTicks.stopTiming(); // Spigot // Paper
- if (chunksTicked[0]++ % 10 == 0) this.world.getMinecraftServer().midTickLoadChunks(); // Paper
+ if ((++ticked & 1) == 0) MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick
}
}
}
- });
+ } // Tuinity start - optimise chunk tick iteration
+ } finally {
+ iterator.finishedIterating();
+ }
+ // Tuinity end - optimise chunk tick iteration
this.world.timings.chunkTicks.stopTiming(); // Paper
this.world.getMethodProfiler().enter("customSpawners");
if (flag1) {
@@ -888,7 +1081,25 @@ public class ChunkProviderServer extends IChunkProvider {
this.world.getMethodProfiler().exit();
}
+ // Tuinity start - controlled flush for entity tracker packets
+ List<net.minecraft.network.NetworkManager> disabledFlushes = new java.util.ArrayList<>(this.world.getPlayers().size());
+ for (EntityPlayer player : this.world.getPlayers()) {
+ net.minecraft.server.network.PlayerConnection connection = player.playerConnection;
+ if (connection != null) {
+ connection.networkManager.disableAutomaticFlush();
+ disabledFlushes.add(connection.networkManager);
+ }
+ }
+ try {
+ // Tuinity end - controlled flush for entity tracker packets
this.playerChunkMap.g();
+ // Tuinity start - controlled flush for entity tracker packets
+ } finally {
+ for (net.minecraft.network.NetworkManager networkManager : disabledFlushes) {
+ networkManager.enableAutomaticFlush();
+ }
+ }
+ // Tuinity end - controlled flush for entity tracker packets
}
private void a(long i, Consumer<Chunk> consumer) {
@@ -1028,44 +1239,12 @@ public class ChunkProviderServer extends IChunkProvider {
ChunkProviderServer.this.world.getMethodProfiler().c("runTask");
super.executeTask(runnable);
}
-
- // Paper start
- private long lastMidTickChunkTask = 0;
- public boolean pollChunkLoadTasks() {
- if (com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ChunkProviderServer.this.world.asyncChunkTaskManager.pollNextChunkTask()) {
- try {
- ChunkProviderServer.this.tickDistanceManager();
- } finally {
- // from below: process pending Chunk loadCallback() and unloadCallback() after each run task
- playerChunkMap.callbackExecutor.run();
- }
- return true;
- }
- return false;
- }
- public void midTickLoadChunks() {
- MinecraftServer server = ChunkProviderServer.this.world.getMinecraftServer();
- // 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.canSleepForTick();) {
- if (this.executeNext()) {
- server.midTickChunksTasksRan++;
- lastMidTickChunkTask = System.nanoTime();
- } else {
- break;
- }
- }
- }
- // Paper end
+ // Tuinity - replace logic
@Override
protected boolean executeNext() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot execute chunk tasks off-main thread");// Tuinity
+ ChunkProviderServer.this.playerChunkMap.playerChunkManager.tickMidTick(); // Tuinity
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
try {
boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ChunkProviderServer.this.world.asyncChunkTaskManager.pollNextChunkTask(); // Paper
diff --git a/src/main/java/net/minecraft/server/level/EntityPlayer.java b/src/main/java/net/minecraft/server/level/EntityPlayer.java
index 75bcfb3a2b4a104aeebb2fe3298714b2e5b569d2..c813077455a463dd558076d1d7474829f76b905a 100644
--- a/src/main/java/net/minecraft/server/level/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/level/EntityPlayer.java
@@ -262,7 +262,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks
- boolean needsChunkCenterUpdate; // Paper - no-tick view distance
+ public boolean needsChunkCenterUpdate; // Paper - no-tick view distance // Tuinity - package-private -> 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 EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) {
@@ -371,7 +371,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
if (blockposition1 != null) {
this.setPositionRotation(blockposition1, 0.0F, 0.0F);
- if (worldserver.getCubes(this)) {
+ if (!worldserver.collidesWithAnyBlockOrWorldBorder(this, this.getBoundingBox(), true, false)) { // Tuinity - make sure this is loaded
break;
}
}
@@ -379,7 +379,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
} else {
this.setPositionRotation(blockposition, 0.0F, 0.0F);
- while (!worldserver.getCubes(this) && this.locY() < 255.0D) {
+ while (worldserver.collidesWithAnyBlockOrWorldBorder(this, this.getBoundingBox(), true, false) && this.locY() < 255.0D) { // Tuinity - make sure this is loaded
this.setPosition(this.locX(), this.locY() + 1.0D, this.locZ());
}
}
@@ -671,6 +671,185 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
}
}
+ /* // TODO remove debug
+ this.networkManager.disableAutomaticFlush();
+
+ if (MinecraftServer.currentTick % 20 == 0) {
+ int centerX = MathHelper.floor(this.locX()) >> 4;
+ int centerZ = MathHelper.floor(this.locZ()) >> 4;
+ byte[] full = new byte[2048];
+ byte[] empty = new byte[2048];
+ java.util.Arrays.fill(full, (byte)-1);
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ int cx = centerX + dx;
+ int cz = centerZ + dz;
+
+ Chunk chunk = this.getWorldServer().getChunkProvider().getChunkAtIfLoadedImmediately(cx, cz);
+
+ if (chunk == null) {
+ continue;
+ }
+
+ for (int y = -1; y <= 16; ++y) {
+ NibbleArray nibble = this.getWorldServer().getChunkProvider().getLightEngine().a(EnumSkyBlock.SKY)
+ .a(new SectionPosition(cx, y, cz));
+ org.bukkit.Color color;
+ org.bukkit.block.data.BlockData blockColor;
+ if (nibble == null) {
+ color = org.bukkit.Color.PURPLE;
+ blockColor = org.bukkit.Material.PURPLE_WOOL.createBlockData();
+ continue;
+ } else {
+ if (nibble.c()) { // is null
+ color = org.bukkit.Color.BLUE;
+ blockColor = org.bukkit.Material.BLUE_WOOL.createBlockData();
+ } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), full)) {
+ color = org.bukkit.Color.RED;
+ blockColor = org.bukkit.Material.RED_WOOL.createBlockData();
+ } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), empty)) {
+ color = org.bukkit.Color.BLACK;
+ blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData();
+ } else {
+ color = org.bukkit.Color.ORANGE;
+ blockColor = org.bukkit.Material.ORANGE_WOOL.createBlockData();
+ }
+ }
+ if (false) {
+ if (y < 0 || y > 15 || chunk.getSections()[y] == null || chunk.getSections()[y].isFullOfAir()) {
+ color = org.bukkit.Color.BLACK;
+ blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData();
+ } else {
+ color = org.bukkit.Color.WHITE;
+ blockColor = org.bukkit.Material.WHITE_WOOL.createBlockData();
+ }
+ }
+
+ org.bukkit.Particle.DustOptions dustOptions = new org.bukkit.Particle.DustOptions(color, 1.7f);
+
+ for (int i = 0; i <= 16; ++i) {
+ // y axis
+
+ double xVal = i == 0 ? 0.5 : (i == 16 ? 15.5 : i);
+
+ // left side
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 0.5,
+ y*16 + xVal,
+ cz * 16 + 0.5,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 0.5,
+ y*16 + xVal,
+ cz * 16 + 15.5,
+ 1,
+ dustOptions
+ );
+
+ // right side
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 15.5,
+ y*16 + xVal,
+ cz * 16 + 0.5,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 15.5,
+ y*16 + xVal,
+ cz * 16 + 15.5,
+ 1,
+ dustOptions
+ );
+
+
+ // x axis
+
+ // bottom
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + xVal,
+ y*16 + 0.5,
+ cz * 16 + 0.5,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + xVal,
+ y*16 + 0.5,
+ cz * 16 + 15.5,
+ 1,
+ dustOptions
+ );
+
+ // top
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + xVal,
+ y*16 + 15.5,
+ cz * 16 + 0.5,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + xVal,
+ y*16 + 15.5,
+ cz * 16 + 15.5,
+ 1,
+ dustOptions
+ );
+
+ // z axis
+ // bottom
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 0.5,
+ y*16 + 0.5,
+ cz * 16 + xVal,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 15.5,
+ y*16 + 0.5,
+ cz * 16 + xVal,
+ 1,
+ dustOptions
+ );
+
+ //top
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 0.5,
+ y*16 + 15.5,
+ cz * 16 + xVal,
+ 1,
+ dustOptions
+ );
+
+ this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+ cx * 16 + 15.5,
+ y*16 + 15.5,
+ cz * 16 + xVal,
+ 1,
+ dustOptions
+ );
+ }
+ }
+ }
+ }
+ }
+
+ this.networkManager.enableAutomaticFlush();
+
+ //System.out.println("Block: " + this.getBukkitEntity().getLocation().getBlock().getLightFromBlocks());
+ //System.out.println("Sky: " + this.getBukkitEntity().getLocation().getBlock().getLightFromSky());
+ */ // TODO remove debug
+
if (this.getHealth() != this.lastHealthSent || this.lastFoodSent != this.foodData.getFoodLevel() || this.foodData.getSaturationLevel() == 0.0F != this.lastSentSaturationZero) {
this.playerConnection.sendPacket(new PacketPlayOutUpdateHealth(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit
this.lastHealthSent = this.getHealth();
@@ -1564,6 +1743,17 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
this.playerConnection.sendPacket(new PacketPlayOutCloseWindow(this.activeContainer.windowId));
this.o();
}
+ // Tuinity start - special close for unloaded inventory
+ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
+ // copied from above
+ CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
+ // Paper end
+ // copied from below
+ this.playerConnection.sendPacket(new PacketPlayOutCloseWindow(this.activeContainer.windowId));
+ this.activeContainer = this.defaultContainer;
+ // do not run close logic
+ }
+ // Tuinity end - special close for unloaded inventory
public void broadcastCarriedItem() {
if (!this.e) {
diff --git a/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java b/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
index 1df8fb8cb3fcf8201e1c5fa8ca13f7a9c632c379..67ca28463f5add7c18f7f16b918c3f36f8feeeda 100644
--- a/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
+++ b/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
@@ -103,6 +103,7 @@ public class EntityTrackerEntry {
public final void tick() { this.a(); } // Paper - OBFHELPER
public void a() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity
List<Entity> list = this.tracker.passengers; // Paper - do not copy list
if (!list.equals(this.p)) {
@@ -184,7 +185,7 @@ public class EntityTrackerEntry {
// Paper end - remove allocation of Vec3D here
boolean flag4 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L;
- if (!flag4 && this.o <= 400 && !this.q && this.r == this.tracker.isOnGround()) {
+ if (!flag4 && this.o <= 400 && !this.q && this.r == this.tracker.isOnGround() && !(com.tuinity.tuinity.config.TuinityConfig.sendFullPosForHardCollidingEntities && this.tracker.hardCollides())) { // Tuinity - send full pos for hard colliding entities to prevent collision problems due to desync
if ((!flag2 || !flag3) && !(this.tracker instanceof EntityArrow)) {
if (flag2) {
packet1 = new PacketPlayOutEntity.PacketPlayOutRelEntityMove(this.tracker.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.tracker.isOnGround());
diff --git a/src/main/java/net/minecraft/server/level/LightEngineGraphSection.java b/src/main/java/net/minecraft/server/level/LightEngineGraphSection.java
index 4ee7070364a8989eece4fa4237b529926821f9c9..f22ab98d2e250081df8949be8a99737069c83d34 100644
--- a/src/main/java/net/minecraft/server/level/LightEngineGraphSection.java
+++ b/src/main/java/net/minecraft/server/level/LightEngineGraphSection.java
@@ -77,8 +77,10 @@ public abstract class LightEngineGraphSection extends LightEngineGraph {
return i == Long.MAX_VALUE ? this.b(j) : k + 1;
}
+ public final int getSource(long coordinate) { return this.b(coordinate); } // Tuinity - OBFHELPER
protected abstract int b(long i);
+ public final void update(long coordinate, int level, boolean flag) { this.b(coordinate, level, flag); } // Tuinity - OBFHELPER
public void b(long i, int j, boolean flag) {
this.a(Long.MAX_VALUE, i, j, flag);
}
diff --git a/src/main/java/net/minecraft/server/level/LightEngineThreaded.java b/src/main/java/net/minecraft/server/level/LightEngineThreaded.java
index 0b80569648c1df01aab52d0b8d47028cda925d86..76ac408021c5124fd634682cba97dc63392642f5 100644
--- a/src/main/java/net/minecraft/server/level/LightEngineThreaded.java
+++ b/src/main/java/net/minecraft/server/level/LightEngineThreaded.java
@@ -2,6 +2,11 @@ package net.minecraft.server.level;
import com.mojang.datafixers.util.Pair;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; // Paper
+// Tuinity start
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+// Tuinity end
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectList;
import it.unimi.dsi.fastutil.objects.ObjectListIterator;
@@ -28,11 +33,12 @@ import org.apache.logging.log4j.Logger;
public class LightEngineThreaded extends LightEngine implements AutoCloseable {
private static final Logger LOGGER = LogManager.getLogger();
- private final ThreadedMailbox<Runnable> b;
+ private final ThreadedMailbox<Runnable> b; private ThreadedMailbox<Runnable> getExecutor() { return this.b; } // Tuinity - OBFHELPER
// Paper start
private static final int MAX_PRIORITIES = PlayerChunkMap.GOLDEN_TICKET + 2;
private boolean isChunkLightStatus(long pair) {
+ if (true) return true; // Tuinity - viewing ticket levels async can result in the viewing of transient levels, and LIGHT ticket isn't guaranteed to exist for all loading chunks thanks to really dumb unloading behaviors with the chunk system
PlayerChunk playerChunk = playerChunkMap.getVisibleChunk(pair);
if (playerChunk == null) {
return false;
@@ -169,13 +175,191 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
private volatile int f = 5;
private final AtomicBoolean g = new AtomicBoolean();
+ // Tuinity start - replace light engine impl
+ protected final com.tuinity.tuinity.chunk.light.StarLightInterface theLightEngine;
+ public final boolean hasBlockLight;
+ public final boolean hasSkyLight;
+ // Tuinity end - replace light engine impl
+
public LightEngineThreaded(ILightAccess ilightaccess, PlayerChunkMap playerchunkmap, boolean flag, ThreadedMailbox<Runnable> threadedmailbox, Mailbox<ChunkTaskQueueSorter.a<Runnable>> mailbox) {
super(ilightaccess, true, flag);
this.d = playerchunkmap; this.playerChunkMap = d; // Paper
this.e = mailbox;
this.b = threadedmailbox;
+ // Tuinity start - replace light engine impl
+ this.hasBlockLight = true;
+ this.hasSkyLight = flag;
+ this.theLightEngine = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? new com.tuinity.tuinity.chunk.light.StarLightInterface(ilightaccess, flag, true) : null;
+ // Tuinity end - replace light engine impl
+ }
+
+ // Tuinity start - replace light engine impl
+ protected final IChunkAccess getChunk(final int chunkX, final int chunkZ) {
+ final WorldServer world = (WorldServer)this.theLightEngine.getWorld();
+ return world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ);
+ }
+
+ protected long relightCounter;
+
+ public int relight(java.util.Set<ChunkCoordIntPair> chunks_param,
+ java.util.function.Consumer<ChunkCoordIntPair> 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<ChunkCoordIntPair> chunks = new java.util.LinkedHashSet<>(chunks_param);
+ // add tickets
+ java.util.Map<ChunkCoordIntPair, Long> ticketIds = new java.util.HashMap<>();
+ int totalChunks = 0;
+ for (java.util.Iterator<ChunkCoordIntPair> iterator = chunks.iterator(); iterator.hasNext();) {
+ final ChunkCoordIntPair chunkPos = iterator.next();
+
+ final IChunkAccess chunk = this.theLightEngine.getWorld().getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z);
+ if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ continue;
+ }
+
+ final Long id = Long.valueOf(this.relightCounter++);
+
+ this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
+ ticketIds.put(chunkPos, id);
+
+ ++totalChunks;
+ }
+
+ this.getExecutor().queue(() -> {
+ this.theLightEngine.relightChunks(chunks, (ChunkCoordIntPair chunkPos) -> {
+ chunkLightCallback.accept(chunkPos);
+ ((java.util.concurrent.Executor)this.theLightEngine.getWorld().getChunkProvider().serverThreadQueue).execute(() -> {
+ this.theLightEngine.getWorld().getChunkProvider().playerChunkMap.getUpdatingChunk(chunkPos.pair()).sendPacketToTrackedPlayers(new net.minecraft.network.protocol.game.PacketPlayOutLightUpdate(chunkPos, this, true), false);
+ this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
+ });
+ }, onComplete);
+ });
+ this.queueUpdate();
+
+ return totalChunks;
+ }
+
+ protected final Long2IntOpenHashMap holdingChunks = new Long2IntOpenHashMap();
+ protected final LongArrayList postWorkTicketRelease = new LongArrayList();
+
+ private void addLightWorkTicket(int chunkX, int chunkZ) {
+ final long coordinate = net.minecraft.server.MCUtil.getCoordinateKey(chunkX, chunkZ);
+ final int current = this.holdingChunks.addTo(coordinate, 1);
+ if (current == 0) {
+ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate);
+ this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos,
+ net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos);
+ }
+ }
+
+ protected final void releaseLightWorkChunk(int chunkX, int chunkZ) {
+ final long coordinate = net.minecraft.server.MCUtil.getCoordinateKey(chunkX, chunkZ);
+ final int current = this.holdingChunks.get(coordinate);
+ if (current == 1) {
+ this.holdingChunks.remove(coordinate);
+ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate);
+ this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos,
+ net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos);
+ } else {
+ this.holdingChunks.put(coordinate, current - 1);
+ }
+ }
+
+ protected final CompletableFuture<IChunkAccess> acquireLightWorkChunk(int chunkX, int chunkZ) {
+ ChunkProviderServer chunkProvider = this.theLightEngine.getWorld().getChunkProvider();
+ PlayerChunkMap chunkMap = chunkProvider.playerChunkMap;
+ int targetLevel = net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT);
+
+ this.addLightWorkTicket(chunkX, chunkZ);
+
+ PlayerChunk playerChunk = chunkMap.getUpdatingChunk(net.minecraft.server.MCUtil.getCoordinateKey(chunkX, chunkZ));
+ ChunkStatus holderStatus;
+ if (playerChunk == null || playerChunk.getTicketLevel() > targetLevel ||
+ (holderStatus = playerChunk.getChunkHolderStatus()) == null ||
+ !holderStatus.isAtLeastStatus(ChunkStatus.LIGHT)) {
+ CompletableFuture<IChunkAccess> ret = new CompletableFuture<>();
+
+ chunkProvider.getChunkAtAsynchronously(chunkX, chunkZ, ChunkStatus.LIGHT, true, false, ret::complete);
+
+ return ret;
+ }
+
+ return CompletableFuture.completedFuture(playerChunk.getAvailableChunkNow());
}
+ // note: task is discarded if the chunk is not at light status or if the chunk is not lit
+ protected final void scheduleLightWorkTask(int chunkX, int chunkZ, LightEngineThreaded.Update type, Runnable task) {
+ IChunkAccess current = this.getChunk(chunkX, chunkZ);
+
+ if (current == null || !current.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+ return;
+ }
+
+ if (current.getChunkStatus() != 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)
+ this.scheduleTask(chunkX, chunkZ, type, task);
+ return;
+ }
+
+ if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ this.playerChunkMap.mainInvokingExecutor.execute(() -> {
+ this.scheduleLightWorkTask(chunkX, chunkZ, type, task);
+ });
+ return;
+ }
+
+ this.acquireLightWorkChunk(chunkX, chunkZ).whenCompleteAsync((chunk, throwable) -> {
+ if (throwable != null) {
+ LOGGER.fatal("Failed to load light chunk for light work", throwable);
+ this.releaseLightWorkChunk(chunkX, chunkZ);
+ } else {
+ this.scheduleTask(chunkX, chunkZ, type, () -> {
+ try {
+ task.run();
+ } finally {
+ this.postWorkTicketRelease.add(net.minecraft.server.MCUtil.getCoordinateKey(chunkX, chunkZ));
+ }
+ });
+ }
+ }, this.playerChunkMap.mainInvokingExecutor);
+ }
+
+ // override things from superclass
+
+ @Override
+ public boolean a() {
+ return this.theLightEngine != null ? false : super.a();
+ }
+
+ @Override
+ public net.minecraft.world.level.lighting.LightEngineLayerEventListener a(EnumSkyBlock var0) {
+ if (this.theLightEngine == null) {
+ return super.a(var0);
+ }
+ if (var0 == EnumSkyBlock.BLOCK) {
+ return this.theLightEngine.getBlockReader();
+ } else {
+ return this.theLightEngine.getSkyReader();
+ }
+ }
+
+ @Override
+ public int b(BlockPosition var0, int var1) {
+ if (this.theLightEngine == null) {
+ return super.b(var0, var1);
+ }
+ int var2 = this.theLightEngine.getSkyReader().b(var0) - var1;
+ int var3 = this.theLightEngine.getBlockReader().b(var0);
+ return Math.max(var3, var2);
+ }
+ // Tuinity end - replace light engine impl
+
public void close() {}
@Override
@@ -192,6 +376,15 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
public void a(BlockPosition blockposition) {
BlockPosition blockposition1 = blockposition.immutableCopy();
+ // Tuinity start - replace light engine impl
+ if (this.theLightEngine != null) {
+ this.scheduleLightWorkTask(blockposition1.getX() >> 4, blockposition1.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, () -> {
+ this.theLightEngine.blockChange(blockposition1);
+ });
+ return;
+ }
+ // Tuinity start - replace light engine impl
+
this.a(blockposition.getX() >> 4, blockposition.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, SystemUtils.a(() -> {
super.a(blockposition1);
}, () -> {
@@ -200,6 +393,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
}
protected void a(ChunkCoordIntPair chunkcoordintpair) {
+ // Tuinity start - replace light impl
+ if (this.theLightEngine != null) {
+ return;
+ }
+ // Tuinity end - replace light impl
this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> {
return 0;
}, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -224,6 +422,14 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
@Override
public void a(SectionPosition sectionposition, boolean flag) {
+ // Tuinity start - replace light engine impl
+ if (this.theLightEngine != null) {
+ this.scheduleLightWorkTask(sectionposition.getX(), sectionposition.getZ(), LightEngineThreaded.Update.POST_UPDATE, () -> {
+ this.theLightEngine.sectionChange(sectionposition, flag);
+ });
+ return;
+ }
+ // Tuinity start - replace light engine impl
this.a(sectionposition.a(), sectionposition.c(), () -> {
return 0;
}, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -235,6 +441,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
@Override
public void a(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
+ // Tuinity start - replace light impl
+ if (this.theLightEngine != null) {
+ return;
+ }
+ // Tuinity end - replace light impl
this.a(chunkcoordintpair.x, chunkcoordintpair.z, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
super.a(chunkcoordintpair, flag);
}, () -> {
@@ -244,6 +455,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
@Override
public void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition, @Nullable NibbleArray nibblearray, boolean flag) {
+ // Tuinity start - replace light impl
+ if (this.theLightEngine != null) {
+ return;
+ }
+ // Tuinity end - replace light impl
this.a(sectionposition.a(), sectionposition.c(), () -> {
return 0;
}, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -253,6 +469,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
}));
}
+ private void scheduleTask(int x, int z, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) { this.a(x, z, lightenginethreaded_update, runnable); } // Tuinity - OBFHELPER
private void a(int i, int j, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) {
this.a(i, j, this.d.c(ChunkCoordIntPair.pair(i, j)), lightenginethreaded_update, runnable);
}
@@ -265,6 +482,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
@Override
public void b(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
+ // Tuinity start - replace light impl
+ if (this.theLightEngine != null) {
+ return;
+ }
+ // Tuinity end - replace light impl
this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> {
return 0;
}, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -277,6 +499,35 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
public CompletableFuture<IChunkAccess> a(IChunkAccess ichunkaccess, boolean flag) {
ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos();
+ // Tuinity start - replace light engine
+ if (this.theLightEngine != null) {
+ // make the completion of this future only depend on pre-update execution
+ // post-update is used for block updates, so by using pre here we prioritise chunk lighting
+ return CompletableFuture.supplyAsync(() -> {
+ Boolean[] emptySections = com.tuinity.tuinity.chunk.light.StarLightEngine.getEmptySectionsForChunk(ichunkaccess);
+ if (!flag) {
+ ichunkaccess.setLit(false);
+ this.theLightEngine.lightChunk(ichunkaccess, emptySections);
+ ichunkaccess.setLit(true);
+ } else {
+ this.theLightEngine.forceLoadInChunk(ichunkaccess, emptySections);
+ this.theLightEngine.checkChunkEdges(chunkcoordintpair.x, chunkcoordintpair.z);
+ }
+
+ // safe to move the release light to here, as all the work required is done now
+ this.playerChunkMap.removeLightTicket(chunkcoordintpair); // copied from below
+ return ichunkaccess;
+ }, (runnable) -> {
+ LightEngineThreaded.this.scheduleTask(chunkcoordintpair.x, chunkcoordintpair.z, LightEngineThreaded.Update.PRE_UPDATE, runnable);
+ LightEngineThreaded.this.queueUpdate();
+ }).whenComplete((IChunkAccess chunk, Throwable throwable) -> {
+ if (throwable != null && !(throwable instanceof ThreadDeath)) {
+ LOGGER.fatal("Failed to light chunk " + ichunkaccess.getPos().toString() + " in world '" + this.theLightEngine.getWorld().getWorld().getName() + "'", throwable);
+ }
+ });
+ }
+ // Tuinity end - replace light engine
+
// Paper start
//ichunkaccess.b(false); // Don't need to disable this
long pair = chunkcoordintpair.pair();
@@ -324,7 +575,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
}
public void queueUpdate() {
- if ((!this.queue.isEmpty() || super.a()) && this.g.compareAndSet(false, true)) { // Paper
+ if ((!this.queue.isEmpty() || (this.theLightEngine == null && super.a())) && this.g.compareAndSet(false, true)) { // Paper // Tuinity - replace light impl
this.b.a((() -> { // Paper - decompile error
this.b();
this.g.set(false);
@@ -338,17 +589,36 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
private final java.util.List<Runnable> pre = new java.util.ArrayList<>();
private final java.util.List<Runnable> post = new java.util.ArrayList<>();
private void b() {
+ //final long start = System.nanoTime(); // TODO remove debug
if (queue.poll(pre, post)) {
pre.forEach(Runnable::run);
pre.clear();
- super.a(Integer.MAX_VALUE, true, true);
+ if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl
post.forEach(Runnable::run);
post.clear();
} else {
// might have level updates to go still
- super.a(Integer.MAX_VALUE, true, true);
+ if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl
+ }
+ // Tuinity start - replace light impl
+ if (this.theLightEngine != null) {
+ this.theLightEngine.propagateChanges();
+ if (!this.postWorkTicketRelease.isEmpty()) {
+ LongArrayList copy = this.postWorkTicketRelease.clone();
+ this.postWorkTicketRelease.clear();
+ this.playerChunkMap.mainInvokingExecutor.execute(() -> {
+ LongIterator iterator = copy.iterator();
+ while (iterator.hasNext()) {
+ long coordinate = iterator.nextLong();
+ this.releaseLightWorkChunk(net.minecraft.server.MCUtil.getCoordinateX(coordinate), net.minecraft.server.MCUtil.getCoordinateZ(coordinate));
+ }
+ });
+ }
}
+ // Tuinity end - replace light impl
// Paper end
+ //final long end = System.nanoTime(); // TODO remove debug
+ //System.out.println("Block updates took " + (end - start) * 1.0e-6 + "ms"); // TODO remove debug
}
public void a(int i) {
diff --git a/src/main/java/net/minecraft/server/level/PlayerChunk.java b/src/main/java/net/minecraft/server/level/PlayerChunk.java
index a323b76f68c273a73cb3f20167a668b2100f4944..86f156587a0939b28c5cf6f64907255c1c4f8b35 100644
--- a/src/main/java/net/minecraft/server/level/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/level/PlayerChunk.java
@@ -3,6 +3,7 @@ package net.minecraft.server.level;
import com.mojang.datafixers.util.Either;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
import it.unimi.dsi.fastutil.shorts.ShortArraySet;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; // Tuinity
import it.unimi.dsi.fastutil.shorts.ShortSet;
import java.util.List;
import java.util.Optional;
@@ -54,7 +55,7 @@ public class PlayerChunk {
private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> entityTickingFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
private CompletableFuture<IChunkAccess> chunkSave;
public int oldTicketLevel;
- private int ticketLevel;
+ private int ticketLevel; public final void setTicketLevel(final int level) { this.ticketLevel = level; } // Tuinity - OBFHELPER
volatile int n; public final int getCurrentPriority() { return n; } // Paper - OBFHELPER - make volatile since this is concurrently accessed
public final ChunkCoordIntPair location; // Paper - private -> public
private boolean p;
@@ -82,6 +83,12 @@ public class PlayerChunk {
long key = net.minecraft.server.MCUtil.getCoordinateKey(this.location);
this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+ // Tuinity start - optimise checkDespawn
+ Chunk chunk = this.getFullChunkIfCached();
+ if (chunk != null) {
+ chunk.updateGeneralAreaCache();
+ }
+ // Tuinity end - optimise checkDespawn
}
// Paper end - optimise isOutsideOfRange
// Paper start - optimize chunk status progression without jumping through thread pool
@@ -392,13 +399,13 @@ public class PlayerChunk {
if (!blockposition.isValidLocation()) return; // Paper - SPIGOT-6086 for all invalid locations; avoid acquiring locks
Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
- if (chunk != null) {
+ if (chunk != null && (blockposition.getY() >= 0 && blockposition.getY() <= 255)) { // Tuinity - updates cannot happen in sections that don't exist
byte b0 = (byte) SectionPosition.a(blockposition.getY());
if (b0 < 0 || b0 >= this.dirtyBlocks.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
if (this.dirtyBlocks[b0] == null) {
this.p = true;
- this.dirtyBlocks[b0] = new ShortArraySet();
+ this.dirtyBlocks[b0] = new ShortOpenHashSet(); // Tuinity - use a set to make setting constant-time
}
this.dirtyBlocks[b0].add(SectionPosition.b(blockposition));
@@ -407,13 +414,14 @@ public class PlayerChunk {
public void a(EnumSkyBlock enumskyblock, int i) {
Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
+ if (this.getAvailableChunkNow() != null) this.getAvailableChunkNow().setNeedsSaving(true); // Tuinity - always mark as needing saving
if (chunk != null) {
chunk.setNeedsSaving(true);
if (enumskyblock == EnumSkyBlock.SKY) {
- this.s |= 1 << i - -1;
+ this.s |= 1 << (i - -1); // Tuinity - fix mojang oopsie
} else {
- this.r |= 1 << i - -1;
+ this.r |= 1 << (i - -1); // Tuinity - fix mojang oopsie
}
}
@@ -451,7 +459,7 @@ public class PlayerChunk {
this.a(world, blockposition, iblockdata);
} else {
ChunkSection chunksection = chunk.getSections()[sectionposition.getY()];
- if (chunksection == null) chunksection = new ChunkSection(sectionposition.getY(), chunk, world, true); // Paper - make a new chunk section if none was found
+ //if (chunksection == null) chunksection = new ChunkSection(sectionposition.getY(), chunk, world, true); // Paper - make a new chunk section if none was found // Tuinity - handled better by spigot
PacketPlayOutMultiBlockChange packetplayoutmultiblockchange = new PacketPlayOutMultiBlockChange(sectionposition, shortset, chunksection, this.x);
this.a(packetplayoutmultiblockchange, false);
@@ -493,7 +501,7 @@ public class PlayerChunk {
// 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<EntityPlayer> players = viewDistanceMap.getObjectsInRange(this.location);
if (players == null) {
return;
@@ -507,6 +515,7 @@ public class PlayerChunk {
continue;
}
EntityPlayer player = (EntityPlayer)temp;
+ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.location.x, this.location.z)) continue; // Tuinity - replace player chunk management
int viewDistance = viewDistanceMap.getLastViewDistance(player);
long lastPosition = viewDistanceMap.getLastCoordinate(player);
@@ -526,6 +535,7 @@ public class PlayerChunk {
continue;
}
EntityPlayer player = (EntityPlayer)temp;
+ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.location.x, this.location.z)) continue; // Tuinity - replace player chunk management
player.playerConnection.sendPacket(packet);
}
}
@@ -534,6 +544,7 @@ public class PlayerChunk {
// Paper end - per player view distance
}
+ public final CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getOrCreateFuture(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) { return this.a(chunkstatus, playerchunkmap); } // Tuinity - OBFHELPER
public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> a(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) {
int i = chunkstatus.c();
CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = (CompletableFuture) this.statusFutures.get(i);
@@ -588,7 +599,11 @@ public class PlayerChunk {
this.ticketLevel = i;
}
+ protected long updateCount; // Tuinity - correctly handle recursion
+ protected final void handleLevelUpdate(PlayerChunkMap playerchunkmap) { this.a(playerchunkmap); } // Tuinity - OBFHELPER
protected void a(PlayerChunkMap playerchunkmap) {
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticket level update"); // Tuinity
+ long updateCount = ++this.updateCount; // Tuinity - correctly handle recursion
ChunkStatus chunkstatus = getChunkStatus(this.oldTicketLevel);
ChunkStatus chunkstatus1 = getChunkStatus(this.ticketLevel);
boolean flag = this.oldTicketLevel <= PlayerChunkMap.GOLDEN_TICKET;
@@ -598,7 +613,8 @@ public class PlayerChunk {
// CraftBukkit start
// ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
if (playerchunk_state.isAtLeast(PlayerChunk.State.BORDER) && !playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) {
- this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main
+ this.getStatusFutureUnchecked(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
Chunk chunk = (Chunk)either.left().orElse(null);
if (chunk != null) {
playerchunkmap.callbackExecutor.execute(() -> {
@@ -617,6 +633,12 @@ public class PlayerChunk {
// Run callback right away if the future was already done
playerchunkmap.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;
@@ -663,7 +685,8 @@ public class PlayerChunk {
if (!flag2 && flag3) {
// Paper start - cache ticking ready status
int expectCreateCount = ++this.fullChunkCreateCount;
- this.fullChunkFuture = playerchunkmap.b(this); ensureMain(this.fullChunkFuture).thenAccept((either) -> { // Paper - ensure main
+ this.fullChunkFuture = playerchunkmap.b(this); this.fullChunkFuture.thenAccept((either) -> { // Paper - ensure main // Tuinity - always fired on main
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full chunk future completion"); // Tuinity
if (either.left().isPresent() && PlayerChunk.this.fullChunkCreateCount == expectCreateCount) {
// note: Here is a very good place to add callbacks to logic waiting on this.
Chunk fullChunk = either.left().get();
@@ -694,7 +717,8 @@ public class PlayerChunk {
if (!flag4 && flag5) {
// Paper start - cache ticking ready status
- this.tickingFuture = playerchunkmap.a(this); ensureMain(this.tickingFuture).thenAccept((either) -> { // Paper - ensure main
+ this.tickingFuture = playerchunkmap.a(this); this.tickingFuture.thenAccept((either) -> { // Paper - ensure main // Tuinity - always completed on main
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticking chunk future completion"); // Tuinity
if (either.left().isPresent()) {
// note: Here is a very good place to add callbacks to logic waiting on this.
Chunk tickingChunk = either.left().get();
@@ -704,6 +728,9 @@ public class PlayerChunk {
// Paper start - rewrite ticklistserver
PlayerChunk.this.chunkMap.world.onChunkSetTicking(PlayerChunk.this.location.x, PlayerChunk.this.location.z);
// Paper end - rewrite ticklistserver
+ // Tuinity start - ticking chunk set
+ PlayerChunk.this.chunkMap.world.getChunkProvider().tickingChunks.add(tickingChunk);
+ // Tuinity end - ticking chunk set
}
});
@@ -714,6 +741,12 @@ public class PlayerChunk {
if (flag4 && !flag5) {
this.tickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
this.tickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
+ // Tuinity start - ticking chunk set
+ Chunk chunkIfCached = this.getFullChunkIfCached();
+ if (chunkIfCached != null) {
+ this.chunkMap.world.getChunkProvider().tickingChunks.remove(chunkIfCached);
+ }
+ // Tuinity end - ticking chunk set
}
boolean flag6 = playerchunk_state.isAtLeast(PlayerChunk.State.ENTITY_TICKING);
@@ -725,13 +758,16 @@ public class PlayerChunk {
}
// Paper start - cache ticking ready status
- this.entityTickingFuture = playerchunkmap.b(this.location); ensureMain(this.entityTickingFuture).thenAccept((either) -> { // Paper ensureMain
+ this.entityTickingFuture = playerchunkmap.b(this.location); this.entityTickingFuture.thenAccept((either) -> { // Paper ensureMain // Tuinity - always completed on main
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async entity ticking chunk future completion"); // Tuinity
if (either.left().isPresent()) {
// note: Here is a very good place to add callbacks to logic waiting on this.
Chunk entityTickingChunk = either.left().get();
PlayerChunk.this.isEntityTickingReady = true;
-
+ // Tuinity start - entity ticking chunk set
+ PlayerChunk.this.chunkMap.world.getChunkProvider().entityTickingChunks.add(entityTickingChunk);
+ // Tuinity end - entity ticking chunk set
}
@@ -743,6 +779,12 @@ public class PlayerChunk {
if (flag6 && !flag7) {
this.entityTickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
this.entityTickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
+ // Tuinity start - entity ticking chunk set
+ Chunk chunkIfCached = this.getFullChunkIfCached();
+ if (chunkIfCached != null) {
+ this.chunkMap.world.getChunkProvider().entityTickingChunks.remove(chunkIfCached);
+ }
+ // Tuinity end - entity ticking chunk set
}
// Paper start - raise IO/load priority if priority changes, use our preferred priority
@@ -768,7 +810,8 @@ public class PlayerChunk {
// CraftBukkit start
// ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
if (!playerchunk_state.isAtLeast(PlayerChunk.State.BORDER) && playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) {
- this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main
+ this.getStatusFutureUnchecked(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
Chunk chunk = (Chunk)either.left().orElse(null);
if (chunk != null) {
playerchunkmap.callbackExecutor.execute(() -> {
diff --git a/src/main/java/net/minecraft/server/level/PlayerChunkMap.java b/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
index 6835401bd7863bbd655502547a9fd4ae0f298da1..d827e18d8c3b9b5869dbb1e233f415ba0efb7c1b 100644
--- a/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
@@ -103,6 +103,7 @@ import net.minecraft.world.level.storage.WorldDataServer;
import net.minecraft.world.level.storage.WorldPersistentData;
import net.minecraft.world.phys.Vec3D;
import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; // Paper
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap; // Tuinity
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -115,8 +116,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
private static final Logger LOGGER = LogManager.getLogger();
public static final int GOLDEN_TICKET = 33 + ChunkStatus.b();
// Paper start - faster copying
- public final Long2ObjectLinkedOpenHashMap<PlayerChunk> updatingChunks = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<>(); // Paper - faster copying
- public final Long2ObjectLinkedOpenHashMap<PlayerChunk> visibleChunks = new ProtectedVisibleChunksMap(); // Paper - faster copying
+ // Tuinity start - Don't copy
+ public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<PlayerChunk> updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>();
+ // Tuinity end - Don't copy
private class ProtectedVisibleChunksMap extends com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk> {
@Override
@@ -139,8 +141,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
}
// Paper end
- public final com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk> pendingVisibleChunks = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk>(); // Paper - this is used if the visible chunks is updated while iterating only
- public transient com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk> visibleChunksClone; // Paper - used for async access of visible chunks, clone and cache only when needed
+ // Tuinity - Don't copy
private final Long2ObjectLinkedOpenHashMap<PlayerChunk> pendingUnload;
public final LongSet loadedChunks; // Paper - private -> public
public final WorldServer world;
@@ -176,31 +177,28 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
// CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
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.ArrayDeque<Runnable> queued = new java.util.ArrayDeque<>();
+ // Tuinity start - revert paper's change
+ private Runnable queued;
@Override
public void execute(Runnable runnable) {
AsyncCatcher.catchOp("Callback Executor execute");
- if (queued == null) {
- queued = 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");
}
- queued.add(runnable);
+ queued = runnable;
}
+ // Tuinity end - revert paper's change
@Override
public void run() {
AsyncCatcher.catchOp("Callback Executor run");
- if (queued == null) {
- return;
- }
- java.util.ArrayDeque<Runnable> queue = queued;
+ // Tuinity start - revert paper's change
+ Runnable task = queued;
queued = null;
- Runnable task;
- while ((task = queue.pollFirst()) != null) {
+ if (task != null) {
+ // Tuinity end - revert paper's change
task.run();
}
}
@@ -220,7 +218,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
// 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().applyTrackingRangeScale(vanilla);
@@ -238,23 +236,17 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
// Paper end - optimise PlayerChunkMap#isOutsideRange
// 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;
+ public final com.tuinity.tuinity.chunk.PlayerChunkLoader playerChunkManager = new com.tuinity.tuinity.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Tuinity - replace chunk loader
// Paper end - no-tick view distance
+ // 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(EntityPlayer player) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity
int chunkX = MCUtil.getChunkCoordinate(player.locX());
int chunkZ = MCUtil.getChunkCoordinate(player.locZ());
// Note: players need to be explicitly added to distance maps before they can be updated
@@ -263,28 +255,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
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
this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, ChunkMapDistance.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.cannotLoadChunks(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;
+ this.playerChunkManager.addPlayer(player); // Tuinity - replace chunk loader
// Paper end - no-tick view distance
+ // Tuinity start - optimise checkDespawn
+ this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS);
+ // Tuinity end - optimise checkDespawn
}
void removePlayerFromDistanceMaps(EntityPlayer player) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity
// Paper start - use distance map to optimise tracker
for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
this.playerEntityTrackerTrackMaps[i].remove(player);
@@ -295,13 +281,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
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);
+ this.playerChunkManager.removePlayer(player); // Tuinity - replace chunk loader
// Paper end - no-tick view distance
+ // Tuinity start - optimise checkDespawn
+ this.playerGeneralAreaMap.remove(player);
+ // Tuinity end - optimise checkDespawn
}
void updateMaps(EntityPlayer player) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity
int chunkX = MCUtil.getChunkCoordinate(player.locX());
int chunkZ = MCUtil.getChunkCoordinate(player.locZ());
// Note: players need to be explicitly added to distance maps before they can be updated
@@ -310,27 +298,124 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
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, ChunkMapDistance.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
+ // Paper end - no-tick view distance
+ // Tuinity start - optimise checkDespawn
+ this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS);
+ // Tuinity end - optimise checkDespawn
+ }
+ // Paper end
- if (!this.cannotLoadChunks(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)
+ // 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 {
+
+ // Tuinity start - optimise notify()
+ private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<EntityInsentient> navigators;
+
+ public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<EntityInsentient> getNavigators() {
+ return this.navigators;
}
- player.needsChunkCenterUpdate = true;
- this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured
- player.needsChunkCenterUpdate = false;
- // Paper end - no-tick view distance
+ public boolean addToNavigators(final EntityInsentient navigator) {
+ if (this.navigators == null) {
+ this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>();
+ }
+ return this.navigators.add(navigator);
+ }
+
+ public boolean removeFromNavigators(final EntityInsentient navigator) {
+ if (this.navigators == null) {
+ return false;
+ }
+ return this.navigators.remove(navigator);
+ }
+ // Tuinity end - optimise notify()
+
}
- // Paper end
+
+ public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData {
+
+ // Tuinity start - optimise notify()
+ private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<EntityInsentient> navigators;
+
+ public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<EntityInsentient> getNavigators() {
+ return this.navigators;
+ }
+
+ public boolean addToNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final EntityInsentient navigator) {
+ if (this.navigators == null) {
+ this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>();
+ }
+ final boolean ret = this.navigators.add(navigator);
+ if (ret) {
+ final DataRegionData data = (DataRegionData)section.getRegion().regionData;
+ if (!data.addToNavigators(navigator)) {
+ throw new IllegalStateException();
+ }
+ }
+ return ret;
+ }
+
+ public boolean removeFromNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final EntityInsentient navigator) {
+ if (this.navigators == null) {
+ return false;
+ }
+ final boolean ret = this.navigators.remove(navigator);
+ if (ret) {
+ final DataRegionData data = (DataRegionData)section.getRegion().regionData;
+ if (!data.removeFromNavigators(navigator)) {
+ throw new IllegalStateException();
+ }
+ }
+ return ret;
+ }
+ // Tuinity end - optimise notify()
+
+ @Override
+ public void removeFromRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section,
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region from) {
+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
+ final DataRegionData fromData = (DataRegionData)from.regionData;
+ // Tuinity start - optimise notify()
+ if (sectionData.navigators != null) {
+ for (final Iterator<EntityInsentient> iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
+ if (!fromData.removeFromNavigators(iterator.next())) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ // Tuinity end - optimise notify()
+ }
+
+ @Override
+ public void addToRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section,
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region oldRegion,
+ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region newRegion) {
+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
+ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData;
+ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData;
+ // Tuinity start - optimise notify()
+ if (sectionData.navigators != null) {
+ for (final Iterator<EntityInsentient> iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
+ if (!newRegionData.addToNavigators(iterator.next())) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ // Tuinity end - optimise notify()
+ }
+ }
+ // Tuiniy end
private final java.util.concurrent.ExecutorService lightThread;
public PlayerChunkMap(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, IAsyncTaskHandler<Runnable> iasynctaskhandler, ILightAccess ilightaccess, ChunkGenerator chunkgenerator, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier, int i, boolean flag) {
@@ -365,9 +450,10 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
this.worldLoadListener = worldloadlistener;
// Paper start - use light thread
+ String threadName = ((WorldDataServer)this.world.getWorldData()).getName() + " - Light"; // Tuinity - make sure playerchunkmap instance is not retained by the thread factory
ThreadedMailbox<Runnable> lightthreaded; ThreadedMailbox<Runnable> threadedmailbox1 = lightthreaded = ThreadedMailbox.a(lightThread = java.util.concurrent.Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r);
- thread.setName(((WorldDataServer)world.getWorldData()).getName() + " - Light");
+ thread.setName(threadName); // Tuinity - make sure playerchunkmap instance is not retained by the thread factory
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY+1);
return thread;
@@ -457,48 +543,29 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
// Paper end - optimise PlayerChunkMap#isOutsideRange
// Paper start - no-tick view distance
this.setNoTickViewDistance(this.world.paperConfig.noTickViewDistance);
- this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+ // Tuinity - replace chunk loading system
+ // Paper end - no-tick view distance
+ // Tuinity start
+ this.dataRegionManager = new com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager(this.world, 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,
(EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
- checkHighPriorityChunks(player);
- if (newState.size() != 1) {
- return;
+ Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+ if (chunk != null) {
+ chunk.updateGeneralAreaCache(newState);
}
- Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ);
- if (chunk == null || !chunk.areNeighboursLoaded(2)) {
- return;
- }
-
- ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
- PlayerChunkMap.this.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update
},
(EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
- if (newState != null) {
- return;
+ Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+ if (chunk != null) {
+ chunk.updateGeneralAreaCache(newState);
}
- ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ);
- PlayerChunkMap.this.world.getChunkProvider().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update
- PlayerChunkMap.this.world.getChunkProvider().clearPriorityTickets(chunkPos);
- }, (player, prevPos, newPos) -> {
- player.lastHighPriorityChecked = -1; // reset and recheck
- checkHighPriorityChunks(player);
- });
- this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
- this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
- (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
- if (player.needsChunkCenterUpdate) {
- player.needsChunkCenterUpdate = false;
- player.playerConnection.sendPacket(new PacketPlayOutViewCentre(currPosX, currPosZ));
- }
- PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded
- },
- (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
- PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), null, true, false); // unloaded, loaded
});
- // Paper end - no-tick view distance
+ // Tuinity end - optimise checkDespawn
}
// Paper start - Chunk Prioritization
public void queueHolderUpdate(PlayerChunk playerchunk) {
@@ -531,6 +598,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
public void checkHighPriorityChunks(EntityPlayer 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;
@@ -538,7 +606,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
player.lastHighPriorityChecked = currentTick;
Long2IntOpenHashMap priorities = new Long2IntOpenHashMap();
- int viewDistance = getEffectiveNoTickViewDistance();
+ int viewDistance = 10;//int viewDistance = getEffectiveNoTickViewDistance(); // Tuinity - replace player chunk loader
BlockPosition.MutableBlockPosition pos = new BlockPosition.MutableBlockPosition();
// Prioritize circular near
@@ -604,7 +672,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
private boolean shouldSkipPrioritization(ChunkCoordIntPair coord) {
- if (playerViewDistanceNoTickMap.getObjectsInRange(coord.pair()) == null) return true;
+ if (true) return true; // Tuinity - replace player chunk loader - unused outside paper player loader logic
PlayerChunk chunk = getUpdatingChunk(coord.pair());
return chunk != null && (chunk.isFullChunkReady());
}
@@ -667,7 +735,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
@Nullable
public PlayerChunk getUpdatingChunk(long i) { // Paper
- return (PlayerChunk) this.updatingChunks.get(i);
+ return this.updatingChunks.getUpdating(i); // Tuinity - Don't copy
}
// Paper start - remove cloning of visible chunks unless accessed as a collection async
@@ -675,47 +743,25 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
private boolean isIterating = false;
private boolean hasPendingVisibleUpdate = false;
public void forEachVisibleChunk(java.util.function.Consumer<PlayerChunk> consumer) {
- org.spigotmc.AsyncCatcher.catchOp("forEachVisibleChunk");
- boolean prev = isIterating;
- isIterating = true;
- try {
- for (PlayerChunk value : this.visibleChunks.values()) {
- consumer.accept(value);
- }
- } finally {
- this.isIterating = prev;
- if (!this.isIterating && this.hasPendingVisibleUpdate) {
- ((ProtectedVisibleChunksMap)this.visibleChunks).copyFrom(this.pendingVisibleChunks);
- this.pendingVisibleChunks.clear();
- this.hasPendingVisibleUpdate = false;
- }
- }
+ throw new UnsupportedOperationException(); // Tuinity - Don't copy
}
public Long2ObjectLinkedOpenHashMap<PlayerChunk> getVisibleChunks() {
- if (Thread.currentThread() == this.world.serverThread) {
- return this.visibleChunks;
- } else {
- synchronized (this.visibleChunks) {
- if (DEBUG_ASYNC_VISIBLE_CHUNKS) new Throwable("Async getVisibleChunks").printStackTrace();
- if (this.visibleChunksClone == null) {
- this.visibleChunksClone = this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.clone() : ((ProtectedVisibleChunksMap)this.visibleChunks).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 PlayerChunk getVisibleChunk(long i) { // Paper - protected -> public
- // Paper start - mt safe get
- if (Thread.currentThread() != this.world.serverThread) {
- synchronized (this.visibleChunks) {
- return (PlayerChunk) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(i) : ((ProtectedVisibleChunksMap)this.visibleChunks).safeGet(i));
- }
+ // Tuinity start - Don't copy
+ if (Thread.currentThread() == this.world.serverThread) {
+ return this.updatingChunks.getVisible(i);
}
- return (PlayerChunk) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(i) : ((ProtectedVisibleChunksMap)this.visibleChunks).safeGet(i));
- // Paper end
+ return this.updatingChunks.getVisibleAsync(i);
+ // Tuinity end - Don't copy
}
protected final IntSupplier getPrioritySupplier(long i) { return c(i); } // Paper - OBFHELPER
@@ -811,6 +857,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
@Nullable
private PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Chunk holder update"); // Tuinity
+ if (this.unloadingPlayerChunk) { MinecraftServer.LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity
if (k > PlayerChunkMap.GOLDEN_TICKET && j > PlayerChunkMap.GOLDEN_TICKET) {
return playerchunk;
} else {
@@ -833,9 +881,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
playerchunk.a(j);
} else {
playerchunk = new PlayerChunk(new ChunkCoordIntPair(i), j, this.lightEngine, this.p, this);
+ // Tuinity start
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
+ this.regionManagers.get(index).addChunk(playerchunk.location.x, playerchunk.location.z);
+ }
+ // Tuinity end
}
+ this.getVillagePlace().dequeueUnload(playerchunk.location.pair()); // Tuinity - unload POI data
- this.updatingChunks.put(i, playerchunk);
+ this.updatingChunks.queueUpdate(i, playerchunk); // Tuinity - Don't copy
this.updatingChunksModified = true;
}
@@ -959,7 +1013,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
- private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
+ public static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more // Tuinity - private -> public
protected void unloadChunks(BooleanSupplier booleansupplier) {
GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
@@ -988,7 +1042,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
while (longiterator.hasNext()) { // Spigot
long j = longiterator.nextLong();
longiterator.remove(); // Spigot
- PlayerChunk playerchunk = (PlayerChunk) this.updatingChunks.remove(j);
+ PlayerChunk playerchunk = this.updatingChunks.queueRemove(j); // Tuinity - Don't copy
if (playerchunk != null) {
this.pendingUnload.put(j, playerchunk);
@@ -1025,7 +1079,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, 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.isNeedsSaving()) {
return;
@@ -1059,7 +1113,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
asyncSaveData = ChunkRegionLoader.getAsyncSaveData(this.world, chunk);
}
- this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY,
+ this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, // Tuinity - use normal priority
asyncSaveData, chunk);
chunk.setLastSaved(this.world.getTime());
@@ -1067,6 +1121,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
// Paper end
+ boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks
+
private void a(long i, PlayerChunk playerchunk) {
CompletableFuture<IChunkAccess> completablefuture = playerchunk.getChunkSave();
Consumer<IChunkAccess> consumer = (ichunkaccess) -> { // CraftBukkit - decompile error
@@ -1075,7 +1131,21 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
if (completablefuture1 != completablefuture) {
this.a(i, playerchunk);
} else {
- if (this.pendingUnload.remove(i, playerchunk) && 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.pendingUnload.remove(i, playerchunk)) && ichunkaccess != null) { // Tuinity end
+ // Tuinity start
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
+ this.regionManagers.get(index).removeChunk(playerchunk.location.x, playerchunk.location.z);
+ }
+ // Tuinity end
+ this.getVillagePlace().queueUnload(playerchunk.location.pair(), MinecraftServer.currentTickLong + 1); // Tuinity - unload POI data
if (ichunkaccess instanceof Chunk) {
((Chunk) ichunkaccess).setLoaded(false);
}
@@ -1098,7 +1168,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
this.lightEngine.a(ichunkaccess.getPos());
this.lightEngine.queueUpdate();
this.worldLoadListener.a(ichunkaccess.getPos(), (ChunkStatus) null);
- }
+ } else if (removed) { // Tuinity start
+ // Tuinity start
+ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
+ this.regionManagers.get(index).removeChunk(playerchunk.location.x, playerchunk.location.z);
+ }
+ // Tuinity end
+ this.getVillagePlace().queueUnload(playerchunk.location.pair(), MinecraftServer.currentTickLong + 1); // Tuinity - unload POI data
+ } // Tuinity end
+ } finally { this.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes while unloading chunks
}
};
@@ -1114,22 +1192,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
protected boolean b() {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update visibleChunks off of the main thread"); // Tuinity
if (!this.updatingChunksModified) {
return false;
} else {
- // Paper start - stop cloning visibleChunks
- synchronized (this.visibleChunks) {
- if (isIterating) {
- hasPendingVisibleUpdate = true;
- this.pendingVisibleChunks.copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk>)this.updatingChunks);
- } else {
- hasPendingVisibleUpdate = false;
- this.pendingVisibleChunks.clear();
- ((ProtectedVisibleChunksMap)this.visibleChunks).copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<PlayerChunk>)this.updatingChunks);
- this.visibleChunksClone = null;
- }
+ // Tuinity start - Don't copy
+ synchronized (this.updatingChunks) {
+ this.updatingChunks.performUpdates();
}
- // Paper end
+ // Tuinity end - Don't copy
this.updatingChunksModified = false;
return true;
@@ -1159,7 +1230,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
if (ichunkaccess.getChunkStatus().b(chunkstatus)) {
CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture1; // Paper
- if (chunkstatus == ChunkStatus.LIGHT) {
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && chunkstatus == ChunkStatus.LIGHT) { // Tuinity - we use edge checks, so loading 1 radius neighbours isn't necessary
completablefuture1 = this.b(playerchunk, chunkstatus);
} else {
completablefuture1 = chunkstatus.a(this.world, this.definedStructureManager, this.lightEngine, (ichunkaccess1) -> {
@@ -1189,6 +1260,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
this.getVillagePlace().loadInData(chunkcoordintpair, chunkHolder.poiData);
chunkHolder.tasks.forEach(Runnable::run);
+ this.getVillagePlace().dequeueUnload(chunkcoordintpair.pair()); // Tuinity
// Paper end
if (chunkHolder.protoChunk != null) {try (Timing ignored2 = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) { // Paper start - timings // Paper - chunk is created async
@@ -1301,9 +1373,13 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
// Paper end
this.mailboxWorldGen.a(ChunkTaskQueueSorter.a(playerchunk, runnable));
- });
+ }).thenComposeAsync((either) -> { // Tuinity start - force competion on the main thread
+ return CompletableFuture.completedFuture(either);
+ }, this.mainInvokingExecutor);
+ // Tuinity end - force competion on the main thread
}
+ protected final void removeLightTicket(ChunkCoordIntPair chunkcoordintpair) { this.c(chunkcoordintpair); } // Tuinity - OBFHELPER
protected void c(ChunkCoordIntPair chunkcoordintpair) {
this.executor.a(SystemUtils.a(() -> {
this.chunkDistanceManager.b(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.a(ChunkStatus.FEATURES), chunkcoordintpair);
@@ -1478,9 +1554,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
chunk.B();
return chunk;
});
- }, (runnable) -> {
- this.mailboxMain.a(ChunkTaskQueueSorter.a(playerchunk, runnable));
- });
+ }, this.executor); // Tuinity - queue to execute immediately so this doesn't delay chunk unloading
}
public int c() {
@@ -1561,39 +1635,27 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
public void setViewDistance(int i) { // Paper - public
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update view distance off of the main thread"); // Tuinity
int j = MathHelper.clamp(i + 1, 3, 33); // Paper - diff on change, these make the lower view distance limit 2 and the upper 32
if (j != this.viewDistance) {
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(MathHelper.clamp(i, 2, 32)); // Tuinity - replace player loader system
}
}
// Paper start - no-tick view distance
public final void setNoTickViewDistance(int viewDistance) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update view distance off of the main thread"); // Tuinity
viewDistance = viewDistance == -1 ? -1 : MathHelper.clamp(viewDistance, 2, 32);
-
- this.noTickViewDistance = viewDistance;
- int loadViewDistance = this.getLoadViewDistance();
- this.chunkDistanceManager.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.world != null && this.world.players != null) { // this can be called from constructor, where these aren't set
- for (EntityPlayer player : this.world.players) {
- PlayerConnection connection = player.playerConnection;
- if (connection != null) {
- // moved in from PlayerList
- connection.sendPacket(new PacketPlayOutViewDistance(loadViewDistance));
- }
- this.updateMaps(player);
- }
- }
+ 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
}
// Paper end - no-tick view distance
- protected void sendChunk(EntityPlayer entityplayer, ChunkCoordIntPair chunkcoordintpair, Packet<?>[] apacket, boolean flag, boolean flag1) {
+ public void sendChunk(EntityPlayer entityplayer, ChunkCoordIntPair chunkcoordintpair, Packet<?>[] apacket, boolean flag, boolean flag1) { // Tuinity - protected -> public
if (entityplayer.world == this.world) {
if (flag1 && !flag) {
PlayerChunk playerchunk = this.getVisibleChunk(chunkcoordintpair.pair());
@@ -1617,7 +1679,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
public int d() {
- return this.visibleChunks.size();
+ return this.updatingChunks.getVisibleMap().size(); // Tuinity - Don't copy
}
protected PlayerChunkMap.a e() {
@@ -1689,7 +1751,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
- com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - writes are async, no need for priority
return;
}
super.write(chunkcoordintpair, nbttagcompound);
@@ -1773,6 +1835,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow();
}
// Paper end
+ // Tuinity start
+ public PlayerChunk getUnloadingPlayerChunk(int chunkX, int chunkZ) {
+ return this.pendingUnload.get(ChunkCoordIntPair.pair(chunkX, chunkZ));
+ }
+ // Tuinity end
// Paper start - async io
@@ -1982,6 +2049,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}*/ // Paper end - replaced by distance map
this.updateMaps(entityplayer); // Paper - distance maps
+ this.playerChunkManager.updatePlayer(entityplayer); // Tuinity - respond to movement immediately
}
@@ -1990,7 +2058,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
// 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<EntityPlayer> inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkcoordintpair);
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> inRange = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkcoordintpair); // Tuinity - replace player chunk loader system
if (inRange == null) {
return Stream.empty();
@@ -2006,8 +2074,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
continue;
}
EntityPlayer player = (EntityPlayer)temp;
- int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player);
- long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player);
+ if (!this.playerChunkManager.isChunkSent(player, chunkcoordintpair.x, chunkcoordintpair.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) - chunkcoordintpair.x);
int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkcoordintpair.z);
@@ -2022,6 +2091,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
continue;
}
EntityPlayer player = (EntityPlayer)temp;
+ if (!this.playerChunkManager.isChunkSent(player, chunkcoordintpair.x, chunkcoordintpair.z)) continue; // Tuinity - replace player chunk management
players.add(player);
}
}
@@ -2237,6 +2307,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
apacket[1] = new PacketPlayOutLightUpdate(chunk.getPos(), this.lightEngine, true);
// Paper start - Fix MC-162253
+ if (com.tuinity.tuinity.config.TuinityConfig.enableMC162253Workaround) { // Tuinity - hide behind config option, this logic has undesirable network effects
final int lightMask = getLightMask(chunk);
int i = 1;
for (int x = -1; x <= 1; x++) {
@@ -2261,10 +2332,12 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
apacket[i] = new PacketPlayOutLightUpdate(new ChunkCoordIntPair(chunk.getPos().x + x, chunk.getPos().z + z), lightEngine, updateLightMask, 0, true);
}
}
+ } // Tuinity - hide behind config option, this logic has undesirable network effects
}
- final int viewDistance = playerViewDistanceBroadcastMap.getLastViewDistance(entityplayer);
- final long lastPosition = playerViewDistanceBroadcastMap.getLastCoordinate(entityplayer);
+ if (com.tuinity.tuinity.config.TuinityConfig.enableMC162253Workaround) { // Tuinity - hide behind config option, this logic has undesirable network effects
+ final int viewDistance = this.playerChunkManager.broadcastMap.getLastViewDistance(entityplayer); // Tuinity - replace player chunk loader
+ final long lastPosition = this.playerChunkManager.broadcastMap.getLastCoordinate(entityplayer); // Tuinity - replace player chunk loader
int j = 1;
for (int x = -1; x <= 1; x++) {
@@ -2289,6 +2362,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
entityplayer.playerConnection.sendPacket(packet);
}
}
+ } // Tuinity - hide behind config option, this logic has undesirable network effects
// Paper end - Fix MC-162253
entityplayer.a(chunk.getPos(), apacket[0], apacket[1]);
@@ -2364,7 +2438,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
// Paper start
// Replace trackedPlayers Set with a Map. The value is true until the player receives
// their first update (which is forced to have absolute coordinates), false afterward.
- public java.util.Map<EntityPlayer, Boolean> trackedPlayerMap = new java.util.HashMap<>();
+ public java.util.Map<EntityPlayer, Boolean> trackedPlayerMap = new Reference2BooleanOpenHashMap<>(); // Tuinity - optimise map impl
public Set<EntityPlayer> trackedPlayers = trackedPlayerMap.keySet();
public EntityTracker(Entity entity, int i, int j, boolean flag) {
@@ -2465,7 +2539,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
double vec3d_dy = entityplayer.locY() - this.tracker.locY();
double vec3d_dz = entityplayer.locZ() - this.tracker.locZ();
// Paper end - remove allocation of Vec3D here
- int i = Math.min(this.b(), (PlayerChunkMap.this.viewDistance - 1) * 16);
+ int i = Math.min(this.b(), (entityplayer.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.tracker.a(entityplayer); // Paper - remove allocation of Vec3D here
if (flag) {
@@ -2475,7 +2549,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(this.tracker.chunkX, this.tracker.chunkZ);
PlayerChunk playerchunk = PlayerChunkMap.this.getVisibleChunk(chunkcoordintpair.pair());
- if (playerchunk != null && playerchunk.getSendingChunk() != null) { // Paper - no-tick view distance
+ if (playerchunk != null && playerchunk.getSendingChunk() != null && PlayerChunkMap.this.playerChunkManager.isChunkSent(entityplayer, MathHelper.floor(this.tracker.locX()) >> 4, MathHelper.floor(this.tracker.locZ()) >> 4)) { // Paper - no-tick view distance // Tuinity - don't broadcast in chunks the player hasn't received
flag1 = PlayerChunkMap.b(chunkcoordintpair, entityplayer, false) <= PlayerChunkMap.this.viewDistance;
}
}
@@ -2515,7 +2589,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
int j = entity.getEntityType().getChunkRange() * 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/PlayerInteractManager.java b/src/main/java/net/minecraft/server/level/PlayerInteractManager.java
index 87722285690d9d3370610e2a2eb809e0d1f497c9..1746a8c1750b494c47f9f46e83b248d8129d2630 100644
--- a/src/main/java/net/minecraft/server/level/PlayerInteractManager.java
+++ b/src/main/java/net/minecraft/server/level/PlayerInteractManager.java
@@ -56,14 +56,29 @@ public class PlayerInteractManager {
private EnumGamemode gamemode;
private EnumGamemode e;
private boolean f;
- private int lastDigTick;
+ private int lastDigTick; private long lastDigTime; // Tuinity - lag compensate block breaking
private BlockPosition h;
private int currentTick;
- private boolean j;
+ private boolean j; private final boolean hasDestroyedTooFast() { return this.j; } // Tuinity - OBFHELPER
private BlockPosition k;
- private int l;
+ private int l; private final int getHasDestroyedTooFastStartTick() { return this.l; } // Tuinity - OBFHELPER
+ private long hasDestroyedTooFastStartTime; // Tuinity - lag compensate block breaking
private int m;
+ // Tuinity start - lag compensate block breaking
+ private int getTimeDiggingLagCompensate() {
+ int lagCompensated = (int)((System.nanoTime() - this.lastDigTime) / (50L * 1000L * 1000L));
+ int tickDiff = this.currentTick - this.lastDigTick;
+ 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.currentTick - this.getHasDestroyedTooFastStartTick();
+ 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 PlayerInteractManager(WorldServer worldserver) {
this.gamemode = EnumGamemode.NOT_SET;
this.e = EnumGamemode.NOT_SET;
@@ -119,7 +134,7 @@ public class PlayerInteractManager {
if (iblockdata == null || iblockdata.isAir()) { // Paper
this.j = false;
} else {
- float f = this.a(iblockdata, this.k, this.l);
+ float f = this.updateBlockBreakAnimation(iblockdata, this.k, this.getTimeDiggingTooFastLagCompensate()); // Tuinity - lag compensate destroying blocks
if (f >= 1.0F) {
this.j = false;
@@ -139,7 +154,7 @@ public class PlayerInteractManager {
this.m = -1;
this.f = false;
} else {
- this.a(iblockdata, this.h, this.lastDigTick);
+ this.updateBlockBreakAnimation(iblockdata, this.h, this.getTimeDiggingLagCompensate()); // Tuinity - lag compensate destroying blocks
}
}
@@ -147,6 +162,12 @@ public class PlayerInteractManager {
private float a(IBlockData iblockdata, BlockPosition blockposition, int i) {
int j = this.currentTick - i;
+ // Tuinity start - change i (startTime) to totalTime
+ return this.updateBlockBreakAnimation(iblockdata, blockposition, j);
+ }
+ private float updateBlockBreakAnimation(IBlockData iblockdata, BlockPosition blockposition, int totalTime) {
+ int j = totalTime;
+ // Tuinity end
float f = iblockdata.getDamage(this.player, this.player.world, blockposition) * (float) (j + 1);
int k = (int) (f * 10.0F);
@@ -214,7 +235,7 @@ public class PlayerInteractManager {
return;
}
- this.lastDigTick = this.currentTick;
+ this.lastDigTick = this.currentTick; this.lastDigTime = System.nanoTime(); // Tuinity - lag compensate block breaking
float f = 1.0F;
iblockdata = this.world.getType(blockposition);
@@ -267,12 +288,12 @@ public class PlayerInteractManager {
int j = (int) (f * 10.0F);
this.world.a(this.player.getId(), blockposition, j);
- this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "actual start of destroying"));
+ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "actual start of destroying")); // Tuinity - on lagging servers this can cause the client to think it's only just started to destroy a block when it already has/will
this.m = j;
}
} else if (packetplayinblockdig_enumplayerdigtype == PacketPlayInBlockDig.EnumPlayerDigType.STOP_DESTROY_BLOCK) {
if (blockposition.equals(this.h)) {
- int k = this.currentTick - this.lastDigTick;
+ int k = this.getTimeDiggingLagCompensate(); // Tuinity - lag compensate block breaking
iblockdata = this.world.getType(blockposition);
if (!iblockdata.isAir()) {
@@ -289,12 +310,18 @@ public class PlayerInteractManager {
this.f = false;
this.j = true;
this.k = blockposition;
- this.l = this.lastDigTick;
+ this.l = this.lastDigTick; 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.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition));
+ } else {
this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "stopped destroying"));
+ }
+ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block
} else if (packetplayinblockdig_enumplayerdigtype == PacketPlayInBlockDig.EnumPlayerDigType.ABORT_DESTROY_BLOCK) {
this.f = false;
if (!Objects.equals(this.h, blockposition) && !BlockPosition.ZERO.equals(this.h)) {
@@ -306,7 +333,7 @@ public class PlayerInteractManager {
}
this.world.a(this.player.getId(), blockposition, -1);
- this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "aborted destroying"));
+ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "aborted destroying")); // Tuinity - this can cause clients on a lagging server to think they stopped destroying a block they're currently destroying
}
}
@@ -316,7 +343,13 @@ public class PlayerInteractManager {
public void a(BlockPosition blockposition, PacketPlayInBlockDig.EnumPlayerDigType packetplayinblockdig_enumplayerdigtype, String s) {
if (this.breakBlock(blockposition)) {
+ // 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.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition));
+ } else {
this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, s));
+ }
+ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block
} else {
this.player.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition)); // CraftBukkit - SPIGOT-5196
}
diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
index 6e5ae954c6eb40590bf8c83f592c22088d489be8..c9b395dd0e4881a2e2e409bf782491b42ff087e6 100644
--- a/src/main/java/net/minecraft/server/level/Ticket.java
+++ b/src/main/java/net/minecraft/server/level/Ticket.java
@@ -5,17 +5,17 @@ import java.util.Objects;
public final class Ticket<T> implements Comparable<Ticket<?>> {
private final TicketType<T> a;
- private final int b;
+ private int b; public final void setTicketLevel(final int value) { this.b = value; } // Tuinity - remove final, add set OBFHELPER
public final T identifier; public final T getObjectReason() { return this.identifier; } // Paper - OBFHELPER
- private long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER
+ private long d; public final long getCreationTick() { return this.d; } public final void setCreationTick(final long value) { this.d = value; } // Paper - OBFHELPER // Tuinity - OBFHELPER
public int priority = 0; // Paper
- public long delayUnloadBy; // Paper
+ boolean isCached; // Tuinity - delay chunk unloads, this defends against really stupid plugins
protected Ticket(TicketType<T> tickettype, int i, T t0) {
this.a = tickettype;
this.b = i;
this.identifier = t0;
- this.delayUnloadBy = tickettype.loadPeriod; // Paper
+ // Tuinity - delay chunk unloads
}
public int compareTo(Ticket<?> ticket) {
@@ -64,8 +64,9 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
this.d = i;
}
+ protected final boolean isExpired(long time) { return this.b(time); } // Tuinity - OBFHELPER
protected boolean b(long i) {
- long j = delayUnloadBy; // Paper
+ long j = this.a.b(); // Tuinity - delay chunk unloads
return j != 0L && i - this.d > j;
}
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
index 3c804c7b20a14ea6e510810e2be10c1cc89ff5c1..3738c51b5e673c439d88a7ef7f4614f338a61c2a 100644
--- a/src/main/java/net/minecraft/server/level/TicketType.java
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
@@ -30,8 +30,22 @@ public class TicketType<T> {
public static final TicketType<Long> ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
public static final TicketType<ChunkCoordIntPair> PRIORITY = a("priority", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
public static final TicketType<ChunkCoordIntPair> URGENT = a("urgent", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
- public static final TicketType<Long> DELAY_UNLOAD = a("delay_unload", Long::compareTo, 300); // Paper
+ public static final TicketType<Long> DELAYED_UNLOAD = a("delayed_unload", Long::compareTo); // Tuinity - delay chunk unloads
+ public static final TicketType<Long> REQUIRED_LOAD = a("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail
+ public static final TicketType<ChunkCoordIntPair> LIGHT_UPDATE = a("light_update", Comparator.comparingLong(ChunkCoordIntPair::pair)); // Tuinity - ensure chunks stay loaded for lighting
+ public static final TicketType<Long> CHUNK_RELIGHT = a("chunk_relight", Long::compareTo); // Tuinity - ensure chunk stays loaded for relighting
+ // Tuinity start - delay chunk unloads
+ boolean delayUnloadViable = true;
+ static {
+ TicketType.LIGHT.delayUnloadViable = false;
+ TicketType.PLUGIN.delayUnloadViable = false;
+ TicketType.PRIORITY.delayUnloadViable = false;
+ TicketType.URGENT.delayUnloadViable = false;
+ TicketType.DELAYED_UNLOAD.delayUnloadViable = false;
+ TicketType.LIGHT_UPDATE.delayUnloadViable = false; // Tuinity - ensure chunks stay loaded for lighting
+ }
+ // Tuinity end - delay chunk unloads
public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
return new TicketType<>(s, comparator, 0L);
}
diff --git a/src/main/java/net/minecraft/server/level/WorldServer.java b/src/main/java/net/minecraft/server/level/WorldServer.java
index f68a252378a94c8fcab622d2d89d738aceab45a4..6fc215df5ed3aa6ef0c23a57e8444602ff9309e8 100644
--- a/src/main/java/net/minecraft/server/level/WorldServer.java
+++ b/src/main/java/net/minecraft/server/level/WorldServer.java
@@ -168,12 +168,13 @@ 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 WorldServer extends World implements GeneratorAccessSeed {
public static final BlockPosition a = new BlockPosition(100, 50, 0);
private static final Logger LOGGER = LogManager.getLogger();
- public final Int2ObjectMap<Entity> entitiesById = new Int2ObjectLinkedOpenHashMap();
+ public final Int2ObjectMap<Entity> entitiesById = new Int2ObjectLinkedOpenHashMap(); final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Entity> entitiesForIteration = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(2048, 0.5f, 2048, 0.2, true); // Tuinity - make removing entities while ticking safe
private final Map<UUID, Entity> entitiesByUUID = Maps.newHashMap();
private final Queue<Entity> entitiesToAdd = Queues.newArrayDeque();
public final List<EntityPlayer> players = Lists.newArrayList(); // Paper - private -> public
@@ -197,7 +198,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
private final PortalTravelAgent portalTravelAgent;
private final TickListServer<Block> nextTickListBlock;
private final TickListServer<FluidType> nextTickListFluid;
- private final Set<NavigationAbstract> navigators;
+ private final Set<NavigationAbstract> navigators; final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<NavigationAbstract> navigatorsForIteration = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(2048, 0.5f, 2048, 0.2, true); // Tuinity - make removing entities while ticking safe
protected final PersistentRaid persistentRaid;
private final ObjectLinkedOpenHashSet<BlockActionData> L;
private boolean ticking;
@@ -319,6 +320,208 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
// Paper end - rewrite ticklistserver
+ // Tuinity start
+ public final boolean areChunksLoadedForMove(AxisAlignedBB 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 = MathHelper.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int maxBlockX = MathHelper.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+ int minBlockZ = MathHelper.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+ int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ChunkProviderServer chunkProvider = this.getChunkProvider();
+
+ 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(AxisAlignedBB axisalignedbb, double toX, double toZ,
+ java.util.function.Consumer<List<IChunkAccess>> onLoad) {
+ if (Thread.currentThread() != this.serverThread) {
+ this.getChunkProvider().serverThreadQueue.execute(() -> {
+ this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad);
+ });
+ return;
+ }
+ List<IChunkAccess> ret = new java.util.ArrayList<>();
+ IntArrayList ticketLevels = new IntArrayList();
+
+ int minBlockX = MathHelper.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int maxBlockX = MathHelper.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+ int minBlockZ = MathHelper.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+ int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ChunkProviderServer chunkProvider = this.getChunkProvider();
+
+ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+ int[] loadedChunks = new int[1];
+
+ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
+
+ java.util.function.Consumer<IChunkAccess> consumer = (IChunkAccess chunk) -> {
+ if (chunk != null) {
+ int ticketLevel = Math.max(33, chunkProvider.playerChunkMap.getUpdatingChunk(chunk.getPos().pair()).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) {
+ ChunkCoordIntPair 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, ChunkStatus.FULL, true, false, consumer);
+ }
+ }
+ }
+ // Tuinity end
+
+ // Tuinity start - execute chunk tasks mid tick
+ public long lastMidTickExecuteFailure;
+ // Tuinity end - execute chunk tasks mid tick
+
+ // Tuinity start - optimise checkDespawn
+ public final List<EntityPlayer> playersAffectingSpawning = new java.util.ArrayList<>();
+ // Tuinity end - optimise checkDespawn
+ // Tuinity start - optimise get nearest players for entity AI
+ public final EntityPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition condition, @Nullable EntityLiving source,
+ double centerX, double centerY, double centerZ) {
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> nearby;
+ if (source != null) {
+ Chunk chunk = source.getCurrentChunk();
+ if (chunk != null && (MathHelper.floor(centerX) >> 4) == chunk.locX &&
+ (MathHelper.floor(centerZ) >> 4) == chunk.locZ) {
+ nearby = chunk.getPlayerGeneralAreaCache();
+ } else {
+ nearby = this.getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(MathHelper.floor(centerX) >> 4, MathHelper.floor(centerZ) >> 4);
+ }
+ } else {
+ nearby = this.getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(MathHelper.floor(centerX) >> 4, MathHelper.floor(centerZ) >> 4);
+ }
+
+ if (nearby == null) {
+ return null;
+ }
+
+ Object[] backingSet = nearby.getBackingSet();
+
+ double closestDistanceSquared = Double.MAX_VALUE;
+ EntityPlayer closest = null;
+
+ for (int i = 0, len = backingSet.length; i < len; ++i) {
+ Object _player = backingSet[i];
+ if (!(_player instanceof EntityPlayer)) {
+ continue;
+ }
+ EntityPlayer player = (EntityPlayer)_player;
+
+ double distanceSquared = player.getDistanceSquared(centerX, centerY, centerZ);
+ if (distanceSquared < closestDistanceSquared && condition.test(source, player)) {
+ closest = player;
+ closestDistanceSquared = distanceSquared;
+ }
+ }
+
+ return closest;
+ }
+
+ @Override
+ public EntityHuman a(net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving) {
+ return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.locX(), entityliving.locY(), entityliving.locZ());
+ }
+
+ @Override
+ public EntityHuman a(net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2) {
+ return this.getNearestPlayer(pathfindertargetcondition, entityliving, d0, d1, d2);
+ }
+
+ @Override
+ public EntityHuman a(net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, double d0, double d1, double d2) {
+ return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2);
+ }
+
+ @Override
+ public List<EntityHuman> a(net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition condition, EntityLiving source, AxisAlignedBB axisalignedbb) {
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> nearby;
+ double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5;
+ double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5;
+ if (source != null) {
+ Chunk chunk = source.getCurrentChunk();
+ if (chunk != null && (MathHelper.floor(centerX) >> 4) == chunk.locX &&
+ (MathHelper.floor(centerZ) >> 4) == chunk.locZ) {
+ nearby = chunk.getPlayerGeneralAreaCache();
+ } else {
+ nearby = this.getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(MathHelper.floor(centerX) >> 4, MathHelper.floor(centerZ) >> 4);
+ }
+ } else {
+ nearby = this.getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(MathHelper.floor(centerX) >> 4, MathHelper.floor(centerZ) >> 4);
+ }
+
+ List<EntityHuman> 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 EntityPlayer)) {
+ continue;
+ }
+ EntityPlayer player = (EntityPlayer)_player;
+
+ if (axisalignedbb.contains(player.locX(), player.locY(), player.locZ()) && condition.test(source, player)) {
+ ret.add(player);
+ }
+ }
+
+ return ret;
+ }
+ // Tuinity end - optimise get nearest players for entity AI
+
+ // Tuinity start - rewrite light engine
+ /**
+ * Cannot be modified during light updates.
+ */
+ public com.tuinity.tuinity.chunk.light.VariableBlockLightHandler customBlockLightHandlers;
+ // Tuinity end - rewrite light engine
+
// Add env and gen to constructor, WorldData -> WorldDataServer
public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey<World> resourcekey, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<MobSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getMethodProfiler, false, flag, i, gen, env, executor); // Paper pass executor
@@ -379,6 +582,251 @@ public class WorldServer extends World implements GeneratorAccessSeed {
this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper
}
+ // Tuinity start - optimise collision
+ public boolean collidesWithAnyBlockOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, boolean loadChunks,
+ boolean collidesWithUnloaded) {
+ return this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, loadChunks, collidesWithUnloaded, true, null);
+ }
+
+ public boolean collidesWithAnyBlockOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb,
+ boolean loadChunks, boolean collidesWithUnloaded,
+ java.util.function.BiPredicate<IBlockData, BlockPosition> predicate) {
+ return this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, loadChunks, collidesWithUnloaded, true, predicate);
+ }
+
+ public final boolean hardCollidesWithAnyEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate<Entity> predicate) {
+ if (axisalignedbb.isEmpty()) {
+ return 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.
+ axisalignedbb = axisalignedbb.grow(-MCUtil.COLLISION_EPSILON, -MCUtil.COLLISION_EPSILON, -MCUtil.COLLISION_EPSILON);
+ List<Entity> entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList();
+ try {
+ if (entity != null && entity.hardCollides()) {
+ this.getEntities(entity, axisalignedbb, predicate, entities);
+ } else {
+ this.getHardCollidingEntities(entity, axisalignedbb, predicate, entities);
+ }
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ Entity otherEntity = entities.get(i);
+
+ if ((entity == null || otherEntity.collisionBoxIsHard()) || entity.hardCollidesWith(otherEntity)) {
+ return true;
+ }
+ }
+
+ return false;
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities);
+ }
+ }
+
+ public final boolean hasAnyCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb) {
+ return this.hasAnyCollisions(entity, axisalignedbb, true);
+ }
+
+ public final boolean hasAnyCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, boolean loadChunks) {
+ return this.collidesWithAnyBlockOrWorldBorder(entity, axisalignedbb, loadChunks, true)
+ || this.hardCollidesWithAnyEntities(entity, axisalignedbb, null);
+ }
+
+ // returns whether any collisions were detected
+ public boolean getCollisionsForBlocksOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<AxisAlignedBB> list,
+ boolean loadChunks, boolean collidesWithUnloaded, boolean checkOnly,
+ java.util.function.BiPredicate<IBlockData, BlockPosition> predicate) {
+ boolean ret = false;
+
+ if (entity != null) {
+ if (this.getWorldBorder().isAlmostCollidingOnBorder(axisalignedbb)) {
+ if (checkOnly) {
+ return true;
+ } else {
+ VoxelShapes.addBoxesTo(this.getWorldBorder().getCollisionShape(), list);
+ ret = true;
+ }
+ }
+ }
+
+ int minBlockX = MathHelper.floor(axisalignedbb.minX - MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockX = MathHelper.floor(axisalignedbb.maxX + MCUtil.COLLISION_EPSILON) + 1;
+
+ int minBlockY = MathHelper.floor(axisalignedbb.minY - MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockY = MathHelper.floor(axisalignedbb.maxY + MCUtil.COLLISION_EPSILON) + 1;
+
+ int minBlockZ = MathHelper.floor(axisalignedbb.minZ - MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + MCUtil.COLLISION_EPSILON) + 1;
+
+
+ BlockPosition.MutableBlockPosition mutablePos = new BlockPosition.MutableBlockPosition();
+ net.minecraft.world.phys.shapes.VoxelShapeCollision collisionShape = null;
+
+ // special cases:
+ if (minBlockY > 255 || maxBlockY < 0) {
+ // no point in checking
+ return ret;
+ }
+
+ int minYIterate = Math.max(0, minBlockY);
+ int maxYIterate = Math.min(255, maxBlockY);
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ChunkProviderServer chunkProvider = (ChunkProviderServer)this.chunkProvider;
+ // 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;
+ Chunk chunk = loadChunks ? chunkProvider.getChunkAt(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
+
+ if (chunk == null) {
+ if (collidesWithUnloaded) {
+ if (checkOnly) {
+ return true;
+ } else {
+ list.add(AxisAlignedBB.getBoxForChunk(currChunkX, currChunkZ));
+ ret = true;
+ }
+ }
+ continue;
+ }
+
+ ChunkSection[] sections = chunk.getSections();
+
+ // bound y
+
+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
+ ChunkSection section = sections[currY >>> 4];
+ if (section == null || section.isFullOfAir()) {
+ // 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.DataPaletteBlock<IBlockData> blocks = section.blockIds;
+
+ 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;
+ }
+
+ IBlockData blockData = blocks.rawGet(localBlockIndex);
+
+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
+ mutablePos.setValues(blockX, blockY, blockZ);
+ if (collisionShape == null) {
+ collisionShape = entity == null ? net.minecraft.world.phys.shapes.VoxelShapeCollision.a() : net.minecraft.world.phys.shapes.VoxelShapeCollision.a(entity);
+ }
+ VoxelShape voxelshape2 = blockData.getCollisionShape(this, mutablePos, collisionShape);
+ if (voxelshape2 != VoxelShapes.getEmptyShape()) {
+ VoxelShape voxelshape3 = voxelshape2.offset((double)blockX, (double)blockY, (double)blockZ);
+
+ if (predicate != null && !predicate.test(blockData, mutablePos)) {
+ continue;
+ }
+
+ if (checkOnly) {
+ if (voxelshape3.intersects(axisalignedbb)) {
+ return true;
+ }
+ } else {
+ ret |= VoxelShapes.addBoxesToIfIntersects(voxelshape3, axisalignedbb, list);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public final void getEntityHardCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate<Entity> predicate, List<AxisAlignedBB> list) {
+ if (axisalignedbb.isEmpty()) {
+ return;
+ }
+
+ // 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.
+ axisalignedbb = axisalignedbb.grow(-MCUtil.COLLISION_EPSILON, -MCUtil.COLLISION_EPSILON, -MCUtil.COLLISION_EPSILON);
+ List<Entity> entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList();
+ try {
+ if (entity != null && entity.hardCollides()) {
+ this.getEntities(entity, axisalignedbb, predicate, entities);
+ } else {
+ this.getHardCollidingEntities(entity, axisalignedbb, predicate, entities);
+ }
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ Entity otherEntity = entities.get(i);
+
+ if ((entity == null || otherEntity.collisionBoxIsHard()) || entity.hardCollidesWith(otherEntity)) {
+ if (!otherEntity.getBoundingBox().isEmpty()) {
+ list.add(otherEntity.getBoundingBox());
+ }
+ }
+ }
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities);
+ }
+ }
+
+ public final void getCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<AxisAlignedBB> list, boolean loadChunks) {
+ this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, list, loadChunks, true, false, null);
+ this.getEntityHardCollisions(entity, axisalignedbb, null, list);
+ }
+
+ @Override
+ public boolean getCubes(AxisAlignedBB axisalignedbb) {
+ return !this.hasAnyCollisions(null, axisalignedbb);
+ }
+
+ @Override
+ public boolean getCubes(Entity entity) {
+ return !this.hasAnyCollisions(entity, entity.getBoundingBox());
+ }
+
+ @Override
+ public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb) {
+ if (entity instanceof net.minecraft.world.entity.decoration.EntityArmorStand && !entity.world.paperConfig.armorStandEntityLookups) return false;
+ return !this.hasAnyCollisions(entity, axisalignedbb);
+ }
+
+ @Override
+ public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ if (entity instanceof net.minecraft.world.entity.decoration.EntityArmorStand && !entity.world.paperConfig.armorStandEntityLookups) return false;
+ return !this.collidesWithAnyBlockOrWorldBorder(entity, axisalignedbb, false, false) && !this.hardCollidesWithAnyEntities(entity, axisalignedbb, predicate);
+ }
+ // Tuinity end - optimise collision
+
// CraftBukkit start
@Override
public TileEntity getTileEntity(BlockPosition pos, boolean validate) {
@@ -434,6 +882,14 @@ public class WorldServer extends World implements GeneratorAccessSeed {
public void doTick(BooleanSupplier booleansupplier) {
GameProfilerFiller gameprofilerfiller = this.getMethodProfiler();
+ // Tuinity start - optimise checkDespawn
+ this.playersAffectingSpawning.clear();
+ for (EntityPlayer player : this.getPlayers()) {
+ if (net.minecraft.world.entity.IEntitySelector.affectsSpawning.test(player)) {
+ this.playersAffectingSpawning.add(player);
+ }
+ }
+ // Tuinity end - optimise checkDespawn
this.ticking = true;
gameprofilerfiller.enter("world border");
@@ -583,7 +1039,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
timings.scheduledBlocks.stopTiming(); // Paper
- this.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
gameprofilerfiller.exitEnter("raid");
this.timings.raids.startTiming(); // Paper - timings
this.persistentRaid.a();
@@ -592,7 +1048,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
timings.doSounds.startTiming(); // Spigot
this.ak();
timings.doSounds.stopTiming(); // Spigot
- this.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.ticking = false;
gameprofilerfiller.exitEnter("entities");
boolean flag3 = true || !this.players.isEmpty() || !this.getForceLoadedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players
@@ -608,13 +1064,12 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
this.tickingEntities = true;
- ObjectIterator objectiterator = this.entitiesById.int2ObjectEntrySet().iterator();
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Entity> objectiterator = this.entitiesForIteration.iterator(); // Tuinity
org.spigotmc.ActivationRange.activateEntities(this); // Spigot
timings.entityTick.startTiming(); // Spigot
while (objectiterator.hasNext()) {
- Entry<Entity> entry = (Entry) objectiterator.next();
- Entity entity = (Entity) entry.getValue();
+ Entity entity = (Entity) objectiterator.next(); // Tuinity
Entity entity1 = entity.getVehicle();
/* CraftBukkit start - We prevent spawning in general, so this butchering is not needed
@@ -630,6 +1085,15 @@ public class WorldServer extends World implements GeneratorAccessSeed {
gameprofilerfiller.enter("checkDespawn");
if (!entity.dead) {
entity.checkDespawn();
+ // Tuinity start - optimise notify()
+ if (entity.inChunk && entity.valid) {
+ if (this.getChunkProvider().isInEntityTickingChunk(entity)) {
+ this.updateNavigatorsInRegion(entity);
+ }
+ } else {
+ this.removeNavigatorsFromData(entity);
+ }
+ // Tuinity end - optimise notify()
}
gameprofilerfiller.exit();
@@ -650,14 +1114,22 @@ public class WorldServer extends World implements GeneratorAccessSeed {
gameprofilerfiller.enter("remove");
if (entity.dead) {
this.removeEntityFromChunk(entity);
- objectiterator.remove();
+ this.entitiesById.remove(entity.getId()); // Tuinity
this.unregisterEntity(entity);
+ } else if (entity.inChunk && entity.valid) { // Tuinity start - optimise notify()
+ if (this.getChunkProvider().isInEntityTickingChunk(entity)) {
+ this.updateNavigatorsInRegion(entity);
+ }
+ } else {
+ this.removeNavigatorsFromData(entity);
}
+ // Tuinity end - optimise notify()
gameprofilerfiller.exit();
}
timings.entityTick.stopTiming(); // Spigot
+ objectiterator.finishedIterating(); // Tuinity
this.tickingEntities = false;
// Paper start
for (java.lang.Runnable run : this.afterEntityTickingTasks) {
@@ -669,7 +1141,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
this.afterEntityTickingTasks.clear();
// Paper end
- this.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
Entity entity2;
@@ -679,7 +1151,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
timings.tickEntities.stopTiming(); // Spigot
- this.getMinecraftServer().midTickLoadChunks(); // Paper
+ // Tuinity - replace logic
this.tickBlockEntities();
}
@@ -726,6 +1198,9 @@ public class WorldServer extends World implements GeneratorAccessSeed {
private final BlockPosition.MutableBlockPosition chunkTickMutablePosition = new BlockPosition.MutableBlockPosition();
private final com.destroystokyo.paper.util.math.ThreadUnsafeRandom randomTickRandom = new com.destroystokyo.paper.util.math.ThreadUnsafeRandom();
// Paper end
+ // Tuinity start - optimise chunk ice snow ticking
+ private final BiomeBase[] biomeBaseCache = new BiomeBase[1];
+ // Tuinity end - optimise chunk ice snow ticking
public void a(Chunk chunk, int i) { final int randomTickSpeed = i; // Paper
ChunkCoordIntPair chunkcoordintpair = chunk.getPos();
@@ -763,28 +1238,32 @@ public class WorldServer extends World implements GeneratorAccessSeed {
gameprofilerfiller.exitEnter("iceandsnow");
if (!this.paperConfig.disableIceAndSnow && this.randomTickRandom.nextInt(16) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking
// Paper start - optimise chunk ticking
+ // Tuinity start - optimise chunk ice snow ticking
+ BiomeBase[] biomeCache = this.biomeBaseCache;
+ biomeCache[0] = null;
+ // Tuinity start - optimise chunk ice snow ticking
this.getRandomBlockPosition(j, 0, k, 15, blockposition);
int normalY = chunk.getHighestBlockY(HeightMap.Type.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15);
int downY = normalY - 1;
blockposition.setY(normalY);
// Paper end
- BiomeBase biomebase = this.getBiome(blockposition);
+ //BiomeBase biomebase = this.getBiome(blockposition); // Tuinity - lazily-get biome
// Paper start - optimise chunk ticking
blockposition.setY(downY);
- if (biomebase.a(this, blockposition)) {
+ if (BiomeBase.canTurnWaterIntoIce(this, blockposition, true, chunk, biomeCache)) { // Tuinity - add chunk parameter, avoid biome lookup
org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.ICE.getBlockData(), null); // CraftBukkit
// Paper end
}
blockposition.setY(normalY); // Paper
- if (flag && biomebase.b(this, blockposition)) {
+ if (flag && BiomeBase.canTurnAirIntoSnow(this, blockposition, chunk, biomeCache)) {
org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.SNOW.getBlockData(), null); // CraftBukkit
}
// Paper start - optimise chunk ticking
blockposition.setY(downY);
- if (flag && this.getBiome(blockposition).c() == BiomeBase.Precipitation.RAIN) {
+ if (flag && chunk.getType(blockposition).getBlock() instanceof net.minecraft.world.level.block.BlockCauldron && this.getBiome(blockposition).c() == BiomeBase.Precipitation.RAIN) { // Tuinity - only cauldron uses that method, and it's not likely to hit. So, avoid the expensive biome lookup and replace it with a block lookup TODO check on update for overrides
chunk.getType(blockposition).getBlock().c((World) this, blockposition);
// Paper end
}
@@ -924,7 +1403,26 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
+ // 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 entityJoinedWorld(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
if (!(entity instanceof EntityHuman) && !this.getChunkProvider().a(entity)) {
this.chunkCheck(entity);
} else {
@@ -977,6 +1475,13 @@ public class WorldServer extends World implements GeneratorAccessSeed {
//} 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
}
public void a(Entity entity, Entity entity1) {
@@ -1035,6 +1540,12 @@ public class WorldServer extends World implements GeneratorAccessSeed {
int i = MathHelper.floor(entity.locX() / 16.0D);
int j = Math.min(15, Math.max(0, MathHelper.floor(entity.locY() / 16.0D))); // Paper - stay consistent with chunk add/remove behavior
int k = MathHelper.floor(entity.locZ() / 16.0D);
+ // Tuinity start
+ int oldRegionX = entity.chunkX >> this.getChunkProvider().playerChunkMap.dataRegionManager.regionChunkShift;
+ int oldRegionZ = entity.chunkZ >> this.getChunkProvider().playerChunkMap.dataRegionManager.regionChunkShift;
+ int newRegionX = i >> this.getChunkProvider().playerChunkMap.dataRegionManager.regionChunkShift;
+ int newRegionZ = k >> this.getChunkProvider().playerChunkMap.dataRegionManager.regionChunkShift;
+ // Tuinity end
if (!entity.inChunk || entity.chunkX != i || entity.chunkY != j || entity.chunkZ != k) {
// Paper start - remove entity if its in a chunk more correctly.
@@ -1044,6 +1555,12 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
// Paper end
+ // Tuinity start
+ if (oldRegionX != newRegionX || oldRegionZ != newRegionZ) {
+ this.removeNavigatorsFromData(entity);
+ }
+ // Tuinity end
+
if (entity.inChunk && this.isChunkLoaded(entity.chunkX, entity.chunkZ)) {
this.getChunkAt(entity.chunkX, entity.chunkZ).a(entity, entity.chunkY);
}
@@ -1055,8 +1572,41 @@ public class WorldServer extends World implements GeneratorAccessSeed {
entity.inChunk = false;
} else {
- this.getChunkAt(i, k).a(entity);
+ // Tuinity start - gotta be careful here, sync load can teleport entity.
+ Chunk chunk = this.getChunkIfLoaded(i, k);
+ if (chunk == null) {
+ // might teleport the entity, so be prepared.
+ // at this point, we are NOT in a chunk. so CLEARLY mark that for a recursive call to see.
+ entity.inChunk = false;
+ // now load
+ chunk = this.getChunkAt(i, k);
+ // are we in a chunk now?
+ if (entity.inChunk) {
+ // yup we teleported and were added to another chunk. do not add to the current one!
+ return;
+ }
+ // did we teleport but did not add to a chunk?
+ // copied from start
+ int in = MathHelper.floor(entity.locX() / 16.0D);
+ int jn = Math.min(15, Math.max(0, MathHelper.floor(entity.locY() / 16.0D))); // Paper - stay consistent with chunk add/remove behavior
+ int kn = MathHelper.floor(entity.locZ() / 16.0D);
+ if (in != i || jn != j || kn != k) {
+ // teleported but were not added to a chunk. try now, let the new call try to add again
+ this.chunkCheck(entity);
+ return;
+ }
+
+ // only now at this point have we verified the new load did not screw up our position, so
+ // now we can add to the chunk
+ }
+ chunk.a(entity);
+ // Tuinity end - gotta be careful here, sync load can teleport entity.
+ }
+ // Tuinity start
+ if (entity.inChunk && (oldRegionX != newRegionX || oldRegionZ != newRegionZ)) {
+ this.addNavigatorsIfPathingToRegion(entity);
}
+ // Tuinity end
}
this.getMethodProfiler().exit();
@@ -1393,10 +1943,14 @@ public class WorldServer extends World implements GeneratorAccessSeed {
// Spigot Start
for (TileEntity tileentity : chunk.getTileEntities().values()) {
if (tileentity instanceof net.minecraft.world.IInventory) {
+ // 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.IInventory) 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
this.tileEntityListUnload.addAll(chunk.getTileEntities().values());
@@ -1412,7 +1966,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
Entity entity = (Entity) iterator.next();
if (!(entity instanceof EntityPlayer)) {
- if (this.tickingEntities) {
+ if (false && this.tickingEntities) { // Tuinity
throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("Removing entity while ticking!")));
}
@@ -1440,6 +1994,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
public void unregisterEntity(Entity entity) {
org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
+ this.entitiesForIteration.remove(entity); // Tuinity
// Paper start - fix entity registration issues
if (entity instanceof EntityComplexPart) {
// Usually this is a no-op for complex parts, and ID's should be removed, but go ahead and remove it anyways
@@ -1506,17 +2061,76 @@ public class WorldServer extends World implements GeneratorAccessSeed {
this.getScoreboard().a(entity);
// CraftBukkit start - SPIGOT-5278
if (entity instanceof EntityDrowned) {
- this.navigators.remove(((EntityDrowned) entity).navigationWater);
- this.navigators.remove(((EntityDrowned) entity).navigationLand);
+ // Tuinity start
+ this.navigators.remove(((EntityDrowned) entity).navigationWater); this.navigatorsForIteration.remove(((EntityDrowned) entity).navigationWater);
+ this.navigators.remove(((EntityDrowned) entity).navigationLand); this.navigatorsForIteration.remove(((EntityDrowned) entity).navigationLand);
+ // Tuinity end
} else
// CraftBukkit end
if (entity instanceof EntityInsentient) {
- this.navigators.remove(((EntityInsentient) entity).getNavigation());
+ // Tuinity start
+ this.navigators.remove(((EntityInsentient) entity).getNavigation()); this.navigatorsForIteration.remove(((EntityInsentient) entity).getNavigation());
+ // Tuinity end
}
new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity()).callEvent(); // Paper - fire while valid
+ this.removeNavigatorsFromData(entity); // Tuinity - optimise notify()
entity.valid = false; // CraftBukkit
}
+ // Tuinity start - optimise notify()
+ void removeNavigatorsIfNotPathingFromRegion(Entity entity) {
+ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section =
+ this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ);
+ if (section != null) {
+ PlayerChunkMap.DataRegionSectionData sectionData = (PlayerChunkMap.DataRegionSectionData)section.sectionData;
+ if (entity instanceof EntityInsentient) {
+ if (!((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) {
+ sectionData.removeFromNavigators(section, (EntityInsentient)entity);
+ }
+ }
+ }
+ }
+
+ void removeNavigatorsFromData(Entity entity) {
+ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section =
+ this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ);
+ if (section != null) {
+ PlayerChunkMap.DataRegionSectionData sectionData = (PlayerChunkMap.DataRegionSectionData)section.sectionData;
+ if (entity instanceof EntityInsentient) {
+ sectionData.removeFromNavigators(section, ((EntityInsentient)entity));
+ }
+ }
+ }
+
+ void addNavigatorsIfPathingToRegion(Entity entity) {
+ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section =
+ this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ);
+ if (section != null) {
+ PlayerChunkMap.DataRegionSectionData sectionData = (PlayerChunkMap.DataRegionSectionData)section.sectionData;
+ if (entity instanceof EntityInsentient) {
+ if (((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) {
+ sectionData.addToNavigators(section, ((EntityInsentient)entity));
+ }
+ }
+ }
+ }
+
+ void updateNavigatorsInRegion(Entity entity) {
+ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section =
+ this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ);
+ if (section != null) {
+ PlayerChunkMap.DataRegionSectionData sectionData = (PlayerChunkMap.DataRegionSectionData)section.sectionData;
+ if (entity instanceof EntityInsentient) {
+ if (((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) {
+ sectionData.addToNavigators(section, ((EntityInsentient)entity));
+ } else {
+ sectionData.removeFromNavigators(section, ((EntityInsentient)entity));
+ }
+ }
+ }
+ }
+ // Tuinity end - optimise notify()
+
private void registerEntity(Entity entity) {
org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
// Paper start - don't double enqueue entity registration
@@ -1527,7 +2141,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
return;
}
// Paper end
- if (this.tickingEntities) {
+ if (false && this.tickingEntities) { // Tuinity
if (!entity.isQueuedForRegister) { // Paper
this.entitiesToAdd.add(entity);
entity.isQueuedForRegister = true; // Paper
@@ -1535,6 +2149,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
} else {
entity.isQueuedForRegister = false; // Paper
this.entitiesById.put(entity.getId(), entity);
+ this.entitiesForIteration.add(entity); // Tuinity
if (entity instanceof EntityEnderDragon) {
EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon) entity).eJ();
int i = aentitycomplexpart.length;
@@ -1543,6 +2158,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
EntityComplexPart entitycomplexpart = aentitycomplexpart[j];
this.entitiesById.put(entitycomplexpart.getId(), entitycomplexpart);
+ this.entitiesForIteration.add(entitycomplexpart); // Tuinity
}
}
@@ -1567,12 +2183,16 @@ public class WorldServer extends World implements GeneratorAccessSeed {
// this.getChunkProvider().addEntity(entity); // Paper - moved down below valid=true
// CraftBukkit start - SPIGOT-5278
if (entity instanceof EntityDrowned) {
- this.navigators.add(((EntityDrowned) entity).navigationWater);
- this.navigators.add(((EntityDrowned) entity).navigationLand);
+ // Tuinity start
+ this.navigators.add(((EntityDrowned) entity).navigationWater); this.navigatorsForIteration.add(((EntityDrowned) entity).navigationWater);
+ this.navigators.add(((EntityDrowned) entity).navigationLand); this.navigatorsForIteration.add(((EntityDrowned) entity).navigationLand);
+ // Tuinity end
} else
// CraftBukkit end
if (entity instanceof EntityInsentient) {
- this.navigators.add(((EntityInsentient) entity).getNavigation());
+ // Tuinity start
+ this.navigators.add(((EntityInsentient) entity).getNavigation()); this.navigatorsForIteration.add(((EntityInsentient) entity).getNavigation());
+ // Tuinity end
}
entity.valid = true; // CraftBukkit
this.getChunkProvider().addEntity(entity); // Paper - from above to be below valid=true
@@ -1588,7 +2208,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
}
public void removeEntity(Entity entity) {
- if (this.tickingEntities) {
+ if (false && this.tickingEntities) { // Tuinity
throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("Removing entity while ticking!")));
} else {
this.removeEntityFromChunk(entity);
@@ -1683,20 +2303,33 @@ public class WorldServer extends World implements GeneratorAccessSeed {
@Override
public void notify(BlockPosition blockposition, IBlockData iblockdata, IBlockData iblockdata1, int i) {
+ org.spigotmc.AsyncCatcher.catchOp("notify call"); // Tuinity
this.getChunkProvider().flagDirty(blockposition);
if(this.paperConfig.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates
VoxelShape voxelshape = iblockdata.getCollisionShape(this, blockposition);
VoxelShape voxelshape1 = iblockdata1.getCollisionShape(this, blockposition);
if (VoxelShapes.c(voxelshape, voxelshape1, OperatorBoolean.NOT_SAME)) {
+ // Tuinity start - optimise notify()
+ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region region = this.getChunkProvider().playerChunkMap.dataRegionManager.getRegion(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ if (region == null) {
+ return;
+ }
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<EntityInsentient> navigatorsFromRegion = ((PlayerChunkMap.DataRegionData)region.regionData).getNavigators();
+ if (navigatorsFromRegion == null) {
+ return;
+ }
boolean wasTicking = this.tickingEntities; this.tickingEntities = true; // Paper
- Iterator iterator = this.navigators.iterator();
+ // Tuinity start
+ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<EntityInsentient> iterator = navigatorsFromRegion.iterator();
+ // Tuinity end - optimise notify()
+ try { // Tuinity end
while (iterator.hasNext()) {
// CraftBukkit start - fix SPIGOT-6362
NavigationAbstract navigationabstract;
try {
- navigationabstract = (NavigationAbstract) iterator.next();
+ navigationabstract = (NavigationAbstract) iterator.next().getNavigation(); // Tuinity - optimise notify
} catch (java.util.ConcurrentModificationException ex) {
// This can happen because the pathfinder update below may trigger a chunk load, which in turn may cause more navigators to register
// In this case we just run the update again across all the iterators as the chunk will then be loaded
@@ -1710,6 +2343,9 @@ public class WorldServer extends World implements GeneratorAccessSeed {
navigationabstract.b(blockposition);
}
}
+ } finally { // Tuinity start
+ iterator.finishedIterating();
+ } // Tuinity end
this.tickingEntities = wasTicking; // Paper
}
diff --git a/src/main/java/net/minecraft/server/network/LoginListener.java b/src/main/java/net/minecraft/server/network/LoginListener.java
index 5b69126142140c7fc96435a4d246752581f47c33..25437a993c02379fa43e5d46159cd9ba2aebf10f 100644
--- a/src/main/java/net/minecraft/server/network/LoginListener.java
+++ b/src/main/java/net/minecraft/server/network/LoginListener.java
@@ -256,7 +256,7 @@ public class LoginListener implements PacketLoginInListener {
s = (new BigInteger(MinecraftEncryption.a("", this.server.getKeyPair().getPublic(), this.loginKey))).toString(16);
this.g = LoginListener.EnumProtocolState.AUTHENTICATING;
- this.networkManager.a(cipher, cipher1);
+ this.networkManager.a(this.loginKey); // Tuinity
} catch (CryptographyException cryptographyexception) {
throw new IllegalStateException("Protocol error", cryptographyexception);
}
diff --git a/src/main/java/net/minecraft/server/network/PlayerConnection.java b/src/main/java/net/minecraft/server/network/PlayerConnection.java
index 4001078e2a408dfd5a93c21f55a58c1fde32abbb..8513795943497ca80232e7d47066a6944c2f45ec 100644
--- a/src/main/java/net/minecraft/server/network/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/network/PlayerConnection.java
@@ -550,6 +550,12 @@ public class PlayerConnection implements PacketListenerPlayIn {
double currDeltaZ = toZ - fromZ;
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.v;
+ double otherFieldY = d4 - this.w - 1.0E-6D;
+ double otherFieldZ = d5 - this.x;
+ 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
@@ -578,7 +584,9 @@ public class PlayerConnection implements PacketListenerPlayIn {
speed *= 2f; // TODO: Get the speed of the vehicle instead of the player
// Paper start - Prevent moving into unloaded chunks
- if (player.world.paperConfig.preventMovingIntoUnloadedChunks && worldserver.getChunkIfLoadedImmediately((int) Math.floor(packetplayinvehiclemove.getX()) >> 4, (int) Math.floor(packetplayinvehiclemove.getZ()) >> 4) == null) {
+ if (player.world.paperConfig.preventMovingIntoUnloadedChunks // Tuinity - improve this check
+ && (!worldserver.areChunksLoadedForMove(this.player.getBoundingBoxAt(this.player.locX(), this.player.locY(), this.player.locZ()).expand(toX - this.player.locX(), toY - this.player.locY(), toZ - this.player.locZ()))) // Tuinity - improve this check
+ || !worldserver.areChunksLoadedForMove(entity.getBoundingBoxAt(entity.locX(), entity.locY(), entity.locZ()).expand(toX - entity.locX(), toY - entity.locY(), toZ - entity.locZ()))) { // Tuinity - improve this check
this.networkManager.sendPacket(new PacketPlayOutVehicleMove(entity));
return;
}
@@ -591,12 +599,14 @@ public class PlayerConnection implements PacketListenerPlayIn {
return;
}
- boolean flag = worldserver.getCubes(entity, entity.getBoundingBox().shrink(0.0625D));
+ //boolean flag = worldserver.getCubes(entity, entity.getBoundingBox().shrink(0.0625D)); // Tuinity - replace with different checks
+ AxisAlignedBB oldBox = entity.getBoundingBox(); // Tuinity - copy from player movement packet
- d6 = d3 - this.v;
- d7 = d4 - this.w - 1.0E-6D;
- d8 = d5 - this.x;
+ d6 = d3 - this.v; // Tuinity - diff on change, used for checking large move vectors above
+ d7 = d4 - this.w - 1.0E-6D; // Tuinity - diff on change, used for checking large move vectors above
+ d8 = d5 - this.x; // Tuinity - diff on change, used for checking large move vectors above
entity.move(EnumMoveType.PLAYER, new Vec3D(d6, d7, d8));
+ boolean didCollide = toX != entity.locX() || toY != entity.locY() || toZ != entity.locZ(); // 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.locX();
@@ -610,16 +620,25 @@ public class PlayerConnection implements PacketListenerPlayIn {
boolean flag1 = false;
if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
- flag1 = true;
+ flag1 = true; // Tuinity - diff on change, this should be moved wrongly
PlayerConnection.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getDisplayName().getString(), this.player.getDisplayName().getString(), Math.sqrt(d10));
}
Location curPos = this.getPlayer().getLocation(); // Spigot
entity.setLocation(d3, d4, d5, f, f1);
player.setLocation(d3, d4, d5, this.player.yaw, this.player.pitch); // CraftBukkit
- boolean flag2 = worldserver.getCubes(entity, entity.getBoundingBox().shrink(0.0625D));
-
- if (flag && (flag1 || !flag2)) {
+ //boolean flag2 = worldserver.getCubes(entity, entity.getBoundingBox().shrink(0.0625D)); // Tuinity - replace with different checks
+
+ // 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
+ AxisAlignedBB 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.setLocation(d0, d1, d2, f, f1);
player.setLocation(d0, d1, d2, this.player.yaw, this.player.pitch); // CraftBukkit
this.networkManager.sendPacket(new PacketPlayOutVehicleMove(entity));
@@ -705,7 +724,32 @@ public class PlayerConnection implements PacketListenerPlayIn {
}
private boolean a(Entity entity) {
- return entity.world.a(entity.getBoundingBox().g(0.0625D).b(0.0D, -0.55D, 0.0D)).allMatch(BlockBase.BlockData::isAir);
+ // Tuinity start - stop using streams, this is already a known fixed problem in Entity#move
+ AxisAlignedBB box = entity.getBoundingBox().g(0.0625D).b(0.0D, -0.55D, 0.0D);
+ int minX = MathHelper.floor(box.minX);
+ int minY = MathHelper.floor(box.minY);
+ int minZ = MathHelper.floor(box.minZ);
+ int maxX = MathHelper.floor(box.maxX);
+ int maxY = MathHelper.floor(box.maxY);
+ int maxZ = MathHelper.floor(box.maxZ);
+
+ World world = entity.world;
+ BlockPosition.MutableBlockPosition pos = new BlockPosition.MutableBlockPosition();
+
+ for (int y = minY; y <= maxY; ++y) {
+ for (int z = minZ; z <= maxZ; ++z) {
+ for (int x = minX; x <= maxX; ++x) {
+ pos.setValues(x, y, z);
+ IBlockData 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
@@ -1232,7 +1276,7 @@ public class PlayerConnection implements PacketListenerPlayIn {
}
if (this.teleportPos != null) {
- if (this.e - this.A > 20) {
+ if (false && this.e - this.A > 20) { // Tuinity - this will greatly screw with clients with > 1000ms RTT
this.A = this.e;
this.a(this.teleportPos.x, this.teleportPos.y, this.teleportPos.z, this.player.yaw, this.player.pitch);
}
@@ -1270,6 +1314,12 @@ public class PlayerConnection implements PacketListenerPlayIn {
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 = d4 - this.o;
+ double otherFieldY = d5 - this.p;
+ double otherFieldZ = d6 - this.q;
+ 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) {
@@ -1302,7 +1352,7 @@ public class PlayerConnection implements PacketListenerPlayIn {
speed = player.abilities.walkSpeed * 10f;
}
// Paper start - Prevent moving into unloaded chunks
- if (player.world.paperConfig.preventMovingIntoUnloadedChunks && (this.player.locX() != toX || this.player.locZ() != toZ) && worldserver.getChunkIfLoadedImmediately((int) Math.floor(toX) >> 4, (int) Math.floor(toZ) >> 4) == null) { // Paper - use getIfLoadedImmediately
+ if (player.world.paperConfig.preventMovingIntoUnloadedChunks && !((WorldServer)this.player.world).areChunksLoadedForMove(this.player.getBoundingBoxAt(this.player.locX(), this.player.locY(), this.player.locZ()).expand(toX - this.player.locX(), toY - this.player.locY(), toZ - this.player.locZ()))) { // Paper - use getIfLoadedImmediately // Tuinity - improve this check
this.internalTeleport(this.player.locX(), this.player.locY(), this.player.locZ(), this.player.yaw, this.player.pitch, Collections.emptySet());
return;
}
@@ -1319,11 +1369,11 @@ public class PlayerConnection implements PacketListenerPlayIn {
}
}
- AxisAlignedBB axisalignedbb = this.player.getBoundingBox();
+ AxisAlignedBB axisalignedbb = this.player.getBoundingBox(); // Tuinity - diff on change, should be old AABB
- d7 = d4 - this.o;
- d8 = d5 - this.p;
- d9 = d6 - this.q;
+ d7 = d4 - this.o; // Tuinity - diff on change, used for checking large move vectors above
+ d8 = d5 - this.p; // Tuinity - diff on change, used for checking large move vectors above
+ d9 = d6 - this.q; // Tuinity - diff on change, used for checking large move vectors above
boolean flag = d8 > 0.0D;
if (this.player.isOnGround() && !packetplayinflying.b() && flag) {
@@ -1358,6 +1408,7 @@ public class PlayerConnection implements PacketListenerPlayIn {
}
this.player.move(EnumMoveType.PLAYER, new Vec3D(d7, d8, d9));
+ boolean didCollide = toX != this.player.locX() || toY != this.player.locY() || toZ != this.player.locZ(); // 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(packetplayinflying.b()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move
// Paper start - prevent position desync
if (this.teleportPos != null) {
@@ -1377,12 +1428,23 @@ public class PlayerConnection implements PacketListenerPlayIn {
boolean flag1 = false;
if (!this.player.H() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.playerInteractManager.isCreative() && this.player.playerInteractManager.getGameMode() != EnumGamemode.SPECTATOR) { // Spigot
- flag1 = true;
+ flag1 = true; // Tuinity - diff on change, this should be moved wrongly
PlayerConnection.LOGGER.warn("{} moved wrongly!", this.player.getDisplayName().getString());
}
this.player.setLocation(d4, d5, d6, f, f1);
- if (!this.player.noclip && !this.player.isSleeping() && (flag1 && worldserver.getCubes(this.player, axisalignedbb) || this.a((IWorldReader) 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.noclip && !this.player.isSleeping() && !teleportBack) {
+ AxisAlignedBB 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.noclip && !this.player.isSleeping() && teleportBack) { // Tuinity end - optimise out extra getCubes
this.a(d0, d1, d2, f, f1);
} else {
// CraftBukkit start - fire PlayerMoveEvent
@@ -1469,6 +1531,26 @@ public class PlayerConnection implements PacketListenerPlayIn {
}
}
+ // Tuinity start - optimise out extra getCubes
+ private boolean hasNewCollision(final WorldServer world, final Entity entity, final AxisAlignedBB oldBox, final AxisAlignedBB newBox) {
+ final List<AxisAlignedBB> collisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList();
+ try {
+ world.getCollisions(entity, newBox, collisions, true);
+
+ for (int i = 0, len = collisions.size(); i < len; ++i) {
+ final AxisAlignedBB box = collisions.get(i);
+ if (!box.voxelShapeIntersect(oldBox)) {
+ return true;
+ }
+ }
+
+ return false;
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(collisions);
+ }
+ }
+ // Tuinity end - optimise out extra getCubes
+
private boolean a(IWorldReader iworldreader, AxisAlignedBB axisalignedbb) {
Stream<VoxelShape> stream = iworldreader.d(this.player, this.player.getBoundingBox().shrink(9.999999747378752E-6D), (entity) -> {
return true;
diff --git a/src/main/java/net/minecraft/server/network/ServerConnection.java b/src/main/java/net/minecraft/server/network/ServerConnection.java
index 06b0ed65905b9829564dfddd29012218af0f403d..69a982832489fc78fd46418341bdcba1e2be69e9 100644
--- a/src/main/java/net/minecraft/server/network/ServerConnection.java
+++ b/src/main/java/net/minecraft/server/network/ServerConnection.java
@@ -96,6 +96,11 @@ public class ServerConnection {
ServerConnection.LOGGER.info("Using default channel type");
}
+ // Tuinity start - indicate Velocity natives in use
+ ServerConnection.LOGGER.info("Tuinity: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity.");
+ ServerConnection.LOGGER.info("Tuinity: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity.");
+ // Tuinity end
+
this.listeningChannels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer<Channel>() {
protected void initChannel(Channel channel) throws Exception {
try {
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index 90776231b1faffb11e4394f555f336ca248e3004..5936075251ef3d6dda3e93866009d0e996598698 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -179,6 +179,7 @@ public abstract class PlayerList {
abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor
public void a(NetworkManager networkmanager, EntityPlayer entityplayer) {
+ entityplayer.isRealPlayer = true; // Paper // Tuinity - this is a better place to write this that works and isn't overriden by plugins
EntityPlayer prev = pendingPlayers.put(entityplayer.getUniqueID(), entityplayer);// Paper
if (prev != null) {
disconnectPendingPlayer(prev);
@@ -269,7 +270,7 @@ public abstract class PlayerList {
boolean flag1 = gamerules.getBoolean(GameRules.REDUCED_DEBUG_INFO);
// Spigot - view distance
- playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), entityplayer.playerInteractManager.c(), BiomeManager.a(worldserver1.getSeed()), worlddata.isHardcore(), this.server.F(), this.s, worldserver1.getDimensionManager(), worldserver1.getDimensionKey(), this.getMaxPlayers(), worldserver1.getChunkProvider().playerChunkMap.getLoadViewDistance(), flag1, !flag, worldserver1.isDebugWorld(), worldserver1.isFlatWorld())); // Paper - no-tick view distance
+ playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), entityplayer.playerInteractManager.c(), BiomeManager.a(worldserver1.getSeed()), worlddata.isHardcore(), this.server.F(), this.s, worldserver1.getDimensionManager(), worldserver1.getDimensionKey(), this.getMaxPlayers(), worldserver1.getChunkProvider().playerChunkMap.playerChunkManager.getLoadDistance(), flag1, !flag, worldserver1.isDebugWorld(), worldserver1.isFlatWorld())); // Paper - no-tick view distance // Tuinity - replace old player chunk management
entityplayer.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
playerconnection.sendPacket(new PacketPlayOutCustomPayload(PacketPlayOutCustomPayload.a, (new PacketDataSerializer(Unpooled.buffer())).a(this.getServer().getServerModName())));
playerconnection.sendPacket(new PacketPlayOutServerDifficulty(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
@@ -729,7 +730,7 @@ public abstract class PlayerList {
SocketAddress socketaddress = loginlistener.networkManager.getSocketAddress();
EntityPlayer entity = new EntityPlayer(this.server, this.server.getWorldServer(World.OVERWORLD), gameprofile, new PlayerInteractManager(this.server.getWorldServer(World.OVERWORLD)));
- entity.isRealPlayer = true; // Paper
+ // 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.networkManager.getRawAddress()).getAddress());
@@ -951,13 +952,13 @@ public abstract class PlayerList {
worldserver1.getChunkProvider().addTicket(TicketType.POST_TELEPORT, new ChunkCoordIntPair(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
entityplayer1.forceCheckHighPriority(); // Player
- while (avoidSuffocation && !worldserver1.getCubes(entityplayer1) && entityplayer1.locY() < 256.0D) {
+ while (avoidSuffocation && worldserver1.collidesWithAnyBlockOrWorldBorder(entityplayer1, entityplayer1.getBoundingBox(), true, false) && entityplayer1.locY() < 256.0D) { // Tuinity - make sure this is loaded
entityplayer1.setPosition(entityplayer1.locX(), entityplayer1.locY() + 1.0D, entityplayer1.locZ());
}
// CraftBukkit start
WorldData worlddata = worldserver1.getWorldData();
entityplayer1.playerConnection.sendPacket(new PacketPlayOutRespawn(worldserver1.getDimensionManager(), worldserver1.getDimensionKey(), BiomeManager.a(worldserver1.getSeed()), entityplayer1.playerInteractManager.getGameMode(), entityplayer1.playerInteractManager.c(), worldserver1.isDebugWorld(), worldserver1.isFlatWorld(), flag));
- entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(worldserver1.getChunkProvider().playerChunkMap.getLoadViewDistance())); // Spigot // Paper - no-tick view distance
+ entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(worldserver1.getChunkProvider().playerChunkMap.playerChunkManager.getLoadDistance())); // Spigot // Paper - no-tick view distance // Tuinity - replace old player chunk management
entityplayer1.spawnIn(worldserver1);
entityplayer1.dead = false;
entityplayer1.playerConnection.teleport(new Location(worldserver1.getWorld(), entityplayer1.locX(), entityplayer1.locY(), entityplayer1.locZ(), entityplayer1.yaw, entityplayer1.pitch));
@@ -1226,7 +1227,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<EntityPlayer> nearbyPlayers = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.fastFloor(d0) >> 4, MCUtil.fastFloor(d2) >> 4);
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> nearbyPlayers = chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.fastFloor(d0) >> 4, MCUtil.fastFloor(d2) >> 4); // Tuinity - replace old player chunk management
if (nearbyPlayers == null) {
return;
}
diff --git a/src/main/java/net/minecraft/server/players/UserCache.java b/src/main/java/net/minecraft/server/players/UserCache.java
index c617b7dd5a992770fc87113987807947ae68be81..7ce5bd130441a3bc32ee5fa3630edce23dadd0b8 100644
--- a/src/main/java/net/minecraft/server/players/UserCache.java
+++ b/src/main/java/net/minecraft/server/players/UserCache.java
@@ -52,6 +52,11 @@ public class UserCache {
private final File g;
private final AtomicLong h = new AtomicLong();
+ // 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 UserCache(GameProfileRepository gameprofilerepository, File file) {
this.e = gameprofilerepository;
this.g = file;
@@ -59,6 +64,7 @@ public class UserCache {
}
private void a(UserCache.UserCacheEntry usercache_usercacheentry) {
+ try { this.stateLock.lock(); // Tuinity - allow better concurrency
GameProfile gameprofile = usercache_usercacheentry.a();
usercache_usercacheentry.a(this.d());
@@ -73,6 +79,7 @@ public class UserCache {
if (uuid != null) {
this.d.put(uuid, usercache_usercacheentry);
}
+ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency
}
@@ -111,7 +118,7 @@ public class UserCache {
}
public void saveProfile(GameProfile gameprofile) { a(gameprofile); } // Paper - OBFHELPER
- public synchronized void a(GameProfile gameprofile) { // Paper - synchronize
+ public void a(GameProfile gameprofile) { // Paper - synchronize // Tuinity - allow better concurrency
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
@@ -128,8 +135,9 @@ public class UserCache {
}
@Nullable
- public synchronized GameProfile getProfile(String s) { // Paper - synchronize
+ public GameProfile getProfile(String s) { // Paper - synchronize // Tuinity start - allow better concurrency
String s1 = s.toLowerCase(Locale.ROOT);
+ boolean stateLocked = true; try { this.stateLock.lock(); // Tuinity - allow better concurrency
UserCache.UserCacheEntry usercache_usercacheentry = (UserCache.UserCacheEntry) this.c.get(s1);
boolean flag = false;
@@ -143,10 +151,14 @@ public class UserCache {
GameProfile gameprofile;
if (usercache_usercacheentry != null) {
+ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency
usercache_usercacheentry.a(this.d());
gameprofile = usercache_usercacheentry.a();
} else {
+ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency
+ try { this.lookupLock.lock(); // Tuinity - allow better concurrency
gameprofile = a(this.e, s); // Spigot - use correct case for offline players
+ } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency
if (gameprofile != null) {
this.a(gameprofile);
flag = false;
@@ -158,6 +170,7 @@ public class UserCache {
}
return gameprofile;
+ } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency
}
// Paper start
@@ -292,7 +305,9 @@ public class UserCache {
}
private Stream<UserCache.UserCacheEntry> a(int i) {
+ try { this.stateLock.lock(); // Tuinity - allow better concurrency
return ImmutableList.copyOf(this.d.values()).stream().sorted(Comparator.comparing(UserCache.UserCacheEntry::c).reversed()).limit((long) i);
+ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency
}
private static JsonElement a(UserCache.UserCacheEntry usercache_usercacheentry, DateFormat dateformat) {
diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
index dca2e9e45116df22d8c95d1be8f0a7e3c2d2b6b1..cb394ec310712cc97d65afe068284b277d7f0483 100644
--- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
@@ -235,7 +235,7 @@ public class WorldUpgrader {
int l = Integer.parseInt(matcher.group(2)) << 5;
try {
- RegionFile regionfile = new RegionFile(file2, file1, true);
+ RegionFile regionfile = new RegionFile(file2, file1, true, true); // Tuinity - allow for chunk regionfiles to regen header
Throwable throwable = null;
try {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index 6e1304f7169c11f67c573b2c8dc11825bcc7da0d..89852779fd9cfd19058afe40feb0cf14ca8d2896 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -228,7 +228,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
public double D;
public double E;
public double F;
- public float G;
+ public float G; public final float getStepHeight() { return this.G; } // Tuinity - OBFHELPER
public boolean noclip;
public float I;
protected final Random random;
@@ -299,6 +299,14 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
// CraftBukkit end
+ // Tuinity start
+ public final AxisAlignedBB getBoundingBoxAt(double x, double y, double z) {
+ double widthHalf = (double)this.size.width / 2.0;
+ double height = (double)this.size.height;
+ return new AxisAlignedBB(x - widthHalf, y, z - widthHalf, x + widthHalf, y + height, z + widthHalf);
+ }
+ // Tuinity end
+
// Paper start - optimise entity tracking
final org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = org.spigotmc.TrackingRange.getTrackingRangeType(this);
@@ -309,8 +317,21 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getPlayersInTrackRange() {
- return ((WorldServer)this.world).getChunkProvider().playerChunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()]
- .getObjectsInRange(MCUtil.getCoordinateKey(this));
+ Collection<Entity> passengers = this.getAllPassengers();
+ net.minecraft.server.level.PlayerChunkMap chunkMap = ((WorldServer)this.world).getChunkProvider().playerChunkMap;
+ 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));
}
// Paper end - optimise entity tracking
@@ -345,6 +366,41 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
// Paper end - make end portalling safe
+ // 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<>());
+ {
+ Boolean hardCollides = cachedOverrides.get(this.getClass());
+ if (hardCollides == null) {
+ try {
+ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("j", Entity.class);
+ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("aZ");
+ 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 = hardCollides.booleanValue();
+ }
+
+ public final boolean hardCollides() {
+ return this.hardCollides;
+ }
+ // Tuinity end
+
public Entity(EntityTypes<?> entitytypes, World world) {
this.id = Entity.entityCount.incrementAndGet();
this.passengers = Lists.newArrayList();
@@ -714,7 +770,39 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return this.onGround;
}
+ // Tuinity start - detailed watchdog information
+ private Vec3D moveVector;
+ private double moveStartX;
+ private double moveStartY;
+ private double moveStartZ;
+
+ public final Vec3D 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(EnumMoveType enummovetype, Vec3D vec3d) {
+ // Tuinity start - detailed watchdog information
+ com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot move an entity off-main");
+ synchronized (this.posLock) {
+ this.moveStartX = this.locX();
+ this.moveStartY = this.locY();
+ this.moveStartZ = this.locZ();
+ this.moveVector = vec3d;
+ }
+ try {
+ // Tuinity end - detailed watchdog information
if (this.noclip) {
this.a(this.getBoundingBox().c(vec3d));
this.recalcPosition();
@@ -742,7 +830,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
// Paper end
vec3d = this.a(vec3d, enummovetype);
- Vec3D vec3d1 = this.g(vec3d);
+ Vec3D vec3d1 = this.performCollision(vec3d); // Tuinity - optimise collisions
if (vec3d1.g() > 1.0E-7D) {
this.a(this.getBoundingBox().c(vec3d1));
@@ -833,7 +921,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
try {
- this.checkBlockCollisions();
+ this.checkBlockCollisions(this.fireTicks <= 0); // Tuinity - move fire checking into method here
} catch (Throwable throwable) {
CrashReport crashreport = CrashReport.a(throwable, "Checking entity block collision");
CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Entity being checked for collision");
@@ -845,11 +933,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
float f2 = this.getBlockSpeedFactor();
this.setMot(this.getMot().d((double) f2, 1.0D, (double) f2));
- if (this.world.c(this.getBoundingBox().shrink(0.001D)).noneMatch((iblockdata1) -> {
- return iblockdata1.a((Tag) TagsBlock.FIRE) || iblockdata1.a(Blocks.LAVA);
- }) && this.fireTicks <= 0) {
- this.setFireTicks(-this.getMaxFireTicks());
- }
+ // Tuinity - move into checkBlockCollisions
if (this.aG() && this.isBurning()) {
this.playSound(SoundEffects.ENTITY_GENERIC_EXTINGUISH_FIRE, 0.7F, 1.6F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F);
@@ -858,6 +942,13 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
this.world.getMethodProfiler().exit();
}
+ // Tuinity start - detailed watchdog information
+ } finally {
+ synchronized (this.posLock) { // Tuinity
+ this.moveVector = null;
+ } // Tuinity
+ }
+ // Tuinity end - detailed watchdog information
}
protected BlockPosition ap() {
@@ -938,6 +1029,137 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return d0;
}
+ // Tuinity start - optimise entity movement
+ private static double performCollisionsX(AxisAlignedBB currentBoundingBox, double value, List<AxisAlignedBB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ AxisAlignedBB target = potentialCollisions.get(i);
+ value = AxisAlignedBB.collideX(target, currentBoundingBox, value);
+ }
+
+ return value;
+ }
+
+ private static double performCollisionsY(AxisAlignedBB currentBoundingBox, double value, List<AxisAlignedBB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ AxisAlignedBB target = potentialCollisions.get(i);
+ value = AxisAlignedBB.collideY(target, currentBoundingBox, value);
+ }
+
+ return value;
+ }
+
+ private static double performCollisionsZ(AxisAlignedBB currentBoundingBox, double value, List<AxisAlignedBB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ AxisAlignedBB target = potentialCollisions.get(i);
+ value = AxisAlignedBB.collideZ(target, currentBoundingBox, value);
+ }
+
+ return value;
+ }
+
+ private static Vec3D performCollisions(Vec3D moveVector, AxisAlignedBB axisalignedbb, List<AxisAlignedBB> potentialCollisions) {
+ double x = moveVector.x;
+ double y = moveVector.y;
+ double z = moveVector.z;
+
+ if (y != 0.0) {
+ y = Entity.performCollisionsY(axisalignedbb, y, potentialCollisions);
+ if (y != 0.0) {
+ axisalignedbb = axisalignedbb.offsetY(y);
+ }
+ }
+
+ boolean xSmaller = Math.abs(x) < Math.abs(z);
+
+ if (xSmaller && z != 0.0) {
+ z = Entity.performCollisionsZ(axisalignedbb, z, potentialCollisions);
+ if (z != 0.0) {
+ axisalignedbb = axisalignedbb.offsetZ(z);
+ }
+ }
+
+ if (x != 0.0) {
+ x = Entity.performCollisionsX(axisalignedbb, x, potentialCollisions);
+ if (!xSmaller && x != 0.0) {
+ axisalignedbb = axisalignedbb.offsetX(x);
+ }
+ }
+
+ if (!xSmaller && z != 0.0) {
+ z = Entity.performCollisionsZ(axisalignedbb, z, potentialCollisions);
+ }
+
+ return new Vec3D(x, y, z);
+ }
+
+ Vec3D performCollision(Vec3D moveVector) {
+ if (moveVector.getX() == 0.0 && moveVector.getY() == 0.0 && moveVector.getZ() == 0.0) {
+ return moveVector;
+ }
+
+ WorldServer world = ((WorldServer)this.world);
+ AxisAlignedBB currBoundingBox = this.getBoundingBox();
+
+ List<AxisAlignedBB> potentialCollisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList();
+ try {
+ AxisAlignedBB collisionBox;
+ double stepHeight = (double)this.getStepHeight();
+
+ if (moveVector.x == 0.0 && moveVector.z == 0.0 && moveVector.y != 0.0) {
+ // a lot of entities just stand still. optimise the search AABB
+ if (moveVector.y > 0.0) {
+ collisionBox = currBoundingBox.cutUpwards(moveVector.y);
+ } else {
+ collisionBox = currBoundingBox.cutDownwards(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 = currBoundingBox.expand(moveVector.x, moveVector.y, moveVector.z).expandUpwards(stepHeight);
+ } else {
+ collisionBox = currBoundingBox.expand(moveVector.x, Math.max(stepHeight, moveVector.y), moveVector.z);
+ }
+ } else {
+ collisionBox = currBoundingBox.expand(moveVector.x, moveVector.y, moveVector.z);
+ }
+ }
+
+ world.getCollisions(this, collisionBox, potentialCollisions, this instanceof EntityPlayer && !this.world.paperConfig.preventMovingIntoUnloadedChunks);
+ if (world.getWorldBorder().isCollidingWithBorderEdge(collisionBox)) {
+ VoxelShapes.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions);
+ }
+
+ Vec3D limitedMoveVector = Entity.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)) {
+ Vec3D vec3d2 = Entity.performCollisions(new Vec3D(moveVector.x, stepHeight, moveVector.z), currBoundingBox, potentialCollisions);
+ Vec3D vec3d3 = Entity.performCollisions(new Vec3D(0.0, stepHeight, 0.0), currBoundingBox.expand(moveVector.x, 0.0, moveVector.z), potentialCollisions);
+
+ if (vec3d3.y < stepHeight) {
+ Vec3D vec3d4 = Entity.performCollisions(new Vec3D(moveVector.x, 0.0D, moveVector.z), currBoundingBox.offset(vec3d3), potentialCollisions).add(vec3d3);
+
+ if (Entity.getXZSquared(vec3d4) > Entity.getXZSquared(vec3d2)) {
+ vec3d2 = vec3d4;
+ }
+ }
+
+ if (Entity.getXZSquared(vec3d2) > Entity.getXZSquared(limitedMoveVector)) {
+ return vec3d2.add(Entity.performCollisions(new Vec3D(0.0D, -vec3d2.y + moveVector.y, 0.0D), currBoundingBox.offset(vec3d2), potentialCollisions));
+ }
+
+ return limitedMoveVector;
+ } else {
+ return limitedMoveVector;
+ }
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(potentialCollisions);
+ }
+ }
+ // Tuinity end - optimise entity movement
+
private Vec3D g(Vec3D vec3d) {
AxisAlignedBB axisalignedbb = this.getBoundingBox();
VoxelShapeCollision voxelshapecollision = VoxelShapeCollision.a(this);
@@ -973,6 +1195,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return vec3d1;
}
+ public static double getXZSquared(Vec3D vec3d) { return Entity.c(vec3d); } // Tuinity - OBFHELPER
public static double c(Vec3D vec3d) {
return vec3d.x * vec3d.x + vec3d.z * vec3d.z;
}
@@ -1085,18 +1308,34 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
protected void checkBlockCollisions() {
+ // Tuinity start
+ this.checkBlockCollisions(false);
+ }
+ protected void checkBlockCollisions(boolean checkFire) {
+ boolean inFire = false;
+ // Tuinity end
AxisAlignedBB axisalignedbb = this.getBoundingBox();
BlockPosition blockposition = new BlockPosition(axisalignedbb.minX + 0.001D, axisalignedbb.minY + 0.001D, axisalignedbb.minZ + 0.001D);
BlockPosition blockposition1 = new BlockPosition(axisalignedbb.maxX - 0.001D, axisalignedbb.maxY - 0.001D, axisalignedbb.maxZ - 0.001D);
BlockPosition.MutableBlockPosition blockposition_mutableblockposition = new BlockPosition.MutableBlockPosition();
if (this.world.areChunksLoadedBetween(blockposition, blockposition1)) {
- for (int i = blockposition.getX(); i <= blockposition1.getX(); ++i) {
- for (int j = blockposition.getY(); j <= blockposition1.getY(); ++j) {
- for (int k = blockposition.getZ(); k <= blockposition1.getZ(); ++k) {
+ // Tuinity start - reorder iteration to more cache aware
+ for (int j = blockposition.getY(); j <= blockposition1.getY(); ++j) {
+ for (int k = blockposition.getZ(); k <= blockposition1.getZ(); ++k) {
+ for (int i = blockposition.getX(); i <= blockposition1.getX(); ++i) {
+ // Tuinity end - reorder iteration to more cache aware
blockposition_mutableblockposition.d(i, j, k);
IBlockData iblockdata = this.world.getType(blockposition_mutableblockposition);
+ // Tuinity start - move fire checking in here - reuse getType from this method
+ if (checkFire) {
+ if (!inFire && (iblockdata.a(TagsBlock.FIRE) || iblockdata.a(Blocks.LAVA))) {
+ inFire = true;
+ }
+ }
+ // Tuinity end - move fire checking in here - reuse getType from this method
+
try {
iblockdata.a(this.world, blockposition_mutableblockposition, this);
this.a(iblockdata);
@@ -1110,6 +1349,11 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
}
}
+ // Tuinity start - move fire checking in here - reuse getType from this method
+ if (checkFire & !inFire) {
+ this.setFireTicks(-this.getMaxFireTicks());
+ }
+ // Tuinity end - move fire checking in here - reuse getType from this method
}
}
@@ -1481,6 +1725,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return d3 * d3 + d4 * d4 + d5 * d5;
}
+ public final double getDistanceSquared(Entity other) { return this.h(other); } // Tuinity - OBFHELPER
public double h(Entity entity) {
return this.e(entity.getPositionVector());
}
@@ -2073,9 +2318,9 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
float f1 = this.size.width * 0.8F;
AxisAlignedBB axisalignedbb = AxisAlignedBB.g((double) f1, 0.10000000149011612D, (double) f1).d(this.locX(), this.getHeadY(), this.locZ());
- return this.world.b(this, axisalignedbb, (iblockdata, blockposition) -> {
+ return ((WorldServer)this.world).collidesWithAnyBlockOrWorldBorder(this, axisalignedbb, false, false, (iblockdata, blockposition) -> { // Tuinity - use optimised method
return iblockdata.o(this.world, blockposition);
- }).findAny().isPresent();
+ }); // Tuinity - use optimised method
}
}
@@ -2083,11 +2328,13 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return EnumInteractionResult.PASS;
}
- public boolean j(Entity entity) {
+ public final boolean hardCollidesWith(Entity other) { return this.j(other); } // Tuinity - OBFHELPER
+ public boolean j(Entity entity) { // Tuinity - diff on change, hard colliding entities override this
return entity.aZ() && !this.isSameVehicle(entity);
}
- public boolean aZ() {
+ public final boolean collisionBoxIsHard() { return this.aZ(); } // Tuinity - OBFHELPER
+ public boolean aZ() {// Tuinity - diff on change, hard colliding entities override this
return false;
}
@@ -2979,7 +3226,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
this.recursiveStream().forEach((entity) -> {
worldserver.chunkCheck(entity);
entity.az = true;
- Iterator iterator = entity.passengers.iterator();
+ Iterator iterator = new java.util.ArrayList<>(entity.passengers).iterator(); // Tuinity - copy list to guard against CME
while (iterator.hasNext()) {
Entity entity1 = (Entity) iterator.next();
@@ -3437,12 +3684,16 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
return this.locBlock;
}
+ public final Object posLock = new Object(); // Tuinity - log detailed entity tick information
+
public Vec3D getMot() {
return this.mot;
}
public void setMot(Vec3D vec3d) {
+ synchronized (this.posLock) { // Tuinity
this.mot = vec3d;
+ } // Tuinity
}
public void setMot(double d0, double d1, double d2) {
@@ -3497,7 +3748,9 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
}
// Paper end
if (this.loc.x != d0 || this.loc.y != d1 || this.loc.z != d2) {
+ synchronized (this.posLock) { // Tuinity
this.loc = new Vec3D(d0, d1, d2);
+ } // Tuinity
int i = MathHelper.floor(d0);
int j = MathHelper.floor(d1);
int k = MathHelper.floor(d2);
diff --git a/src/main/java/net/minecraft/world/entity/EntityInsentient.java b/src/main/java/net/minecraft/world/entity/EntityInsentient.java
index 6c024a5fdd290e5eb219e677e28718d663b72f25..7ba59ff9a7ed39bf69c46973a85f874c43134dc1 100644
--- a/src/main/java/net/minecraft/world/entity/EntityInsentient.java
+++ b/src/main/java/net/minecraft/world/entity/EntityInsentient.java
@@ -779,7 +779,13 @@ public abstract class EntityInsentient extends EntityLiving {
if (this.world.getDifficulty() == EnumDifficulty.PEACEFUL && this.L()) {
this.die();
} else if (!this.isPersistent() && !this.isSpecialPersistence()) {
- EntityHuman entityhuman = this.world.findNearbyPlayer(this, -1.0D, IEntitySelector.affectsSpawning); // Paper
+ // Tuinity start - optimise checkDespawn
+ net.minecraft.world.level.chunk.Chunk chunk = this.getCurrentChunk();
+ EntityHuman entityhuman = chunk == null || this.world.paperConfig.hardDespawnDistance >= (net.minecraft.server.level.PlayerChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED) ? this.world.findNearbyPlayer(this, -1.0D, IEntitySelector.affectsSpawning) : chunk.findNearestPlayer(this.locX(), this.locY(), this.locZ(), -1.0, IEntitySelector.affectsSpawning); // Paper
+ if (entityhuman == null) {
+ entityhuman = ((WorldServer)this.world).playersAffectingSpawning.isEmpty() ? null : ((WorldServer)this.world).playersAffectingSpawning.get(0);
+ }
+ // Tuinity end - optimise checkDespawn
if (entityhuman != null) {
double d0 = entityhuman.h((Entity) this); // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/world/entity/EntityLightning.java b/src/main/java/net/minecraft/world/entity/EntityLightning.java
index 85f571a791bce63989890f277857bc7bdeec0cb5..9e4137768c7d8966759324a4b368330c35faa8a5 100644
--- a/src/main/java/net/minecraft/world/entity/EntityLightning.java
+++ b/src/main/java/net/minecraft/world/entity/EntityLightning.java
@@ -81,8 +81,9 @@ public class EntityLightning extends Entity {
// CraftBukkit start - Use relative location for far away sounds
// this.world.playSound((EntityHuman) null, this.locX(), this.locY(), this.locZ(), SoundEffects.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.WEATHER, 10000.0F, 0.8F + this.random.nextFloat() * 0.2F);
float pitch = 0.8F + this.random.nextFloat() * 0.2F;
- int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16;
+ //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Tuinity - apply view distance patch
for (EntityPlayer player : (List<EntityPlayer>) (List) this.world.getPlayers()) {
+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch
double deltaX = this.locX() - player.locX();
double deltaZ = this.locZ() - player.locZ();
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
diff --git a/src/main/java/net/minecraft/world/entity/EntityLiving.java b/src/main/java/net/minecraft/world/entity/EntityLiving.java
index b7b07b652b29e6f84f87fc92add99ce68f8bbd09..2ff3297fb8c0e4f8c969ba2727eecb7fe06525c4 100644
--- a/src/main/java/net/minecraft/world/entity/EntityLiving.java
+++ b/src/main/java/net/minecraft/world/entity/EntityLiving.java
@@ -2973,7 +2973,11 @@ public abstract class EntityLiving extends Entity {
return;
}
// Paper - end don't run getEntities if we're not going to use its result
- List<Entity> list = this.world.getEntities(this, this.getBoundingBox(), IEntitySelector.pushable(this, world.paperConfig.fixClimbingBypassingCrammingRule)); // Paper - fix climbing bypassing cramming rule
+ // Tuinity start - reduce memory allocation from collideNearby
+ List<Entity> list = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList();
+ this.world.getEntities(this, this.getBoundingBox(), IEntitySelector.pushable(this, world.paperConfig.fixClimbingBypassingCrammingRule), list); // Paper - fix climbing bypassing cramming rule
+ try {
+ // Tuinity end - reduce memory allocation from collideNearby
if (!list.isEmpty()) {
// Paper - move up
@@ -3002,6 +3006,9 @@ public abstract class EntityLiving extends Entity {
this.C(entity);
}
}
+ } finally { // Tuinity start - reduce memory allocation from collideNearby
+ com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(list);
+ } // Tuinity end - reduce memory allocation from collideNearby
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java
index a30b92736d8b36f750eb721d4a056bdfc98845b5..717e5f71fb919ac8952a077714d7f4581d546a28 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/Behavior.java
@@ -11,7 +11,7 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus;
public abstract class Behavior<E extends EntityLiving> {
protected final Map<MemoryModuleType<?>, MemoryStatus> a;
- private Behavior.Status b;
+ private Behavior.Status b; public final Behavior.Status getStatus() { return this.b; } // Tuinity - OBFHELPER
private long c;
private final int d;
private final int e;
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorFindPosition.java b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorFindPosition.java
index a04d4dc665f34687b5d744fea56bc46263f27235..bc8786e2aaeab4dbae4e9c7666ad816bc5bfac3f 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorFindPosition.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorFindPosition.java
@@ -84,7 +84,11 @@ public class BehaviorFindPosition extends Behavior<EntityCreature> {
return true;
}
};
- Set<BlockPosition> set = (Set) villageplace.b(this.b.c(), predicate, entitycreature.getChunkCoordinates(), 48, VillagePlace.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet());
+ // Tuinity start - optimise POI access
+ java.util.List<BlockPosition> poiposes = new java.util.ArrayList<>();
+ com.tuinity.tuinity.util.PoiAccess.findNearestPoiPositions(villageplace, this.b.c(), predicate, entitycreature.getChunkCoordinates(), 48, 48*48, VillagePlace.Occupancy.HAS_SPACE, false, 5, poiposes);
+ Set<BlockPosition> set = new java.util.HashSet<>(poiposes);
+ // Tuinity end - optimise POI access
PathEntity pathentity = entitycreature.getNavigation().a(set, this.b.d());
if (pathentity != null && pathentity.j()) {
@@ -94,7 +98,7 @@ public class BehaviorFindPosition extends Behavior<EntityCreature> {
villageplace.a(this.b.c(), (blockposition1) -> {
return blockposition1.equals(blockposition);
}, blockposition, 1);
- entitycreature.getBehaviorController().setMemory(this.c, (Object) GlobalPos.create(worldserver.getDimensionKey(), blockposition));
+ entitycreature.getBehaviorController().setMemory(this.c, GlobalPos.create(worldserver.getDimensionKey(), blockposition)); // Tuinity - decompile fix
this.e.ifPresent((obyte) -> {
worldserver.broadcastEntityEffect(entitycreature, obyte);
});
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorGate.java b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorGate.java
index 2d4345de154fb2d31f34695672ebdb4dac31b181..c46cdffe3d877bff70b843766c8189eae06148ff 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorGate.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorGate.java
@@ -17,7 +17,7 @@ public class BehaviorGate<E extends EntityLiving> extends Behavior<E> {
private final Set<MemoryModuleType<?>> b;
private final BehaviorGate.Order c;
private final BehaviorGate.Execution d;
- private final WeightedList<Behavior<? super E>> e = new WeightedList<>(false); // Paper - don't use a clone
+ private final WeightedList<Behavior<? super E>> e = new WeightedList<>(false); protected final WeightedList<Behavior<? super E>> getList() { return this.e; } // Paper - don't use a clone // Tuinity - OBFHELPER
public BehaviorGate(Map<MemoryModuleType<?>, MemoryStatus> map, Set<MemoryModuleType<?>> set, BehaviorGate.Order behaviorgate_order, BehaviorGate.Execution behaviorgate_execution, List<Pair<Behavior<? super E>, Integer>> list) {
super(map);
@@ -31,11 +31,17 @@ public class BehaviorGate<E extends EntityLiving> extends Behavior<E> {
@Override
protected boolean b(WorldServer worldserver, E e0, long i) {
- return this.e.c().filter((behavior) -> {
- return behavior.a() == Behavior.Status.RUNNING;
- }).anyMatch((behavior) -> {
- return behavior.b(worldserver, e0, i);
- });
+ // Tuinity start - remove streams
+ List<WeightedList.a<Behavior<? super E>>> list = this.getList().getList();
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ Behavior<? super E> behavior = list.get(index).getValue();
+ if (behavior.getStatus() == Status.RUNNING && behavior.b(worldserver, e0, i)) { // copied from removed code, make sure to update
+ return true;
+ }
+ }
+
+ return false;
+ // Tuinity end - remove streams
}
@Override
@@ -51,20 +57,28 @@ public class BehaviorGate<E extends EntityLiving> extends Behavior<E> {
@Override
protected void d(WorldServer worldserver, E e0, long i) {
- this.e.c().filter((behavior) -> {
- return behavior.a() == Behavior.Status.RUNNING;
- }).forEach((behavior) -> {
- behavior.f(worldserver, e0, i);
- });
+ // Tuinity start - remove streams
+ List<WeightedList.a<Behavior<? super E>>> list = this.getList().getList();
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ Behavior<? super E> behavior = list.get(index).getValue();
+ if (behavior.getStatus() == Behavior.Status.RUNNING) {
+ behavior.f(worldserver, e0, i); // copied from removed code, make sure to update
+ }
+ }
+ // Tuinity end - remove streams
}
@Override
protected void c(WorldServer worldserver, E e0, long i) {
- this.e.c().filter((behavior) -> {
- return behavior.a() == Behavior.Status.RUNNING;
- }).forEach((behavior) -> {
- behavior.g(worldserver, e0, i);
- });
+ // Tuinity start - remove streams
+ List<WeightedList.a<Behavior<? super E>>> list = this.getList().getList();
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ Behavior<? super E> behavior = list.get(index).getValue();
+ if (behavior.getStatus() == Behavior.Status.RUNNING) {
+ behavior.g(worldserver, e0, i); // copied from removed code, make sure to update
+ }
+ }
+ // Tuinity end - remove streams
BehaviorController behaviorcontroller = e0.getBehaviorController();
this.b.forEach(behaviorcontroller::removeMemory); // Paper - decomp fix
@@ -84,21 +98,29 @@ public class BehaviorGate<E extends EntityLiving> extends Behavior<E> {
RUN_ONE {
@Override
public <E extends EntityLiving> void a(WeightedList<Behavior<? super E>> weightedlist, WorldServer worldserver, E e0, long i) {
- weightedlist.c().filter((behavior) -> {
- return behavior.a() == Behavior.Status.STOPPED;
- }).filter((behavior) -> {
- return behavior.e(worldserver, e0, i);
- }).findFirst();
+ // Tuinity start - remove streams
+ List<WeightedList.a<Behavior<? super E>>> list = weightedlist.getList();
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ Behavior<? super E> behavior = list.get(index).getValue();
+ if (behavior.getStatus() == Behavior.Status.STOPPED && behavior.e(worldserver, e0, i)) { // copied from removed code, make sure to update
+ break;
+ }
+ }
+ // Tuinity end - remove streams
}
},
TRY_ALL {
@Override
public <E extends EntityLiving> void a(WeightedList<Behavior<? super E>> weightedlist, WorldServer worldserver, E e0, long i) {
- weightedlist.c().filter((behavior) -> {
- return behavior.a() == Behavior.Status.STOPPED;
- }).forEach((behavior) -> {
- behavior.e(worldserver, e0, i);
- });
+ // Tuinity start - remove streams
+ List<WeightedList.a<Behavior<? super E>>> list = weightedlist.getList();
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ Behavior<? super E> behavior = list.get(index).getValue();
+ if (behavior.getStatus() == Behavior.Status.STOPPED) {
+ behavior.e(worldserver, e0, i); // copied from removed code, make sure to update
+ }
+ }
+ // Tuinity end - remove streams
}
};
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorLookInteract.java b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorLookInteract.java
index f56072c77b9dfd0eeafb7a6970eecf593315f63e..196a5659a1f2c8c20a10f36c3fa81a8edc43a00c 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorLookInteract.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorLookInteract.java
@@ -14,7 +14,7 @@ import net.minecraft.world.entity.ai.memory.MemoryStatus;
public class BehaviorLookInteract extends Behavior<EntityLiving> {
private final EntityTypes<?> b;
- private final int c;
+ private final int c; private final int getMaxRange() { return this.c; } // Tuinity - OBFHELPER
private final Predicate<EntityLiving> d;
private final Predicate<EntityLiving> e;
@@ -36,7 +36,20 @@ public class BehaviorLookInteract extends Behavior<EntityLiving> {
@Override
public boolean a(WorldServer worldserver, EntityLiving entityliving) {
- return this.e.test(entityliving) && this.b(entityliving).stream().anyMatch(this::a);
+ // Tuinity start - remove streams
+ if (!this.e.test(entityliving)) {
+ return false;
+ }
+
+ List<EntityLiving> list = this.b(entityliving);
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ if (this.a(list.get(index))) {
+ return true;
+ }
+ }
+
+ return false;
+ // Tuinity end - remove streams
}
@Override
@@ -44,16 +57,28 @@ public class BehaviorLookInteract extends Behavior<EntityLiving> {
super.a(worldserver, entityliving, i);
BehaviorController<?> behaviorcontroller = entityliving.getBehaviorController();
- behaviorcontroller.getMemory(MemoryModuleType.VISIBLE_MOBS).ifPresent((list) -> {
- list.stream().filter((entityliving1) -> {
- return entityliving1.h((Entity) entityliving) <= (double) this.c;
- }).filter(this::a).findFirst().ifPresent((entityliving1) -> {
- behaviorcontroller.setMemory(MemoryModuleType.INTERACTION_TARGET, (Object) entityliving1);
- behaviorcontroller.setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new BehaviorPositionEntity(entityliving1, true)));
- });
- });
+ // Tuinity start - remove streams
+ List<EntityLiving> inLOS = behaviorcontroller.getMemory(MemoryModuleType.VISIBLE_MOBS).orElse(null);
+ if (inLOS != null) {
+ double maxRangeSquared = this.getMaxRange();
+ for (int index = 0, len = inLOS.size(); index < len; ++index) {
+ EntityLiving entity = inLOS.get(index);
+ if (!this.canTarget(entity)) {
+ continue;
+ }
+ double distance = entity.getDistanceSquared(entityliving.locX(), entityliving.locY(), entityliving.locZ());
+ if (distance > maxRangeSquared) {
+ continue;
+ }
+ behaviorcontroller.setMemory(MemoryModuleType.INTERACTION_TARGET, entity); // Tuinity - decompile fix
+ behaviorcontroller.setMemory(MemoryModuleType.LOOK_TARGET, (new BehaviorPositionEntity(entity, true))); // Tuinity - decompile fix
+ break;
+ }
+ }
+ // Tuinity end - remove streams
}
+ private final boolean canTarget(EntityLiving entityliving) { return this.a(entityliving); } // Tuinity - OBFHELPER
private boolean a(EntityLiving entityliving) {
return this.b.equals(entityliving.getEntityType()) && this.d.test(entityliving);
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/WeightedList.java b/src/main/java/net/minecraft/world/entity/ai/behavior/WeightedList.java
index 1ca9b0595ae9d914d23590ec0b0c2e857c39b250..e2b5d6155bebdbf99b0850de7f9e1f5d342f9e2f 100644
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/WeightedList.java
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/WeightedList.java
@@ -14,7 +14,7 @@ import java.util.stream.Stream;
public class WeightedList<U> {
- protected final List<WeightedList.a<U>> list; // Paper - decompile conflict
+ protected final List<WeightedList.a<U>> list; public final List<WeightedList.a<U>> getList() { return this.list; } // Paper - decompile conflict // Tuinity - OBFHELPER
private final Random b;
private final boolean isUnsafe; // Paper
@@ -74,7 +74,7 @@ public class WeightedList<U> {
public static class a<T> {
- private final T a;
+ private final T a; public final T getValue() { return this.a; } // Tuinity - OBFHELPER
private final int b;
private double c;
diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/NavigationAbstract.java b/src/main/java/net/minecraft/world/entity/ai/navigation/NavigationAbstract.java
index e6ba9b7fbf08ae0dd083a1ebee8eb7ed8a937751..148bdbc2cffb002d8b6dd05e70854ab503804949 100644
--- a/src/main/java/net/minecraft/world/entity/ai/navigation/NavigationAbstract.java
+++ b/src/main/java/net/minecraft/world/entity/ai/navigation/NavigationAbstract.java
@@ -30,7 +30,7 @@ import net.minecraft.world.phys.Vec3D;
public abstract class NavigationAbstract {
- protected final EntityInsentient a; public Entity getEntity() { return a; } // Paper - OBFHELPER
+ protected final EntityInsentient a; public final EntityInsentient getEntity() { return a; } // Paper - OBFHELPER // Tuinity - match types
protected final World b;
@Nullable
protected PathEntity c; protected final PathEntity getCurrentPath() { return this.c; } // Paper - OBFHELPER
@@ -43,7 +43,7 @@ public abstract class NavigationAbstract {
protected long j;
protected double k;
protected float l;
- protected boolean m;
+ protected boolean m; protected final boolean needsPathRecalculation() { return this.m; } // Tuinity - OBFHELPER
protected long n;
protected PathfinderAbstract o;
private BlockPosition p;
@@ -52,6 +52,13 @@ public abstract class NavigationAbstract {
private final Pathfinder s; public Pathfinder getPathfinder() { return this.s; } // Paper - OBFHELPER
private boolean t;
+ // Tuinity start
+ public boolean isViableForPathRecalculationChecking() {
+ return !this.needsPathRecalculation() &&
+ (this.c != null && !this.c.c() && this.c.e() != 0);
+ }
+ // Tuinity end
+
public NavigationAbstract(EntityInsentient entityinsentient, World world) {
this.g = Vec3D.ORIGIN;
this.h = BaseBlockPosition.ZERO;
@@ -415,7 +422,7 @@ public abstract class NavigationAbstract {
}
public void b(BlockPosition blockposition) {
- if (this.c != null && !this.c.c() && this.c.e() != 0) {
+ if (this.c != null && !this.c.c() && this.c.e() != 0) { // Tuinity - diff on change - needed for isViableForPathRecalculationChecking()
PathPoint pathpoint = this.c.d();
Vec3D vec3d = new Vec3D(((double) pathpoint.a + this.a.locX()) / 2.0D, ((double) pathpoint.b + this.a.locY()) / 2.0D, ((double) pathpoint.c + this.a.locZ()) / 2.0D);
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestBed.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestBed.java
index 1e58a06860125243d0f7c062aca095dd2aae98f2..9427072575df9c0f1b9dfb065b6bef43df1f8fc5 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestBed.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestBed.java
@@ -47,15 +47,19 @@ public class SensorNearestBed extends Sensor<EntityInsentient> {
return true;
}
};
- Stream<BlockPosition> stream = villageplace.a(VillagePlaceType.r.c(), predicate, entityinsentient.getChunkCoordinates(), 48, VillagePlace.Occupancy.ANY);
- PathEntity pathentity = entityinsentient.getNavigation().a(stream, VillagePlaceType.r.d());
+ // Tuinity start - optimise POI access
+ java.util.List<BlockPosition> poiposes = new java.util.ArrayList<>();
+ // don't ask me why it's unbounded. ask mojang.
+ com.tuinity.tuinity.util.PoiAccess.findAnyPoiPositions(villageplace, VillagePlaceType.r.c(), predicate, entityinsentient.getChunkCoordinates(), 48, VillagePlace.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes);
+ PathEntity pathentity = entityinsentient.getNavigation().a(new java.util.HashSet<>(poiposes), VillagePlaceType.r.d());
+ // Tuinity end - optimise POI access
if (pathentity != null && pathentity.j()) {
BlockPosition blockposition = pathentity.m();
Optional<VillagePlaceType> optional = villageplace.c(blockposition);
if (optional.isPresent()) {
- entityinsentient.getBehaviorController().setMemory(MemoryModuleType.NEAREST_BED, (Object) blockposition);
+ entityinsentient.getBehaviorController().setMemory(MemoryModuleType.NEAREST_BED, blockposition); // Tuinity - decompile fix
}
} else if (this.b < 5) {
this.a.long2LongEntrySet().removeIf((entry) -> {
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestItems.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestItems.java
index 2e3149a0b15299468079796bd3ea56eabdb4998c..943e269b99321cbd7373181a898dcdfa6cbb6a89 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestItems.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestItems.java
@@ -24,19 +24,24 @@ public class SensorNearestItems extends Sensor<EntityInsentient> {
protected void a(WorldServer worldserver, EntityInsentient entityinsentient) {
BehaviorController<?> behaviorcontroller = entityinsentient.getBehaviorController();
- List<EntityItem> list = worldserver.a(EntityItem.class, entityinsentient.getBoundingBox().grow(8.0D, 4.0D, 8.0D), (entityitem) -> {
- return true;
+ // Tuinity start - remove streams
+ List<EntityItem> list = worldserver.a(EntityItem.class, entityinsentient.getBoundingBox().grow(8.0D, 4.0D, 8.0D), (EntityItem item) -> {
+ return entityinsentient.i(item.getItemStack()) && item.a((Entity)entityinsentient, 9.0D); // copied from removed code, make sure to update - move here so we sort less
});
- // Paper start - remove streams in favour of lists
- list.sort(Comparator.comparingDouble(entityinsentient::h));
+
+ list.sort(Comparator.comparingDouble(entityinsentient::h)); // better to take the sort perf hit than using line of sight more than we need to.
EntityItem nearest = null;
- for (EntityItem entityItem : list) {
- if (entityinsentient.i(entityItem.getItemStack()) && entityItem.a(entityinsentient, 9.0D) && entityinsentient.hasLineOfSight(entityItem)) {
- nearest = entityItem;
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ EntityItem item = list.get(index);
+ if (entityinsentient.hasLineOfSight(item)) {
+ nearest = item;
break;
}
}
- behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest));
- // Paper end
+
+ Optional<EntityItem> optional = Optional.ofNullable(nearest);
+ // Tuinity end - remove streams
+
+ behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional);
}
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestLivingEntities.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestLivingEntities.java
index 0bc17d148e91277efdf72541e5470fa56d455670..845ed38e48fe404df1d2b0d5aba657628b1e2056 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestLivingEntities.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestLivingEntities.java
@@ -27,11 +27,16 @@ public class SensorNearestLivingEntities extends Sensor<EntityLiving> {
BehaviorController<?> behaviorcontroller = entityliving.getBehaviorController();
behaviorcontroller.setMemory(MemoryModuleType.MOBS, list); // Paper - decompile error
- // Paper start - remove streams in favour of lists
- List<EntityLiving> visibleMobs = new java.util.ArrayList<>(list);
- visibleMobs.removeIf(otherEntityLiving -> !Sensor.a(entityliving, otherEntityLiving));
- behaviorcontroller.setMemory(MemoryModuleType.VISIBLE_MOBS, visibleMobs);
- // Paper end
+ // Tuinity start - remove streams
+ List<EntityLiving> visible = new java.util.ArrayList<>(list.size());
+ for (int index = 0, len = list.size(); index < len; ++index) {
+ EntityLiving nearby = list.get(index);
+ if (Sensor.a(entityliving, nearby)) { // copied from removed code, make sure to update
+ visible.add(nearby);
+ }
+ }
+ behaviorcontroller.setMemory(MemoryModuleType.VISIBLE_MOBS, visible);
+ // Tuinity end - remove streams
}
@Override
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestPlayers.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestPlayers.java
index 60e4da9217d4d950b5077baf6b50eaee20f8df09..c2afe3481265eb9390592ad46ba78d5be48d500f 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestPlayers.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorNearestPlayers.java
@@ -26,26 +26,31 @@ public class SensorNearestPlayers extends Sensor<EntityLiving> {
@Override
protected void a(WorldServer worldserver, EntityLiving entityliving) {
- // Paper start - remove streams in favour of lists
- List<EntityHuman> players = new java.util.ArrayList<>(worldserver.getPlayers());
- players.removeIf(player -> !IEntitySelector.notSpectator().test(player) || !entityliving.a(player, 16.0D)); // Paper - removeIf only re-allocates once compared to iterator
- players.sort(Comparator.comparingDouble(entityliving::h));
-
+ // Tuinity start - remove streams
+ List<EntityHuman> nearby = (List)worldserver.getNearbyPlayers(entityliving, entityliving.locX(), entityliving.locY(), entityliving.locZ(),
+ 16.0, IEntitySelector.g);
+ nearby.sort((e1, e2) -> Double.compare(entityliving.getDistanceSquared(e1), entityliving.getDistanceSquared(e2)));
BehaviorController<?> behaviorcontroller = entityliving.getBehaviorController();
- behaviorcontroller.setMemory(MemoryModuleType.NEAREST_PLAYERS, players);
- EntityHuman nearest = null, nearestTargetable = null;
- for (EntityHuman player : players) {
- if (Sensor.a(entityliving, player)) {
- if (nearest == null) nearest = player;
- if (IEntitySelector.canAITarget().test(player)) {
- nearestTargetable = player;
- break; // Both variables are assigned, no reason to loop further
- }
+ behaviorcontroller.setMemory(MemoryModuleType.NEAREST_PLAYERS, nearby);
+ EntityHuman first = null;
+ EntityHuman firstNonSpectator = null;
+ for (int index = 0, len = nearby.size(); index < len; ++index) {
+ EntityHuman entity = nearby.get(index);
+ if (!Sensor.a(entityliving, (EntityLiving)entity)) { // copied from removed code, make sure to update
+ continue;
+ }
+ if (first == null) {
+ first = entity;
+ }
+ if (IEntitySelector.f.test(entity)) { // copied from removed code, make sure to update
+ firstNonSpectator = entity;
+ break;
}
}
- behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, nearest);
- behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_TARGETABLE_PLAYER, nearestTargetable);
- // Paper end
+
+ behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, first);
+ behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_TARGETABLE_PLAYER, Optional.ofNullable(firstNonSpectator));
+ // Tuinity end - remove streams
}
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorVillagerBabies.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorVillagerBabies.java
index e5e246143f014d78dac765c97a5d9e3ac91a7793..aaf31114f3e09304db83270f4f521fa7f566da06 100644
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/SensorVillagerBabies.java
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SensorVillagerBabies.java
@@ -21,11 +21,23 @@ public class SensorVillagerBabies extends Sensor<EntityLiving> {
@Override
protected void a(WorldServer worldserver, EntityLiving entityliving) {
- entityliving.getBehaviorController().setMemory(MemoryModuleType.VISIBLE_VILLAGER_BABIES, (Object) this.a(entityliving));
+ entityliving.getBehaviorController().setMemory(MemoryModuleType.VISIBLE_VILLAGER_BABIES, this.a(entityliving)); // Tuinity - decompile fix
}
private List<EntityLiving> a(EntityLiving entityliving) {
- return (List) this.c(entityliving).stream().filter(this::b).collect(Collectors.toList());
+ // Tuinity start - remove streams
+ List<EntityLiving> nearby = this.c(entityliving); // copied from removed code, make sure to update
+ List<EntityLiving> ret = new java.util.ArrayList<>();
+
+ for (int index = 0, len = nearby.size(); index < len; ++index) {
+ EntityLiving entity = nearby.get(index);
+ if (this.b(entity)) { // copied from removed code, make sure to update
+ ret.add(entity);
+ }
+ }
+
+ return ret;
+ // Tuinity end - remove streams
}
private boolean b(EntityLiving entityliving) {
diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java b/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
index e6988f7ea428f1503e3db63876b13e57f898ee30..88972dd8252bd2d2d8e384d616484ff682949fa8 100644
--- a/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
+++ b/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
@@ -55,6 +55,7 @@ public class PathfinderTargetCondition {
return this;
}
+ public final boolean test(@Nullable EntityLiving entityliving, EntityLiving entityliving1) { return this.a(entityliving, entityliving1); } // Tuinity - OBFHELPER
public boolean a(@Nullable EntityLiving entityliving, EntityLiving entityliving1) {
if (entityliving == entityliving1) {
return false;
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlace.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlace.java
index ce165233739c7b92a76031b949f269bd0a11149c..13d94ecd703b3cd0412e138532d2dd74e5bf250d 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlace.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlace.java
@@ -4,6 +4,7 @@ import com.mojang.datafixers.DataFixer;
import com.mojang.datafixers.util.Pair;
import it.unimi.dsi.fastutil.longs.Long2ByteMap;
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Tuinity
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import java.io.File;
@@ -35,8 +36,24 @@ import net.minecraft.world.level.chunk.storage.RegionFileSection;
public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
- private final VillagePlace.a a = new VillagePlace.a();
- private final LongSet b = new LongOpenHashSet();
+ // Tuinity start - unload poi data
+ // the vanilla tracker needs to be replaced because it does not support level removes
+ private final com.tuinity.tuinity.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new com.tuinity.tuinity.util.misc.Delayed26WayDistancePropagator3D();
+ static final int POI_DATA_SOURCE = 7;
+ public static int convertBetweenLevels(final int level) {
+ return POI_DATA_SOURCE - level;
+ }
+
+ protected void updateDistanceTracking(long section) {
+ if (this.isSectionDistanceTrackerSource(section)) {
+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
+ } else {
+ this.villageDistanceTracker.removeSource(section);
+ }
+ }
+ // Tuinity end - unload poi data
+
+ private final LongSet b = new LongOpenHashSet(); private final LongSet getLoadedChunks() { return this.b; } // Tuinity - OBFHELPER
private final WorldServer world; // Paper
@@ -47,9 +64,124 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
public VillagePlace(File file, DataFixer datafixer, boolean flag, WorldServer world) {
super(file, VillagePlaceSection::a, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK, flag);
this.world = world;
+ if (world == null) { throw new IllegalStateException("world must be non-null"); }// Tuinity - require non-null
// Paper end - add world parameter
}
+ // Tuinity start - actually unload POI data
+ private final java.util.TreeSet<QueuedUnload> queuedUnloads = new java.util.TreeSet<>();
+ private final Long2ObjectOpenHashMap<QueuedUnload> queuedUnloadsByCoordinate = new Long2ObjectOpenHashMap<>();
+
+ static final class QueuedUnload implements Comparable<QueuedUnload> {
+
+ private final long unloadTick;
+ private final long coordinate;
+
+ public QueuedUnload(long unloadTick, long coordinate) {
+ this.unloadTick = unloadTick;
+ this.coordinate = coordinate;
+ }
+
+ @Override
+ public int compareTo(QueuedUnload other) {
+ if (other.unloadTick == this.unloadTick) {
+ return Long.compare(this.coordinate, other.coordinate);
+ } else {
+ return Long.compare(this.unloadTick, other.unloadTick);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 1;
+ hash = hash * 31 + Long.hashCode(this.unloadTick);
+ hash = hash * 31 + Long.hashCode(this.coordinate);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || obj.getClass() != QueuedUnload.class) {
+ return false;
+ }
+ QueuedUnload other = (QueuedUnload)obj;
+ return other.unloadTick == this.unloadTick && other.coordinate == this.coordinate;
+ }
+ }
+
+ long determineDelay(long coordinate) {
+ if (this.isEmpty(coordinate)) {
+ return 5 * 60 * 20;
+ } else {
+ return 60 * 20;
+ }
+ }
+
+ public void queueUnload(long coordinate, long minTarget) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload queue");
+ QueuedUnload unload = new QueuedUnload(minTarget + this.determineDelay(coordinate), coordinate);
+ QueuedUnload existing = this.queuedUnloadsByCoordinate.put(coordinate, unload);
+ if (existing != null) {
+ this.queuedUnloads.remove(existing);
+ }
+ this.queuedUnloads.add(unload);
+ }
+
+ public void dequeueUnload(long coordinate) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload dequeue");
+ QueuedUnload unload = this.queuedUnloadsByCoordinate.remove(coordinate);
+ if (unload != null) {
+ this.queuedUnloads.remove(unload);
+ }
+ }
+
+ public void pollUnloads(BooleanSupplier canSleepForTick) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload");
+ long currentTick = net.minecraft.server.MinecraftServer.currentTickLong;
+ net.minecraft.server.level.ChunkProviderServer chunkProvider = this.world.getChunkProvider();
+ net.minecraft.server.level.PlayerChunkMap playerChunkMap = chunkProvider.playerChunkMap;
+ // copied target determination from PlayerChunkMap
+ int target = Math.min(this.queuedUnloads.size() - 100, (int) (this.queuedUnloads.size() * net.minecraft.server.level.PlayerChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive
+ for (java.util.Iterator<QueuedUnload> iterator = this.queuedUnloads.iterator();
+ iterator.hasNext() && (this.queuedUnloads.size() > target || canSleepForTick.getAsBoolean());) {
+ QueuedUnload unload = iterator.next();
+ if (unload.unloadTick > currentTick) {
+ break;
+ }
+
+ long coordinate = unload.coordinate;
+
+ iterator.remove();
+ this.queuedUnloadsByCoordinate.remove(coordinate);
+
+ if (playerChunkMap.getUnloadingPlayerChunk(net.minecraft.server.MCUtil.getCoordinateX(coordinate), net.minecraft.server.MCUtil.getCoordinateZ(coordinate)) != null
+ || playerChunkMap.getUpdatingChunk(coordinate) != null) {
+ continue;
+ }
+
+ this.unloadData(coordinate);
+ }
+ }
+
+ @Override
+ public void unloadData(long coordinate) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async unloading poi data");
+ super.unloadData(coordinate);
+ }
+
+ @Override
+ protected void onUnload(long coordinate) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload callback");
+ this.getLoadedChunks().remove(coordinate);
+ int chunkX = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
+ int chunkZ = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
+ for (int section = 0; section < 16; ++section) {
+ long sectionPos = SectionPosition.asLong(chunkX, section, chunkZ);
+ this.updateDistanceTracking(sectionPos);
+ }
+ }
+ // Tuinity end - actually unload POI data
+
public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) {
((VillagePlaceSection) this.e(SectionPosition.a(blockposition).s())).a(blockposition, villageplacetype);
}
@@ -108,31 +240,47 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
}
public Optional<BlockPosition> c(Predicate<VillagePlaceType> predicate, Predicate<BlockPosition> predicate1, BlockPosition blockposition, int i, VillagePlace.Occupancy villageplace_occupancy) {
- return this.a(predicate, predicate1, blockposition, i, villageplace_occupancy).findFirst();
+ // Tuinity start - re-route to faster logic
+ BlockPosition ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiPosition(this, predicate, predicate1, blockposition, i, villageplace_occupancy, false);
+ return Optional.ofNullable(ret);
+ // Tuinity end - re-route to faster logic
}
public Optional<BlockPosition> d(Predicate<VillagePlaceType> predicate, BlockPosition blockposition, int i, VillagePlace.Occupancy villageplace_occupancy) {
- return this.c(predicate, blockposition, i, villageplace_occupancy).map(VillagePlaceRecord::f).min(Comparator.comparingDouble((blockposition1) -> {
- return blockposition1.j(blockposition);
- }));
+ // Tuinity start - re-route to faster logic
+ BlockPosition ret = com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataPosition(this, predicate, null, blockposition, i, i*i, villageplace_occupancy, false);
+ return Optional.ofNullable(ret);
+ // Tuinity end - re-route to faster logic
}
public Optional<BlockPosition> a(Predicate<VillagePlaceType> predicate, Predicate<BlockPosition> predicate1, BlockPosition blockposition, int i) {
- return this.c(predicate, blockposition, i, VillagePlace.Occupancy.HAS_SPACE).filter((villageplacerecord) -> {
- return predicate1.test(villageplacerecord.f());
- }).findFirst().map((villageplacerecord) -> {
- villageplacerecord.b();
- return villageplacerecord.f();
- });
+ // Tuinity start - re-route to faster logic
+ VillagePlaceRecord ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecord(this, predicate, predicate1, blockposition, i,
+ VillagePlace.Occupancy.HAS_SPACE, false);
+ if (ret == null) {
+ return Optional.empty();
+ }
+ // copy from the map() from the old code
+ ret.b();
+ return Optional.ofNullable(ret.f());
+ // Tuinity end - re-route to faster logic
}
public Optional<BlockPosition> a(Predicate<VillagePlaceType> predicate, Predicate<BlockPosition> predicate1, VillagePlace.Occupancy villageplace_occupancy, BlockPosition blockposition, int i, Random random) {
- List<VillagePlaceRecord> list = (List) this.c(predicate, blockposition, i, villageplace_occupancy).collect(Collectors.toList());
-
- Collections.shuffle(list, random);
- return list.stream().filter((villageplacerecord) -> {
- return predicate1.test(villageplacerecord.f());
- }).findFirst().map(VillagePlaceRecord::f);
+ // Tuinity start - re-route to faster logic
+ List<VillagePlaceRecord> list = new java.util.ArrayList<>();
+ com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecords(this, predicate, predicate1, blockposition, i, villageplace_occupancy, false, Integer.MAX_VALUE, list);
+ // Tuinity end - re-route to faster logic
+
+ // Tuinity start - rewrite this to use improved logic
+ // the old method shuffled the list and then tried to find the first element in it that
+ // matched predicate1, however we moved predicate1 into the poi search. This means we can avoid a shuffle
+ // entirely, and just pick a random element from list
+ if (list.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(list.get(random.nextInt(list.size())).f());
+ // Tuinity end - rewrite this to use improved logic
}
public boolean b(BlockPosition blockposition) {
@@ -153,10 +301,11 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
}
public int a(SectionPosition sectionposition) {
- this.a.a();
- return this.a.c(sectionposition.s());
+ this.villageDistanceTracker.propagateUpdates(); // Tuinity - replace distance tracking util
+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(net.minecraft.server.MCUtil.getSectionKey(sectionposition))); // Tuinity - replace distance tracking util
}
+ private boolean isSectionDistanceTrackerSource(long section) { return this.f(section); } // Tuinity - OBFHELPER
private boolean f(long i) {
Optional<VillagePlaceSection> optional = this.c(i);
@@ -172,7 +321,7 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
super.a(booleansupplier);
} else {
//super.a(booleansupplier); // re-implement below
- while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) {
+ while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean() && !this.world.isSavingDisabled()) { // Tuinity - unload POI data - don't write to disk if saving is disabled
ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).r();
NBTTagCompound data;
@@ -180,22 +329,27 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
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
}
}
+ // Tuinity start - unload POI data
+ if (!this.world.isSavingDisabled()) { // don't write to disk if saving is disabled
+ this.pollUnloads(booleansupplier);
+ }
+ // Tuinity end - unload POI data
// Paper end
- this.a.a();
+ this.villageDistanceTracker.propagateUpdates(); // Tuinity - replace distance tracking until
}
@Override
protected void a(long i) {
super.a(i);
- this.a.b(i, this.a.b(i), false);
+ this.updateDistanceTracking(i); // Tuinity - move to new distance tracking util
}
@Override
protected void b(long i) {
- this.a.b(i, this.a.b(i), false);
+ this.updateDistanceTracking(i); // Tuinity - move to new distance tracking util
}
public void a(ChunkCoordIntPair chunkcoordintpair, ChunkSection chunksection) {
@@ -260,7 +414,7 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
@Override
protected int b(long i) {
- return VillagePlace.this.f(i) ? 0 : 7;
+ return VillagePlace.this.f(i) ? 0 : 7; // Tuinity - unload poi data - diff on change, this specifies the source level to use for distance tracking
}
@Override
@@ -305,7 +459,7 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
- com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - writes are async, no need for priority
return;
}
super.write(chunkcoordintpair, nbttagcompound);
@@ -324,6 +478,7 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
this.d = predicate;
}
+ public final Predicate<? super VillagePlaceRecord> getPredicate() { return this.a(); } // Tuinity - OBFHELPER
public Predicate<? super VillagePlaceRecord> a() {
return this.d;
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceRecord.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceRecord.java
index 3fcbf223c4835b92a7da0df9e5443ca1e5217bb4..29a291bc08d8043e5bd8353798be930582455feb 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceRecord.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceRecord.java
@@ -8,7 +8,7 @@ import net.minecraft.core.IRegistry;
public class VillagePlaceRecord {
- private final BlockPosition a;
+ private final BlockPosition a; public final BlockPosition getPosition() { return this.a; } // Tuinity - OBFHELPER
private final VillagePlaceType b;
private int c;
private final Runnable d;
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceSection.java
index a72bd9ce8f683ae3f4d9c08c973292e3433e8f16..41ffad7cbb6c77713736f37b3728b201d315f6d4 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceSection.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/VillagePlaceSection.java
@@ -27,12 +27,12 @@ public class VillagePlaceSection {
private static final Logger LOGGER = LogManager.getLogger();
private final Short2ObjectMap<VillagePlaceRecord> b;
- private final Map<VillagePlaceType, Set<VillagePlaceRecord>> c;
+ private final Map<VillagePlaceType, Set<VillagePlaceRecord>> c; public final Map<VillagePlaceType, Set<VillagePlaceRecord>> getData() { return this.c; } // Tuinity - OBFHELPER
private final Runnable d;
private boolean e;
public static Codec<VillagePlaceSection> a(Runnable runnable) {
- Codec codec = RecordCodecBuilder.create((instance) -> {
+ Codec<VillagePlaceSection> codec = RecordCodecBuilder.create((instance) -> { // Tuinity - decompile fix
return instance.group(RecordCodecBuilder.point(runnable), Codec.BOOL.optionalFieldOf("Valid", false).forGetter((villageplacesection) -> {
return villageplacesection.e;
}), VillagePlaceRecord.a(runnable).listOf().fieldOf("Records").forGetter((villageplacesection) -> {
diff --git a/src/main/java/net/minecraft/world/entity/animal/EntityCat.java b/src/main/java/net/minecraft/world/entity/animal/EntityCat.java
index 054a1a74c48a3d936e45cc6b62a9fd6a90663cbc..c8f529b3fd227ce3354f442038b27cd716f57254 100644
--- a/src/main/java/net/minecraft/world/entity/animal/EntityCat.java
+++ b/src/main/java/net/minecraft/world/entity/animal/EntityCat.java
@@ -355,7 +355,7 @@ public class EntityCat extends EntityTameableAnimal {
WorldServer worldserver = worldaccess.getMinecraftWorld();
- if (worldserver instanceof WorldServer && ((WorldServer) worldserver).getStructureManager().a(this.getChunkCoordinates(), true, StructureGenerator.SWAMP_HUT).e()) {
+ if (worldserver instanceof WorldServer && ((WorldServer) worldserver).getStructureManager().getStructureStarts(this.getChunkCoordinates(), true, StructureGenerator.SWAMP_HUT, worldaccess).e()) { // Tuinity - fix deadlock on chunk gen
this.setCatType(10);
this.setPersistent();
}
diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EntityEnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EntityEnderDragon.java
index c296fcf80c2f3f210fa020416973ec8d5db541ba..07160de8725787551df327c0790b2d6e0876524f 100644
--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EntityEnderDragon.java
+++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EntityEnderDragon.java
@@ -625,9 +625,9 @@ public class EntityEnderDragon extends EntityInsentient implements IMonster {
if (this.deathAnimationTicks == 1 && !this.isSilent()) {
// CraftBukkit start - Use relative location for far away sounds
// this.world.b(1028, this.getChunkCoordinates(), 0);
- int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API
+ //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API // Tuinity - apply view distance patch
for (net.minecraft.server.level.EntityPlayer player : (List<net.minecraft.server.level.EntityPlayer>) ((WorldServer)world).getPlayers()) {
- // final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch
+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch
// Paper end
double deltaX = this.locX() - player.locX();
double deltaZ = this.locZ() - player.locZ();
diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/EntityWither.java b/src/main/java/net/minecraft/world/entity/boss/wither/EntityWither.java
index 635b1493eeb6c13cc5ef489bd747ac557bc131d8..3cfe3173f710fc54ec77a47a9cbb8f5ff6a6f0b6 100644
--- a/src/main/java/net/minecraft/world/entity/boss/wither/EntityWither.java
+++ b/src/main/java/net/minecraft/world/entity/boss/wither/EntityWither.java
@@ -264,9 +264,9 @@ public class EntityWither extends EntityMonster implements IRangedEntity {
if (!this.isSilent()) {
// CraftBukkit start - Use relative location for far away sounds
// this.world.b(1023, new BlockPosition(this), 0);
- int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API
+ //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API // Tuinity - apply view distance patch
for (EntityPlayer player : (List<EntityPlayer>)this.world.getPlayers()) {
- // final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch
+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch
double deltaX = this.locX() - player.locX();
double deltaZ = this.locZ() - player.locZ();
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
diff --git a/src/main/java/net/minecraft/world/entity/item/EntityItem.java b/src/main/java/net/minecraft/world/entity/item/EntityItem.java
index 8b79220c27292f9b92d9884bbbe4b16d7762343c..077990f1d95ded2c8b89c38978ec25a56df3a984 100644
--- a/src/main/java/net/minecraft/world/entity/item/EntityItem.java
+++ b/src/main/java/net/minecraft/world/entity/item/EntityItem.java
@@ -561,7 +561,7 @@ public class EntityItem extends Entity {
// Paper start - fix MC-4
public void setPositionRaw(double x, double y, double z) {
- if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) {
+ if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert
// encode/decode from PacketPlayOutEntity
x = MathHelper.floorLong(x * 4096.0D) * (1 / 4096.0D);
y = MathHelper.floorLong(y * 4096.0D) * (1 / 4096.0D);
diff --git a/src/main/java/net/minecraft/world/entity/player/EntityHuman.java b/src/main/java/net/minecraft/world/entity/player/EntityHuman.java
index c39c50e53549e9cb9d3520bc7e8b7e89cfa20163..5bce47fa8f191bc1d33c04c9865cb0efd492a9a2 100644
--- a/src/main/java/net/minecraft/world/entity/player/EntityHuman.java
+++ b/src/main/java/net/minecraft/world/entity/player/EntityHuman.java
@@ -458,6 +458,12 @@ public abstract class EntityHuman extends EntityLiving {
this.activeContainer = this.defaultContainer;
}
// Paper end
+ // Tuinity start - special close for unloaded inventory
+ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
+ closeInventory();
+ this.activeContainer = this.defaultContainer;
+ }
+ // Tuinity end - special close for unloaded inventory
public void closeInventory() {
this.activeContainer = this.defaultContainer;
diff --git a/src/main/java/net/minecraft/world/entity/projectile/EntityArrow.java b/src/main/java/net/minecraft/world/entity/projectile/EntityArrow.java
index b0e8e9934edbb0cf7ac063e4903452be526afbc7..42c1a7e8d51868c74e92d97f1df8d36fcaab6252 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/EntityArrow.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/EntityArrow.java
@@ -545,7 +545,8 @@ public abstract class EntityArrow extends IProjectile {
}
itemstack = item.getItemStack();
}
- boolean flag = this.fromPlayer == EntityArrow.PickupStatus.ALLOWED || this.fromPlayer == EntityArrow.PickupStatus.CREATIVE_ONLY && entityhuman.abilities.canInstantlyBuild || this.t() && this.getShooter().getUniqueID() == entityhuman.getUniqueID();
+ Entity shooter; // Tuinity - fix NPE here
+ boolean flag = this.fromPlayer == EntityArrow.PickupStatus.ALLOWED || this.fromPlayer == EntityArrow.PickupStatus.CREATIVE_ONLY && entityhuman.abilities.canInstantlyBuild || this.t() && ((shooter = this.getShooter()) != null && shooter.getUniqueID() == entityhuman.getUniqueID()); // Tuinity - fix NPE here
if (this.fromPlayer == EntityArrow.PickupStatus.ALLOWED && !entityhuman.inventory.pickup(itemstack)) {
// CraftBukkit end
diff --git a/src/main/java/net/minecraft/world/item/ItemEnderEye.java b/src/main/java/net/minecraft/world/item/ItemEnderEye.java
index f74685a7cdb905af8e9712ca8597e7ed3dc8b120..feedbd8c97c1b1c56eaff359e6a940696d416906 100644
--- a/src/main/java/net/minecraft/world/item/ItemEnderEye.java
+++ b/src/main/java/net/minecraft/world/item/ItemEnderEye.java
@@ -60,9 +60,10 @@ public class ItemEnderEye extends Item {
// CraftBukkit start - Use relative location for far away sounds
// world.b(1038, blockposition1.b(1, 0, 1), 0);
- int viewDistance = world.getServer().getViewDistance() * 16;
+ //int viewDistance = world.getServer().getViewDistance() * 16; // Tuinity - apply view distance patch
BlockPosition soundPos = blockposition1.b(1, 0, 1);
for (EntityPlayer player : world.getServer().getServer().getPlayerList().players) {
+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch
double deltaX = soundPos.getX() - player.locX();
double deltaZ = soundPos.getZ() - player.locZ();
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
diff --git a/src/main/java/net/minecraft/world/level/ChunkCache.java b/src/main/java/net/minecraft/world/level/ChunkCache.java
index 7a760ef0264c9041c38bdfb8fd31333052c26139..b547eb352f90f68cf36ffb82e43ad7acb1892f6a 100644
--- a/src/main/java/net/minecraft/world/level/ChunkCache.java
+++ b/src/main/java/net/minecraft/world/level/ChunkCache.java
@@ -1,5 +1,6 @@
package net.minecraft.world.level;
+import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nullable;
@@ -26,6 +27,156 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess {
protected boolean d;
protected final World e; protected final World getWorld() { return e; } // Paper - OBFHELPER
+ // Tuinity start - optimise pathfinder collision detection
+ @Override
+ public boolean getCubes(Entity entity) {
+ return !this.getCollisionsForBlocksOrWorldBorder(entity, entity.getBoundingBox(), null, true, null);
+ }
+
+ @Override
+ public boolean getCubes(Entity entity, AxisAlignedBB axisalignedbb) {
+ return !this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, true, null);
+ }
+
+ @Override
+ public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ return !this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, true, null);
+ }
+
+ public boolean getCollisionsForBlocksOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<AxisAlignedBB> list,
+ boolean collidesWithUnloaded,
+ java.util.function.BiPredicate<IBlockData, BlockPosition> predicate) {
+ boolean ret = false;
+ final boolean checkOnly = true;
+
+ if (entity != null) {
+ if (this.getWorldBorder().isAlmostCollidingOnBorder(axisalignedbb)) {
+ if (checkOnly) {
+ return true;
+ } else {
+ net.minecraft.world.phys.shapes.VoxelShapes.addBoxesTo(this.getWorldBorder().getCollisionShape(), list);
+ ret = true;
+ }
+ }
+ }
+
+ int minBlockX = net.minecraft.util.MathHelper.floor(axisalignedbb.minX - net.minecraft.server.MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockX = net.minecraft.util.MathHelper.floor(axisalignedbb.maxX + net.minecraft.server.MCUtil.COLLISION_EPSILON) + 1;
+
+ int minBlockY = net.minecraft.util.MathHelper.floor(axisalignedbb.minY - net.minecraft.server.MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockY = net.minecraft.util.MathHelper.floor(axisalignedbb.maxY + net.minecraft.server.MCUtil.COLLISION_EPSILON) + 1;
+
+ int minBlockZ = net.minecraft.util.MathHelper.floor(axisalignedbb.minZ - net.minecraft.server.MCUtil.COLLISION_EPSILON) - 1;
+ int maxBlockZ = net.minecraft.util.MathHelper.floor(axisalignedbb.maxZ + net.minecraft.server.MCUtil.COLLISION_EPSILON) + 1;
+
+
+ BlockPosition.MutableBlockPosition mutablePos = new BlockPosition.MutableBlockPosition();
+ net.minecraft.world.phys.shapes.VoxelShapeCollision collisionShape = null;
+
+ // special cases:
+ if (minBlockY > 255 || maxBlockY < 0) {
+ // no point in checking
+ return ret;
+ }
+
+ int minYIterate = Math.max(0, minBlockY);
+ int maxYIterate = Math.min(255, maxBlockY);
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ // 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;
+ net.minecraft.world.level.chunk.Chunk chunk = (net.minecraft.world.level.chunk.Chunk)this.getChunkIfLoaded(currChunkX, currChunkZ);
+
+ if (chunk == null) {
+ if (collidesWithUnloaded) {
+ if (checkOnly) {
+ return true;
+ } else {
+ list.add(AxisAlignedBB.getBoxForChunk(currChunkX, currChunkZ));
+ ret = true;
+ }
+ }
+ continue;
+ }
+
+ net.minecraft.world.level.chunk.ChunkSection[] sections = chunk.getSections();
+
+ // bound y
+
+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
+ net.minecraft.world.level.chunk.ChunkSection section = sections[currY >>> 4];
+ if (section == null || section.isFullOfAir()) {
+ // 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.DataPaletteBlock<IBlockData> blocks = section.blockIds;
+
+ 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;
+ }
+
+ IBlockData blockData = blocks.rawGet(localBlockIndex);
+
+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
+ mutablePos.setValues(blockX, blockY, blockZ);
+ if (collisionShape == null) {
+ collisionShape = entity == null ? net.minecraft.world.phys.shapes.VoxelShapeCollision.a() : net.minecraft.world.phys.shapes.VoxelShapeCollision.a(entity);
+ }
+ VoxelShape voxelshape2 = blockData.getCollisionShape(this, mutablePos, collisionShape);
+ if (voxelshape2 != net.minecraft.world.phys.shapes.VoxelShapes.getEmptyShape()) {
+ VoxelShape voxelshape3 = voxelshape2.offset((double)blockX, (double)blockY, (double)blockZ);
+
+ if (predicate != null && !predicate.test(blockData, mutablePos)) {
+ continue;
+ }
+
+ if (checkOnly) {
+ if (voxelshape3.intersects(axisalignedbb)) {
+ return true;
+ }
+ } else {
+ ret |= net.minecraft.world.phys.shapes.VoxelShapes.addBoxesToIfIntersects(voxelshape3, axisalignedbb, list);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return ret;
+ }
+ // Tuinity end - optimise pathfinder collision detection
+
public ChunkCache(World world, BlockPosition blockposition, BlockPosition blockposition1) {
this.e = world;
this.a = blockposition.getX() >> 4;
diff --git a/src/main/java/net/minecraft/world/level/IBlockAccess.java b/src/main/java/net/minecraft/world/level/IBlockAccess.java
index 21ce19b9caf3150535a3f84027242a93bdd0d263..e612e1d30f76e217b1aa23488ab025adce048f57 100644
--- a/src/main/java/net/minecraft/world/level/IBlockAccess.java
+++ b/src/main/java/net/minecraft/world/level/IBlockAccess.java
@@ -67,7 +67,8 @@ public interface IBlockAccess {
return MovingObjectPositionBlock.a(raytrace1.a(), EnumDirection.a(vec3d.x, vec3d.y, vec3d.z), new BlockPosition(raytrace1.a()));
}
// Paper end
- Fluid fluid = this.getFluid(blockposition);
+ if (iblockdata.isAir()) return null; // Tuinity - optimise air cases
+ Fluid fluid = iblockdata.getFluid(); // Tuinity - don't need to go to world state again
Vec3D vec3d = raytrace1.b();
Vec3D vec3d1 = raytrace1.a();
VoxelShape voxelshape = raytrace1.a(iblockdata, this, blockposition);
diff --git a/src/main/java/net/minecraft/world/level/ICollisionAccess.java b/src/main/java/net/minecraft/world/level/ICollisionAccess.java
index fcb3e2f9dea97138e0fd4cd2eb11b54799e1d3b5..1c66d6c69148e6aa45641c0351d63c9b96ffb1d3 100644
--- a/src/main/java/net/minecraft/world/level/ICollisionAccess.java
+++ b/src/main/java/net/minecraft/world/level/ICollisionAccess.java
@@ -36,6 +36,11 @@ public interface ICollisionAccess extends IBlockAccess {
}
default boolean b(AxisAlignedBB axisalignedbb) {
+ // Tuinity start - allow overriding in WorldServer
+ return this.getCubes(axisalignedbb);
+ }
+ default boolean getCubes(AxisAlignedBB axisalignedbb) {
+ // Tuinity end - allow overriding in WorldServer
return this.b((Entity) null, axisalignedbb, (entity) -> {
return true;
});
@@ -54,6 +59,11 @@ public interface ICollisionAccess extends IBlockAccess {
}
default boolean b(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ // Tuinity start - allow overriding in WorldServer
+ return this.getCubes(entity, axisalignedbb, predicate);
+ }
+ default boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ // Tuinity end - allow overriding in WorldServer
try { if (entity != null) entity.collisionLoadChunks = true; // Paper
return this.d(entity, axisalignedbb, predicate).allMatch(VoxelShape::isEmpty);
} finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper
diff --git a/src/main/java/net/minecraft/world/level/IEntityAccess.java b/src/main/java/net/minecraft/world/level/IEntityAccess.java
index 1ff9e771788a4ab52129070e355ca48df2949470..a7f2304acf8ee0a15d6eae8c42060e003be13ae7 100644
--- a/src/main/java/net/minecraft/world/level/IEntityAccess.java
+++ b/src/main/java/net/minecraft/world/level/IEntityAccess.java
@@ -64,16 +64,26 @@ public interface IEntityAccess {
return this.b(oclass, axisalignedbb, IEntitySelector.g);
}
+ // Tuinity start - optimise hard collision
+ /**
+ * Not guaranteed to only return hard colliding entities
+ */
+ default List<Entity> getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ return this.getEntities(entity, axisalignedbb, predicate);
+ }
+ // Tuinity end - optimise hard collision
+
default Stream<VoxelShape> c(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
if (axisalignedbb.a() < 1.0E-7D) {
return Stream.empty();
} else {
- AxisAlignedBB axisalignedbb1 = axisalignedbb.g(1.0E-7D);
+ AxisAlignedBB axisalignedbb1 = axisalignedbb.g(-1.0E-7D); // Tuinity - to comply with vanilla intersection rules, expand by -epsilon so we only get stuff we definitely collide with. expanding by +epsilon gives us stuff we don't collide with, which will screw over callers in some cases.
- return this.getEntities(entity, axisalignedbb1, predicate.and((entity1) -> {
+ if (predicate == null) predicate = (e) -> true; // Tuinity - allow nullable
+ predicate = predicate.and((entity1) -> { // Tuinity - optimise entity hard collisions // Tuinity - allow nullable
boolean flag;
- if (entity1.getBoundingBox().c(axisalignedbb1)) {
+ if (true || entity1.getBoundingBox().c(axisalignedbb1)) { // Tuinity - always true, wtf did they think this.getEntities(entity, axisalignedbb1) does?
label25:
{
if (entity == null) {
@@ -91,13 +101,13 @@ public interface IEntityAccess {
flag = false;
return flag;
- })).stream().map(Entity::getBoundingBox).map(VoxelShapes::a);
+ }); return ((entity != null && entity.hardCollides()) ? this.getEntities(entity, axisalignedbb1, predicate) : this.getHardCollidingEntities(entity, axisalignedbb1, predicate)).stream().map(Entity::getBoundingBox).map(VoxelShapes::a); // Tuinity - optimise entity hard collisions
}
}
default EntityHuman findNearbyPlayer(Entity entity, double d0, @Nullable Predicate<Entity> predicate) { return this.findNearbyPlayer(entity.locX(), entity.locY(), entity.locZ(), d0, predicate); } // Paper
@Nullable default EntityHuman findNearbyPlayer(double d0, double d1, double d2, double d3, @Nullable Predicate<Entity> predicate) { return a(d0, d1, d2, d3, predicate); } // Paper - OBFHELPER
- @Nullable default EntityHuman a(double d0, double d1, double d2, double d3, @Nullable Predicate<Entity> predicate) { // Paper
+ @Nullable default EntityHuman a(double d0, double d1, double d2, double d3, @Nullable Predicate<Entity> predicate) { // Paper // Tuinity - diff on change, override in World - this should be "get closest player that matches predicate"
double d4 = -1.0D;
EntityHuman entityhuman = null;
Iterator iterator = this.getPlayers().iterator();
@@ -198,27 +208,27 @@ public interface IEntityAccess {
}
@Nullable
- default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving) {
+ default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving) { // Tuinity - diff on change, override in World - this should be "get closest player that matches path finder target condition"
return (EntityHuman) this.a(this.getPlayers(), pathfindertargetcondition, entityliving, entityliving.locX(), entityliving.locY(), entityliving.locZ());
}
@Nullable
- default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving, double d0, double d1, double d2) {
+ default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving, double d0, double d1, double d2) { // Tuinity - diff on change, override in World - this should be "get closest player that matches path finder target condition"
return (EntityHuman) this.a(this.getPlayers(), pathfindertargetcondition, entityliving, d0, d1, d2);
}
@Nullable
- default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, double d0, double d1, double d2) {
+ default EntityHuman a(PathfinderTargetCondition pathfindertargetcondition, double d0, double d1, double d2) { // Tuinity - diff on change, override in World - this should be "get closest player that matches path finder target condition"
return (EntityHuman) this.a(this.getPlayers(), pathfindertargetcondition, (EntityLiving) null, d0, d1, d2);
}
@Nullable
- default <T extends EntityLiving> T a(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+ default <T extends EntityLiving> T a(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition"
return this.a(this.a(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix
}
@Nullable
- default <T extends EntityLiving> T b(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+ default <T extends EntityLiving> T b(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition"
return this.a(this.b(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix
}
@@ -244,7 +254,7 @@ public interface IEntityAccess {
return t0;
}
- default List<EntityHuman> a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving, AxisAlignedBB axisalignedbb) {
+ default List<EntityHuman> a(PathfinderTargetCondition pathfindertargetcondition, EntityLiving entityliving, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get players that matches path finder target condition"
List<EntityHuman> list = Lists.newArrayList();
Iterator iterator = this.getPlayers().iterator();
diff --git a/src/main/java/net/minecraft/world/level/SpawnerCreature.java b/src/main/java/net/minecraft/world/level/SpawnerCreature.java
index 24771c3522ea74ac12058591137eafc21adf3762..9b55da1f4d40ae36b2d2b8e7b3b18989dc4f6006 100644
--- a/src/main/java/net/minecraft/world/level/SpawnerCreature.java
+++ b/src/main/java/net/minecraft/world/level/SpawnerCreature.java
@@ -64,9 +64,9 @@ public final class SpawnerCreature {
public static SpawnerCreature.d a(int i, Iterable<Entity> iterable, SpawnerCreature.b spawnercreature_b) {
// Paper start - add countMobs parameter
- return countMobs(i, iterable, spawnercreature_b, false);
+ return countMobs(i, iterable, spawnercreature_b, false, null); // Tuinity - it'll still be broken no matter what
}
- public static SpawnerCreature.d countMobs(int i, Iterable<Entity> iterable, SpawnerCreature.b spawnercreature_b, boolean countMobs) {
+ public static SpawnerCreature.d countMobs(int i, Iterable<Entity> iterable, SpawnerCreature.b spawnercreature_b, boolean countMobs, net.minecraft.server.level.ChunkProviderServer chunkProvider) { // Tuinity - add CPS param
// Paper end - add countMobs parameter
SpawnerCreatureProbabilities spawnercreatureprobabilities = new SpawnerCreatureProbabilities();
Object2IntOpenHashMap<EnumCreatureType> object2intopenhashmap = new Object2IntOpenHashMap();
@@ -97,7 +97,16 @@ public final class SpawnerCreature {
BlockPosition blockposition = entity.getChunkCoordinates();
long j = ChunkCoordIntPair.pair(blockposition.getX() >> 4, blockposition.getZ() >> 4);
- spawnercreature_b.query(j, (chunk) -> {
+ // Tuinity start - remove chunk lookup and lambda
+ Chunk chunk = entity.getCurrentChunk();
+ if (chunk == null || j != chunk.coordinateKey) { // no chunk or coordinate doesn't match
+ chunk = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ if (chunk == null) {
+ // unloaded chunk! no longer FULL.
+ continue;
+ }
+ }
+ // Tuinity end - remove chunk lookup and lambda
BiomeSettingsMobs.b biomesettingsmobs_b = b(blockposition, chunk).b().a(entity.getEntityType());
if (biomesettingsmobs_b != null) {
@@ -107,10 +116,10 @@ public final class SpawnerCreature {
object2intopenhashmap.addTo(enumcreaturetype, 1);
// Paper start
if (countMobs) {
- ((WorldServer)chunk.world).getChunkProvider().playerChunkMap.updatePlayerMobTypeMap(entity);
+ chunkProvider.playerChunkMap.updatePlayerMobTypeMap(entity); // Tuinity - directly use chunk provider
}
// Paper end
- });
+ // Tuinity - remove chunk lookup and lambda
}
}
@@ -250,7 +259,7 @@ public final class SpawnerCreature {
blockposition_mutableblockposition.d(l, i, i1);
double d0 = (double) l + 0.5D;
double d1 = (double) i1 + 0.5D;
- EntityHuman entityhuman = worldserver.a(d0, (double) i, d1, -1.0D, false);
+ EntityHuman entityhuman = (!(ichunkaccess instanceof Chunk)) ? worldserver.a(d0, (double) i, d1, -1.0D, false) : ((Chunk)ichunkaccess).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.IEntitySelector.notSpectator()); // Tuinity - use chunk's player cache to optimize search in range
if (entityhuman != null) {
double d2 = entityhuman.h(d0, (double) i, d1);
@@ -322,7 +331,7 @@ public final class SpawnerCreature {
}
private static boolean a(WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition.MutableBlockPosition blockposition_mutableblockposition, double d0) {
- if (d0 <= 576.0D) {
+ if (d0 <= 576.0D) { // Tuinity - diff on change, copy into caller
return false;
} else if (worldserver.getSpawn().a((IPosition) (new Vec3D((double) blockposition_mutableblockposition.getX() + 0.5D, (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + 0.5D)), 24.0D)) {
return false;
diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java
index 07dcfd56af1014ad159828dd9ee2d89c2010b9f4..43418273f00f3703c7bd86586847d469af92c18f 100644
--- a/src/main/java/net/minecraft/world/level/StructureManager.java
+++ b/src/main/java/net/minecraft/world/level/StructureManager.java
@@ -46,8 +46,13 @@ public class StructureManager {
// Paper start - remove structure streams
public java.util.List<StructureStart<?>> getFeatureStarts(SectionPosition sectionPosition, StructureGenerator<?> structureGenerator) {
+ // Tuinity start - add world parameter
+ return this.getFeatureStarts(sectionPosition, structureGenerator, null);
+ }
+ public java.util.List<StructureStart<?>> getFeatureStarts(SectionPosition sectionPosition, StructureGenerator<?> structureGenerator, IWorldReader world) {
+ // Tuinity end - add world parameter
java.util.List<StructureStart<?>> list = new ObjectArrayList<>();
- for (Long curLong: getLevel().getChunkAt(sectionPosition.a(), sectionPosition.c(), ChunkStatus.STRUCTURE_REFERENCES).b(structureGenerator)) {
+ for (Long curLong: (world == null ? getLevel() : world).getChunkAt(sectionPosition.a(), sectionPosition.c(), ChunkStatus.STRUCTURE_REFERENCES).b(structureGenerator)) { // Tuinity - fix deadlock on world gen - chunk can be unloaded while generating, so we should be using the generator's regionlimitedaccess so we always get the chunk
SectionPosition sectionPosition1 = SectionPosition.a(new ChunkCoordIntPair(curLong), 0);
StructureStart<?> structurestart = a(sectionPosition1, structureGenerator, getLevel().getChunkAt(sectionPosition1.a(), sectionPosition1.c(), ChunkStatus.STRUCTURE_STARTS));
if (structurestart != null && structurestart.e()) {
@@ -76,8 +81,12 @@ public class StructureManager {
}
public StructureStart<?> a(BlockPosition blockposition, boolean flag, StructureGenerator<?> structuregenerator) {
+ // Tuinity start - add world parameter
+ return this.getStructureStarts(blockposition,flag, structuregenerator, null);
+ }
+ public StructureStart<?> getStructureStarts(BlockPosition blockposition, boolean flag, StructureGenerator<?> structuregenerator, IWorldReader world) {
// Paper start - remove structure streams
- for (StructureStart<?> structurestart : getFeatureStarts(SectionPosition.a(blockposition), structuregenerator)) {
+ for (StructureStart<?> structurestart : getFeatureStarts(SectionPosition.a(blockposition), structuregenerator, world)) { // Tuinity end - add world parameter
if (structurestart.c().b(blockposition)) {
if (!flag) {
return structurestart;
diff --git a/src/main/java/net/minecraft/world/level/VoxelShapeSpliterator.java b/src/main/java/net/minecraft/world/level/VoxelShapeSpliterator.java
index 03584572fa5bf0d96fc4cecece573547f9c94cea..8bc965a3b3d0d4140c6b94636f0b33b2805c5867 100644
--- a/src/main/java/net/minecraft/world/level/VoxelShapeSpliterator.java
+++ b/src/main/java/net/minecraft/world/level/VoxelShapeSpliterator.java
@@ -106,7 +106,7 @@ public class VoxelShapeSpliterator extends AbstractSpliterator<VoxelShape> {
VoxelShape voxelshape = iblockdata.b((IBlockAccess) this.g, this.e, this.c);
if (voxelshape == VoxelShapes.b()) {
- if (!this.b.a((double) i, (double) j, (double) k, (double) i + 1.0D, (double) j + 1.0D, (double) k + 1.0D)) {
+ if (!this.b.voxelShapeIntersect((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 AxisAlignedBB
continue;
}
diff --git a/src/main/java/net/minecraft/world/level/World.java b/src/main/java/net/minecraft/world/level/World.java
index f7f593a9e58b537109fa6ca1c783f6614f4bfad5..cb8064df7e9f1b8b4d4292486e2193680d83663c 100644
--- a/src/main/java/net/minecraft/world/level/World.java
+++ b/src/main/java/net/minecraft/world/level/World.java
@@ -154,6 +154,8 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper
public final 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 BlockPosition lastPhysicsProblem; // Spigot
private org.spigotmc.TickLimiter entityLimiter;
@@ -189,11 +191,69 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
});
}
// Paper end - fix and optimise world upgrading
+ // Tuinity start - optimise checkDespawn
+ public final List<EntityPlayer> getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY, double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+ Chunk chunk;
+ if (source == null || (chunk = source.getCurrentChunk()) == null || maxRange < 0.0 || maxRange >= net.minecraft.server.level.PlayerChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE) {
+ return this.getNearbyPlayersSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate);
+ }
+
+ List<EntityPlayer> ret = new java.util.ArrayList<>();
+ chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret);
+ return ret;
+ }
+
+ private List<EntityPlayer> getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY, double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+ List<EntityPlayer> ret = new java.util.ArrayList<>();
+ double maxRangeSquared = maxRange * maxRange;
+
+ for (EntityPlayer player : (List<EntityPlayer>)this.getPlayers()) {
+ if ((maxRange < 0.0 || player.getDistanceSquared(sourceX, sourceY, sourceZ) < maxRangeSquared)) {
+ if (predicate == null || predicate.test(player)) {
+ ret.add(player);
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ private EntityPlayer getNearestPlayerSlow(@Nullable Entity source, double sourceX, double sourceY, double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+ EntityPlayer closest = null;
+ double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
+
+ for (EntityPlayer player : (List<EntityPlayer>)this.getPlayers()) {
+ double distanceSquared = player.getDistanceSquared(sourceX, sourceY, sourceZ);
+ if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) {
+ closest = player;
+ closestRangeSquared = distanceSquared;
+ }
+ }
+
+ return closest;
+ }
+
+
+ public final EntityPlayer getNearestPlayer(@Nullable Entity source, double sourceX, double sourceY, double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+ Chunk chunk;
+ if (source == null || (chunk = source.getCurrentChunk()) == null || maxRange < 0.0 || maxRange >= net.minecraft.server.level.PlayerChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE) {
+ return this.getNearestPlayerSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate);
+ }
+
+ return chunk.findNearestPlayer(sourceX, sourceY, sourceZ, maxRange, predicate);
+ }
+
+ @Override
+ public @Nullable EntityHuman a(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 World(WorldDataMutable worlddatamutable, ResourceKey<World> resourcekey, final DimensionManager dimensionmanager, Supplier<GameProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper
this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.WorldDataServer) worlddatamutable).getName()); // Spigot
this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.WorldDataServer) worlddatamutable).getName(), this.spigotConfig); // Paper
this.chunkPacketBlockController = this.paperConfig.antiXray ? new ChunkPacketBlockControllerAntiXray(this, executor) : ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
+ this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.WorldDataServer)worlddatamutable).getName()); // Tuinity - Server Config
this.generator = gen;
this.world = new CraftWorld((WorldServer) this, gen, env);
this.ticksPerAnimalSpawns = this.getServer().getTicksPerAnimalSpawns(); // CraftBukkit
@@ -355,6 +415,15 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
@Override
public final Chunk getChunkAt(int i, int j) { // Paper - final to help inline
+ // Tuinity start - make sure loaded chunks get the inlined variant of this function
+ net.minecraft.server.level.ChunkProviderServer cps = ((WorldServer)this).chunkProvider;
+ if (cps.serverThread == Thread.currentThread()) {
+ Chunk ifLoaded = cps.getChunkAtIfLoadedMainThread(i, j);
+ if (ifLoaded != null) {
+ return ifLoaded;
+ }
+ }
+ // Tuinity end - make sure loaded chunks get the inlined variant of this function
return (Chunk) this.getChunkAt(i, j, ChunkStatus.FULL, true); // Paper - avoid a method jump
}
@@ -429,6 +498,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
@Override
public boolean a(BlockPosition blockposition, IBlockData iblockdata, int i, int j) {
+ org.spigotmc.AsyncCatcher.catchOp("set type call"); // Tuinity
// CraftBukkit start - tree generation
if (this.captureTreeGeneration) {
// Paper start
@@ -530,6 +600,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
// CraftBukkit start - Split off from above in order to directly send client and physic updates
public void notifyAndUpdatePhysics(BlockPosition blockposition, Chunk chunk, IBlockData oldBlock, IBlockData newBlock, IBlockData actualBlock, int i, int j) {
+ com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async notify and update"); // Tuinity
IBlockData iblockdata = newBlock;
IBlockData iblockdata1 = oldBlock;
IBlockData iblockdata2 = actualBlock;
@@ -542,7 +613,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
this.notify(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 || ((WorldServer)this).getChunkProvider().playerChunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) {
+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((WorldServer)this).getChunkProvider().playerChunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Tuinity - replace old player chunk management
((WorldServer)this).getChunkProvider().flagDirty(blockposition);
// Paper end - per player view distance
}
@@ -964,6 +1035,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
return;
// Paper end
}
+ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick
}
// Paper start - Prevent armor stands from doing entity lookups
@Override
@@ -1147,10 +1219,44 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
return this.getChunkAt(i, j, ChunkStatus.FULL, false);
}
+ // Tuinity start - optimise hard collision handling
+ @Override
+ public List<Entity> getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate) {
+ return this.getHardCollidingEntities(entity, axisalignedbb, predicate, Lists.newArrayList());
+ }
+
+ public List<Entity> getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate<Entity> predicate, List<Entity> list) {
+ // copied from below
+ int i = MathHelper.floor((axisalignedbb.minX - 2.0D) / 16.0D);
+ int j = MathHelper.floor((axisalignedbb.maxX + 2.0D) / 16.0D);
+ int k = MathHelper.floor((axisalignedbb.minZ - 2.0D) / 16.0D);
+ int l = MathHelper.floor((axisalignedbb.maxZ + 2.0D) / 16.0D);
+
+ net.minecraft.server.level.ChunkProviderServer chunkProvider = ((WorldServer)this).getChunkProvider();
+
+ for (int i1 = i; i1 <= j; ++i1) {
+ for (int j1 = k; j1 <= l; ++j1) {
+ Chunk chunk = chunkProvider.getChunkAtIfLoadedMainThread(i1, j1);
+
+ if (chunk != null) {
+ chunk.getHardCollidingEntities(entity, axisalignedbb, list, predicate);
+ }
+ }
+ }
+
+ return list;
+ }
+ // Tuinity end - optimise hard collision handling
+
@Override
public List<Entity> getEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate<? super Entity> predicate) {
- this.getMethodProfiler().c("getEntities");
+ // Tuinity start - add list parameter
List<Entity> list = Lists.newArrayList();
+ return this.getEntities(entity, axisalignedbb, predicate, list);
+ }
+ public List<Entity> getEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate<? super Entity> predicate, List<Entity> list) {
+ // Tuinity end - add list parameter
+ this.getMethodProfiler().c("getEntities");
int i = MathHelper.floor((axisalignedbb.minX - 2.0D) / 16.0D);
int j = MathHelper.floor((axisalignedbb.maxX + 2.0D) / 16.0D);
int k = MathHelper.floor((axisalignedbb.minZ - 2.0D) / 16.0D);
@@ -1206,7 +1312,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
if (chunk != null) {
- chunk.a(oclass, axisalignedbb, list, predicate);
+ chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class
}
}
}
@@ -1229,7 +1335,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
if (chunk != null) {
- chunk.a(oclass, axisalignedbb, list, predicate);
+ chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class
}
}
}
@@ -1237,6 +1343,106 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
return list;
}
+ // Tuinity start
+ @Override
+ public <T extends net.minecraft.world.entity.EntityLiving> T b(Class<? extends T> oclass, net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, @Nullable net.minecraft.world.entity.EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+ return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb);
+ }
+
+ @Override
+ public <T extends net.minecraft.world.entity.EntityLiving> T a(Class<? extends T> oclass, net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, @Nullable net.minecraft.world.entity.EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+ return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb);
+ }
+
+ public final <T extends net.minecraft.world.entity.EntityLiving> T getClosestEntity(Class<? extends T> clazz,
+ net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition condition,
+ @Nullable net.minecraft.world.entity.EntityLiving source,
+ double x, double y, double z,
+ AxisAlignedBB boundingBox) {
+ org.bukkit.craftbukkit.util.UnsafeList<Entity> entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList();
+ try {
+ int lowerX = MCUtil.fastFloor((boundingBox.minX - 2.0D)) >> 4;
+ int upperX = MCUtil.fastFloor((boundingBox.maxX + 2.0D)) >> 4;
+ int lowerZ = MCUtil.fastFloor((boundingBox.minZ - 2.0D)) >> 4;
+ int upperZ = MCUtil.fastFloor((boundingBox.maxZ + 2.0D)) >> 4;
+
+ org.bukkit.craftbukkit.util.UnsafeList<Chunk> chunks = com.tuinity.tuinity.util.CachedLists.getTempGetChunksList();
+ try {
+ T closest = null;
+ double closestDistance = Double.MAX_VALUE;
+ net.minecraft.server.level.ChunkProviderServer chunkProvider = ((WorldServer)this).getChunkProvider();
+
+ int centerX = (lowerX + upperX) >> 1;
+ int centerZ = (lowerZ + upperZ) >> 1;
+ // Copied from MCUtil.getSpiralOutChunks
+ Chunk temp;
+ if ((temp = chunkProvider.getChunkAtIfLoadedImmediately(centerX, centerZ)) != null && temp.hasEntitiesMaybe(clazz)) {
+ chunks.add(temp);
+ }
+ int radius = Math.max((upperX - lowerX + 1) >> 1, (upperZ - lowerZ + 1) >> 1);
+ for (int r = 1; r <= radius; r++) {
+ int ox = -r;
+ int oz = r;
+
+ // Iterates the edge of half of the box; then negates for other half.
+ while (ox <= r && oz > -r) {
+ {
+ int cx = centerX + ox;
+ int cz = centerZ + oz;
+ if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ &&
+ (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null &&
+ temp.hasEntitiesMaybe(clazz)) {
+ chunks.add(temp);
+ }
+ }
+ {
+ int cx = centerX - ox;
+ int cz = centerZ - oz;
+ if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ &&
+ (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null &&
+ temp.hasEntitiesMaybe(clazz)) {
+ chunks.add(temp);
+ }
+ }
+
+ if (ox < r) {
+ ox++;
+ } else {
+ oz--;
+ }
+ }
+ }
+
+ Object[] chunkData = chunks.getRawDataArray();
+ for (int cindex = 0, clen = chunks.size(); cindex < clen; ++cindex) {
+ final Chunk chunk = (Chunk)chunkData[cindex];
+
+ chunk.getEntitiesClass(clazz, source, boundingBox, null, entities);
+
+ Object[] entityData = entities.getRawDataArray();
+ for (int eindex = 0, entities_len = entities.size(); eindex < entities_len; ++eindex) {
+ T entity = (T)entityData[eindex];
+ double distance = entity.getDistanceSquared(x, y, z);
+ // check distance first, as it's the least expensive
+ if (distance < closestDistance && condition.test(source, entity)) {
+ closest = entity;
+ closestDistance = distance;
+ }
+ }
+
+ entities.setSize(0);
+ }
+
+ return closest;
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempGetChunksList(chunks);
+ }
+ } finally {
+ com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities);
+ }
+ }
+ // Tuinity end
+
@Nullable
public abstract Entity getEntity(int i);
diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeBase.java b/src/main/java/net/minecraft/world/level/biome/BiomeBase.java
index 15096a9c2719b8b4c099f62d0a1c808e56b63a8e..9bbd175f7e20591bbefdbddcb5e998e7098c5adb 100644
--- a/src/main/java/net/minecraft/world/level/biome/BiomeBase.java
+++ b/src/main/java/net/minecraft/world/level/biome/BiomeBase.java
@@ -175,14 +175,33 @@ public final class BiomeBase {
}
public boolean a(IWorldReader iworldreader, BlockPosition blockposition, boolean flag) {
- if (this.getAdjustedTemperature(blockposition) >= 0.15F) {
+ // Tuinity start - add chunk parameter and lazily get biome
+ return canTurnWaterIntoIce(iworldreader, blockposition, flag, null, new BiomeBase[] { this });
+ }
+ public static boolean canTurnWaterIntoIce(IWorldReader iworldreader, BlockPosition blockposition, boolean flag, IChunkAccess chunk, BiomeBase[] biomeAbove) {
+ // Tuinity end - add chunk parameter and lazily get biome
+ if (false && biomeAbove[0].getAdjustedTemperature(blockposition) >= 0.15F) { // Tuinity - move this down, this check is expensive
return false;
} else {
if (blockposition.getY() >= 0 && blockposition.getY() < 256 && iworldreader.getBrightness(EnumSkyBlock.BLOCK, blockposition) < 10) {
- IBlockData iblockdata = iworldreader.getType(blockposition);
- Fluid fluid = iworldreader.getFluid(blockposition);
+ // Tuinity start - add chunk parameter
+ if (chunk == null) {
+ chunk = iworldreader.getChunkAt(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ }
+ // Tuinity end - add chunk parameter
+ IBlockData iblockdata = chunk.getType(blockposition); // Tuinity - skip chunk lookup, we got the chunk
+ Fluid fluid = iblockdata.getFluid(); // Tuinity - skip another block lookup, we have the blockdata
if (fluid.getType() == FluidTypes.WATER && iblockdata.getBlock() instanceof BlockFluids) {
+ // Tuinity start - moved down from top, only run when we actually encounter water
+ if (biomeAbove[0] == null) {
+ // lazily-get biome
+ biomeAbove[0] = iworldreader.getBiome(blockposition.up()); // TODO - avoid blockpos alloc
+ }
+ if (biomeAbove[0].getAdjustedTemperature(blockposition) >= 0.15F) {
+ return false;
+ }
+ // Tuinity end - moved down from top, only run when we actually encounter water
if (!flag) {
return true;
}
@@ -200,13 +219,32 @@ public final class BiomeBase {
}
public boolean b(IWorldReader iworldreader, BlockPosition blockposition) {
- if (this.getAdjustedTemperature(blockposition) >= 0.15F) {
+ // Tuinity start - add chunk parameter and lazily get biome
+ return canTurnAirIntoSnow(iworldreader, blockposition, null, new BiomeBase[]{ this });
+ }
+ public static boolean canTurnAirIntoSnow(IWorldReader iworldreader, BlockPosition blockposition, IChunkAccess chunk, BiomeBase[] biomeAbove) {
+ // Tuinity end - add chunk parameter and lazily get biome
+ if (false && biomeAbove[0].getAdjustedTemperature(blockposition) >= 0.15F) { // Tuinity - move this down, this check is expensive
return false;
} else {
if (blockposition.getY() >= 0 && blockposition.getY() < 256 && iworldreader.getBrightness(EnumSkyBlock.BLOCK, blockposition) < 10) {
- IBlockData iblockdata = iworldreader.getType(blockposition);
+ // Tuinity start - add chunk parameter
+ if (chunk == null) {
+ chunk = iworldreader.getChunkAt(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ }
+ // Tuinity end - add chunk parameter
+ IBlockData iblockdata = chunk.getType(blockposition); // Tuinity - skip chunk lookup, we got the chunk
if (iblockdata.isAir() && Blocks.SNOW.getBlockData().canPlace(iworldreader, blockposition)) {
+ // Tuinity start - moved down from top, only run when we actually encounter water
+ if (biomeAbove[0] == null) {
+ // lazily-get biome
+ biomeAbove[0] = iworldreader.getBiome(blockposition.up()); // TODO - avoid blockpos alloc
+ }
+ if (biomeAbove[0].getAdjustedTemperature(blockposition) >= 0.15F) {
+ return false;
+ }
+ // Tuinity end - moved down from top, only run when we actually encounter water
return true;
}
}
diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBase.java b/src/main/java/net/minecraft/world/level/block/state/BlockBase.java
index 0d26250887f80d0c250bcd6bc7de303362427d3e..1d82f719440c95765c01a588f4785d630b8b527a 100644
--- a/src/main/java/net/minecraft/world/level/block/state/BlockBase.java
+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBase.java
@@ -348,21 +348,23 @@ public abstract class BlockBase {
public abstract static class BlockData extends IBlockDataHolder<Block, IBlockData> {
- private final int b;
- private final boolean e;
+ private final int b; public final int getEmittedLight() { return this.b; } // Tuinity - OBFHELPER
+ private final boolean e; public final boolean isTransparentOnSomeFaces() { return this.e; } // Tuinity - OBFHELPER
private final boolean f;
private final Material g;
private final MaterialMapColor h;
public final float strength;
private final boolean j;
- private final boolean k;
+ private final boolean k; public final boolean isOpaque() { return this.k; } // Tuinity - OBFHELPER
private final BlockBase.e l;
private final BlockBase.e m;
private final BlockBase.e n;
private final BlockBase.e o;
private final BlockBase.e p;
@Nullable
- protected BlockBase.BlockData.Cache a;
+ protected BlockBase.BlockData.Cache a; public final BlockBase.BlockData.Cache getShapeCache() { return this.a; } // Tuinity - OBFHELPER
+ public net.minecraft.world.level.pathfinder.PathType staticPathType; // Tuinity - cache static path types
+ public net.minecraft.world.level.pathfinder.PathType neighbourOverridePathType; // Tuinity - cache static path types
protected BlockData(Block block, ImmutableMap<IBlockState<?>, Comparable<?>> immutablemap, MapCodec<IBlockData> mapcodec) {
super(block, immutablemap, mapcodec);
@@ -381,6 +383,7 @@ public abstract class BlockBase {
this.n = blockbase_info.s;
this.o = blockbase_info.t;
this.p = blockbase_info.u;
+ 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;
@@ -396,12 +399,63 @@ public abstract class BlockBase {
protected Fluid fluid;
// Paper end
+ // Tuinity start - micro the hell out of this call
+ 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 a() {
this.fluid = this.getBlock().d(this.p()); // Paper - moved from getFluid()
this.isTicking = this.getBlock().isTicking(this.p()); // Paper - moved from isTicking()
if (!this.getBlock().o()) {
this.a = new BlockBase.BlockData.Cache(this.p());
}
+ this.shapeExceedsCube = this.a == null || this.a.c; // Tuinity - moved from actual method to here
+ this.staticPathType = null; // Tuinity - cache static path type
+ this.neighbourOverridePathType = null; // Tuinity - cache static path types
+ this.opacityIfCached = this.a == null || this.isConditionallyFullOpaque() ? -1 : this.a.getOpacity(); // Tuinity - cache opacity for light
+ // Tuinity start - optimise culling shape cache for light
+ if (this.a != null && this.a.getCullingShapeCache() != null) {
+ for (int i = 0, len = this.a.getCullingShapeCache().length; i < len; ++i) {
+ VoxelShape face = this.a.getCullingShapeCache()[i].simplify();
+ if (face.isEmpty()) {
+ this.a.getCullingShapeCache()[i] = VoxelShapes.getEmptyShape();
+ continue;
+ }
+ List<net.minecraft.world.phys.AxisAlignedBB> boxes = face.getBoundingBoxesRepresentation();
+
+ if (boxes.size() == 1) {
+ net.minecraft.world.phys.AxisAlignedBB boundingBox = boxes.get(0);
+ if (boundingBox.equals(VoxelShapes.optimisedFullCube.aabb)) {
+ this.a.getCullingShapeCache()[i] = VoxelShapes.fullCube();
+ } else {
+ this.a.getCullingShapeCache()[i] = VoxelShapes.of(boundingBox);
+ if (!(this.a.getCullingShapeCache()[i] instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) &&
+ this.a.getCullingShapeCache()[i].getBoundingBoxesRepresentation().size() == 1) {
+ this.a.getCullingShapeCache()[i] = new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBox);
+ }
+ }
+ continue;
+ }
+ this.a.getCullingShapeCache()[i] = face;
+ }
+ }
+ // Tuinity end - optimise culling shape cache for light
}
@@ -425,10 +479,12 @@ public abstract class BlockBase {
return this.a != null ? this.a.g : this.getBlock().b(this.p(), iblockaccess, blockposition);
}
+ public final int getOpacity(IBlockAccess iblockaccess, BlockPosition blockposition) { return this.b(iblockaccess, blockposition); } // Tuinity - OBFHELPER
public int b(IBlockAccess iblockaccess, BlockPosition blockposition) {
return this.a != null ? this.a.h : this.getBlock().f(this.p(), iblockaccess, blockposition);
}
+ public final VoxelShape getCullingFace(IBlockAccess iblockaccess, BlockPosition blockposition, EnumDirection enumdirection) { return this.a(iblockaccess, blockposition, enumdirection); } // Tuinity - OBFHELPER
public VoxelShape a(IBlockAccess iblockaccess, BlockPosition blockposition, EnumDirection enumdirection) {
return this.a != null && this.a.i != null ? this.a.i[enumdirection.ordinal()] : VoxelShapes.a(this.c(iblockaccess, blockposition), enumdirection);
}
@@ -438,7 +494,7 @@ public abstract class BlockBase {
}
public final boolean d() { // Paper
- return this.a == null || this.a.c;
+ return this.shapeExceedsCube; // Tuinity - moved into shape cache init
}
public final boolean e() { // Paper
@@ -729,9 +785,9 @@ public abstract class BlockBase {
private static final int f = EnumBlockSupport.values().length;
protected final boolean a;
private final boolean g;
- private final int h;
+ private final int h; private final int getOpacity() { return this.h; } // Tuinity - OBFHELPER
@Nullable
- private final VoxelShape[] i;
+ private final VoxelShape[] i; private final VoxelShape[] getCullingShapeCache () { return this.i; } // Tuinity - OBFHELPER
protected final VoxelShape b;
protected final boolean c;
private final boolean[] j;
diff --git a/src/main/java/net/minecraft/world/level/block/state/IBlockDataHolder.java b/src/main/java/net/minecraft/world/level/block/state/IBlockDataHolder.java
index ba046cffdd8331c7e0427f19fa54d0c7a99077d9..9f9a3210a68e489dcdeabaeeddf01b59775eebd2 100644
--- a/src/main/java/net/minecraft/world/level/block/state/IBlockDataHolder.java
+++ b/src/main/java/net/minecraft/world/level/block/state/IBlockDataHolder.java
@@ -40,11 +40,13 @@ public abstract class IBlockDataHolder<O, S> {
private final ImmutableMap<IBlockState<?>, Comparable<?>> b;
private Table<IBlockState<?>, Comparable<?>, S> e;
protected final MapCodec<S> d;
+ protected com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Tuinity - optimise state lookup
protected IBlockDataHolder(O o0, ImmutableMap<IBlockState<?>, Comparable<?>> immutablemap, MapCodec<S> mapcodec) {
this.c = o0;
this.b = immutablemap;
this.d = mapcodec;
+ this.optimisedTable = new com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable(this, immutablemap); // Tuinity - optimise state lookup
}
public <T extends Comparable<T>> S a(IBlockState<T> iblockstate) {
@@ -86,11 +88,11 @@ public abstract class IBlockDataHolder<O, S> {
public <T extends Comparable<T>> boolean contains(IBlockState<T> iblockstate) { return this.b(iblockstate); } // Paper - OBFHELPER
public <T extends Comparable<T>> boolean b(IBlockState<T> iblockstate) {
- return this.b.containsKey(iblockstate);
+ return this.optimisedTable.get(iblockstate) != null; // Tuinity - optimise state lookup
}
public <T extends Comparable<T>> T get(IBlockState<T> iblockstate) {
- Comparable<?> comparable = (Comparable) this.b.get(iblockstate);
+ final Comparable<?> comparable = this.optimisedTable.get(iblockstate); // Tuinity - optimise state lookup
if (comparable == null) {
throw new IllegalArgumentException("Cannot get property " + iblockstate + " as it does not exist in " + this.c);
@@ -100,27 +102,21 @@ public abstract class IBlockDataHolder<O, S> {
}
public <T extends Comparable<T>> Optional<T> d(IBlockState<T> iblockstate) {
- Comparable<?> comparable = (Comparable) this.b.get(iblockstate);
+ final Comparable<?> comparable = this.optimisedTable.get(iblockstate); // Tuinity - optimise state lookup
return comparable == null ? Optional.empty() : Optional.of(iblockstate.getType().cast(comparable));
}
public <T extends Comparable<T>, V extends T> S set(IBlockState<T> iblockstate, V v0) {
- Comparable<?> comparable = (Comparable) this.b.get(iblockstate);
+ // Tuinity start - optimise state lookup
+ final S ret = (S)this.optimisedTable.get(iblockstate, v0);
- if (comparable == null) {
- throw new IllegalArgumentException("Cannot set property " + iblockstate + " as it does not exist in " + this.c);
- } else if (comparable == v0) {
- return (S) this; // Paper - decompile error
- } else {
- S s0 = this.e.get(iblockstate, v0);
-
- if (s0 == null) {
- throw new IllegalArgumentException("Cannot set property " + iblockstate + " to " + v0 + " on " + this.c + ", it is not an allowed value");
- } else {
- return s0;
- }
+ if (ret == null) {
+ throw new IllegalArgumentException("Cannot set property " + iblockstate + " to " + v0 + " on " + this.c + ", it is not an allowed value");
}
+
+ return ret;
+ // Tuinity end - optimise state lookup
}
public void a(Map<Map<IBlockState<?>, Comparable<?>>, S> map) {
@@ -144,7 +140,8 @@ public abstract class IBlockDataHolder<O, S> {
}
}
- this.e = (Table) (table.isEmpty() ? table : ArrayTable.create(table));
+ this.e = (Table) (table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.e, this.b); // Tuinity - optimise state lookup
+
}
}
diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateBoolean.java b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateBoolean.java
index f2f94950681b198ae7a4c31a044fd62e98e448ab..2c7306c2b0c1d9e701bc3aa2c115434abeab3d72 100644
--- a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateBoolean.java
+++ b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateBoolean.java
@@ -12,6 +12,13 @@ public class BlockStateBoolean extends IBlockState<Boolean> {
super(s, Boolean.class);
}
+ // 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
+
@Override
public Collection<Boolean> getValues() {
return this.a;
diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateEnum.java b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateEnum.java
index 3079cd13ea1465f4221fde4fec7df639f7c1eb49..a11069695b64e515f0e76e1d6ccddc33503492cd 100644
--- a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateEnum.java
+++ b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateEnum.java
@@ -18,6 +18,15 @@ public class BlockStateEnum<T extends Enum<T> & INamable> extends IBlockState<T>
private final ImmutableSet<T> a;
private final Map<String, T> b = 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 BlockStateEnum(String s, Class<T> oclass, Collection<T> collection) {
super(s, oclass);
this.a = ImmutableSet.copyOf(collection);
@@ -33,6 +42,14 @@ public class BlockStateEnum<T extends Enum<T> & INamable> extends IBlockState<T>
this.b.put(s1, t0);
}
+ // Tuinity start - optimise iblockdata state lookup
+ int id = 0;
+ this.idLookupTable = new int[oclass.getEnumConstants().length];
+ java.util.Arrays.fill(this.idLookupTable, -1);
+ for (final T value : this.getValues()) {
+ this.idLookupTable[value.ordinal()] = id++;
+ }
+ // Tuinity end - optimise iblockdata state lookup
}
diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateInteger.java b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateInteger.java
index 190978c889222185b47065e9e5f96a82e59c7b4e..e8165d4de4acca241924aa4076fc8868e1f453d9 100644
--- a/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateInteger.java
+++ b/src/main/java/net/minecraft/world/level/block/state/properties/BlockStateInteger.java
@@ -13,6 +13,16 @@ public class BlockStateInteger extends IBlockState<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 BlockStateInteger(String s, int i, int j) {
super(s, Integer.class);
this.min = i;
diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IBlockState.java b/src/main/java/net/minecraft/world/level/block/state/properties/IBlockState.java
index 759d6a4adaa511488ace5e2650eb685cbb6c4c16..21250d10a51bd0697709f0aa059908f9f71491d5 100644
--- a/src/main/java/net/minecraft/world/level/block/state/properties/IBlockState.java
+++ b/src/main/java/net/minecraft/world/level/block/state/properties/IBlockState.java
@@ -16,6 +16,17 @@ public abstract class IBlockState<T extends Comparable<T>> {
private final Codec<T> d;
private final Codec<IBlockState.a<T>> e;
+ // 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 IBlockState(String s, Class<T> oclass) {
this.d = Codec.STRING.comapFlatMap((s1) -> this.b(s1).map(DataResult::success).orElseGet(() -> { // Paper - decompile error
return DataResult.error("Unable to read property: " + this + " with value: " + s1);
diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java
index 3c25021835d6d8fd112fc89636616bfd744e7f1a..aa49565cd364db3781a110ee138ee1a4edbfa288 100644
--- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java
+++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java
@@ -60,11 +60,59 @@ public class WorldBorder {
return axisalignedbb.maxX > this.e() && axisalignedbb.minX < this.g() && axisalignedbb.maxZ > this.f() && axisalignedbb.minZ < this.h();
}
+ // Tuinity start - optimise collisions
+ // determines whether we are almost colliding with the world border
+ // for clear collisions, this rets false
+ public final boolean isAlmostCollidingOnBorder(AxisAlignedBB boundingBox) {
+ return this.isAlmostCollidingOnBorder(boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
+ }
+
+ public final boolean isAlmostCollidingOnBorder(double boxMinX, double boxMaxX, double boxMinZ, double boxMaxZ) {
+ double borderMinX = this.getMinX();
+ double borderMaxX = this.getMaxX();
+
+ double borderMinZ = this.getMinZ();
+ double borderMaxZ = this.getMaxZ();
+
+ return
+ // Not intersecting if we're smaller
+ !AxisAlignedBB.voxelShapeIntersect(
+ boxMinX + net.minecraft.server.MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + net.minecraft.server.MCUtil.COLLISION_EPSILON,
+ boxMaxX - net.minecraft.server.MCUtil.COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - net.minecraft.server.MCUtil.COLLISION_EPSILON,
+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ
+ )
+ &&
+
+ // Are intersecting if we're larger
+ AxisAlignedBB.voxelShapeIntersect(
+ boxMinX - net.minecraft.server.MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - net.minecraft.server.MCUtil.COLLISION_EPSILON,
+ boxMaxX + net.minecraft.server.MCUtil.COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + net.minecraft.server.MCUtil.COLLISION_EPSILON,
+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ
+ )
+ ;
+ }
+
+ public final boolean isCollidingWithBorderEdge(AxisAlignedBB boundingBox) {
+ return this.isCollidingWithBorderEdge(boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
+ }
+
+ public final boolean isCollidingWithBorderEdge(double boxMinX, double boxMaxX, double boxMinZ, double boxMaxZ) {
+ double borderMinX = this.getMinX() + net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ double borderMaxX = this.getMaxX() - net.minecraft.server.MCUtil.COLLISION_EPSILON;
+
+ double borderMinZ = this.getMinZ() + net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ double borderMaxZ = this.getMaxZ() - net.minecraft.server.MCUtil.COLLISION_EPSILON;
+
+ return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ;
+ }
+ // Tuinity end - optimise collisions
+
public double a(Entity entity) {
return this.b(entity.locX(), entity.locZ());
}
public final VoxelShape asVoxelShape(){ return c();} // Paper - OBFHELPER
+ public final VoxelShape getCollisionShape() { return this.c(); } // Tuinity - OBFHELPER
public VoxelShape c() {
return this.j.m();
}
@@ -80,18 +128,22 @@ public class WorldBorder {
return Math.min(d6, d3);
}
+ public final double getMinX() { return this.e(); } // Tuinity - OBFHELPER
public double e() {
return this.j.a();
}
+ public final double getMinZ() { return this.f(); } // Tuinity - OBFHELPER
public double f() {
return this.j.c();
}
+ public final double getMaxX() { return this.g(); } // Tuinity - OBFHELPER
public double g() {
return this.j.b();
}
+ public final double getMaxZ() { return this.h(); } // Tuinity - OBFHELPER
public double h() {
return this.j.d();
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/Chunk.java b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
index 0727b12b5ff146b4efa9204bf4f495f2f1aa20b9..fc07e2014e961da5d97095c4ee6f972e2ece3ec3 100644
--- a/src/main/java/net/minecraft/world/level/chunk/Chunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
@@ -137,6 +137,158 @@ public class Chunk implements IChunkAccess {
private final int[] inventoryEntityCounts = new int[16];
// Paper end
+ // Tuinity start - optimise hard collision handling
+ // Tuinity - optimised entity slices
+
+ public final void getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<Entity> into, Predicate<Entity> predicate) {
+ this.entitySlicesManager.getHardCollidingEntities(entity, axisalignedbb, into, predicate); // Tuinity
+ }
+ // Tuinity end - optimise hard collision handling
+ // Tuinity start - rewrite light engine
+ protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+ protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+ protected volatile boolean[] skyEmptinessMap;
+ protected volatile boolean[] blockEmptinessMap;
+
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+ return this.blockNibbles;
+ }
+
+ @Override
+ public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+ this.blockNibbles = nibbles;
+ }
+
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+ return this.skyNibbles;
+ }
+
+ @Override
+ public void setSkyNibbles(com.tuinity.tuinity.chunk.light.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
+
+ // Tuinity start - optimised entity slices
+ protected final com.tuinity.tuinity.world.ChunkEntitySlices entitySlicesManager;
+
+ public final boolean hasEntitiesMaybe(Class<?> clazz) { // Tuinity start
+ return true; // Tuinity end
+ }
+
+ public final void getEntitiesClass(Class<?> clazz, Entity entity, AxisAlignedBB boundingBox, Predicate<Entity> predicate, List<Entity> into) {
+ this.entitySlicesManager.getEntities((Class)clazz, entity, boundingBox, (List)into, (Predicate)predicate); // Tuinity
+ }
+ // Tuinity end - optimised entity slices
+
+ // Tuinity start - optimise checkDespawn
+ private boolean playerGeneralAreaCacheSet;
+ private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playerGeneralAreaCache;
+
+ public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getPlayerGeneralAreaCache() {
+ if (!this.playerGeneralAreaCacheSet) {
+ this.updateGeneralAreaCache();
+ }
+ return this.playerGeneralAreaCache;
+ }
+
+ public void updateGeneralAreaCache() {
+ this.updateGeneralAreaCache(((WorldServer)this.world).getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey));
+ }
+
+ public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> value) {
+ this.playerGeneralAreaCacheSet = true;
+ this.playerGeneralAreaCache = value;
+ }
+
+ public EntityPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, double maxRange, Predicate<Entity> predicate) {
+ if (!this.playerGeneralAreaCacheSet) {
+ this.updateGeneralAreaCache();
+ }
+
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> nearby = this.playerGeneralAreaCache;
+
+ if (nearby == null) {
+ return null;
+ }
+
+ Object[] backingSet = nearby.getBackingSet();
+ double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
+ EntityPlayer closest = null;
+ for (int i = 0, len = backingSet.length; i < len; ++i) {
+ Object _player = backingSet[i];
+ if (!(_player instanceof EntityPlayer)) {
+ continue;
+ }
+ EntityPlayer player = (EntityPlayer)_player;
+
+ double distance = player.getDistanceSquared(sourceX, sourceY, sourceZ);
+ if (distance < closestDistance && predicate.test(player)) {
+ closest = player;
+ closestDistance = distance;
+ }
+ }
+
+ return closest;
+ }
+
+ public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, Predicate<Entity> predicate, double range, List<EntityPlayer> ret) {
+ if (!this.playerGeneralAreaCacheSet) {
+ this.updateGeneralAreaCache();
+ }
+
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> 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 EntityPlayer)) {
+ continue;
+ }
+ EntityPlayer player = (EntityPlayer)_player;
+
+ if (range >= 0.0) {
+ double distanceSquared = player.getDistanceSquared(sourceX, sourceY, sourceZ);
+ if (distanceSquared > rangeSquared) {
+ continue;
+ }
+ }
+
+ if (predicate == null || predicate.test(player)) {
+ ret.add(player);
+ }
+ }
+ }
+ // Tuinity end - optimise checkDespawn
+
public Chunk(World world, ChunkCoordIntPair chunkcoordintpair, BiomeStorage biomestorage, ChunkConverter chunkconverter, TickList<Block> ticklist, TickList<FluidType> ticklist1, long i, @Nullable ChunkSection[] achunksection, @Nullable Consumer<Chunk> consumer) {
this.sections = new ChunkSection[16];
this.e = Maps.newHashMap();
@@ -180,6 +332,7 @@ public class Chunk implements IChunkAccess {
// CraftBukkit start
this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
+ this.entitySlicesManager = new com.tuinity.tuinity.world.ChunkEntitySlices(this.world, this.loc.x, this.loc.z, 0, 15); // TODO update for 1.17 // Tuinity
}
public org.bukkit.Chunk bukkitChunk;
@@ -253,12 +406,12 @@ public class Chunk implements IChunkAccess {
PlayerChunkMap chunkMap = chunkProviderServer.playerChunkMap;
// 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.serverThreadQueue.execute(() -> {
// double check that this condition still holds.
- if (Chunk.this.areNeighboursLoaded(2) && chunkMap.playerViewDistanceTickMap.getObjectsInRange(Chunk.this.coordinateKey) != null) {
- chunkProviderServer.addTicketAtLevel(TicketType.PLAYER, Chunk.this.loc, 31, Chunk.this.loc); // 31 -> entity ticking, TODO check on update
+ if (Chunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(Chunk.this.coordinateKey) != null) { // Tuinity - replace old player chunk loading system
+ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.loc.x, this.loc.z); // Tuinity - replace old player chunk loading system
}
});
}
@@ -266,31 +419,18 @@ public class Chunk implements IChunkAccess {
// 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.mailboxMain.a(ChunkTaskQueueSorter.a(chunkMap.getUpdatingChunk(this.coordinateKey), (() -> { // Copied frm PlayerChunkMap
- // double check that this condition still holds.
- if (!Chunk.this.areNeighboursLoaded(1)) {
- return;
- }
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> inRange = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(Chunk.this.coordinateKey);
- if (inRange == null) {
- return;
- }
-
- // broadcast
- Object[] backingSet = inRange.getBackingSet();
- Packet[] chunkPackets = new Packet[10];
- for (int index = 0, len = backingSet.length; index < len; ++index) {
- Object temp = backingSet[index];
- if (!(temp instanceof EntityPlayer)) {
- continue;
- }
- EntityPlayer player = (EntityPlayer)temp;
- chunkMap.sendChunk(player, chunkPackets, Chunk.this);
- }
- })));
- }
+ // Tuinity start - replace old player chunk loading system
+ chunkProviderServer.serverThreadQueue.execute(() -> {
+ if (!Chunk.this.areNeighboursLoaded(1)) {
+ return;
+ }
+ Chunk.this.A();
+ if (!Chunk.this.areNeighboursLoaded(1)) {
+ return;
+ }
+ chunkMap.playerChunkManager.onChunkSendReady(this.loc.x, this.loc.z);
+ });
+ // Tuinity end - replace old player chunk loading system
}
// Paper end - no-tick view distance
}
@@ -344,6 +484,12 @@ public class Chunk implements IChunkAccess {
public Chunk(World world, ProtoChunk protochunk) {
this(world, protochunk.getPos(), protochunk.getBiomeIndex(), protochunk.p(), protochunk.n(), protochunk.o(), protochunk.getInhabitedTime(), protochunk.getSections(), (Consumer) null);
+ // 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.y().iterator();
while (iterator.hasNext()) {
@@ -640,8 +786,9 @@ public class Chunk implements IChunkAccess {
entity.chunkX = this.loc.x;
entity.chunkY = k;
entity.chunkZ = this.loc.z;
- this.entities.add(entity); // Paper - per chunk entity list
- this.entitySlices[k].add(entity);
+ this.entities.add(entity); // Tuinity
+ this.entitySlices[k].add(entity); // Tuinity
+ this.entitySlicesManager.addEntity(entity, k); // Tuinity
// Paper start
if (entity instanceof EntityItem) {
itemCounts[k]++;
@@ -679,7 +826,8 @@ public class Chunk implements IChunkAccess {
entity.entitySlice = null;
entity.inChunk = false;
}
- if (!this.entitySlices[i].remove(entity)) {
+ this.entitySlicesManager.removeEntity(entity, i); // Tuinity
+ if (!this.entitySlices[i].remove(entity)) { // Tuinity - optimise hard colliding entities // Tuinity - entities by class // Tuinity
return;
}
if (entity instanceof EntityItem) {
@@ -866,6 +1014,7 @@ public class Chunk implements IChunkAccess {
// Paper end - neighbour cache
org.bukkit.Server server = this.world.getServer();
((WorldServer)this.world).getChunkProvider().addLoadedChunk(this); // Paper
+ ((WorldServer)this.world).getChunkProvider().playerChunkMap.playerChunkManager.onChunkLoad(this.loc.x, this.loc.z); // Tuinity - rewrite player chunk management
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
@@ -930,116 +1079,18 @@ public class Chunk implements IChunkAccess {
}
public void a(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<Entity> list, @Nullable Predicate<? super Entity> predicate) {
- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
- int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
- i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
- j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
- for (int k = i; k <= j; ++k) {
- List<Entity> entityslice = this.entitySlices[k]; // Spigot
- List<Entity> list1 = entityslice; // Spigot
- int l = list1.size();
-
- for (int i1 = 0; i1 < l; ++i1) {
- Entity entity1 = (Entity) list1.get(i1);
- if (entity1.shouldBeRemoved) continue; // Paper
-
- if (entity1.getBoundingBox().c(axisalignedbb) && entity1 != entity) {
- if (predicate == null || predicate.test(entity1)) {
- list.add(entity1);
- }
-
- if (entity1 instanceof EntityEnderDragon) {
- EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon) entity1).eJ();
- int j1 = aentitycomplexpart.length;
-
- for (int k1 = 0; k1 < j1; ++k1) {
- EntityComplexPart entitycomplexpart = aentitycomplexpart[k1];
-
- if (entitycomplexpart != entity && entitycomplexpart.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.test(entitycomplexpart))) {
- list.add(entitycomplexpart);
- }
- }
- }
- }
- }
- }
+ this.entitySlicesManager.getEntities(entity, axisalignedbb, list, predicate); // Tuinity - optimised entity slices
}
public <T extends Entity> void a(@Nullable EntityTypes<?> entitytypes, AxisAlignedBB axisalignedbb, List<? super T> list, Predicate<? super T> predicate) {
- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
- int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
- i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
- j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
- for (int k = i; k <= j; ++k) {
- Iterator iterator = this.entitySlices[k].iterator(); // Spigot
-
- // Paper start - Don't search for inventories if we have none, and that is all we want
- /*
- * We check if they want inventories by seeing if it is the static `IEntitySelector.d`
- *
- * Make sure the inventory selector stays in sync.
- * It should be the one that checks `var1 instanceof IInventory && var1.isAlive()`
- */
- if (predicate == IEntitySelector.isInventory() && inventoryEntityCounts[k] <= 0) continue;
- while (iterator.hasNext()) {
- T entity = (T) iterator.next(); // CraftBukkit - decompile error
- if (entity.shouldBeRemoved) continue; // Paper
-
- if ((entitytypes == null || entity.getEntityType() == entitytypes) && entity.getBoundingBox().c(axisalignedbb) && predicate.test(entity)) {
- list.add(entity);
- }
- }
- }
+ this.entitySlicesManager.getEntities(entitytypes, axisalignedbb, (List)list, (Predicate)predicate); // Tuinity - optimised entity slices
}
+ public final <T extends Entity> void getEntities(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, List<T> list, @Nullable Predicate<? super T> predicate) { this.a(oclass, axisalignedbb, list, predicate); } // Tuinity - OBFHELPER
public <T extends Entity> void a(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, List<T> list, @Nullable Predicate<? super T> predicate) {
- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
- int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
- i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
- j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
- // Paper start
- int[] counts;
- if (EntityItem.class.isAssignableFrom(oclass)) {
- counts = itemCounts;
- } else if (IInventory.class.isAssignableFrom(oclass)) {
- counts = inventoryEntityCounts;
- } else {
- counts = null;
- }
- // Paper end
- for (int k = i; k <= j; ++k) {
- if (counts != null && counts[k] <= 0) continue; // Paper - Don't check a chunk if it doesn't have the type we are looking for
- Iterator iterator = this.entitySlices[k].iterator(); // Spigot
-
- // Paper start - Don't search for inventories if we have none, and that is all we want
- /*
- * We check if they want inventories by seeing if it is the static `IEntitySelector.d`
- *
- * Make sure the inventory selector stays in sync.
- * It should be the one that checks `var1 instanceof IInventory && var1.isAlive()`
- */
- if (predicate == IEntitySelector.isInventory() && inventoryEntityCounts[k] <= 0) continue;
- // Paper end
- while (iterator.hasNext()) {
- T t0 = (T) iterator.next(); // CraftBukkit - decompile error
- if (t0.shouldBeRemoved) continue; // Paper
-
- if (oclass.isInstance(t0) && t0.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.test(t0))) { // Spigot - instance check
- list.add(t0);
- }
- }
- }
+ this.entitySlicesManager.getEntities(oclass, null, axisalignedbb, list, predicate); // Tuinity - optimised entity slices
}
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 f2621f61cb372ec436fe81e7a93f1aef7d360f3f..316287af6e405ff224636255c2964f46003215ce 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
@@ -171,7 +171,7 @@ public abstract class ChunkGenerator {
// Get origin location (re)defined by event call.
blockposition = new BlockPosition(event.getOrigin().getBlockX(), event.getOrigin().getBlockY(), event.getOrigin().getBlockZ());
// Get world (re)defined by event call.
- worldserver = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle();
+ //worldserver = ((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.
i = event.getRadius();
flag = event.shouldFindUnexplored();
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/ChunkSection.java
index 8fe060c3b2ad0873f96218eb7d02cdff3279224e..b5eb43174d2c2f34bb17bbcdb803aafe58989678 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkSection.java
@@ -18,7 +18,7 @@ public class ChunkSection {
short nonEmptyBlockCount; // Paper - package-private
short tickingBlockCount; // Paper - private -> package-private
private short e;
- final DataPaletteBlock<IBlockData> blockIds; // Paper - package-private
+ public final DataPaletteBlock<IBlockData> blockIds; // Paper - package-private // Tuinity - public
public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper
@@ -103,6 +103,7 @@ public class ChunkSection {
return iblockdata1;
}
+ public final boolean isFullOfAir() { return this.c(); } // Tuinity - OBFHELPER
public boolean c() {
return this.nonEmptyBlockCount == 0;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
index f094ddf6b4d155f3c7a08a3b811c98b0862fd098..f4a4d63a2e21b08580023cf0dcd15a68d192cf14 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
@@ -120,7 +120,7 @@ public class ChunkStatus {
private final ChunkStatus.c w;
private final int x;
private final ChunkStatus.Type y;
- private final EnumSet<HeightMap.Type> z;
+ private final EnumSet<HeightMap.Type> z; public final HeightMap.Type[] heightMaps; // Tuinity
private static CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> a(ChunkStatus chunkstatus, LightEngineThreaded lightenginethreaded, IChunkAccess ichunkaccess) {
boolean flag = a(chunkstatus, ichunkaccess);
@@ -182,7 +182,7 @@ public class ChunkStatus {
this.w = chunkstatus_c;
this.x = i;
this.y = chunkstatus_type;
- this.z = enumset;
+ this.z = enumset; this.heightMaps = new java.util.ArrayList<>(this.z).toArray(new HeightMap.Type[0]); // Tuinity
this.t = chunkstatus == null ? 0 : chunkstatus.c() + 1;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/DataPaletteBlock.java b/src/main/java/net/minecraft/world/level/chunk/DataPaletteBlock.java
index 86dfab740883c138a0df8a3da9dfb4eb9acefaa3..a6937366cd9c9d708edb5cd1ab3ac096e7b2032e 100644
--- a/src/main/java/net/minecraft/world/level/chunk/DataPaletteBlock.java
+++ b/src/main/java/net/minecraft/world/level/chunk/DataPaletteBlock.java
@@ -172,6 +172,7 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
return this.a(j << 8 | k << 4 | i); // Paper - inline
}
+ public final T rawGet(int index) { return this.a(index); } // Tuinity - OBFHELPER
protected T a(int i) {
T t0 = this.h.a(this.a.a(i));
diff --git a/src/main/java/net/minecraft/world/level/chunk/IChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/IChunkAccess.java
index cdf612d7553a8f4aaebb5e0e66bd2a47a280457a..3a7039ceb770e3bb97bf77c9c57e6479ef8224e0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/IChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/IChunkAccess.java
@@ -37,6 +37,36 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
}
// Paper end
+ // Tuinity start
+ default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+ throw new UnsupportedOperationException(this.getClass().getName());
+ }
+ default void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+ throw new UnsupportedOperationException(this.getClass().getName());
+ }
+
+ default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+ throw new UnsupportedOperationException(this.getClass().getName());
+ }
+ default void setSkyNibbles(com.tuinity.tuinity.chunk.light.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
+
IBlockData getType(final int x, final int y, final int z); // Paper
@Nullable
IBlockData setType(BlockPosition blockposition, IBlockData iblockdata, boolean flag);
@@ -135,6 +165,7 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
@Nullable
NBTTagCompound j(BlockPosition blockposition);
+ default Stream<BlockPosition> getLightSources() { return this.m(); } // Tuinity - OBFHELPER
Stream<BlockPosition> m();
TickList<Block> n();
@@ -155,7 +186,9 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
return ashortlist[i];
}
+ default boolean isLit() { return this.r(); } // Tuinity - OBFHELPER
boolean r();
+ default void setLit(boolean lit) { this.b(lit); } // Tuinity - OBFHELPER
void b(boolean flag);
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/ILightAccess.java b/src/main/java/net/minecraft/world/level/chunk/ILightAccess.java
index 43b8361e8ad0a8c429406cb6ff538020f670bdbd..bb63ed58e1eaeb474e99992e39d811b2589f88d9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ILightAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ILightAccess.java
@@ -7,9 +7,10 @@ import net.minecraft.world.level.IBlockAccess;
public interface ILightAccess {
- @Nullable
- IBlockAccess c(int i, int j);
+ default @Nullable IBlockAccess getFeaturesReadyChunk(int i, int j) { return this.c(i, j); } // Tuinity - OBFHELPER
+ @Nullable IBlockAccess c(int i, int j);
+ default void markLightSectionDirty(EnumSkyBlock enumskyblock, SectionPosition sectionposition) { this.a(enumskyblock, sectionposition); } // Tuinity - OBFHELPER
default void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition) {}
IBlockAccess getWorld();
diff --git a/src/main/java/net/minecraft/world/level/chunk/NibbleArray.java b/src/main/java/net/minecraft/world/level/chunk/NibbleArray.java
index 0fec15e141051863dbf51a2b3e1ace5028cd2fc1..d7757e60402be9939fc2d90ad79b2bb76c5249ca 100644
--- a/src/main/java/net/minecraft/world/level/chunk/NibbleArray.java
+++ b/src/main/java/net/minecraft/world/level/chunk/NibbleArray.java
@@ -59,6 +59,7 @@ public class NibbleArray {
boolean poolSafe = false;
public java.lang.Runnable cleaner;
private void registerCleaner() {
+ if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) return; // Tuinity - purge cleaner usage
if (!poolSafe) {
cleaner = MCUtil.registerCleaner(this, this.a, NibbleArray::releaseBytes);
} else {
@@ -66,7 +67,7 @@ public class NibbleArray {
}
}
// Paper end
- @Nullable protected byte[] a;
+ @Nullable protected byte[] a; public final byte[] justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist() { return this.a; }
public NibbleArray() {}
@@ -77,7 +78,7 @@ public class NibbleArray {
}
public NibbleArray(byte[] abyte, boolean isSafe) {
this.a = abyte;
- if (!isSafe) this.a = getCloneIfSet(); // Paper - clone for safety
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && !isSafe) this.a = getCloneIfSet(); // Paper - clone for safety // Tuinity - no need to clone
registerCleaner();
// Paper end
if (abyte.length != 2048) {
@@ -165,7 +166,7 @@ public class NibbleArray {
public NibbleArray copy() { return this.b(); } // Paper - OBFHELPER
public NibbleArray b() {
- return this.a == null ? new NibbleArray() : new NibbleArray(this.a); // Paper - clone in ctor
+ return this.a == null ? new NibbleArray() : new NibbleArray(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? this.a.clone() : this.a); // Paper - clone in ctor // Tuinity - no longer clone in constructor
}
public String toString() {
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 7bfac4e852c4a6697435647dab173913df6034e9..1658f0bb379653c205d08c771a7c23242d50f66d 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -54,7 +54,7 @@ public class ProtoChunk implements IChunkAccess {
private final Map<BlockPosition, NBTTagCompound> i;
private final ChunkSection[] j;
private final List<NBTTagCompound> k;
- private final List<BlockPosition> l;
+ private final List<BlockPosition> l; private final java.util.concurrent.atomic.AtomicBoolean lightSourcesLocked = new java.util.concurrent.atomic.AtomicBoolean(); // Tuinity - warn when light sources are accessed while locked
private final ShortList[] m;
private final Map<StructureGenerator<?>, StructureStart<?>> n;
private final Map<StructureGenerator<?>, LongSet> o;
@@ -66,6 +66,53 @@ public class ProtoChunk implements IChunkAccess {
private volatile boolean u;
final World world; // Paper - Anti-Xray - Add world // Paper - private -> default
+ // Tuinity start - rewrite light engine
+ protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+ protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+ protected volatile boolean[] skyEmptinessMap;
+ protected volatile boolean[] blockEmptinessMap;
+
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+ return this.blockNibbles;
+ }
+
+ @Override
+ public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+ this.blockNibbles = nibbles;
+ }
+
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+ return this.skyNibbles;
+ }
+
+ @Override
+ public void setSkyNibbles(com.tuinity.tuinity.chunk.light.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 - Anti-Xray - Add world
@Deprecated public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter) { this(chunkcoordintpair, chunkconverter, null); } // Notice for updates: Please make sure this constructor isn't used anywhere
public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, World world) {
@@ -191,20 +238,17 @@ public class ProtoChunk implements IChunkAccess {
ChunkSection chunksection = this.a(j >> 4);
IBlockData iblockdata1 = chunksection.setType(i & 15, j & 15, k & 15, iblockdata);
- if (this.g.b(ChunkStatus.FEATURES) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) {
+ if ((com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (this.g.b(ChunkStatus.LIGHT)) : (this.g.b(ChunkStatus.FEATURES))) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) { // Tuinity - move block updates to only happen after lighting occurs (or during, thanks chunk system)
LightEngine lightengine = this.e();
lightengine.a(blockposition);
}
- EnumSet<HeightMap.Type> enumset = this.getChunkStatus().h();
+ HeightMap.Type[] enumset = this.getChunkStatus().heightMaps; // Tuinity - reduce iterator creation
EnumSet<HeightMap.Type> enumset1 = null;
- Iterator iterator = enumset.iterator();
-
- HeightMap.Type heightmap_type;
+ // Tuinity - reduce iterator creation
- while (iterator.hasNext()) {
- heightmap_type = (HeightMap.Type) iterator.next();
+ for (HeightMap.Type heightmap_type : enumset) { // Tuinity - reduce iterator creation
HeightMap heightmap = (HeightMap) this.f.get(heightmap_type);
if (heightmap == null) {
@@ -220,10 +264,9 @@ public class ProtoChunk implements IChunkAccess {
HeightMap.a(this, enumset1);
}
- iterator = enumset.iterator();
-
- while (iterator.hasNext()) {
- heightmap_type = (HeightMap.Type) iterator.next();
+ // Tuinity start - reduce iterator creation
+ for (HeightMap.Type heightmap_type : enumset) {
+ // Tuinity end - reduce iterator creation
((HeightMap) this.f.get(heightmap_type)).a(i & 15, j, k & 15, iblockdata);
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunkExtension.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunkExtension.java
index 7a82d43d51d80a3054e0871bf4b9aa7635920efc..980727261b328dc69f133f33906e957aa75f8600 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunkExtension.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunkExtension.java
@@ -24,7 +24,50 @@ import net.minecraft.world.level.material.FluidTypes;
public class ProtoChunkExtension extends ProtoChunk {
- private final Chunk a;
+ private final Chunk a; public final Chunk getWrappedChunk() { return this.a; } // Tuinity - OBFHELPER
+
+ // Tuinity start - rewrite light engine
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+ return this.getWrappedChunk().getBlockNibbles();
+ }
+
+ @Override
+ public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+ this.getWrappedChunk().setBlockNibbles(nibbles);
+ }
+
+ @Override
+ public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+ return this.getWrappedChunk().getSkyNibbles();
+ }
+
+ @Override
+ public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+ this.getWrappedChunk().setSkyNibbles(nibbles);
+ }
+
+ @Override
+ public boolean[] getSkyEmptinessMap() {
+ return this.getWrappedChunk().getSkyEmptinessMap();
+ }
+
+ @Override
+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {
+ this.getWrappedChunk().setSkyEmptinessMap(emptinessMap);
+ }
+
+ @Override
+ public boolean[] getBlockEmptinessMap() {
+ return this.getWrappedChunk().getBlockEmptinessMap();
+ }
+
+ @Override
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
+ this.getWrappedChunk().setBlockEmptinessMap(emptinessMap);
+ }
+
+ // Tuinity end - rewrite light engine
public ProtoChunkExtension(Chunk chunk) {
super(chunk.getPos(), ChunkConverter.a, chunk.world); // Paper - Anti-Xray - Add parameter
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkRegionLoader.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkRegionLoader.java
index 4eaf497d048324a85ce49fc1c6e9559991c20df7..ec2b238480413ba9c123d9ddeaa787d9520e1b74 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkRegionLoader.java
@@ -68,6 +68,14 @@ public class ChunkRegionLoader {
private static final Logger LOGGER = LogManager.getLogger();
+ // Tuinity start
+ // TODO: Check on update
+ public static long getLastWorldSaveTime(NBTTagCompound chunkData) {
+ NBTTagCompound levelData = chunkData.getCompound("Level");
+ return levelData.getLong("LastUpdate");
+ }
+ // Tuinity end
+
// Paper start - guard against serializing mismatching coordinates
// TODO Note: This needs to be re-checked each update
public static ChunkCoordIntPair getChunkCoordinate(NBTTagCompound chunkData) {
@@ -100,6 +108,13 @@ public class ChunkRegionLoader {
private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
// Paper end
+ // Tuinity start - rewrite light engine
+ private static final int STARLIGHT_LIGHT_VERSION = 4;
+
+ private static final String UNINITIALISED_SKYLIGHT_TAG = "starlight.skylight_uninit";
+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
+ // Tuinity end - rewrite light engine
+
public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) {
ArrayDeque<Runnable> tasksToExecuteOnMain = new ArrayDeque<>();
// Paper end
@@ -129,13 +144,17 @@ public class ChunkRegionLoader {
ProtoChunkTickList<FluidType> protochunkticklist1 = new ProtoChunkTickList<>((fluidtype) -> {
return fluidtype == null || fluidtype == FluidTypes.EMPTY;
}, chunkcoordintpair, nbttagcompound1.getList("LiquidsToBeTicked", 9));
- boolean flag = nbttagcompound1.getBoolean("isLightOn");
+ boolean flag = (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION : nbttagcompound1.getBoolean("isLightOn")); boolean canUseSkyLight = flag && getStatus(nbttagcompound).isAtLeastStatus(ChunkStatus.LIGHT); boolean canUseBlockLight = canUseSkyLight; // Tuinity
NBTTagList nbttaglist = nbttagcompound1.getList("Sections", 10);
boolean flag1 = true;
ChunkSection[] achunksection = new ChunkSection[16];
boolean flag2 = worldserver.getDimensionManager().hasSkyLight();
ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider();
LightEngine lightengine = chunkproviderserver.getLightEngine();
+ com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl
+ com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl
+ final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver);
+ final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver);
if (flag) {
tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
@@ -163,6 +182,7 @@ public class ChunkRegionLoader {
if (flag) {
if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) {
+ if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseBlockLight) blockNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("BlockLight").clone()); // Tuinity - replace light impl
// Paper start - delay this task since we're executing off-main
NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight"));
tasksToExecuteOnMain.add(() -> {
@@ -172,13 +192,14 @@ public class ChunkRegionLoader {
}
if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) {
+ if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseSkyLight) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("SkyLight").clone()); // Tuinity - replace light impl
// Paper start - delay this task since we're executing off-main
NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight"));
tasksToExecuteOnMain.add(() -> {
lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight, true);
});
// Paper end - delay this task since we're executing off-main
- }
+ } else if (flag2 && nbttagcompound2.getBoolean(UNINITIALISED_SKYLIGHT_TAG)) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(); // Tuinity - replace light impl
}
}
@@ -217,8 +238,12 @@ public class ChunkRegionLoader {
object = new Chunk(worldserver.getMinecraftWorld(), chunkcoordintpair, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, j, 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
+ ((Chunk)object).setBlockNibbles(blockNibbles); // Tuinity - replace light impl
+ ((Chunk)object).setSkyNibbles(skyNibbles); // Tuinity - replace light impl
} else {
ProtoChunk protochunk = new ProtoChunk(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, worldserver); // Paper - Anti-Xray - Add parameter
+ protochunk.setBlockNibbles(blockNibbles); // Tuinity - replace light impl
+ protochunk.setSkyNibbles(skyNibbles); // Tuinity - replace light impl
protochunk.a(biomestorage);
object = protochunk;
@@ -397,15 +422,20 @@ public class ChunkRegionLoader {
NibbleArray[] blockLight = new NibbleArray[17 - (-1)];
NibbleArray[] skyLight = new NibbleArray[17 - (-1)];
+ // 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);
+ // Tuinity end - rewrite light impl
+
for (int i = -1; i < 17; ++i) {
- NibbleArray blockArray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i));
- NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i));
+ NibbleArray blockArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (chunk.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded
+ NibbleArray skyArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (chunk.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded
// copy data for safety
- if (blockArray != null) {
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && blockArray != null) { // Tuinity - data already copied
blockArray = blockArray.copy();
}
- if (skyArray != null) {
+ if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && skyArray != null) { // Tuinity - data already copied
skyArray = skyArray.copy();
}
@@ -440,15 +470,19 @@ public class ChunkRegionLoader {
}
public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess, AsyncSaveData asyncsavedata) {
// Paper end
+ // Tuinity start - rewrite light impl
+ final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver);
+ final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver);
+ // Tuinity end - rewrite light impl
ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos();
NBTTagCompound nbttagcompound = new NBTTagCompound();
NBTTagCompound nbttagcompound1 = new NBTTagCompound();
nbttagcompound.setInt("DataVersion", SharedConstants.getGameVersion().getWorldVersion());
- nbttagcompound.set("Level", nbttagcompound1);
+ nbttagcompound.set("Level", nbttagcompound1); // Tuinity - diff on change
nbttagcompound1.setInt("xPos", chunkcoordintpair.x);
nbttagcompound1.setInt("zPos", chunkcoordintpair.z);
- nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading
+ nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading // Tuinity - diff on change
nbttagcompound1.setLong("InhabitedTime", ichunkaccess.getInhabitedTime());
nbttagcompound1.setString("Status", ichunkaccess.getChunkStatus().d());
ChunkConverter chunkconverter = ichunkaccess.p();
@@ -473,8 +507,8 @@ public class ChunkRegionLoader {
NibbleArray nibblearray; // block light
NibbleArray nibblearray1; // sky light
if (asyncsavedata == null) {
- nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData)
- nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData)
+ nibblearray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (ichunkaccess.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded
+ nibblearray1 = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (ichunkaccess.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded
} else {
nibblearray = asyncsavedata.blockLight[i + 1]; // +1 to offset the -1 starting index
nibblearray1 = asyncsavedata.skyLight[i + 1]; // +1 to offset the -1 starting index
@@ -488,12 +522,12 @@ public class ChunkRegionLoader {
}
if (nibblearray != null && !nibblearray.c()) {
- nbttagcompound2.setByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper
+ nbttagcompound2.setByteArray("BlockLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned
}
if (nibblearray1 != null && !nibblearray1.c()) {
- nbttagcompound2.setByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper
- }
+ nbttagcompound2.setByteArray("SkyLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned
+ } else if (nibblearray1 != null && com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound2.setBoolean(UNINITIALISED_SKYLIGHT_TAG, true); // Tuinity - store uninitialised tags
nbttaglist.add(nbttagcompound2);
}
@@ -501,7 +535,7 @@ public class ChunkRegionLoader {
nbttagcompound1.set("Sections", nbttaglist);
if (flag) {
- nbttagcompound1.setBoolean("isLightOn", true);
+ nbttagcompound1.setBoolean("isLightOn", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? false : true); if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound1.setInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Tuinity
}
BiomeStorage biomestorage = ichunkaccess.getBiomeIndex();
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/IChunkLoader.java b/src/main/java/net/minecraft/world/level/chunk/storage/IChunkLoader.java
index e20b9e6c46093d48d5fa5eb3006087d4e998c205..2d03f5710bcbb059e9ce4761e00349e527e937c0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/IChunkLoader.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/IChunkLoader.java
@@ -39,13 +39,14 @@ public class IChunkLoader implements AutoCloseable {
public final RegionFileCache regionFileCache;
public IChunkLoader(File file, DataFixer datafixer, boolean flag) {
- this.regionFileCache = new RegionFileCache(file, flag); // Paper - nuke IOWorker
+ this.regionFileCache = new RegionFileCache(file, flag, true); // Paper - nuke IOWorker // Tuinity
this.b = datafixer;
// Paper - nuke IOWorker
}
// CraftBukkit start
private boolean check(ChunkProviderServer 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"
ChunkCoordIntPair pos = new ChunkCoordIntPair(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/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
index 1b0535ba211904b2384cc80c02c21ed1a606e752..7cdac33e153ccba0a7e8b5aa8fbcbb58ade6a9d2 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
@@ -6,6 +6,7 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -36,15 +37,350 @@ public class RegionFile implements AutoCloseable {
private static final Logger LOGGER = LogManager.getLogger();
private static final ByteBuffer c = ByteBuffer.allocateDirect(1);
private final FileChannel dataFile;
- private final Path e;
- private final RegionFileCompression f;
+ private final Path e; private final java.nio.file.Path getContainingDataFolder() { return this.e; } // Tuinity - OBFHELPER
+ private final RegionFileCompression f; private final RegionFileCompression getRegionFileCompression() { return this.f; } // Tuinity - OBFHELPER
private final ByteBuffer g;
- private final IntBuffer h;
- private final IntBuffer i;
+ private final IntBuffer h; private final IntBuffer getOffsets() { return this.h; } // Tuinity - OBFHELPER
+ private final IntBuffer i; private final IntBuffer getTimestamps() { return this.i; } // Tuinity - OBFHELPER
@VisibleForTesting
protected final RegionFileBitSet freeSectors;
public final File file; // 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 NBTTagCompound OVERSIZED_COMPOUND = new NBTTagCompound();
+
+ private NBTTagCompound 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.dataFile.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;
+ }
+
+ RegionFileCompression compression = RegionFileCompression.getByType(compressionType);
+ if (compression == null) {
+ return null;
+ }
+
+ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position()));
+
+ return NBTCompressedStreamTools.readNBT((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.dataFile.read(length, sector * 4096L)) {
+ return -1;
+ }
+
+ return length.getInt(0);
+ }
+
+ private void backupRegionFile() {
+ File backup = new File(this.file.getParent(), this.file.getName() + "." + new java.util.Random().nextLong() + ".backup");
+ this.backupRegionFile(backup);
+ }
+
+ private void backupRegionFile(File to) {
+ try {
+ this.dataFile.force(true);
+ LOGGER.warn("Backing up regionfile \"" + this.file.getAbsolutePath() + "\" to " + to.getAbsolutePath());
+ java.nio.file.Files.copy(this.file.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.file.getAbsolutePath(), new Throwable());
+
+ // try to backup file so maybe it could be sent to us for further investigation
+
+ this.backupRegionFile();
+ NBTTagCompound[] compounds = new NBTTagCompound[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.dataFile.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);
+ NBTTagCompound compound = this.attemptRead(i, chunkDataLength, fileLength);
+ if (compound == null || compound == OVERSIZED_COMPOUND) {
+ continue;
+ }
+
+ ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(compound);
+ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5);
+
+ NBTTagCompound otherCompound = compounds[location];
+
+ if (otherCompound != null && ChunkRegionLoader.getLastWorldSaveTime(otherCompound) > ChunkRegionLoader.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 {
+ NBTTagCompound aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z);
+ if (ChunkRegionLoader.getLastWorldSaveTime(compound) == ChunkRegionLoader.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.file.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.getContainingDataFolder();
+ File[] regionFiles = containingFolder.toFile().listFiles();
+ boolean[] oversized = new boolean[32 * 32];
+ RegionFileCompression[] oversizedCompressionTypes = new RegionFileCompression[32 * 32];
+
+ if (regionFiles != null) {
+ ChunkCoordIntPair ourLowerLeftPosition = RegionFileCache.getRegionFileCoordinates(this.file);
+
+ if (ourLowerLeftPosition == null) {
+ LOGGER.fatal("Unable to get chunk location of regionfile " + this.file.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) {
+ ChunkCoordIntPair 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;
+ }
+
+ NBTTagCompound compound = null;
+
+ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them
+ RegionFileCompression compression = null;
+ for (RegionFileCompression compressionType : RegionFileCompression.getCompressionTypes().values()) {
+ try {
+ DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java
+ compound = NBTCompressedStreamTools.readNBT((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 || ChunkRegionLoader.getLastWorldSaveTime(compound) > ChunkRegionLoader.getLastWorldSaveTime(compounds[location])) {
+ oversized[location] = true;
+ oversizedCompressionTypes[location] = compression;
+ }
+ }
+ }
+ }
+
+ // now we need to calculate a new offset header
+
+ int[] calculatedOffsets = new int[32 * 32];
+ RegionFileBitSet newSectorAllocations = new RegionFileBitSet();
+ newSectorAllocations.allocate(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.file.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.allocateNewSpace(1);
+ int sectorLength = 1;
+
+ try {
+ this.dataFile.write(this.getOversizedChunkHolderData(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.file.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.file.getAbsolutePath(), ex);
+ this.getOversizedMetaFile().delete();
+ }
+ } else {
+ this.getOversizedMetaFile().delete();
+ }
+
+ this.freeSectors.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.file.getAbsolutePath());
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ int oldOffset = this.getOffsets().get(location);
+ int newOffset = calculatedOffsets[location];
+
+ if (oldOffset == newOffset) {
+ continue;
+ }
+
+ this.getOffsets().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.file.getAbsolutePath());
+ } else if (newOffset == 0) {
+ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.file.getAbsolutePath() + ", it will be regenerated");
+ } else {
+ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.file.getAbsolutePath());
+ }
+ }
+ }
+
+ LOGGER.info("End of change summary for regionfile " + this.file.getAbsolutePath());
+
+ // simply destroy the timestamp header, it's not used
+
+ for (int i = 0; i < 32 * 32; ++i) {
+ this.getTimestamps().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.flushHeader();
+ this.dataFile.force(true); // try to ensure it goes through...
+ LOGGER.info("Successfully wrote new header to disk for regionfile " + this.file.getAbsolutePath());
+ } catch (IOException ex) {
+ LOGGER.fatal("Failed to write new header to disk for regionfile " + this.file.getAbsolutePath(), ex);
+ }
+ }
+ }
+
+ final boolean canRecalcHeader; // final forces compile fail on new constructor
+ // Tuinity end
+
public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
// Paper start - Cache chunk status
@@ -72,10 +408,21 @@ public class RegionFile implements AutoCloseable {
// Paper end
public RegionFile(File file, File file1, boolean flag) throws IOException {
+ // Tuinity start - add can recalc flag
this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag);
}
+ public RegionFile(File file, File file1, boolean flag, boolean canRecalcHeader) throws IOException {
+ this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag, canRecalcHeader);
+ // Tuinity end - add can recalc flag
+ }
public RegionFile(Path path, Path path1, RegionFileCompression regionfilecompression, boolean flag) throws IOException {
+ // Tuinity start - add can recalc flag
+ this(path, path1, regionfilecompression, flag, false);
+ }
+ public RegionFile(Path path, Path path1, RegionFileCompression regionfilecompression, boolean flag, boolean canRecalcHeader) throws IOException {
+ this.canRecalcHeader = canRecalcHeader;
+ // Tuinity start - add can recalc flag
this.g = ByteBuffer.allocateDirect(8192);
this.file = path.toFile(); // Paper
initOversizedState(); // Paper
@@ -104,14 +451,16 @@ public class RegionFile implements AutoCloseable {
RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i);
}
- long j = Files.size(path);
+ final long j = Files.size(path); 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.h.get(k);
+ int l = this.h.get(k); final int headerLocation = k; // Tuinity - we expect this to be the header location
if (l != 0) {
- int i1 = b(l);
- int j1 = a(l);
+ final int i1 = b(l); final int offset = i1; // Tuinity - we expect this to be offset in file in sectors
+ int j1 = a(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
@@ -119,33 +468,105 @@ public class RegionFile implements AutoCloseable {
this.dataFile.read(realLen, i1 * 4096);
j1 = (realLen.getInt(0) + 4) / 4096 + 1;
}
+ sectorLength = j1; // Tuinity - diff on change, we expect this to be sector length of region
// Spigot end
if (i1 < 2) {
RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", path, k, i1);
- this.h.put(k, 0);
- } else if (j1 == 0) {
+ //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change
+ } else if (j1 <= 0) { // Tuinity - <= 0, not ==
RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, k);
- this.h.put(k, 0);
+ //this.h.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", path, k, i1);
- this.h.put(k, 0);
+ //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change
} else {
- this.freeSectors.a(i1, j1);
+ //this.freeSectors.a(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.file.getAbsolutePath() + "! Recalculating header...");
+ needsHeaderRecalc = true;
+ break;
+ } else {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() +
+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.getOffsets().put(headerLocation, 0); // delete the entry from header
+ continue;
+ }
+ }
+ boolean failedToAllocate = !this.freeSectors.tryAllocate(offset, sectorLength);
+ if (failedToAllocate) {
+ LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.file.getAbsolutePath());
}
+ if (failedToAllocate & !canRecalcHeader) {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() +
+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header");
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.getOffsets().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.file.getAbsolutePath() + ", header gave erroneous offsets & locations");
+ this.recalculateHeader();
+ }
+ // Tuinity end
}
}
}
+ private final java.nio.file.Path getOversizedChunkPath(ChunkCoordIntPair chunkcoordintpair) { return this.e(chunkcoordintpair); } // Tuinity - OBFHELPER
private Path e(ChunkCoordIntPair chunkcoordintpair) {
- String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc";
+ String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc"; // Tuinity - diff on change
return this.e.resolve(s);
}
+ // Tuinity start
+ private static ChunkCoordIntPair 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 ChunkCoordIntPair(x, z);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+ // Tuinity end
+
@Nullable public synchronized DataInputStream getReadStream(ChunkCoordIntPair chunkCoordIntPair) throws IOException { return a(chunkCoordIntPair);} // Paper - OBFHELPER
@Nullable
public synchronized DataInputStream a(ChunkCoordIntPair chunkcoordintpair) throws IOException {
@@ -170,6 +591,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 {}", chunkcoordintpair, l, bytebuffer.remaining());
+ // Tuinity start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ // Tuinity end
return null;
} else {
int i1 = bytebuffer.getInt();
@@ -177,6 +604,12 @@ public class RegionFile implements AutoCloseable {
if (i1 == 0) {
RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", chunkcoordintpair);
+ // Tuinity start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ // Tuinity end - recalculate header on regionfile corruption
return null;
} else {
int j1 = i1 - 1;
@@ -184,17 +617,49 @@ public class RegionFile implements AutoCloseable {
if (a(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.getReadStream(chunkcoordintpair);
+ }
+ // Tuinity end - recalculate header on regionfile corruption
}
- return this.a(chunkcoordintpair, b(b0));
+ // Tuinity start - recalculate header on regionfile corruption
+ DataInputStream ret = this.a(chunkcoordintpair, b(b0));
+ if (ret == null && this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ return ret;
+ // Tuinity end - recalculate header on regionfile corruption
} else if (j1 > bytebuffer.remaining()) {
RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", chunkcoordintpair, j1, bytebuffer.remaining());
+ // Tuinity start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ // Tuinity end
return null;
} else if (j1 < 0) {
RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, chunkcoordintpair);
+ // Tuinity start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ // Tuinity end - recalculate header on regionfile corruption
return null;
} else {
- return this.a(chunkcoordintpair, b0, a(bytebuffer, j1));
+ // Tuinity start - recalculate header on regionfile corruption
+ DataInputStream ret = this.a(chunkcoordintpair, b0, a(bytebuffer, j1));
+ if (ret == null && this.canRecalcHeader) {
+ this.recalculateHeader();
+ return this.getReadStream(chunkcoordintpair);
+ }
+ return ret;
+ // Tuinity end - recalculate header on regionfile corruption
}
}
}
@@ -354,10 +819,15 @@ public class RegionFile implements AutoCloseable {
}
private ByteBuffer b() {
+ // Tuinity start - add compressionType param
+ return this.getOversizedChunkHolderData(this.getRegionFileCompression());
+ }
+ private ByteBuffer getOversizedChunkHolderData(RegionFileCompression compressionType) {
+ // Tuinity end
ByteBuffer bytebuffer = ByteBuffer.allocate(5);
bytebuffer.putInt(1);
- bytebuffer.put((byte) (this.f.a() | 128));
+ bytebuffer.put((byte) (compressionType.compressionTypeId() | 128)); // Tuinity - replace with compressionType
((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
return bytebuffer;
}
@@ -394,6 +864,7 @@ public class RegionFile implements AutoCloseable {
};
}
+ private final void flushHeader() throws IOException { this.c(); } // Tuinity - OBFHELPER
private void c() throws IOException {
((java.nio.Buffer) this.g).position(0); // CraftBukkit - decompile error
this.dataFile.write(this.g, 0L);
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileBitSet.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileBitSet.java
index c96eac4b0b519b2807153fa5a8ebf5a020a2b140..b5c8d9b17c2d5e229db5b48448709194d1fc04f8 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileBitSet.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileBitSet.java
@@ -4,18 +4,42 @@ import java.util.BitSet;
public class RegionFileBitSet {
- private final BitSet a = new BitSet();
+ private final BitSet a = new BitSet(); private final BitSet getBitset() { return this.a; } // Tuinity - OBFHELPER
public RegionFileBitSet() {}
+ public final void allocate(int from, int length) { this.a(from, length); } // Tuinity - OBFHELPER
public void a(int i, int j) {
this.a.set(i, i + j);
}
+ public final void free(int from, int length) { this.b(from, length); } // Tuinity - OBFHELPER
public void b(int i, int j) {
this.a.clear(i, i + j);
}
+ // Tuinity start
+ public final void copyFrom(RegionFileBitSet other) {
+ BitSet thisBitset = this.getBitset();
+ BitSet otherBitset = other.getBitset();
+
+ 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.getBitset();
+ int firstSet = bitset.nextSetBit(from);
+ if (firstSet > 0 && firstSet < (from + length)) {
+ return false;
+ }
+ bitset.set(from, from + length);
+ return true;
+ }
+ // Tuinity end
+
+ public final int allocateNewSpace(final int requiredLength) { return this.a(requiredLength); } // Tuinity - OBFHELPER
public int a(int i) {
int j = 0;
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCache.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCache.java
index 74d826853389b8e01ffe2b076cf2b179d29da216..6ef54d8929814172427cb0dba5dc7711f6be69d1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCache.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCache.java
@@ -22,8 +22,15 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
public final Long2ObjectLinkedOpenHashMap<RegionFile> cache = new Long2ObjectLinkedOpenHashMap();
private final File b;
private final boolean c;
+ private final boolean isChunkData; // Tuinity
RegionFileCache(File file, boolean flag) {
+ // Tuinity start - add isChunkData param
+ this(file, flag, false);
+ }
+ RegionFileCache(File file, boolean flag, boolean isChunkData) {
+ this.isChunkData = isChunkData;
+ // Tuinity end - add isChunkData param
this.b = file;
this.c = flag;
}
@@ -83,9 +90,9 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
this.b.mkdirs();
}
- File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca");
+ File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Tuinity - diff on change
if (existingOnly && !file.exists()) return null; // CraftBukkit
- RegionFile regionfile1 = new RegionFile(file, this.b, this.c);
+ RegionFile regionfile1 = new RegionFile(file, this.b, this.c, this.isChunkData); // Tuinity - allow for chunk regionfiles to regen header
this.cache.putAndMoveToFirst(i, regionfile1);
// Paper start
@@ -174,6 +181,13 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
return null;
}
// CraftBukkit end
+ // Tuinity start - Add regionfile parameter
+ return this.readFromRegionFile(regionfile, chunkcoordintpair);
+ }
+ private NBTTagCompound readFromRegionFile(RegionFile regionfile, ChunkCoordIntPair chunkcoordintpair) 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
try { // Paper
DataInputStream datainputstream = regionfile.a(chunkcoordintpair);
// Paper start
@@ -189,6 +203,17 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final
try {
if (datainputstream != null) {
nbttagcompound = NBTCompressedStreamTools.a((DataInput) datainputstream);
+ // Tuinity start - recover from corrupt regionfile header
+ if (this.isChunkData) {
+ ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(nbttagcompound);
+ if (!chunkPos.equals(chunkcoordintpair)) {
+ MinecraftServer.LOGGER.error("Attempting to read chunk data at " + chunkcoordintpair.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.file.getAbsolutePath());
+ regionfile.recalculateHeader();
+ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once.
+ return this.readFromRegionFile(regionfile, chunkcoordintpair);
+ }
+ }
+ // Tuinity end - recover from corrupt regionfile header
return nbttagcompound;
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCompression.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCompression.java
index 78728e064479e7056b7c69e306854330691faa12..d94640f8bdc71d708efca9a95e1986872e1a00d7 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCompression.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileCompression.java
@@ -13,7 +13,7 @@ import javax.annotation.Nullable;
public class RegionFileCompression {
- private static final Int2ObjectMap<RegionFileCompression> d = new Int2ObjectOpenHashMap();
+ private static final Int2ObjectMap<RegionFileCompression> d = new Int2ObjectOpenHashMap(); static final Int2ObjectMap<RegionFileCompression> getCompressionTypes() { return RegionFileCompression.d; } // Tuinity - OBFHELPER
public static final RegionFileCompression a = a(new RegionFileCompression(1, GZIPInputStream::new, GZIPOutputStream::new));
public static final RegionFileCompression b = a(new RegionFileCompression(2, InflaterInputStream::new, DeflaterOutputStream::new));
public static final RegionFileCompression c = a(new RegionFileCompression(3, (inputstream) -> {
@@ -36,8 +36,8 @@ public class RegionFileCompression {
return regionfilecompression;
}
- @Nullable
- public static RegionFileCompression a(int i) {
+ @Nullable public static RegionFileCompression getByType(int type) { return RegionFileCompression.a(type); } // Tuinity - OBFHELPER
+ @Nullable public static RegionFileCompression a(int i) { // Tuinity - OBFHELPER
return (RegionFileCompression) RegionFileCompression.d.get(i);
}
@@ -45,6 +45,7 @@ public class RegionFileCompression {
return RegionFileCompression.d.containsKey(i);
}
+ public final int compressionTypeId() { return this.a(); } // Tuinity - OBFHELPER
public int a() {
return this.e;
}
@@ -53,6 +54,7 @@ public class RegionFileCompression {
return (OutputStream) this.g.wrap(outputstream);
}
+ public final InputStream wrap(InputStream inputstream) throws IOException { return this.a(inputstream); } // Tuinity - OBFHELPER
public InputStream a(InputStream inputstream) throws IOException {
return (InputStream) this.f.wrap(inputstream);
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileSection.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileSection.java
index d3b9a9e4695655860c72db5f2188472681e8d37a..f70c14385c95763b5f270a6e2ce372cf047ba7bb 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileSection.java
@@ -34,8 +34,8 @@ public class RegionFileSection<R> extends RegionFileCache implements AutoCloseab
private static final Logger LOGGER = LogManager.getLogger();
// Paper - nuke IOWorker
- private final Long2ObjectMap<Optional<R>> c = new Long2ObjectOpenHashMap();
- public final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); // Paper - private -> public
+ private final Long2ObjectMap<Optional<R>> c = new Long2ObjectOpenHashMap(); protected final Long2ObjectMap<Optional<R>> getDataBySection() { return this.c; } // Tuinity - OBFHELPER
+ public final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); protected final LongLinkedOpenHashSet getDirtySections() { return this.d; } // Paper - private -> public // Tuinity - OBFHELPER
private final Function<Runnable, Codec<R>> e;
private final Function<Runnable, R> f;
private final DataFixer g;
@@ -59,11 +59,46 @@ public class RegionFileSection<R> extends RegionFileCache implements AutoCloseab
}
- @Nullable
- protected Optional<R> c(long i) {
+ // Tuinity start - actually unload POI data
+ public void unloadData(long coordinate) {
+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate);
+ this.writeDirtyData(chunkPos);
+
+ Long2ObjectMap<Optional<R>> data = this.getDataBySection();
+ int before = data.size();
+
+ for (int section = 0; section < 16; ++section) {
+ data.remove(SectionPosition.asLong(chunkPos.x, section, chunkPos.z));
+ }
+
+ if (before != data.size()) {
+ this.onUnload(coordinate);
+ }
+ }
+
+ protected void onUnload(long coordinate) {}
+
+ public boolean isEmpty(long coordinate) {
+ Long2ObjectMap<Optional<R>> data = this.getDataBySection();
+ int x = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
+ int z = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
+ for (int section = 0; section < 16; ++section) {
+ Optional<R> optional = data.get(SectionPosition.asLong(x, section, z));
+ if (optional != null && optional.orElse(null) != null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ // Tuinity end - actually unload POI data
+
+ @Nullable public final Optional<R> getIfLoaded(long value) { return this.c(value); } // Tuinity - OBFHELPER // Tuinity - OBFHELPER
+ @Nullable protected Optional<R> c(long i) { // Tuinity - OBFHELPER
return (Optional) this.c.get(i);
}
+ public final Optional<R> getOrLoad(long coordinate) { return this.d(coordinate); } // Tuinity - OBFHELPER
protected Optional<R> d(long i) {
SectionPosition sectionposition = SectionPosition.a(i);
@@ -159,6 +194,7 @@ public class RegionFileSection<R> extends RegionFileCache implements AutoCloseab
});
}
}
+ if (this instanceof net.minecraft.world.entity.ai.village.poi.VillagePlace) { ((net.minecraft.world.entity.ai.village.poi.VillagePlace)this).queueUnload(chunkcoordintpair.pair(), net.minecraft.server.MinecraftServer.currentTickLong + 1); } // Tuinity - unload POI data
}
@@ -230,6 +266,7 @@ public class RegionFileSection<R> extends RegionFileCache implements AutoCloseab
return dynamic.get("DataVersion").asInt(1945);
}
+ public final void writeDirtyData(ChunkCoordIntPair chunkcoordintpair) { this.a(chunkcoordintpair); } // Tuinity - OBFHELPER
public void a(ChunkCoordIntPair chunkcoordintpair) {
if (!this.d.isEmpty()) {
for (int i = 0; i < 16; ++i) {
diff --git a/src/main/java/net/minecraft/world/level/levelgen/HeightMap.java b/src/main/java/net/minecraft/world/level/levelgen/HeightMap.java
index bcb620e9b7f47341f51af0f3bb7fbd6a348f9739..79214a93c533839d0c560a1e4f5904c62265590e 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/HeightMap.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/HeightMap.java
@@ -109,6 +109,7 @@ public class HeightMap {
}
}
+ public final int get(int x, int z) { return this.a(x, z); } // Tuinity - OBFHELPER
public int a(int i, int j) {
return this.a(c(i, j));
}
@@ -145,7 +146,7 @@ public class HeightMap {
private final String h;
private final HeightMap.Use i;
private final Predicate<IBlockData> j;
- private static final Map<String, HeightMap.Type> k = (Map) SystemUtils.a((Object) Maps.newHashMap(), (hashmap) -> {
+ private static final Map<String, HeightMap.Type> k = (Map) SystemUtils.a(Maps.newHashMap(), (hashmap) -> { // Tuinity - decompile fix
HeightMap.Type[] aheightmap_type = values();
int i = aheightmap_type.length;
@@ -157,7 +158,7 @@ public class HeightMap {
});
- private Type(String s, HeightMap.Use heightmap_use, Predicate predicate) {
+ private Type(String s, HeightMap.Use heightmap_use, Predicate<IBlockData> predicate) { // Tuinity - decompile fix
this.h = s;
this.i = heightmap_use;
this.j = predicate;
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/DefinedStructure.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/DefinedStructure.java
index 9b82ff37faaafc3a799413f6949fb88a993aa9a0..13983f3271d33ab6e4c7030de5865edbd7b0cd8a 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/DefinedStructure.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/DefinedStructure.java
@@ -374,7 +374,13 @@ public class DefinedStructure {
if (pair1.getSecond() != null) {
tileentity = worldaccess.getTileEntity(blockposition3);
if (tileentity != null) {
+ // Tuinity start - do not update TE's in generating chunks, it might cause a deadlock
+ if (!(worldaccess instanceof World)) {
+ tileentity.invalidateBlockCache();
+ } else {
+ // Tuinity end - do not update TE's in generating chunks, it might cause a deadlock
tileentity.update();
+ } // Tuinity - do not update TE's in generating chunks, it might cause a deadlock
}
}
}
diff --git a/src/main/java/net/minecraft/world/level/lighting/LightEngineStorage.java b/src/main/java/net/minecraft/world/level/lighting/LightEngineStorage.java
index fc0162e7f543d230277457638f208a66537560d7..3825a3ddea21e7dd14c455daac1a6af5b0871e00 100644
--- a/src/main/java/net/minecraft/world/level/lighting/LightEngineStorage.java
+++ b/src/main/java/net/minecraft/world/level/lighting/LightEngineStorage.java
@@ -30,7 +30,8 @@ public abstract class LightEngineStorage<M extends LightEngineStorageArray<M>> e
protected final M f; protected final M updating; // Paper - diff on change, should be "updating"
protected final LongSet g = new LongOpenHashSet();
protected final LongSet h = new LongOpenHashSet(); LongSet dirty = h; // Paper - OBFHELPER
- protected final Long2ObjectMap<NibbleArray> i = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap());
+ protected final Long2ObjectOpenHashMap<NibbleArray> i_synchronized_map_real = new Long2ObjectOpenHashMap<>(); // Tuinity - store wrapped map, we need fastIterator
+ protected final Long2ObjectMap<NibbleArray> i = Long2ObjectMaps.synchronize(this.i_synchronized_map_real); // Tuinity - store wrapped map, we need fastIterator
private final LongSet n = new LongOpenHashSet();
private final LongSet o = new LongOpenHashSet();
private final LongSet p = new LongOpenHashSet();
@@ -254,7 +255,7 @@ public abstract class LightEngineStorage<M extends LightEngineStorageArray<M>> e
this.p.clear();
this.j = false;
- ObjectIterator objectiterator = this.i.long2ObjectEntrySet().iterator();
+ ObjectIterator objectiterator = this.i_synchronized_map_real.long2ObjectEntrySet().fastIterator(); // Tuinity - use fast iterator to reduce entry creation
Entry entry;
long j;
@@ -291,7 +292,7 @@ public abstract class LightEngineStorage<M extends LightEngineStorageArray<M>> e
}
this.n.clear();
- objectiterator = this.i.long2ObjectEntrySet().iterator();
+ objectiterator = this.i_synchronized_map_real.long2ObjectEntrySet().fastIterator(); // Tuinity - use fast iterator to reduce entry creation;
while (objectiterator.hasNext()) {
entry = (Entry) objectiterator.next();
diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathType.java b/src/main/java/net/minecraft/world/level/pathfinder/PathType.java
index fd20802155097d4951cbe273f64de4809dee5c96..a07612f68f08aaaf75c3c656b6f90886b587d7ee 100644
--- a/src/main/java/net/minecraft/world/level/pathfinder/PathType.java
+++ b/src/main/java/net/minecraft/world/level/pathfinder/PathType.java
@@ -4,6 +4,8 @@ public enum PathType {
BLOCKED(-1.0F), OPEN(0.0F), WALKABLE(0.0F), WALKABLE_DOOR(0.0F), TRAPDOOR(0.0F), FENCE(-1.0F), LAVA(-1.0F), WATER(8.0F), WATER_BORDER(8.0F), RAIL(0.0F), UNPASSABLE_RAIL(-1.0F), DANGER_FIRE(8.0F), DAMAGE_FIRE(16.0F), DANGER_CACTUS(8.0F), DAMAGE_CACTUS(-1.0F), DANGER_OTHER(8.0F), DAMAGE_OTHER(-1.0F), DOOR_OPEN(0.0F), DOOR_WOOD_CLOSED(-1.0F), DOOR_IRON_CLOSED(-1.0F), BREACH(4.0F), LEAVES(-1.0F), STICKY_HONEY(8.0F), COCOA(0.0F);
+ PathType belowOverride; // Tuinity
+
private final float y;
private PathType(float f) {
diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathfinderNormal.java b/src/main/java/net/minecraft/world/level/pathfinder/PathfinderNormal.java
index a0c7d3ab747ba1a3cf07e716f3591663a8a9e14b..7b92a54cfb64fb77af99e6bf66eacbdb9e769dc1 100644
--- a/src/main/java/net/minecraft/world/level/pathfinder/PathfinderNormal.java
+++ b/src/main/java/net/minecraft/world/level/pathfinder/PathfinderNormal.java
@@ -444,6 +444,12 @@ public class PathfinderNormal extends PathfinderAbstract {
if (pathtype == PathType.OPEN && j >= 1) {
PathType pathtype1 = b(iblockaccess, blockposition_mutableblockposition.d(i, j - 1, k));
+ // Tuinity start - reduce pathfinder branches
+ if (pathtype1.belowOverride != null) {
+ pathtype = pathtype1.belowOverride;
+ } else {
+ PathType original1 = pathtype1;
+ // Tuinity end - reduce pathfinder branches
pathtype = pathtype1 != PathType.WALKABLE && pathtype1 != PathType.OPEN && pathtype1 != PathType.WATER && pathtype1 != PathType.LAVA ? PathType.WALKABLE : PathType.OPEN;
if (pathtype1 == PathType.DAMAGE_FIRE) {
pathtype = PathType.DAMAGE_FIRE;
@@ -460,6 +466,7 @@ public class PathfinderNormal extends PathfinderAbstract {
if (pathtype1 == PathType.STICKY_HONEY) {
pathtype = PathType.STICKY_HONEY;
}
+ original1.belowOverride = pathtype; } // Tuinity - reduce pathfinder branches
}
if (pathtype == PathType.WALKABLE) {
@@ -485,22 +492,29 @@ public class PathfinderNormal extends PathfinderAbstract {
pathtype = PathType.BLOCKED;
} else {
// Paper end
-
+ // Tuinity start - reduce pathfinder branching
+ if (iblockdata.neighbourOverridePathType == PathType.OPEN) {
+ continue;
+ } else if (iblockdata.neighbourOverridePathType != null) {
+ return iblockdata.neighbourOverridePathType;
+ }
+ // Tuinity end - reduce pathfinder branching
if (iblockdata.a(Blocks.CACTUS)) {
- return PathType.DANGER_CACTUS;
+ return iblockdata.neighbourOverridePathType = PathType.DANGER_CACTUS; // Tuinity - reduce pathfinder branching
}
if (iblockdata.a(Blocks.SWEET_BERRY_BUSH)) {
- return PathType.DANGER_OTHER;
+ return iblockdata.neighbourOverridePathType = PathType.DANGER_OTHER; // Tuinity - reduce pathfinder branching
}
if (a(iblockdata)) {
- return PathType.DANGER_FIRE;
+ return iblockdata.neighbourOverridePathType = PathType.DANGER_FIRE; // Tuinity - reduce pathfinder branching
}
if (iblockdata.getFluid().a((Tag) TagsFluid.WATER)) { // Paper - remove another getType call
- return PathType.WATER_BORDER;
+ return iblockdata.neighbourOverridePathType = PathType.WATER_BORDER; // Tuinity - reduce pathfinder branching
}
+ iblockdata.neighbourOverridePathType = PathType.OPEN; // Tuinity - reduce pathfinder branching
} // Paper
}
}
@@ -513,6 +527,20 @@ public class PathfinderNormal extends PathfinderAbstract {
protected static PathType b(IBlockAccess iblockaccess, BlockPosition blockposition) {
IBlockData iblockdata = iblockaccess.getTypeIfLoaded(blockposition); // Paper
if (iblockdata == null) return PathType.BLOCKED; // Paper
+ // Tuinity start - reduce pathfinder branches
+ if (iblockdata.staticPathType != null) {
+ return iblockdata.staticPathType;
+ }
+ if (iblockdata.getShapeCache() == null) {
+ // while it might be called static, it might vary on shape! However, only a few blocks have variable shape.
+ // So we rarely enter here.
+ return getStaticTypeSlow(iblockaccess, blockposition, iblockdata);
+ } else {
+ return iblockdata.staticPathType = getStaticTypeSlow(iblockaccess, blockposition, iblockdata);
+ }
+ }
+ protected static PathType getStaticTypeSlow(IBlockAccess iblockaccess, BlockPosition blockposition, IBlockData iblockdata) {
+ // Tuinity end - reduce pathfinder branches
Block block = iblockdata.getBlock();
Material material = iblockdata.getMaterial();
diff --git a/src/main/java/net/minecraft/world/level/portal/PortalTravelAgent.java b/src/main/java/net/minecraft/world/level/portal/PortalTravelAgent.java
index 77dfa7eaf178baa55041a829c9dec4851efeedfc..0de6225e11b98cc3d22c002d5f27748cde42a5a0 100644
--- a/src/main/java/net/minecraft/world/level/portal/PortalTravelAgent.java
+++ b/src/main/java/net/minecraft/world/level/portal/PortalTravelAgent.java
@@ -39,16 +39,34 @@ public class PortalTravelAgent {
// int i = flag ? 16 : 128;
// CraftBukkit end
- villageplace.a(this.world, blockposition, i);
- Optional<VillagePlaceRecord> optional = villageplace.b((villageplacetype) -> {
- return villageplacetype == VillagePlaceType.v;
- }, blockposition, i, VillagePlace.Occupancy.ANY).sorted(Comparator.comparingDouble((VillagePlaceRecord villageplacerecord) -> { // CraftBukkit - decompile error
- return villageplacerecord.f().j(blockposition);
- }).thenComparingInt((villageplacerecord) -> {
- return villageplacerecord.f().getY();
- })).filter((villageplacerecord) -> {
- return this.world.getType(villageplacerecord.f()).b(BlockProperties.E);
- }).findFirst();
+ // Tuinity start - optimise portals
+ //villageplace.a(this.world, blockposition, i);
+ Optional<VillagePlaceRecord> optional;
+ java.util.List<VillagePlaceRecord> records = new java.util.ArrayList<>();
+ com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataRecords(villageplace,
+ (VillagePlaceType villageplacetype) -> {
+ return villageplacetype == VillagePlaceType.v; // this should break this entire diff on update if it changes, so TODO check that on diff break
+ },
+ (BlockPosition pos) -> {
+ net.minecraft.world.level.chunk.IChunkAccess lowest = this.world.getChunkAt(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.ChunkStatus.EMPTY);
+ if (!lowest.getChunkStatus().isAtLeastStatus(net.minecraft.world.level.chunk.ChunkStatus.FULL)) {
+ // why would we generate the chunk?
+ return false;
+ }
+ return lowest.getType(pos).contains(BlockProperties.E); // this should break this entire diff on update if it changes, so TODO check that on diff break
+ }, blockposition, i, Double.MAX_VALUE, VillagePlace.Occupancy.ANY, true, records);
+ // this gets us most of the way there, but we bias towards lower y values.
+ VillagePlaceRecord lowestYRecord = null;
+ for (VillagePlaceRecord record : records) {
+ if (lowestYRecord == null) {
+ lowestYRecord = record;
+ } else if (lowestYRecord.getPosition().getY() > record.getPosition().getY()) {
+ lowestYRecord = record;
+ }
+ }
+ // now we're done
+ optional = Optional.ofNullable(lowestYRecord);
+ // Tuinity end - optimise portals
return optional.map((villageplacerecord) -> {
BlockPosition blockposition1 = villageplacerecord.f();
diff --git a/src/main/java/net/minecraft/world/phys/AxisAlignedBB.java b/src/main/java/net/minecraft/world/phys/AxisAlignedBB.java
index 62513c812b497bb9d8dafe1d9c2f574059aebf15..0248ff18bf3f2dede4d0dda90df5e0eea56b7708 100644
--- a/src/main/java/net/minecraft/world/phys/AxisAlignedBB.java
+++ b/src/main/java/net/minecraft/world/phys/AxisAlignedBB.java
@@ -17,6 +17,157 @@ public class AxisAlignedBB {
public final double maxY;
public final double maxZ;
+ // Tuinity start
+ public final boolean isEmpty() {
+ return (this.maxX - this.minX) < net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxY - this.minY) < net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxZ - this.minZ) < net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ }
+
+ public static AxisAlignedBB getBoxForChunk(int chunkX, 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 AxisAlignedBB(x - 3*net.minecraft.server.MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*net.minecraft.server.MCUtil.COLLISION_EPSILON, x + (16.0 + 3*net.minecraft.server.MCUtil.COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*net.minecraft.server.MCUtil.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 final boolean voxelShapeIntersect(AxisAlignedBB other) {
+ return (this.minX - other.maxX) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxX - other.minX) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (this.minY - other.maxY) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxY - other.minY) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (this.minZ - other.maxZ) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxZ - other.minZ) > net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ }
+
+ public final boolean voxelShapeIntersect(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
+ return (this.minX - maxX) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxX - minX) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (this.minY - maxY) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxY - minY) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (this.minZ - maxZ) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (this.maxZ - minZ) > net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ }
+
+ public static boolean voxelShapeIntersect(double minX1, double minY1, double minZ1, double maxX1, double maxY1, double maxZ1,
+ double minX2, double minY2, double minZ2, double maxX2, double maxY2, double maxZ2) {
+ return (minX1 - maxX2) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (maxX1 - minX2) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (minY1 - maxY2) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (maxY1 - minY2) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (minZ1 - maxZ2) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (maxZ1 - minZ2) > net.minecraft.server.MCUtil.COLLISION_EPSILON;
+ }
+
+ public static double collideX(AxisAlignedBB target, AxisAlignedBB source, double source_move) {
+ if (source_move == 0.0) {
+ return 0.0;
+ }
+
+ if ((source.minY - target.maxY) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxY - target.minY) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (source.minZ - target.maxZ) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxZ - target.minZ) > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+
+ if (source_move >= 0.0) {
+ double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
+ if (max_move < -net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
+ if (max_move > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ public static double collideY(AxisAlignedBB target, AxisAlignedBB source, double source_move) {
+ if (source_move == 0.0) {
+ return 0.0;
+ }
+
+ if ((source.minX - target.maxX) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxX - target.minX) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (source.minZ - target.maxZ) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxZ - target.minZ) > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ if (source_move >= 0.0) {
+ double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
+ if (max_move < -net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
+ if (max_move > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ public static double collideZ(AxisAlignedBB target, AxisAlignedBB source, double source_move) {
+ if (source_move == 0.0) {
+ return 0.0;
+ }
+
+ if ((source.minX - target.maxX) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxX - target.minX) > net.minecraft.server.MCUtil.COLLISION_EPSILON &&
+ (source.minY - target.maxY) < -net.minecraft.server.MCUtil.COLLISION_EPSILON && (source.maxY - target.minY) > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ if (source_move >= 0.0) {
+ double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
+ if (max_move < -net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
+ if (max_move > net.minecraft.server.MCUtil.COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ public final AxisAlignedBB offsetX(double dx) {
+ return new AxisAlignedBB(this.minX + dx, this.minY, this.minZ, this.maxX + dx, this.maxY, this.maxZ, false);
+ }
+
+ public final AxisAlignedBB offsetY(double dy) {
+ return new AxisAlignedBB(this.minX, this.minY + dy, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false);
+ }
+
+ public final AxisAlignedBB offsetZ(double dz) {
+ return new AxisAlignedBB(this.minX, this.minY, this.minZ + dz, this.maxX, this.maxY, this.maxZ + dz, false);
+ }
+
+ public AxisAlignedBB(double d0, double d1, double d2, double d3, double d4, double d5, boolean dummy) {
+ this.minX = d0;
+ this.minY = d1;
+ this.minZ = d2;
+ this.maxX = d3;
+ this.maxY = d4;
+ this.maxZ = d5;
+ }
+
+ public final AxisAlignedBB expandUpwards(double dy) {
+ return new AxisAlignedBB(this.minX, this.minY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false);
+ }
+
+ public final AxisAlignedBB cutUpwards(final double dy) { // dy > 0.0
+ return new AxisAlignedBB(this.minX, this.maxY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false);
+ }
+
+ public final AxisAlignedBB cutDownwards(final double dy) { // dy < 0.0
+ return new AxisAlignedBB(this.minX, this.minY + dy, this.minZ, this.maxX, this.minY, this.maxZ, false);
+ }
+
+ public final AxisAlignedBB expandUpwardsAndCutBelow(double dy) {
+ return new AxisAlignedBB(this.minX, this.maxY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false);
+ }
+ // Tuinity end
+
public AxisAlignedBB(double d0, double d1, double d2, double d3, double d4, double d5) {
this.minX = Math.min(d0, d3);
this.minY = Math.min(d1, d4);
@@ -189,6 +340,7 @@ public class AxisAlignedBB {
return new AxisAlignedBB(d0, d1, d2, d3, d4, d5);
}
+ public final AxisAlignedBB offset(double d0, double d1, double d2) { return this.d(d0, d1, d2); } // Tuinity - OBFHELPER
public AxisAlignedBB d(double d0, double d1, double d2) {
return new AxisAlignedBB(this.minX + d0, this.minY + d1, this.minZ + d2, this.maxX + d0, this.maxY + d1, this.maxZ + d2);
}
@@ -197,6 +349,7 @@ public class AxisAlignedBB {
return new AxisAlignedBB(this.minX + (double) blockposition.getX(), this.minY + (double) blockposition.getY(), this.minZ + (double) blockposition.getZ(), this.maxX + (double) blockposition.getX(), this.maxY + (double) blockposition.getY(), this.maxZ + (double) blockposition.getZ());
}
+ public final AxisAlignedBB offset(Vec3D vec3d) { return this.b(vec3d); } // Tuinity - OBFHELPER
public AxisAlignedBB c(Vec3D vec3d) {
return this.d(vec3d.x, vec3d.y, vec3d.z);
}
@@ -216,6 +369,7 @@ public class AxisAlignedBB {
return this.e(vec3d.x, vec3d.y, vec3d.z);
}
+ public final boolean contains(double d0, double d1, double d2) { return this.e(d0, d1, d2); } // Tuinity - OBFHELPER
public boolean e(double d0, double d1, double d2) {
return d0 >= this.minX && d0 < this.maxX && d1 >= this.minY && d1 < this.maxY && d2 >= this.minZ && d2 < this.maxZ;
}
diff --git a/src/main/java/net/minecraft/world/phys/Vec3D.java b/src/main/java/net/minecraft/world/phys/Vec3D.java
index b71e119eed6fa283d99dc033144c8be7b336d9c4..a19a26a88f247d359354902efeece9923f3e0e0b 100644
--- a/src/main/java/net/minecraft/world/phys/Vec3D.java
+++ b/src/main/java/net/minecraft/world/phys/Vec3D.java
@@ -9,11 +9,17 @@ import net.minecraft.util.MathHelper;
public class Vec3D implements IPosition {
- public static final Vec3D ORIGIN = new Vec3D(0.0D, 0.0D, 0.0D);
+ public static final Vec3D ORIGIN = new Vec3D(0.0D, 0.0D, 0.0D); public static Vec3D getZeroVector() { return Vec3D.ORIGIN; } // Tuinity - OBFHELPER
public final double x;
public final double y;
public final double z;
+ // Tuinity start
+ public final double magnitudeXZSquared() {
+ return (this.x * this.x) + (this.z * this.z);
+ }
+ // Tuinity end
+
public static Vec3D a(BaseBlockPosition baseblockposition) {
return new Vec3D((double) baseblockposition.getX() + 0.5D, (double) baseblockposition.getY() + 0.5D, (double) baseblockposition.getZ() + 0.5D);
}
@@ -66,6 +72,7 @@ public class Vec3D implements IPosition {
return this.add(-d0, -d1, -d2);
}
+ public final Vec3D add(Vec3D vec3d) { return this.e(vec3d); } // Tuinity - OBFHELPER
public Vec3D e(Vec3D vec3d) {
return this.add(vec3d.x, vec3d.y, vec3d.z);
}
@@ -114,10 +121,12 @@ public class Vec3D implements IPosition {
return new Vec3D(this.x * d0, this.y * d1, this.z * d2);
}
+ public final double magnitude() { return this.f(); } // Tuinity - OBFHELPER
public double f() {
return (double) MathHelper.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
+ public final double magnitudeSquared() { return this.g(); } // Tuinity - OBFHELPER
public double g() {
return this.x * this.x + this.y * this.y + this.z * this.z;
}
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 887016224c16f8a38c10a98eb0e2ae6cb353a153..9567630593da7c77a0173b93c9a57ceb7887a59b 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,11 @@ import net.minecraft.world.phys.Vec3D;
public abstract class VoxelShape {
- protected final VoxelShapeDiscrete a;
+ protected final VoxelShapeDiscrete a; public final VoxelShapeDiscrete getShape() { return this.a; } // Tuinity - OBFHELPER
@Nullable
private VoxelShape[] b;
- VoxelShape(VoxelShapeDiscrete voxelshapediscrete) {
+ protected VoxelShape(VoxelShapeDiscrete voxelshapediscrete) { // Tuinity
this.a = voxelshapediscrete;
}
@@ -56,9 +56,16 @@ public abstract class VoxelShape {
public final VoxelShape offset(double x, double y, double z) { return this.a(x, y, z); } // Paper - OBFHELPER
public VoxelShape a(double d0, double d1, double d2) {
- return (VoxelShape) (this.isEmpty() ? VoxelShapes.a() : new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2)));
+ return (VoxelShape) (this.isEmpty() ? VoxelShapes.a() : new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2))); // Tuinity - diff on change, copied into VoxelShapeArray override
}
+ // Tuinity start - optimise multi-aabb shapes
+ public boolean intersects(final AxisAlignedBB axisalingedbb) {
+ return VoxelShapes.applyOperation(this, new com.tuinity.tuinity.voxel.AABBVoxelShape(axisalingedbb), OperatorBoolean.AND);
+ }
+ // Tuinity end - optimise multi-aabb shapes
+
+ public final VoxelShape simplify() { return this.c(); } // Tuinity - OBFHELPER
public VoxelShape c() {
VoxelShape[] avoxelshape = new VoxelShape[]{VoxelShapes.a()};
@@ -78,6 +85,7 @@ public abstract class VoxelShape {
}, true);
}
+ public final List<AxisAlignedBB> getBoundingBoxesRepresentation() { return this.d(); } // Tuinity - OBFHELPER
public List<AxisAlignedBB> d() {
List<AxisAlignedBB> list = Lists.newArrayList();
diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShapeArray.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShapeArray.java
index 56f0b3a74f676d288d81671a4791337e169b9758..5a0f8a9f5bfb3f271ccf8293bbe9fe99dcb116a1 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShapeArray.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShapeArray.java
@@ -12,11 +12,25 @@ public final class VoxelShapeArray extends VoxelShape {
private final DoubleList c;
private final DoubleList d;
+ // Tuinity start - optimise multi-aabb shapes
+ static final net.minecraft.world.phys.AxisAlignedBB[] EMPTY = new net.minecraft.world.phys.AxisAlignedBB[0];
+ final net.minecraft.world.phys.AxisAlignedBB[] boundingBoxesRepresentation;
+
+ final double offsetX;
+ final double offsetY;
+ final double offsetZ;
+ // Tuinity end - optimise multi-aabb shapes
+
protected VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, double[] adouble, double[] adouble1, double[] adouble2) {
this(voxelshapediscrete, (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble, voxelshapediscrete.b() + 1)), (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble1, voxelshapediscrete.c() + 1)), (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble2, voxelshapediscrete.d() + 1)));
}
VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, DoubleList doublelist, DoubleList doublelist1, DoubleList doublelist2) {
+ // Tuinity start - optimise multi-aabb shapes
+ this(voxelshapediscrete, doublelist, doublelist1, doublelist2, null, null, 0.0, 0.0, 0.0);
+ }
+ VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, DoubleList doublelist, DoubleList doublelist1, DoubleList doublelist2, VoxelShapeArray original, net.minecraft.world.phys.AxisAlignedBB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) {
+ // Tuinity end - optimise multi-aabb shapes
super(voxelshapediscrete);
int i = voxelshapediscrete.b() + 1;
int j = voxelshapediscrete.c() + 1;
@@ -29,6 +43,18 @@ public final class VoxelShapeArray extends VoxelShape {
} else {
throw (IllegalArgumentException) SystemUtils.c((Throwable) (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.getBoundingBoxesRepresentation().toArray(EMPTY) : boundingBoxesRepresentation; // Tuinity - optimise multi-aabb shapes
+ if (original == null) {
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ this.offsetZ = offsetZ;
+ } else {
+ this.offsetX = offsetX + original.offsetX;
+ this.offsetY = offsetY + original.offsetY;
+ this.offsetZ = offsetZ + original.offsetZ;
+ }
+ // Tuinity end - optimise multi-aabb shapes
}
@Override
@@ -44,4 +70,63 @@ public final class VoxelShapeArray extends VoxelShape {
throw new IllegalArgumentException();
}
}
+
+ // Tuinity start - optimise multi-aabb shapes
+ @Override
+ public VoxelShape a(double d0, double d1, double d2) {
+ if (this == VoxelShapes.getEmptyShape() || this.boundingBoxesRepresentation.length == 0) {
+ return this;
+ }
+ return new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2), this, this.boundingBoxesRepresentation, d0, d1, d2);
+ }
+
+ @Override
+ public java.util.List<net.minecraft.world.phys.AxisAlignedBB> d() { // getBoundingBoxesRepresentation
+ if (this.boundingBoxesRepresentation == null) {
+ return super.d();
+ }
+ java.util.List<net.minecraft.world.phys.AxisAlignedBB> 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.AxisAlignedBB boundingBox : this.boundingBoxesRepresentation) {
+ ret.add(boundingBox.offset(offX, offY, offZ));
+ }
+
+ return ret;
+ }
+
+ public final net.minecraft.world.phys.AxisAlignedBB[] getBoundingBoxesRepresentationRaw() {
+ return this.boundingBoxesRepresentation;
+ }
+
+ public final double getOffsetX() {
+ return this.offsetX;
+ }
+
+ public final double getOffsetY() {
+ return this.offsetY;
+ }
+
+ public final double getOffsetZ() {
+ return this.offsetZ;
+ }
+
+ public final boolean intersects(net.minecraft.world.phys.AxisAlignedBB 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.AxisAlignedBB boundingBox : this.boundingBoxesRepresentation) {
+ if (axisalingedbb.voxelShapeIntersect(boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ,
+ boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ // Tuinity end - optimise multi-aabb shapes
}
diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShapes.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShapes.java
index 858d4689e618c72250447adb61e0bcc3c156f8f3..98e787e6383a39de1708428137fd7f9e057ff153 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShapes.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShapes.java
@@ -27,18 +27,101 @@ public final class VoxelShapes {
voxelshapebitset.a(0, 0, 0, true, true);
return new VoxelShapeCube(voxelshapebitset);
- });
+ }); public static final VoxelShape getFullUnoptimisedCube() { return VoxelShapes.b; } // Tuinity - OBFHELPER
public static final VoxelShape a = create(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
- private static final VoxelShape c = new VoxelShapeArray(new VoxelShapeBitSet(0, 0, 0), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}));
+ private static final VoxelShape c = new VoxelShapeArray(new VoxelShapeBitSet(0, 0, 0), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D})); public static final VoxelShape getEmptyShape() { return VoxelShapes.c; } // Tuinity - OBFHELPER
+
+ // Tuinity start - optimise voxelshapes
+ public static boolean isEmpty(VoxelShape voxelshape) {
+ // helper function for determining empty shapes fast
+ return voxelshape == getEmptyShape() || voxelshape.isEmpty();
+ }
+ // Tuinity end - optimise voxelshapes
public static final VoxelShape empty() {return a();} // Paper - OBFHELPER
public static VoxelShape a() {
return VoxelShapes.c;
}
+ public static final com.tuinity.tuinity.voxel.AABBVoxelShape optimisedFullCube = new com.tuinity.tuinity.voxel.AABBVoxelShape(new AxisAlignedBB(0, 0, 0, 1.0, 1.0, 1.0)); // Tuinity - optimise voxelshape
+
+ // Tuinity start - optimise voxelshapes
+ public static boolean addBoxesToIfIntersects(VoxelShape shape, AxisAlignedBB aabb, java.util.List<AxisAlignedBB> list) {
+ if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) {
+ com.tuinity.tuinity.voxel.AABBVoxelShape shapeCasted = (com.tuinity.tuinity.voxel.AABBVoxelShape)shape;
+ if (!shapeCasted.aabb.isEmpty() && shapeCasted.aabb.voxelShapeIntersect(aabb)) {
+ list.add(shapeCasted.aabb);
+ return true;
+ }
+ return false;
+ } else if (shape instanceof VoxelShapeArray) {
+ VoxelShapeArray shapeCasted = (VoxelShapeArray)shape;
+ // this can be optimised by checking an "overall shape" first, but not needed
+
+ double offX = shapeCasted.offsetX;
+ double offY = shapeCasted.offsetY;
+ double offZ = shapeCasted.offsetZ;
+
+ boolean ret = false;
+
+ for (AxisAlignedBB boundingBox : shapeCasted.boundingBoxesRepresentation) {
+ double minX, minY, minZ, maxX, maxY, maxZ;
+ if (aabb.voxelShapeIntersect(minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ,
+ maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ)) {
+ AxisAlignedBB box = new AxisAlignedBB(minX, minY, minZ, maxX, maxY, maxZ, false);
+ if (!box.isEmpty()) {
+ list.add(box);
+ ret = true;
+ }
+ }
+ }
+
+ return ret;
+ } else {
+ boolean ret = false;
+
+ java.util.List<AxisAlignedBB> boxes = shape.getBoundingBoxesRepresentation();
+ for (int i = 0, len = boxes.size(); i < len; ++i) {
+ AxisAlignedBB box = boxes.get(i);
+ if (!box.isEmpty() && box.voxelShapeIntersect(aabb)) {
+ list.add(box);
+ ret = true;
+ }
+ }
+
+ return ret;
+ }
+ }
+
+ public static void addBoxesTo(VoxelShape shape, java.util.List<AxisAlignedBB> list) {
+ if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) {
+ com.tuinity.tuinity.voxel.AABBVoxelShape shapeCasted = (com.tuinity.tuinity.voxel.AABBVoxelShape)shape;
+ if (!shapeCasted.isEmpty()) {
+ list.add(shapeCasted.aabb);
+ }
+ } else if (shape instanceof VoxelShapeArray) {
+ VoxelShapeArray shapeCasted = (VoxelShapeArray)shape;
+
+ for (AxisAlignedBB boundingBox : shapeCasted.boundingBoxesRepresentation) {
+ if (!boundingBox.isEmpty()) {
+ list.add(boundingBox.offset(shapeCasted.offsetX, shapeCasted.offsetY, shapeCasted.offsetZ));
+ }
+ }
+ } else {
+ java.util.List<AxisAlignedBB> boxes = shape.getBoundingBoxesRepresentation();
+ for (int i = 0, len = boxes.size(); i < len; ++i) {
+ AxisAlignedBB box = boxes.get(i);
+ if (!box.isEmpty()) {
+ list.add(box);
+ }
+ }
+ }
+ }
+ // Tuinity end - optimise voxelshapes
+
public static final VoxelShape fullCube() {return b();} // Paper - OBFHELPER
public static VoxelShape b() {
- return VoxelShapes.b;
+ return VoxelShapes.optimisedFullCube; // Tuinity - optimise voxelshape
}
public static VoxelShape create(double d0, double d1, double d2, double d3, double d4, double d5) {
@@ -77,7 +160,7 @@ public final class VoxelShapes {
return new VoxelShapeCube(voxelshapebitset);
}
} else {
- return new VoxelShapeArray(VoxelShapes.b.a, new double[]{axisalignedbb.minX, axisalignedbb.maxX}, new double[]{axisalignedbb.minY, axisalignedbb.maxY}, new double[]{axisalignedbb.minZ, axisalignedbb.maxZ});
+ return new com.tuinity.tuinity.voxel.AABBVoxelShape(axisalignedbb); // Tuinity - optimise VoxelShapes for single AABB shapes
}
}
@@ -142,6 +225,20 @@ public final class VoxelShapes {
public static final boolean applyOperation(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) { return VoxelShapes.c(voxelshape, voxelshape1, operatorboolean); } // Paper - OBFHELPER
public static boolean c(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) {
+ // Tuinity start - optimise voxelshape
+ if (operatorboolean == OperatorBoolean.AND) {
+ if (voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) {
+ return ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb.voxelShapeIntersect(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb);
+ } else if (voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape1 instanceof VoxelShapeArray) {
+ return ((VoxelShapeArray)voxelshape1).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb);
+ } else if (voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape instanceof VoxelShapeArray) {
+ return ((VoxelShapeArray)voxelshape).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb);
+ }
+ }
+ return abstract_c(voxelshape, voxelshape1, operatorboolean);
+ }
+ public static boolean abstract_c(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) {
+ // Tuinity end - optimise voxelshape
if (operatorboolean.apply(false, false)) {
throw (IllegalArgumentException) SystemUtils.c((Throwable) (new IllegalArgumentException()));
} else if (voxelshape == voxelshape1) {
@@ -324,8 +421,52 @@ public final class VoxelShapes {
}
}
+ public static boolean combinationOccludes(VoxelShape voxelshape, VoxelShape voxelshape1) { return b(voxelshape, voxelshape1); } // Tuinity - OBFHELPER
public static boolean b(VoxelShape voxelshape, VoxelShape voxelshape1) {
- return voxelshape != b() && voxelshape1 != b() ? (voxelshape.isEmpty() && voxelshape1.isEmpty() ? false : !c(b(), b(voxelshape, voxelshape1, OperatorBoolean.OR), OperatorBoolean.ONLY_FIRST)) : true;
+ if (voxelshape == getFullUnoptimisedCube() || voxelshape == optimisedFullCube
+ || voxelshape1 == getFullUnoptimisedCube() || voxelshape1 == optimisedFullCube) {
+ return true;
+ }
+ boolean v1Empty = voxelshape == getEmptyShape();
+ boolean v2Empty = voxelshape1 == getEmptyShape();
+ if (v1Empty && v2Empty) {
+ return false;
+ }
+ if ((voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v1Empty) && (voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v2Empty)) {
+ if (!v1Empty && !v2Empty && (voxelshape != voxelshape1)) {
+ AxisAlignedBB boundingBox1 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb;
+ AxisAlignedBB boundingBox2 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).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 > net.minecraft.server.MCUtil.COLLISION_EPSILON || maxY < (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON)) {
+ return false;
+ }
+ double minX = Math.min(boundingBox1.minX, boundingBox2.minX);
+ double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX);
+ if (minX > net.minecraft.server.MCUtil.COLLISION_EPSILON || maxX < (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON)) {
+ return false;
+ }
+ double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ);
+ double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ);
+ if (minZ > net.minecraft.server.MCUtil.COLLISION_EPSILON || maxZ < (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON)) {
+ return false;
+ }
+ // fall through to full merge check
+ } else {
+ AxisAlignedBB boundingBox = v1Empty ? ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb : ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb;
+ // check if the bounding box encloses the full cube
+ return (boundingBox.minY <= net.minecraft.server.MCUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON)) &&
+ (boundingBox.minX <= net.minecraft.server.MCUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON)) &&
+ (boundingBox.minZ <= net.minecraft.server.MCUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - net.minecraft.server.MCUtil.COLLISION_EPSILON));
+ }
+ }
+ return b_rare(voxelshape, voxelshape1);
+ }
+ public static boolean b_rare(VoxelShape voxelshape, VoxelShape voxelshape1) {
+ return (voxelshape != b() || voxelshape != getFullUnoptimisedCube()) && (voxelshape1 != b() || voxelshape1 != getFullUnoptimisedCube()) ? ((voxelshape == VoxelShapes.getEmptyShape() || voxelshape.isEmpty()) && (voxelshape1 == VoxelShapes.getEmptyShape() || voxelshape1.isEmpty()) ? false : !c(b(), b(voxelshape, voxelshape1, OperatorBoolean.OR), OperatorBoolean.ONLY_FIRST)) : true; // Tuinity - optimise call by checking against more constant shapes
}
@VisibleForTesting
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
index e50731723d266ba65b2163df2e935afb8b013a93..a6736d15282715d920bab85eb92074cd2b4f57d6 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
@@ -37,7 +37,7 @@ public class CraftCrashReport implements CrashReportCallable<Object> {
value.append("\n Force Loaded Chunks: {");
for (World world : Bukkit.getWorlds()) {
value.append(' ').append(world.getName()).append(": {");
- for (Map.Entry<Plugin, Collection<Chunk>> entry : world.getPluginChunkTickets().entrySet()) {
+ for (Map.Entry<Plugin, Collection<net.minecraft.world.level.ChunkCoordIntPair>> entry : ((CraftWorld)world).getPluginChunkTicketsCoordinates().entrySet()) { // Tuinity - do not load chunks in crash reports
value.append(' ').append(entry.getKey().getDescription().getFullName()).append(": ").append(Integer.toString(entry.getValue().size())).append(',');
}
value.append("},");
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index cebecee640ed5a7fc2b978e00ff7eb012228267d..507c5255542ba1b958470b4db2c35b1b0b779f17 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"; // Paper // Tuinity
private final String serverVersion;
private final String bukkitVersion = Versioning.getBukkitVersion();
private final Logger logger = Logger.getLogger("Minecraft");
@@ -880,6 +880,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 (WorldServer world : console.getWorlds()) {
world.worldDataServer.setDifficulty(config.difficulty);
world.setSpawnFlags(config.spawnMonsters, config.spawnAnimals);
@@ -914,6 +915,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
@@ -1937,7 +1939,10 @@ public final class CraftServer implements Server {
@Override
public boolean isPrimaryThread() {
- return Thread.currentThread().equals(console.serverThread) || Thread.currentThread().equals(net.minecraft.server.MinecraftServer.getServer().shutdownThread); // Paper - Fix issues with detecting main thread properly, the only time Watchdog will be used is during a crash shutdown which is a "try our best" scenario
+ // Tuinity start
+ final Thread currThread = Thread.currentThread();
+ return currThread == console.serverThread || currThread instanceof com.tuinity.tuinity.util.TickThread || currThread.equals(net.minecraft.server.MinecraftServer.getServer().shutdownThread); // Paper - Fix issues with detecting main thread properly, the only time Watchdog will be used is during a crash shutdown which is a "try our best" scenario
+ // Tuinity End
}
// Paper start
@@ -2371,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 c0b49a0eaeda06b89a4fb425eec3d5bfa9717379..3562c20dee06913d03aee49d12cb27831c008842 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -298,7 +298,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<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.visibleChunks;
+ Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map
int size = 0;
for (PlayerChunk playerchunk : chunks.values()) {
net.minecraft.world.level.chunk.Chunk chunk = playerchunk.getChunk();
@@ -317,7 +317,7 @@ public class CraftWorld implements World {
return net.minecraft.server.MCUtil.ensureMain(() -> {
int ret = 0;
- for (PlayerChunk chunkHolder : world.getChunkProvider().playerChunkMap.visibleChunks.values()) {
+ for (PlayerChunk chunkHolder : world.getChunkProvider().playerChunkMap.updatingChunks.getVisibleMap().values()) { // Tuinity - change updating chunks map
if (chunkHolder.getChunk() != null) {
++ret;
}
@@ -433,14 +433,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.Chunk chunk = world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z);
- if (chunk == null) {
- addTicket(x, z);
- chunk = this.world.getChunkProvider().getChunkAt(x, z, true);
- }
- return chunk.bukkitChunk;
- // Paper end
+ return this.world.getChunkProvider().getChunkAt(x, z, true).bukkitChunk; // Tuinity - revert paper diff
}
// Paper start
@@ -488,13 +481,16 @@ public class CraftWorld implements World {
public Chunk[] getLoadedChunks() {
// Paper start
if (Thread.currentThread() != world.getMinecraftWorld().serverThread) {
- synchronized (world.getChunkProvider().playerChunkMap.visibleChunks) {
- Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.visibleChunks;
- return chunks.values().stream().map(PlayerChunk::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.Chunk::getBukkitChunk).toArray(Chunk[]::new);
+ // Tuinity start - change updating chunks map
+ Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks;
+ synchronized (world.getChunkProvider().playerChunkMap.updatingChunks) {
+ chunks = world.getChunkProvider().playerChunkMap.updatingChunks.getVisibleMap().clone();
}
+ return chunks.values().stream().map(PlayerChunk::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.Chunk::getBukkitChunk).toArray(Chunk[]::new);
+ // Tuinity end - change updating chunks map
}
// Paper end
- Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.visibleChunks;
+ Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map
return chunks.values().stream().map(PlayerChunk::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.Chunk::getBukkitChunk).toArray(Chunk[]::new);
}
@@ -523,6 +519,7 @@ public class CraftWorld implements World {
org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot
if (isChunkLoaded(x, z)) {
world.getChunkProvider().removeTicket(TicketType.PLUGIN, new ChunkCoordIntPair(x, z), 0, Unit.INSTANCE); // Paper
+ ((ChunkMapDistance)world.getChunkProvider().playerChunkMap.chunkDistanceManager).removeTickets(ChunkCoordIntPair.pair(x, z), TicketType.DELAYED_UNLOAD); // Tuinity - delay chunk unloads - let plugins override
}
return true;
@@ -736,6 +733,30 @@ public class CraftWorld implements World {
return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build()));
}
+ // Tuinity start - don't load chunks for crash reports
+ public Map<Plugin, Collection<ChunkCoordIntPair>> getPluginChunkTicketsCoordinates() {
+ // Copied from above
+ Map<Plugin, ImmutableList.Builder<ChunkCoordIntPair>> ret = new HashMap<>();
+ ChunkMapDistance chunkDistanceManager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager;
+
+ for (Long2ObjectMap.Entry<ArraySetSorted<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
+ long chunkKey = chunkTickets.getLongKey();
+ ArraySetSorted<Ticket<?>> tickets = chunkTickets.getValue();
+
+ ChunkCoordIntPair chunk = new ChunkCoordIntPair(chunkKey);
+ for (Ticket<?> ticket : tickets) {
+ if (ticket.getTicketType() != TicketType.PLUGIN_TICKET) {
+ continue;
+ }
+
+ ret.computeIfAbsent((Plugin) ticket.identifier, (key) -> ImmutableList.builder()).add(chunk);
+ }
+ }
+
+ return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build()));
+ }
+ // Tuinity end - don't load chunks for crash reports
+
@Override
public boolean isChunkForceLoaded(int x, int z) {
return getHandle().getForceLoadedChunks().contains(ChunkCoordIntPair.pair(x, z));
@@ -2668,7 +2689,7 @@ public class CraftWorld implements World {
}
return this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
net.minecraft.world.level.chunk.Chunk chunk = (net.minecraft.world.level.chunk.Chunk) either.left().orElse(null);
- if (chunk != null) addTicket(x, z); // Paper
+ if (false && chunk != null) addTicket(x, z); // Paper // Tuinity - revert
return CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
}, net.minecraft.server.MinecraftServer.getServer());
}
@@ -2693,14 +2714,14 @@ public class CraftWorld implements World {
throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
}
PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap;
- if (viewDistance != chunkMap.getEffectiveViewDistance()) {
+ if (true) { // Tuinity - replace old player chunk management
chunkMap.setViewDistance(viewDistance);
}
}
@Override
public int getNoTickViewDistance() {
- return getHandle().getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance();
+ return getHandle().getChunkProvider().playerChunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Tuinity - replace old player chunk management
}
@Override
@@ -2709,11 +2730,22 @@ public class CraftWorld implements World {
throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
}
PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap;
- 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().getChunkProvider().playerChunkMap.playerChunkManager.getTargetSendDistance();
+ }
+
+ @Override
+ public void setSendViewDistance(int viewDistance) {
+ getHandle().getChunkProvider().playerChunkMap.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 409fbeeb66fb8f58a72d94686bc9515299927b75..57180e164ac51b1aac070c00d820792d13d67258 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");
+ /* Conctete End - Server Config */
// Paper start
acceptsAll(asList("server-name"), "Name of the server")
@@ -265,7 +272,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 295ffab08672d77d88aca368cb5b56f80bc4f1b5..dee4d12a49468d38f077784b219199f0070786f2 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -512,27 +512,36 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
entity.setHeadRotation(yaw);
}
- @Override// Paper start
- public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location loc, @javax.annotation.Nonnull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
- PlayerChunkMap playerChunkMap = ((CraftWorld) loc.getWorld()).getHandle().getChunkProvider().playerChunkMap;
- java.util.concurrent.CompletableFuture<Boolean> future = new java.util.concurrent.CompletableFuture<>();
-
- loc.getWorld().getChunkAtAsyncUrgently(loc).thenCompose(chunk -> {
- ChunkCoordIntPair pair = new ChunkCoordIntPair(chunk.getX(), chunk.getZ());
- ((CraftWorld) loc.getWorld()).getHandle().getChunkProvider().addTicketAtLevel(TicketType.POST_TELEPORT, pair, 31, 0);
- PlayerChunk updatingChunk = playerChunkMap.getUpdatingChunk(pair.pair());
- if (updatingChunk != null) {
- return updatingChunk.getEntityTickingFuture();
- } 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.WorldServer 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.ChunkProviderServer chunkProviderServer = world.getChunkProvider();
+ for (net.minecraft.world.level.chunk.IChunkAccess 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 cfe6898dc373fe55a08acf5c90e200061aa7d0fc..ed1bb89ae7b85bf4017315d6189d6cbf595aefe5 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -2265,15 +2265,70 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
}
}
+ // Tuinity start - implement view distances
+ @Override
+ public int getSendViewDistance() {
+ net.minecraft.server.level.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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.PlayerChunkMap chunkMap = this.getHandle().getWorldServer().getChunkProvider().playerChunkMap;
+ 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(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 fd32d1450a6a2ede3405be7d31697cd16957f553..c38e514b004a4684026d5a89c606399a4fd7efe1 100644
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
@@ -25,6 +25,10 @@ class CraftAsyncTask extends CraftTask {
@Override
public void run() {
final Thread thread = Thread.currentThread();
+ // Tuinity start - name threads according to running plugin
+ final String nameBefore = thread.getName();
+ thread.setName(nameBefore + " - " + this.getOwner().getName()); try {
+ // Tuinity end - name threads according to running plugin
synchronized (workers) {
if (getPeriod() == CraftTask.CANCEL) {
// Never continue running after cancelled.
@@ -92,6 +96,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 9c30a79cc55c6455aa18e3798728deaacc3434ca..332e4bb8a5a426b27b1f580e7a2d77dc1a13064c 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(IScoreboardCriteria criteria, String name, Consumer<ScoreboardScore> 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 : scoreboards) {
Scoreboard board = scoreboard.board;
board.getObjectivesForCriteria(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/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
index f72c13bedaa6fa45e26f5dcad564835bdd4af61f..7c0d90552eeb6de7dab174e2ba4acfc89a7b3db0 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
@@ -35,6 +35,13 @@ public class UnsafeList<E> extends AbstractList<E> implements List<E>, RandomAcc
iterPool[0] = new Itr();
}
+ // Tuinity start
+ @Override
+ public void sort(java.util.Comparator<? super E> c) {
+ Arrays.sort((E[])this.data, 0, size, c);
+ }
+ // Tuinity end
+
public UnsafeList(int capacity) {
this(capacity, 5);
}
@@ -119,6 +126,32 @@ public class UnsafeList<E> extends AbstractList<E> implements List<E>, RandomAcc
return 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 674096cab190d62622f9947853b056f57d43a2a5..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/com.destroystokyo.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/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
index 9f7d2ef932ab41cef5d3d0736d20a7c7e4a2c888..51e9c54cddf4b28ba3d3d892322c487774bdab70 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 ( enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
+ if ( ( enabled || com.tuinity.tuinity.util.TickThread.STRICT_THREAD_CHECKS ) && !org.bukkit.Bukkit.isPrimaryThread() ) // 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 c58de4de8d98c6b1e79d83cc7fcd46a7590ed2a0..b650f9b6901f15b7fec7a426cd77660039eecd66 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -65,6 +65,78 @@ public class WatchdogThread extends Thread
}
}
+ // 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.Vec3D mot;
+ double moveStartX, moveStartY, moveStartZ;
+ net.minecraft.world.phys.Vec3D moveVec;
+ synchronized (entity.posLock) {
+ posX = entity.locX();
+ posY = entity.locY();
+ posZ = entity.locZ();
+ mot = entity.getMot();
+ moveStartX = entity.getMoveStartX();
+ moveStartY = entity.getMoveStartY();
+ moveStartZ = entity.getMoveStartZ();
+ moveVec = entity.getMoveVector();
+ }
+
+ String entityType = entity.getMinecraftKey().toString();
+ java.util.UUID entityUUID = entity.getUniqueID();
+ net.minecraft.world.level.World world = entity.getWorld();
+
+ log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName());
+ log.log(Level.SEVERE, "Entity status: dead: " + entity.dead + ", 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.WorldServer.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.PlayerConnectionUtils.getCurrentPacketProcessors()) {
+ if (packetListener instanceof net.minecraft.server.network.PlayerConnection) {
+ net.minecraft.server.level.EntityPlayer player = ((net.minecraft.server.network.PlayerConnection)packetListener).player;
+ long totalPackets = net.minecraft.network.protocol.PlayerConnectionUtils.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
+
@Override
public void run()
{
@@ -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
ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
+ this.dumpTickingInfo(); // Tuinity - log detailed tick information
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
zd}Jy{Mikh2Cde*;KknNM`8?j|e_7Hu0<j1q1LUpB<FinspM;Xq<gta9JQg~hR<eh}
z1)Dd0n=bikPhI8&CN;lq{}*H9Mq^~F57(naq@=WsZ!3W5*hp}6&2(6{R~pzhVC<5W
zSx3d5qgk_+Q>}ts)-l4>D)kCqJJ@MG2$69ph0jzwI8ry1u8D@CyinC$oT?7S*Z}Eg
zYs}PWLqr4u@)w}#!{cMx;KxO6W2H6~3k$laJjAt+C{0mmCRnfs=OJYbh}HMh&e`#>
zj;jrpjqKCh41OK{FOS`@_sPP$iCm46G^EMNk8(l-1f>!gEV+4vMVRZ#8infUenP+k
zL^tBOH<Dy~_q00gFa0MCF2!V_H~B^qX7J|lG;N2kCTQLZ>F^=)k&U-Tw{gfijqQ&^
z-RHHII5yp}2|o8pTsf6x7$teW9Em!~iy2DN?D@|U)g%I6VG%JBO$|~;c~1Q^3|x`1
z6HRbq1#~Ke)wWpALcc&@P;m+*sGavR0{aOx3=IwUE3YPWAwV45pzD$~02inxi7(6X
z$zk683M=_r#M*+6fQ)&FK0y|lm7JLwS)K=t&ZJk!U_-y%_o@fhr{s37MUEQOF*M)3
zB$;4>Zx;Xk*(hwFjb>1iJ1f*D#nyWL{=>{2|9*^vCNN!%bF8Oe<`xz#s;jFz<K{4R
zUiG<loryQZd^?a`T<DWCEaU9ORMaI$N;;k@N!r=#Rvq@*TRyKtm;5TGUEW^q5ck@x
z#5u;EM<(ba5eQ&oREnC@fH)6<z(f@ICH?es$@7jwt}*U@^#kS8@M6loP;)th%#0`-
z8UzjlO`nmk72w=Mg-7mz#%l}UcH=&7{FDEbkCr4W*<{QZTi1pZ9!M7#FJ|!`l%5kP
zof2j0gVOFSQlJKFE<Hxbq~B;Y+0iI-AZ&9MAG7x?dMU|&97E6?yqt~dQ-aZMA!34R
zluH+&C2<Gu=jV67&mIt!Ao6G<{iG4^Qzuik0#}KVP8A%%GKu8Hug8}obm-2tQ`P^u
z>?;I}4M3lL;!fy_;J-E96O<!9q%smKF{YakPa);H$LQ>f+;sG%K=fZdR)99pJ}fM(
zq%(s8UrsEL{NrdF`!#RY+VjFyPpE_vtqPMM!MQ+QnE)+_g9Z^{4^;k&Sa<mC?dik&
zG&>^=w*yuxB_*Z!U%!3{_9Qr)Jfz4<bDOz@=g~Ht`yS3s<dx-tdo~wm{04hN5Tkex
zPfl`XUl*)bJ66jjo<*o_U~tI6QYwUSe|WZnI}eWv50pH%g?emZ1rEz5uO??N<&63s
zZ;nOjyGDxQwqo!Zd!7>IeS#io4oj_Kqhq`HCUub|Ke!v$1-$v=kc+O#rlCej?%dhY
zxxKUTsFPG1nfoFp3%7@gh9S?vM<nq?jd$w4RoB{jAO3JpBl0vfK0bc5opGX{7^jky
z_d8xz0q+C~RxW??%>0N27#*fpJyaX;Vy{!pt*}!9_mX9uC#J5RyjknW2Dm3dCvZYU
zSW?0kvI9!o2un}*%`AYhr^CQT1aZF=-Nt^atn@Kt%b2!hT(pK!|MclbBv3-<+6{>_
z8toMfWc9rpOk(8|KW>Z-k>Fr(xc_+q9ocf`8!_n}XYUrW?Ax|*_|=5m*4F0V+46wJ
z1IGS^Z5t=0Zj86J2Mf<IyOfR^5fZU$qK8D`Linev1K{10+j54=1@ueR*W)wENE<#=
z+5Rh068E7G$0<udnuh-mn$jG9L?+S;3#p%Pe{{doFt_fX{J0tW-&%ay?khH<Sd~ew
zPAq0e6zI$tgLVhxa@RMdkQjU-@%JWnbVm$$0GsW0Ddqc~O7P3c%I3<-y;IfiXm>Jc
zUq#WKCfhoB<;P2&&`*_G4^_0uqDR20m!>T8ay_rxSzA&9_v5##g6tzXTkx+KRfz32
z9vvpp?+YxHTxDthCBu7)&Q052y4s9*$M4_2w-OdPyK?F-EBoUuSsIk@@(!gA*A_!0
z2eu1y;-Q$Ut(M>8FCOtw?vZR-%*ly^x)<95vK@P0tJoZws@+M*NGhg<JM4ut*Kbs=
z>_NU`!}DZnWBHQz%*@6))$BWN;EM0xAF+B4Mph#S??J?K+&viwPmes*n^HGDL9iBf
zCk|mDu46wwughN!isu&G((DO>Ws`(VLY?^#w=RONx<Y#sLz9wh4(stkQnM_%!NUOu
z&}G0mmW>UgFGby--Y=5NJ|(>qXOS`;lZhmXyMEyBdVM@jJh71E-})~`?t4w8^Kwy)
z<+KACjs!F^TS-;FT24_iWF+=l(<z7_pRw$iwy9+<gk-ore&fdtevcw1eQH|T<onD$
zLhx$6xs1l{MS6hA1MUdULP`UqE4(3q5_(9@wab?3b=tf<var%-(>nR}<L>j7U#;Vd
z)IT3=b&}A}1PU<W2V}5C6E;reR}0F!X0bE`bqOGHr(_S5Ff&I$28hko?)DBGARKL{
zAm)UP#K*kfCmW6@r<FnhI5QD@jiF^U42)#8<{z8>KFa6DKfgHkJci!~7u?a%k<bAO
z39qF71Xeu9;#EdY;3|uBKmbh+R>9h7Rri^{y`|;;xNDoQbV}+oJ=LdApL}|77o@C=
z;~aed)XpbrMtt1x3gHPW<dNqflNn2eUeC(N^=;pyL~v6xFfg#>xbliQH4nKBCew{9
z*-_PTyn~`1VrwKcc4ZrhI^!MsZ{D0O0%O2!SHHi^Dfyr9*x*DGFKwc()b;q6nM*M7
zvA$x_?$BMJJHN5HIn9Ps{_7-sn79~BZegaa5V;s(BA<5BnU?^AeJHXtd)cIj_UCjA
zW|N@MjV~vrJz{sE0Dzv}tXxUDQAXm)1(kX7C_ZVFX%!TlZ850i(P1A0BxaJu)#LcH
zoxMFRzxoxw$bM=B6gpuMD#<QBON5;Wh=~6jUAFX-N8#S1bc$rbVVp+xFmaSImrA+2
z3)_Z?yLbabpj%w$pCG=tu%JoH>vcsa^00?%=D+T9-dQqV*=zD|)W!3BLun2&^n)~$
z2_^{i9~sGXOAsF_S=k&4mWJ@`mD+G%MiPTl<D3N^Y#a?Gmws%y>huomboeFNwHb(<
zVpVR!mwf;JmpO3JL|B%L-!;@7TG}+`HZA;-{VIlQGY|T=f|!9!S=!c?sq5|KeEQ*~
zm!1xeZcJPbSsfjU<fs*ikm;&K=qr{7NcyzX=8+*7<42C!-ATj|Xkow*h~}Q*fk(}~
zPU?p-;CF<$gC5no0ic(7fcF>9e>K|=Ni<+YgrIG!|5@|Z>4bjx+`1j^O-{QK8XARf
zUG$nLRiTEtt;)9F30rvw>nj)@vCF{$d7>o2n>}~Y2^^C79l@s`uXRZOcuy>^%2@t-
zRGv={pKlDXFUgvG_^DWGR==il1rIzn{$p4r(FVOQxZi!_*Ksfl2hR{Aj>01RbFAM=
zpr0wzMwlOwlkt4|JLK)$>VL+{4nv>^`yMa)T;(9f*B(9;{T+)_=M4dN>M&&hS-#(G
z)-sW(WxVkHR)`x#g)25Lu7qnN;~Q-bvK<Bi>DZ=;^fyLy@okDpvt&ZU{!U)WVtmnp
zAN-CzM{jPFWep9NAKDDq@=kynkGi_GQ@Z2y_Wn)xc_q3-&+9`qdGy_{PF-2c^$)%x
zd0sonEJhtG*2|<U!Py~$;b=E=Fv&a+%q}FBi9InZo|rkRFM==Jq8M7{pVAwZnQj{z
zxE3wSx8N*L5D*YlH8eslFJ1E`W0|P+yL{VJYFJm`L<d8I_>P*Q-f_3`Akk96HzBz2
z!5tnJaCcA2hGQrSw*{F)epvfYX?7toP=O0dN<w4xSn<TAAv<v(v(f35+?0KJ{v=P>
zizY2w`>O@4Vqff!dBhQ^><#TjMP}loM9ProiD-Og@$V=*zQ|Avg0D!+96lr^u(1fl
z3J52PHoJYDdvdiIW?q?JIC*r?88VruLx#bp0ly<EtEzmzbg=g!M^Z*bN7G1c_p!!V
z2n6Su_0f-h!k3Pgt;AQCp!8A(ONO`yVo9N&85&Nt6RWGh&>s39v$(c6uC*j}2IFFh
zViOX|K+DH18cd9%Rgjs$*sXuoW<>p^Fv-7CV|zpgTUnj812pyyX-nhA4TZ^UyYY9;
z?}BOarTT1q;0xSTjV_DPWE11?Y2+wSA*ybzebDoy8JwhznKa6SvYxE$WswX7Z6pG$
zsA2GgHFFL3^zA@XTYK{a+6$Q8di%@1-|q9U15y+~R-L7Kwx8*xr(<KeA$al9V~r0;
zR4vK6dswz^{@t(o(S;W4g`=z>FP{g*JDPa`e((jSl#~?Rx=3ne(nLfeP9k0grubJK
zU4euzZqt~$Cl%k^{-!e6YQZi|D3#+MUS}VsYZ)0S>y@)kyqRI?A_esvAu-{`1Uq@!
zC+b`wnMK&<_mitl+k@e*$*{&S>vayX*>D>Q5sw2FZ?l(8ff%(8lo<^mBMrwQXOXe+
z*7sZdWzBTIwZO$y^F)qZL1XbOMY<@M_a56y{({Vg@YN<_y}toq41V%~w=+4ZQvg)X
zVw~l$z-sId^nKU%dlk7W(mG}eS&KV2BdYqNJnX-p=YrG&&`_m0fzA_|iKD${5?oL*
zdS$heR@%Q+(3!!T&k;tIN|v2j=UI))rgkvyC7MTTrKP3g>Fma@_R0`GE5(tL%sS$7
zG4<G{z<=awc^y@m*i@AvEb;NuK3Td(#kwE?Pp4PGgyEk?)mkZA0CG)1H~nam;OHy^
znGx*W%cw)|7dCVl91aVm8>1ag%(Y(xZ5cjlk=R~(3XC+$25r*Fo=G5Oh<FY_42R=|
zue7?*+O~6lB~I+3D{-w`K{9;M*&qpZATfcr)9vphi6b*Nr@1?JGQcOYrTIR-6;I|0
zgVVQi`b9l<%7HgU&JdtNN_`Oim&~)ZhCF5`%5$31@^YibB5)G-c+M~}7KvG*ux-VE
z3y}-5F3)S)R*&sXDc1ScBk&1363zt%r$|+ACkT-uljjVAJZ}8<s7=F|Abd-7d$PLg
zS&h>GgR}i!nDoG?^sult?Eo*x$x6CH-3L@LtZ0dfq!Bbbw-S}RwlN%lpH8c=4l2qH
z1wRszHSPh~=esnWvXD8B{D4<}?}6cA+@Ob1760Is6`g!zl@WL(L&={LA}SxAt0>Tw
z%b7<SOz2?a4~+!akApjVHjh>i^&yNKM;(vGcN<Sf&AXV>wuxAK{g|S3Y1&pH_6U1G
z3M4zx5FU=O;=l_?VzQ-~bx~xN1axPgYI0am3d25BjYmfSTX7Q}==Vcryl6@Se0(Jv
zxKW_o%H`jdnC7QXlkFbCsACHN1Dx=0gf<~@PW-&<=`1H<kp3Ee;L6<7@+MfgKar*z
zKG6%MqS37pG+^K|h<_I=D#SoV9jaVTJL%>d)@#ypH7%OpalDj-P=ts<mf5I<tc%M$
zwqK$_5?Vu$GP?{5cGIBplUQN7<vY&JMOisLL*b6^>+3^~yWs~TV}BD20HjkW6zc1L
z0#HzMkn3JV%7N-18_@tgE82*YnmEzxirriDSx#_|<|q1vL{k}7>^mRzO(ueTSN2~H
zG}kxp)Qn!&)><3|e>62+GXSpQKcemfqU!<SHW6kia-R1eVlE`-(RUe%Z0%uTVe?%P
zmr>&BHZ5Ca;DT<63bBM&uV1BDS?MM$M;x8w>gShAPMxJM^BbMZn}Unm{OC9^4x3%%
zlmX8!km-u$<EVfJKu(+M+HRbtKi|Ftw)BZbQ0kb-YB3>N4fQXQ>jRe`7)3+RFGjhz
z18zf(Fo2<>YV^7LJO^UTZ2Ivd#mpN}o?7pBV&q=f%ID>haV7M8R3jsF*@a%iwIy>|
zsZ!-y{!%&j7`B?W8TcF4NH-RHH1xZ{;7BsA<#APu!;cND)te)FhoXz$BIU}2&^7WP
zT}TX>ZO58$VNPuh6JV7~s(W$vAj`^%AtUamex3YdVl3~4+pqk?G)qUibNMrj0*M25
zY>5Ac|Dnv6xBQmV#$3JA?&HTN(lYl~J}@$l{*TY^kORrCB)3dDO}^^v!dcLf^CHty
zanjllIQeSLmpuG+h&ae`r*v!C*0A&W^a&q>93?BAX<LcxXTLTY2s-6mH5j{so$!U)
zu}GH={~iAH-oKo{`^-k$uv|gU@UC4_<$uGT_*PO2t4s{LaCE29O~fBc4&VlcPd2*)
z#zvJQFe!(OUoSHPjpu{IuNCg}wvAkG*g_RT_(rGw(0Zu9j`9{G-~QKRP!RaH-`)BE
zvb7r!*44{1+{Ru&`NGNjM?^V`yK=J!{8AiUDYu$_ww(r(8nuu2!3mW4qlNqo>zG7n
z2*3TGPIcN`-_hY9&oaiv#fiv~>}7`T`4=pInEqWX*3e8+yPm^9h-tr&ts55$l+388
zW)~F}2JH!}VLbQ>?6~H@&k`MnSsTeVj0TRVP4jGbP*!!CwM6`Z11c)yI2w$+R0zxo
zT|obYS1&&`{>>Z9(jnVU&=yI*%PGe*f78ie*_9oap?sd7fx7<i@Un5>{r^WT>=XHF
zl`f{=UJEn2?tRw`Fem?eRE6#*nOes(ebRcmaK3~a3{a3EyE1zXSF0p7I_iDJ&%;3V
zU;AS}e?*mH#Yh2P9E3QBigIqu2iXf=@t)2+I~f*_E^JtEP1@IR{CBfTj%T}E3e#n%
zUa{@vU?D$l4DEANwkkK@ruP4ta)E*e^KLGg%$PizyPmHvKNMWtuJQ6sPXY=(1m#>W
z7V?9E!Vj}>a|KfQx5ESpH+q6$@gAp-P#~lbz`aj1_?xinN>3o8b2-Z3w>UZ3QZ}W0
zWg-!>p>AADDcU^4;0*L4UFgB0QLlXd^y1E&4>txV!T|!`RwjZGl`;-4ZgFf>luHIy
zZ8d8Rh{I3r!g-ht6mAZxMB<QvHOCHoM?w@=LivZWhXfo8s>6VxRqnA0UY`h|mJZy2
z17BazT$jMKFL3J6Ue_HL1^)4s%$Jj~Qx~1HG#tS@kwL(KP_ZI3d<ID(%K-Jz%rzpL
zsA)k#LG81%YTeo!sF8uO!$+DGU<1Nfx9Mn8P7WN{%pH&do{3^Xz``S44|M@5Jl{RU
znCqoV1?&LR)04NzJ2p@Q%|yHrE%pEDSBC<fWlAZcHH^p5r5BjvDjdb?OI|_IH$bi8
zEZ-8Ug1a>Wz0SH(sqj#-*TNGsIWqPj>cj?!GyWvfdEiNOu4$>MIqL=F&Cc0{g*~L5
zA1wt)=_zMFUkCT5$l!G{1-Y9QtGQ#qm5E(3fYPms_EP*sSVI)bfXN|uNO`BqVuCvd
zv)z8IGRgtM1<_trndVhQ^xA)wn~*W~#d*X@E=W)jcQWI8+?kdzHe;DZ`%+JE%gE}m
z6H=FO8rJxM{N90S=Gi!Mel)TyanxPa;E}C?hJ<QZq(s_1DBn*w@r6I}eqF<^`B7!9
z<>l@e9UWad->;S|v;axgFjrY$z3(rV{MiJ}<CJ0+{mbYzcbmjjreGu1p-RaeH~n0n
zN%H*>3M)t;Q?P5wZy0e3G{dcDO7n}3slDXLMrB$;#*W@Qv)D$=?Xs$F(8eT<r=NZm
zQ(qCW$1QM0^+pQvqF2C5h>cyGIQ~IWgD%Gn&E>F9y#o>cR-7spE;Rur<_E~Pu)e0I
z#&y1|@8D~8c55<|KMf;&x;hg!A%VOZ38_+uk`jH4#=b9M&xcpxV-7cMN{jXVRnKSe
zlKJJ%=VBV{$DNeI1QkiA;DfdVT?$;O#22z6v6bTK9)fjrfIh!Hq__l~KzuNqT{&kA
zKs@YV6^1ZLGjTgR%(=NHS-DvWnnP)NM#qbHINqmQ<pv;&O&G={*ghh8^NuD!$&xpB
zUaWmlRE4t;%CCAT`7Wu|;O#HN$?fUQI{s(5KHb_gg*+-&Twj`?7#mNLR5h4`7-O5G
znwYVh`W220J5TvL5iVFsek%qw$WN*X8HwusSg=%#UcHSPsaYnns5*}s(}omD=Idd@
zcp!dv`2^$NMQ209b#6d1hn7`TFiDakunCFNsOl{1FRRlqXIYGI(RupP?)F_bwx~@v
zK25H83lZ(&L^?qpkUH5YgKR?S(4rW4cRl;SK27oWXak-FJfS+MGH~P9l!+jjE(QB2
zT!p|EsR7EJ3o=>dCE5??co$3nuikqgm=s7*#Kd*+j_weKrZjMeLeHEoiJm>zuDRU`
zh~ggr^knn<c9LCD(ZRt%{B|L`TFuhy2nE%WcC9UvOP<FLK>eWU!Nn}AQt=0Id6Hk;
z4bJqse|V$H`stT?NS0yreYvaZ9YF!fw+N}{3#yXRU!C7?exl35BDC%+!jDMGT^DN#
zN9FGd#5t#;$h}5UgQ?q-Gr15>C6=nLUszle9<+_!!oi_m@_L^-R>_Qty7_g|C%m|5
z-7^5X5V_ARi?h9_LW%2vByD3X_IvUktqBv{%SYXO1&;e&O#Ll_cfC`Wv1u+l_#RI<
zQ5K<R7woH(6ii>ly0;P`%TXaQN(heOg~>V&L{d+ZDA%eq-UKo#1)$rkjSm=nzAE2r
z5--RyKhxfXoGVU3^ab{5XGlyL1+26foG)4H<n?S$srX0vX6KzP;OowPO*ZX%@I+1B
zd^@lo9?A;<O@!{!hM0O{WRMM~5i4ZzMz$S+?@pI$+h94nzP-Ku;G^TOYaI;@+>ZvN
zG@&I3h0fnK5lIjcrg*XxPy1(gK3_TN`&VYnxP;C|j$~0rT$0f|*#=OzM^NbE-1T5D
z%Csnt)n!sx3N#b(8G&+G3W~Q_B#StA6jZZ=p#wuu`DrAMXm{T@#S;ku4Dme@{Njmk
zCtrh3z6O>o)~o{&Htx+6kn*)$NNBH-biu^a<WFnLup`-{UAH45I`7I&(sBY>YtWUq
z(G>4rCEKr#tO>!x8A@%W@6g)Xs%2Hq!y#Mbb@9R2@GDWi&!{jhZvzQ1D9nMuPoOS+
z+cj{9nx5X{jJOIavbFf)Kz5Jnbe5Bu#(XE-z$j&iaP%c9W59OoT0~|N#D*(N2kz={
zs(|)nH!_+_g1)#ZH2xk>ZTG#6WN#qa3BxZM{NWxq`*#$H255k6Ky?hw*hSA6`c_fl
zT@Ua%E5Ez3;~`kQFmrC#$Nlvc_Uy3#yzhd-6UYuuIwgIBZZC-`dwOBJbfurL(FfhH
z{YkjE+9OrOveY`{t{sGw&51YO1@{iO4)Ki=!Z5#q=m_Hi)_j0`>?;t2j);vv%BUif
z;wpTZdLQLsGvZ()DCdxYudn^Pt;BZ}Rin$4F8h{R`HxT2z`uc&aMXIQOvwgA5%{&)
zFW52MiN!$!EXgx}Px~e1!EMp;#&kY65oDho95j~!qD%YJr`+aK4jCJ4UJ^;q>w@Lf
zvDfg|M`S^@DGxu+7aR3Cx#;<xgSDhwzwCQFIk|AAJB5B~mR_Gk(_}Nh)Llbo_PTq*
zKpXMTD^GyEo^B+xzR09t;)E_El^4Cc<Kvq++Uz8RmrWYXyyI_c`->%?advj&1~L-m
zJqCP9&TW3migV*`Z$#)Qa>3>Jf)g9D6Ki2<I<i}IfTAEzE|UIp4RQWwg_TSlZn09=
zE|{&Qi(^_E>8P@iX(us<lk2S8)o-+`jX3TqT@qu1J!6hFJc$<zY3b>o)hic8Dp1F<
zeF;(n8Po8A*~^T{De(<avPjs6y<_Gz2B@0~;F2Mwv*H|*Y`w#F#O7bs#2<?tYX^_4
z_8^68Yi=w7O#3;Y=2-K^)&J8`g%MZN)bz1eP`L5w?DTnrl-(^+z&W4YztC_*O06i-
z{GQG1d)tx$D+D03_+eow{(8DlwY5Du1x{6UPm3bS$kqWgkq~g0tAde@t;WJAyXsM5
zGJ`JQx>J)Z2nqLl@Vv3yoSlGwq0aeOg4ymI(KIkTeur-=J-yp9z?qe)it6gq-wl@I
z0D-_I{|T<5kwD9uH3yf1GWXp5*8eOgJf*q0IRoK|+r{}Fug&0WpNDKMTC@(Xc)9K8
zy`lByMn!1fnY)1KYP(0Je1)c~WilUuh<&Q8^OE?L9Q^xK*Y@M$`6D6TDCZ^@l8{|}
zxmmNw)mng$hYBii+&ZqedxWT0<Y>dnV#LG4zC%+kzcK+-??vEHT>Q-T8zu<!_QuSc
WX&3$!%>|s_1IbA#OV)^+1pg1OmmZn`