diff --git a/api/pom.xml b/api/pom.xml
index db802b650..d4d16e816 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -4,7 +4,7 @@
4.0.0
akarin-api
- 1.13.2-R0.1-SNAPSHOT
+ 1.14.4-R0.1-SNAPSHOT
jar
diff --git a/pom.xml b/pom.xml
index 38e7be573..8ed7e81ea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,16 +3,16 @@
4.0.0
akarin
jar
- 1.13.2-R0.1-SNAPSHOT
+ 1.14.4-R0.1-SNAPSHOT
Akarin
https://github.com/Akarin-project/Akarin
UTF-8
- 1.13.2-R0.1-SNAPSHOT
- 1.13.2
- 1_13_R2
+ 1.14.4-R0.1-SNAPSHOT
+ 1.14.4
+ 1_14_R1
git-Bukkit-
yyyyMMdd-HHmm
@@ -31,7 +31,7 @@
com.destroystokyo.paper
- akarin-api
+ paper-api
${api.version}
compile
@@ -91,7 +91,7 @@
org.xerial
sqlite-jdbc
- 3.25.2
+ 3.28.0
runtime
diff --git a/scripts/build.sh b/scripts/build.sh
index 1504fd8b6..0589d03fd 100644
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -50,8 +50,8 @@ echo "[Akarin] Ready to build"
minecraftversion=$(cat "$paperworkdir/BuildData/info.json" | grep minecraftVersion | cut -d '"' -f 4)
rawjar="$paperbasedir/Paper-Server/target/akarin-$minecraftversion.jar"
\cp -rf "$rawjar" "$basedir/akarin-$minecraftversion.jar"
- rawapi="$paperbasedir/Paper-API/target/akarin-api-1.13.2-R0.1-SNAPSHOT.jar"
- \cp -rf "$rawapi" "$basedir/akarin-api-1.13.2-R0.1-SNAPSHOT.jar"
+ rawapi="$paperbasedir/Paper-API/target/akarin-api-1.14.4-R0.1-SNAPSHOT.jar"
+ \cp -rf "$rawapi" "$basedir/akarin-api-1.14.4-R0.1-SNAPSHOT.jar"
echo ""
echo "[Akarin] Build successful"
diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
index 987727a94..c6818bc86 100644
--- a/src/main/java/co/aikar/timings/MinecraftTimings.java
+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
@@ -1,17 +1,18 @@
package co.aikar.timings;
-import com.github.benmanes.caffeine.cache.Cache;
import com.google.common.collect.MapMaker;
import net.minecraft.server.*;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
-import org.checkerframework.checker.nullness.qual.NonNull;
+
import org.bukkit.craftbukkit.scheduler.CraftTask;
import java.util.Map;
+// TODO: Re-implement missing timers
public final class MinecraftTimings {
+ public static final Timing serverOversleep = Timings.ofSafe("Server Oversleep");
public static final Timing playerListTimer = Timings.ofSafe("Player List");
public static final Timing commandFunctionsTimer = Timings.ofSafe("Command Functions");
public static final Timing connectionTimer = Timings.ofSafe("Connection Handler");
@@ -32,6 +33,7 @@ public final class MinecraftTimings {
public static final Timing structureGenerationTimer = Timings.ofSafe("Structure Generation");
public static final Timing processQueueTimer = Timings.ofSafe("processQueue");
+ public static final Timing processTasksTimer = Timings.ofSafe("processTasks");
public static final Timing playerCommandTimer = Timings.ofSafe("playerCommand");
@@ -40,7 +42,7 @@ public final class MinecraftTimings {
public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update");
public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate");
- private static final Cache, String> taskNameCache = com.github.benmanes.caffeine.cache.Caffeine.newBuilder().weakKeys().build(); // Akarin - caffeine
+ private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap();
private MinecraftTimings() {}
@@ -65,7 +67,7 @@ public final class MinecraftTimings {
plugin = TimingsManager.getPluginByClassloader(taskClass);
}
- final String taskname = taskNameCache.get(taskClass, clazz -> // Akarin - caffeine
+ final String taskname = taskNameCache.computeIfAbsent(taskClass, clazz ->
clazz.isAnonymousClass() || clazz.isLocalClass()
? clazz.getName()
: clazz.getCanonicalName());
diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
index eff9dcf54..ddec62fbf 100644
--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
@@ -6,6 +6,7 @@ import net.minecraft.server.WorldServer;
/**
* Set of timers per world, to track world specific timings.
*/
+// TODO: Re-implement missing timers
public class WorldTimingsHandler {
public final Timing mobSpawn;
public final Timing doChunkUnload;
@@ -35,12 +36,19 @@ public class WorldTimingsHandler {
public final Timing tracker2;
public final Timing doTick;
public final Timing tickEntities;
+ public final Timing chunks;
+ public final Timing newEntities;
+ public final Timing raids;
+ public final Timing chunkProviderTick;
+ public final Timing broadcastChunkUpdates;
+ public final Timing countNaturalMobs;
public final Timing syncChunkLoadTimer;
public final Timing syncChunkLoadDataTimer;
public final Timing syncChunkLoadStructuresTimer;
public final Timing syncChunkLoadPostTimer;
public final Timing syncChunkLoadPopulateTimer;
+ public final Timing chunkAwait;
public final Timing chunkLoadLevelTimer;
public final Timing chunkGeneration;
public final Timing chunkIOStage1;
@@ -50,7 +58,23 @@ public class WorldTimingsHandler {
public final Timing worldSaveLevel;
public final Timing chunkSaveData;
- public final Timing lightingQueueTimer;
+
+ public final Timing miscMobSpawning;
+ public final Timing chunkRangeCheckBig;
+ public final Timing chunkRangeCheckSmall;
+
+ public final Timing poiUnload;
+ public final Timing chunkUnload;
+ public final Timing poiSaveDataSerialization;
+ public final Timing chunkSave;
+ public final Timing chunkSaveOverwriteCheck;
+ public final Timing chunkSaveDataSerialization;
+ public final Timing chunkSaveIOWait;
+ public final Timing chunkUnloadPrepareSave;
+ public final Timing chunkUnloadPOISerialization;
+ public final Timing chunkUnloadDataSave;
+
+ public final Timing playerMobDistanceMapUpdate;
public WorldTimingsHandler(World server) {
String name = server.worldData.getName() +" - ";
@@ -85,6 +109,7 @@ public class WorldTimingsHandler {
syncChunkLoadStructuresTimer = Timings.ofSafe(name + "chunkLoad - recreateStructures");
syncChunkLoadPostTimer = Timings.ofSafe(name + "chunkLoad - Post");
syncChunkLoadPopulateTimer = Timings.ofSafe(name + "chunkLoad - Populate");
+ chunkAwait = Timings.ofSafe(name + "chunkAwait");
chunkLoadLevelTimer = Timings.ofSafe(name + "chunkLoad - Load Level");
chunkGeneration = Timings.ofSafe(name + "chunkGeneration");
chunkIOStage1 = Timings.ofSafe(name + "ChunkIO Stage 1 - DiskIO");
@@ -99,7 +124,30 @@ public class WorldTimingsHandler {
doTick = Timings.ofSafe(name + "doTick");
tickEntities = Timings.ofSafe(name + "tickEntities");
- lightingQueueTimer = Timings.ofSafe(name + "Lighting Queue");
+ chunks = Timings.ofSafe(name + "Chunks");
+ newEntities = Timings.ofSafe(name + "New entity registration");
+ raids = Timings.ofSafe(name + "Raids");
+ chunkProviderTick = Timings.ofSafe(name + "Chunk provider tick");
+ broadcastChunkUpdates = Timings.ofSafe(name + "Broadcast chunk updates");
+ countNaturalMobs = Timings.ofSafe(name + "Count natural mobs");
+
+
+ miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc");
+ chunkRangeCheckBig = Timings.ofSafe(name + "Chunk Tick Range - Big");
+ chunkRangeCheckSmall = Timings.ofSafe(name + "Chunk Tick Range - Small");
+
+ poiUnload = Timings.ofSafe(name + "Chunk unload - POI");
+ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk");
+ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization");
+ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk");
+ chunkSaveOverwriteCheck = Timings.ofSafe(name + "Chunk save - Chunk Overwrite Check");
+ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization");
+ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait");
+ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare");
+ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization");
+ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization");
+
+ playerMobDistanceMapUpdate = Timings.ofSafe(name + "Per Player Mob Spawning - Distance Map Update");
}
public static Timing getTickList(WorldServer worldserver, String timingsType) {
diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
index cc0c74aaa..e257d6b36 100644
--- a/src/main/java/com/destroystokyo/paper/Metrics.java
+++ b/src/main/java/com/destroystokyo/paper/Metrics.java
@@ -31,7 +31,7 @@ public class Metrics {
public static final int B_STATS_VERSION = 1;
// The url to which the data is sent
- private static final String URL = "https://bStats.org/submitData/bukkit"; // Akarin
+ private static final String URL = "https://bStats.org/submitData/server-implementation";
// Should failed requests be logged?
private static boolean logFailedRequests = false;
@@ -103,7 +103,6 @@ public class Metrics {
JSONObject data = new JSONObject();
data.put("pluginName", name); // Append the name of the server software
- data.put("pluginVersion", Metrics.class.getPackage().getImplementationVersion() != null ? Metrics.class.getPackage().getImplementationVersion() : "unknown"); // Akarin
JSONArray customCharts = new JSONArray();
for (CustomChart customChart : charts) {
// Add the data of the custom charts
@@ -124,24 +123,13 @@ public class Metrics {
* @return The server specific data.
*/
private JSONObject getServerData() {
- // Akarin start - Minecraft specific data
- int playerAmount = Bukkit.getOnlinePlayers().size();
- int onlineMode = Bukkit.getOnlineMode() ? 1 : 0;
- String bukkitVersion = org.bukkit.Bukkit.getVersion();
- bukkitVersion = bukkitVersion.substring(bukkitVersion.indexOf("MC: ") + 4, bukkitVersion.length() - 1);
-
- JSONObject data = new JSONObject();
- data.put("playerAmount", playerAmount);
- data.put("onlineMode", onlineMode);
- data.put("bukkitVersion", bukkitVersion);
- // Akarin end
// OS specific data
String osName = System.getProperty("os.name");
String osArch = System.getProperty("os.arch");
String osVersion = System.getProperty("os.version");
int coreCount = Runtime.getRuntime().availableProcessors();
- //JSONObject data = new JSONObject(); // Akarin
+ JSONObject data = new JSONObject();
data.put("serverUUID", serverUUID);
@@ -590,10 +578,8 @@ public class Metrics {
boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
// Only start Metrics, if it's enabled in the config
if (config.getBoolean("enabled", true)) {
- // Akarin start
- Metrics metrics = new Metrics("Torch", serverUUID, logFailedRequests, Bukkit.getLogger());
+ Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
- /*
metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
String minecraftVersion = Bukkit.getVersion();
minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1);
@@ -634,8 +620,6 @@ public class Metrics {
return map;
}));
- */
- // Akarin end
}
}
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index 8e1bda4de..132397b3f 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -1,9 +1,13 @@
package com.destroystokyo.paper;
+import com.destroystokyo.paper.io.SyncLoadFinder;
import com.google.common.base.Functions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.gson.JsonObject;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
import net.minecraft.server.*;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
@@ -18,6 +22,9 @@ import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Player;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.io.StringWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@@ -28,14 +35,14 @@ public class PaperCommand extends Command {
public PaperCommand(String name) {
super(name);
this.description = "Paper related commands";
- this.usageMessage = "/paper [heap | entity | reload | version]";
+ this.usageMessage = "/paper [heap | entity | reload | version | debug | chunkinfo]";
this.setPermission("bukkit.command.paper");
}
@Override
public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException {
if (args.length <= 1)
- return getListMatchingLast(args, "heap", "entity", "reload", "version");
+ return getListMatchingLast(args, "heap", "entity", "reload", "version", "debug", "chunkinfo");
switch (args[0].toLowerCase(Locale.ENGLISH))
{
@@ -45,6 +52,21 @@ public class PaperCommand extends Command {
if (args.length == 3)
return getListMatchingLast(args, EntityTypes.getEntityNameList().stream().map(MinecraftKey::toString).sorted().toArray(String[]::new));
break;
+ case "debug":
+ if (args.length == 2) {
+ return getListMatchingLast(args, "help", "chunks");
+ }
+ break;
+ case "chunkinfo":
+ List worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 2) {
+ return getListMatchingLast(args, worldNames);
+ }
+ break;
}
return Collections.emptyList();
}
@@ -109,6 +131,15 @@ public class PaperCommand extends Command {
case "reload":
doReload(sender);
break;
+ case "debug":
+ doDebug(sender, args);
+ break;
+ case "chunkinfo":
+ doChunkInfo(sender, args);
+ break;
+ case "syncloadinfo":
+ this.doSyncLoadInfo(sender, args);
+ break;
case "ver":
case "version":
Command ver = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version");
@@ -125,6 +156,130 @@ public class PaperCommand extends Command {
return true;
}
+ private void doSyncLoadInfo(CommandSender sender, String[] args) {
+ if (!SyncLoadFinder.ENABLED) {
+ sender.sendMessage(ChatColor.RED + "This command requires the server startup flag '-Dpaper.debug-sync-loads=true' to be set.");
+ return;
+ }
+ File file = new File(new File(new File("."), "debug"),
+ "sync-load-info" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
+ file.getParentFile().mkdirs();
+ sender.sendMessage(ChatColor.GREEN + "Writing sync load info to " + file.toString());
+
+
+ try {
+ final JsonObject data = SyncLoadFinder.serialize();
+
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+ jsonWriter.setLenient(false);
+ Streams.write(data, jsonWriter);
+
+ String fileData = stringWriter.toString();
+
+ try (
+ PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")
+ ) {
+ out.print(fileData);
+ }
+ sender.sendMessage(ChatColor.GREEN + "Successfully written sync load information!");
+ } catch (Throwable thr) {
+ sender.sendMessage(ChatColor.RED + "Failed to write sync load information");
+ thr.printStackTrace();
+ }
+ }
+
+ private void doChunkInfo(CommandSender sender, String[] args) {
+ List worlds;
+ if (args.length < 2 || args[1].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length - 1);
+ for (int i = 1; i < args.length; ++i) {
+ org.bukkit.World world = Bukkit.getWorld(args[i]);
+ if (world == null) {
+ sender.sendMessage(ChatColor.RED + "World '" + args[i] + "' is invalid");
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ for (org.bukkit.World bukkitWorld : worlds) {
+ WorldServer world = ((CraftWorld)bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int border = 0;
+ int ticking = 0;
+ int entityTicking = 0;
+
+ for (PlayerChunk chunk : world.getChunkProvider().playerChunkMap.updatingChunks.values()) {
+ if (chunk.getFullChunkIfCached() == null) {
+ continue;
+ }
+
+ ++total;
+
+ PlayerChunk.State state = PlayerChunk.getChunkState(chunk.getTicketLevel());
+
+ switch (state) {
+ case INACCESSIBLE:
+ ++inactive;
+ continue;
+ case BORDER:
+ ++border;
+ continue;
+ case TICKING:
+ ++ticking;
+ continue;
+ case ENTITY_TICKING:
+ ++entityTicking;
+ continue;
+ }
+ }
+
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + bukkitWorld.getName() + ChatColor.DARK_AQUA + ":");
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + total + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
+ + inactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + border + ChatColor.BLUE + " Ticking: "
+ + ChatColor.DARK_AQUA + ticking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + entityTicking);
+ }
+ }
+
+ private void doDebug(CommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+
+ String debugType = args[1].toLowerCase(Locale.ENGLISH);
+ switch (debugType) {
+ case "chunks":
+ if (args.length >= 3 && args[2].toLowerCase(Locale.ENGLISH).equals("help")) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug chunks to dump loaded chunk information to a file");
+ break;
+ }
+ File file = new File(new File(new File("."), "debug"),
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
+ sender.sendMessage(ChatColor.GREEN + "Writing chunk information dump to " + file.toString());
+ try {
+ MCUtil.dumpChunks(file);
+ sender.sendMessage(ChatColor.GREEN + "Successfully written chunk information!");
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(ChatColor.RED + "Failed to dump chunk information, see console");
+ }
+
+ break;
+ case "help":
+ // fall through to default
+ default:
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+ }
+
/*
* Ported from MinecraftForge - author: LexManos - License: LGPLv2.1
*/
@@ -173,8 +328,11 @@ public class PaperCommand extends Command {
}
WorldServer world = ((CraftWorld) Bukkit.getWorld(worldName)).getHandle();
- List entities = world.entityList;
+ Collection entities = world.entitiesById.values();
entities.forEach(e -> {
+ if (!e.isChunkLoaded()) {
+ return;
+ }
MinecraftKey key = e.getMinecraftKey();
if (e.shouldBeRemoved) return; // Paper
@@ -216,11 +374,14 @@ public class PaperCommand extends Command {
}
private void dumpHeap(CommandSender sender) {
- File file = new File(new File(new File("."), "dumps"),
- "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + "-server.hprof");
- Command.broadcastCommandMessage(sender, ChatColor.YELLOW + "Writing JVM heap data to " + file);
- if (CraftServer.dumpHeap(file)) {
- Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Heap dump complete");
+ java.nio.file.Path dir = java.nio.file.Paths.get("./dumps");
+ String name = "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now());
+
+ Command.broadcastCommandMessage(sender, ChatColor.YELLOW + "Writing JVM heap data...");
+
+ java.nio.file.Path file = CraftServer.dumpHeap(dir, name);
+ if (file != null) {
+ Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Heap dump saved to " + file);
} else {
Command.broadcastCommandMessage(sender, ChatColor.RED + "Failed to write heap dump, see sever log for details");
}
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
index 9f240c35d..93c0c422d 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -1,5 +1,6 @@
package com.destroystokyo.paper;
+import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
@@ -71,8 +72,8 @@ public class PaperConfig {
commands = new HashMap();
commands.put("paper", new PaperCommand("paper"));
- version = getInt("config-version", 18);
- set("config-version", 18);
+ version = getInt("config-version", 20);
+ set("config-version", 20);
readConfig(PaperConfig.class, null);
}
@@ -199,13 +200,7 @@ public class PaperConfig {
return config.getString(path, config.getString(path));
}
- public static int maxTickMsLostLightQueue;
- private static void lightQueue() {
- int badSetting = config.getInt("queue-light-updates-max-loss", 10);
- config.set("queue-light-updates-max-loss", null);
- maxTickMsLostLightQueue = getInt("settings.queue-light-updates-max-loss", badSetting);
- }
-
+ public static String timingsServerName;
private static void timings() {
boolean timings = getBoolean("timings.enabled", true);
boolean verboseTimings = getBoolean("timings.verbose", true);
@@ -213,6 +208,7 @@ public class PaperConfig {
TimingsManager.hiddenConfigs = getList("timings.hidden-config-entries", Lists.newArrayList("database", "settings.bungeecord-addresses"));
int timingHistoryInterval = getInt("timings.history-interval", 300);
int timingHistoryLength = getInt("timings.history-length", 3600);
+ timingsServerName = getString("timings.server-name", "Unknown Server");
Timings.setVerboseTimingsEnabled(verboseTimings);
@@ -223,13 +219,8 @@ public class PaperConfig {
log("Timings: " + timings +
" - Verbose: " + verboseTimings +
" - Interval: " + timeSummary(Timings.getHistoryInterval() / 20) +
- " - Length: " + timeSummary(Timings.getHistoryLength() / 20));
- }
-
- public static boolean enableFileIOThreadSleep;
- private static void enableFileIOThreadSleep() {
- enableFileIOThreadSleep = getBoolean("settings.sleep-between-chunk-saves", false);
- if (enableFileIOThreadSleep) Bukkit.getLogger().info("Enabled sleeping between chunk saves, beware of memory issues");
+ " - Length: " + timeSummary(Timings.getHistoryLength() / 20) +
+ " - Server Name: " + timingsServerName);
}
public static boolean loadPermsBeforePlugins = true;
@@ -239,7 +230,7 @@ public class PaperConfig {
public static int regionFileCacheSize = 256;
private static void regionFileCacheSize() {
- regionFileCacheSize = getInt("settings.region-file-cache-size", 256);
+ regionFileCacheSize = Math.max(getInt("settings.region-file-cache-size", 256), 4);
}
public static boolean enablePlayerCollisions = true;
@@ -277,17 +268,6 @@ public class PaperConfig {
flyingKickVehicleMessage = getString("messages.kick.flying-vehicle", flyingKickVehicleMessage);
}
- public static int playerAutoSaveRate = -1;
- public static int maxPlayerAutoSavePerTick = 10;
- private static void playerAutoSaveRate() {
- playerAutoSaveRate = getInt("settings.player-auto-save-rate", -1);
- maxPlayerAutoSavePerTick = getInt("settings.max-player-auto-save-per-tick", -1);
- if (maxPlayerAutoSavePerTick == -1) { // -1 Automatic / "Recommended"
- // 10 should be safe for everyone unless your mass spamming player auto save
- maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20;
- }
- }
-
public static boolean suggestPlayersWhenNullTabCompletions = true;
private static void suggestPlayersWhenNull() {
suggestPlayersWhenNullTabCompletions = getBoolean("settings.suggest-player-names-when-null-tab-completions", suggestPlayersWhenNullTabCompletions);
@@ -389,59 +369,6 @@ public class PaperConfig {
}
}
- public static boolean asyncChunks = false;
- public static boolean asyncChunkGeneration = true;
- public static boolean asyncChunkGenThreadPerWorld = true;
- public static int asyncChunkLoadThreads = -1;
- private static void asyncChunks() {
- if (version < 15) {
- boolean enabled = config.getBoolean("settings.async-chunks", true);
- ConfigurationSection section = config.createSection("settings.async-chunks");
- section.set("enable", enabled);
- section.set("load-threads", -1);
- section.set("generation", true);
- section.set("thread-per-world-generation", true);
- }
-
- asyncChunks = getBoolean("settings.async-chunks.enable", true);
- asyncChunkGeneration = getBoolean("settings.async-chunks.generation", true);
- asyncChunkGenThreadPerWorld = getBoolean("settings.async-chunks.thread-per-world-generation", true);
- asyncChunkLoadThreads = getInt("settings.async-chunks.load-threads", -1);
- if (asyncChunkLoadThreads <= 0) {
- asyncChunkLoadThreads = (int) Math.min(Integer.getInteger("paper.maxChunkThreads", 8), Runtime.getRuntime().availableProcessors() * 1.5);
- }
-
- // Let Shared Host set some limits
- String sharedHostEnvGen = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_GEN");
- String sharedHostEnvLoad = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_LOAD");
- if ("1".equals(sharedHostEnvGen)) {
- log("Async Chunks - Generation: Your host has requested to use a single thread world generation");
- asyncChunkGenThreadPerWorld = false;
- } else if ("2".equals(sharedHostEnvGen)) {
- log("Async Chunks - Generation: Your host has disabled async world generation - You will experience lag from world generation");
- asyncChunkGeneration = false;
- }
-
- if (sharedHostEnvLoad != null) {
- try {
- asyncChunkLoadThreads = Math.max(1, Math.min(asyncChunkLoadThreads, Integer.parseInt(sharedHostEnvLoad)));
- } catch (NumberFormatException ignored) {}
- }
-
- if (!asyncChunks) {
- log("Async Chunks: Disabled - Chunks will be managed synchronosuly, and will cause tremendous lag.");
- } else {
- log("Async Chunks: Enabled - Chunks will be loaded much faster, without lag.");
- if (!asyncChunkGeneration) {
- log("Async Chunks - Generation: Disabled - Chunks will be generated synchronosuly, and will cause tremendous lag.");
- } else if (asyncChunkGenThreadPerWorld) {
- log("Async Chunks - Generation: Enabled - Chunks will be generated much faster, without lag.");
- } else {
- log("Async Chunks - Generation: Enabled (Single Thread) - Chunks will be generated much faster, without lag.");
- }
- }
- }
-
public static boolean velocitySupport;
public static boolean velocityOnlineMode;
public static byte[] velocitySecretKey;
@@ -462,4 +389,64 @@ public class PaperConfig {
maxBookPageSize = getInt("settings.book-size.page-max", maxBookPageSize);
maxBookTotalSizeMultiplier = getDouble("settings.book-size.total-multiplier", maxBookTotalSizeMultiplier);
}
+
+ public static boolean asyncChunks = false;
+ //public static boolean asyncChunkGeneration = true; // Leave out for now until we can control this
+ //public static boolean asyncChunkGenThreadPerWorld = true; // Leave out for now until we can control this
+ public static int asyncChunkLoadThreads = -1;
+ private static void asyncChunks() {
+ if (version < 15) {
+ boolean enabled = config.getBoolean("settings.async-chunks", true);
+ ConfigurationSection section = config.createSection("settings.async-chunks");
+ section.set("enable", enabled);
+ section.set("load-threads", -1);
+ section.set("generation", true);
+ section.set("thread-per-world-generation", true);
+ }
+
+ // TODO load threads now control async chunk save for unloading chunks, look into renaming this?
+
+ asyncChunks = getBoolean("settings.async-chunks.enable", true);
+ //asyncChunkGeneration = getBoolean("settings.async-chunks.generation", true); // Leave out for now until we can control this
+ //asyncChunkGenThreadPerWorld = getBoolean("settings.async-chunks.thread-per-world-generation", true); // Leave out for now until we can control this
+ asyncChunkLoadThreads = getInt("settings.async-chunks.load-threads", -1);
+ if (asyncChunkLoadThreads <= 0) {
+ asyncChunkLoadThreads = (int) Math.min(Integer.getInteger("paper.maxChunkThreads", 8), Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
+ }
+
+ // Let Shared Host set some limits
+ String sharedHostEnvGen = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_GEN");
+ String sharedHostEnvLoad = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_LOAD");
+ /* Ignore temporarily - we cannot control the gen threads (for now)
+ if ("1".equals(sharedHostEnvGen)) {
+ log("Async Chunks - Generation: Your host has requested to use a single thread world generation");
+ asyncChunkGenThreadPerWorld = false;
+ } else if ("2".equals(sharedHostEnvGen)) {
+ log("Async Chunks - Generation: Your host has disabled async world generation - You will experience lag from world generation");
+ asyncChunkGeneration = false;
+ }
+ */
+
+ if (sharedHostEnvLoad != null) {
+ try {
+ asyncChunkLoadThreads = Math.max(1, Math.min(asyncChunkLoadThreads, Integer.parseInt(sharedHostEnvLoad)));
+ } catch (NumberFormatException ignored) {}
+ }
+
+ if (!asyncChunks) {
+ log("Async Chunks: Disabled - Chunks will be managed synchronosuly, and will cause tremendous lag.");
+ } else {
+ ChunkTaskManager.initGlobalLoadThreads(asyncChunkLoadThreads);
+ log("Async Chunks: Enabled - Chunks will be loaded much faster, without lag.");
+ /* Ignore temporarily - we cannot control the gen threads (for now)
+ if (!asyncChunkGeneration) {
+ log("Async Chunks - Generation: Disabled - Chunks will be generated synchronosuly, and will cause tremendous lag.");
+ } else if (asyncChunkGenThreadPerWorld) {
+ log("Async Chunks - Generation: Enabled - Chunks will be generated much faster, without lag.");
+ } else {
+ log("Async Chunks - Generation: Enabled (Single Thread) - Chunks will be generated much faster, without lag.");
+ }
+ */
+ }
+ }
}
diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
new file mode 100644
index 000000000..513cddb9b
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
@@ -0,0 +1,133 @@
+package com.destroystokyo.paper;
+
+import com.destroystokyo.paper.util.VersionFetcher;
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import com.google.gson.*;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class PaperVersionFetcher implements VersionFetcher {
+ private static final java.util.regex.Pattern VER_PATTERN = java.util.regex.Pattern.compile("^([0-9\\.]*)\\-.*R"); // R is an anchor, will always give '-R' at end
+ private static final String GITHUB_BRANCH_NAME = "ver/1.14";
+ private static @Nullable String mcVer;
+
+ @Override
+ public long getCacheTime() {
+ return 720000;
+ }
+
+ @Nonnull
+ @Override
+ public String getVersionMessage(@Nonnull String serverVersion) {
+ String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]");
+ String updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]);
+ String history = getHistory();
+
+ return history != null ? history + "\n" + updateMessage : updateMessage;
+ }
+
+ private static @Nullable String getMinecraftVersion() {
+ if (mcVer == null) {
+ java.util.regex.Matcher matcher = VER_PATTERN.matcher(org.bukkit.Bukkit.getBukkitVersion());
+ if (matcher.find()) {
+ String result = matcher.group();
+ mcVer = result.substring(0, result.length() - 2); // strip 'R' anchor and trailing '-'
+ } else {
+ org.bukkit.Bukkit.getLogger().warning("Unable to match version to pattern! Report to PaperMC!");
+ org.bukkit.Bukkit.getLogger().warning("Pattern: " + VER_PATTERN.toString());
+ org.bukkit.Bukkit.getLogger().warning("Version: " + org.bukkit.Bukkit.getBukkitVersion());
+ }
+ }
+
+ return mcVer;
+ }
+
+ private static String 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) {
+ versionInfo = versionInfo.replace("\"", "");
+ distance = fetchDistanceFromGitHub(repo, branch, versionInfo);
+ }
+
+ switch (distance) {
+ case -1:
+ return "Error obtaining version information";
+ case 0:
+ return "You are running the latest version";
+ case -2:
+ return "Unknown version";
+ default:
+ return "You are " + distance + " version(s) behind";
+ }
+ }
+
+ private static int fetchDistanceFromSiteApi(int jenkinsBuild, @Nullable String siteApiVersion) {
+ if (siteApiVersion == null) { return -1; }
+ try {
+ try (BufferedReader reader = Resources.asCharSource(
+ new URL("https://papermc.io/api/v1/paper/" + siteApiVersion + "/latest"),
+ Charsets.UTF_8
+ ).openBufferedStream()) {
+ JsonObject json = new Gson().fromJson(reader, JsonObject.class);
+ int latest = json.get("build").getAsInt();
+ return latest - jenkinsBuild;
+ } catch (JsonSyntaxException ex) {
+ ex.printStackTrace();
+ return -1;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return -1;
+ }
+ }
+
+ // Contributed by Techcable in GH-65
+ private static int fetchDistanceFromGitHub(@Nonnull String repo, @Nonnull String branch, @Nonnull String hash) {
+ try {
+ HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/" + repo + "/compare/" + branch + "..." + hash).openConnection();
+ connection.connect();
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return -2; // Unknown commit
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
+ JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
+ String status = obj.get("status").getAsString();
+ switch (status) {
+ case "identical":
+ return 0;
+ case "behind":
+ return obj.get("behind_by").getAsInt();
+ default:
+ return -1;
+ }
+ } catch (JsonSyntaxException | NumberFormatException e) {
+ e.printStackTrace();
+ return -1;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return -1;
+ }
+ }
+
+ @Nullable
+ private String getHistory() {
+ final VersionHistoryManager.VersionData data = VersionHistoryManager.INSTANCE.getVersionData();
+ if (data == null) {
+ return null;
+ }
+
+ final String oldVersion = data.getOldVersion();
+ if (oldVersion == null) {
+ return null;
+ }
+
+ return "Previous version: " + oldVersion;
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index bfd690ecc..3e5571f7d 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -1,12 +1,17 @@
package com.destroystokyo.paper;
import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray.ChunkEdgeMode;
import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray.EngineMode;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.spigotmc.SpigotWorldConfig;
@@ -78,10 +83,15 @@ public class PaperWorldConfig {
}
- public double babyZombieMovementSpeed;
- private void babyZombieMovementSpeed() {
- babyZombieMovementSpeed = getDouble("baby-zombie-movement-speed", 0.5D); // Player moves at 0.1F, for reference
- log("Baby zombies will move at the speed of " + babyZombieMovementSpeed);
+ public double babyZombieMovementModifier;
+ private void babyZombieMovementModifier() {
+ babyZombieMovementModifier = getDouble("baby-zombie-movement-modifier", 0.5D);
+ if (PaperConfig.version < 20) {
+ babyZombieMovementModifier = getDouble("baby-zombie-movement-speed", 0.5D);
+ set("baby-zombie-movement-modifier", babyZombieMovementModifier);
+ }
+
+ log("Baby zombies will move at the speed of " + babyZombieMovementModifier);
}
public int fishingMinTicks;
@@ -144,14 +154,6 @@ public class PaperWorldConfig {
}
}
- public boolean queueLightUpdates;
- private void queueLightUpdates() {
- queueLightUpdates = getBoolean("queue-light-updates", false);
- log("Lighting Queue enabled: " + queueLightUpdates);
- log("Warning: This feature may help reduce TPS loss from light, but comes at the cost of buggy light data");
- log("We are working to improve this feature.");
- }
-
public boolean disableEndCredits;
private void disableEndCredits() {
disableEndCredits = getBoolean("game-mechanics.disable-end-credits", false);
@@ -269,11 +271,6 @@ public class PaperWorldConfig {
}
}
- public boolean firePhysicsEventForRedstone = false;
- private void firePhysicsEventForRedstone() {
- firePhysicsEventForRedstone = getBoolean("fire-physics-event-for-redstone", firePhysicsEventForRedstone);
- }
-
public int fixedInhabitedTime;
private void fixedInhabitedTime() {
if (PaperConfig.version < 16) {
@@ -290,12 +287,6 @@ public class PaperWorldConfig {
log("Grass Spread Tick Rate: " + grassUpdateRate);
}
- public short keepLoadedRange;
- private void keepLoadedRange() {
- keepLoadedRange = (short) (getInt("keep-spawn-loaded-range", Math.min(spigotConfig.viewDistance, 8)) * 16);
- log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16));
- }
-
public boolean useVanillaScoreboardColoring;
private void useVanillaScoreboardColoring() {
useVanillaScoreboardColoring = getBoolean("use-vanilla-world-scoreboard-name-coloring", false);
@@ -343,40 +334,6 @@ public class PaperWorldConfig {
log("Prevent TNT from moving in water: " + preventTntFromMovingInWater);
}
- public long delayChunkUnloadsBy;
- private void delayChunkUnloadsBy() {
- delayChunkUnloadsBy = PaperConfig.getSeconds(getString("delay-chunk-unloads-by", "10s"));
- if (delayChunkUnloadsBy > 0) {
- log("Delaying chunk unloads by " + delayChunkUnloadsBy + " seconds");
- delayChunkUnloadsBy *= 1000;
- }
- }
-
- public boolean skipEntityTickingInChunksScheduledForUnload = true;
- private void skipEntityTickingInChunksScheduledForUnload() {
- skipEntityTickingInChunksScheduledForUnload = getBoolean("skip-entity-ticking-in-chunks-scheduled-for-unload", skipEntityTickingInChunksScheduledForUnload);
- }
-
- public int autoSavePeriod = -1;
- private void autoSavePeriod() {
- autoSavePeriod = getInt("auto-save-interval", -1);
- if (autoSavePeriod > 0) {
- log("Auto Save Interval: " +autoSavePeriod + " (" + (autoSavePeriod / 20) + "s)");
- } else if (autoSavePeriod < 0) {
- autoSavePeriod = MinecraftServer.getServer().autosavePeriod;
- }
- }
-
- public int maxAutoSaveChunksPerTick = 24;
- private void maxAutoSaveChunksPerTick() {
- maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24);
- }
-
- public int queueSizeAutoSaveThreshold = 50;
- private void queueSizeAutoSaveThreshold() {
- queueSizeAutoSaveThreshold = getInt("save-queue-limit-for-auto-save", 50);
- }
-
public boolean removeCorruptTEs = false;
private void removeCorruptTEs() {
removeCorruptTEs = getBoolean("remove-corrupt-tile-entities", false);
@@ -429,26 +386,6 @@ public class PaperWorldConfig {
log("Experience Merge Max Value: " + expMergeMaxValue);
}
- public int maxChunkSendsPerTick = 81;
- private void maxChunkSendsPerTick() {
- maxChunkSendsPerTick = getInt("max-chunk-sends-per-tick", maxChunkSendsPerTick);
- if (maxChunkSendsPerTick <= 0) {
- maxChunkSendsPerTick = 81;
- }
- log("Max Chunk Sends Per Tick: " + maxChunkSendsPerTick);
- }
-
- public int maxChunkGensPerTick = 10;
- private void maxChunkGensPerTick() {
- maxChunkGensPerTick = getInt("max-chunk-gens-per-tick", maxChunkGensPerTick);
- if (maxChunkGensPerTick <= 0) {
- maxChunkGensPerTick = Integer.MAX_VALUE;
- log("Max Chunk Gens Per Tick: Unlimited (NOT RECOMMENDED)");
- } else {
- log("Max Chunk Gens Per Tick: " + maxChunkGensPerTick);
- }
- }
-
public double squidMaxSpawnHeight;
private void squidMaxSpawnHeight() {
squidMaxSpawnHeight = getDouble("squid-spawn-height.maximum", 0.0D);
@@ -474,14 +411,6 @@ public class PaperWorldConfig {
log("Disable Unloaded Chunk Enderpearl Exploit: " + (disableEnderpearlExploit ? "enabled" : "disabled"));
}
- public boolean villagesLoadChunks = false;
- private void villagesLoadChunks() {
- villagesLoadChunks = getBoolean("game-mechanics.villages-load-chunks", false);
- if (villagesLoadChunks) {
- log("Villages can load chunks - Warning this can cause intense TPS loss. Strongly consider disabling this.");
- }
- }
-
public int shieldBlockingDelay = 5;
private void shieldBlockingDelay() {
shieldBlockingDelay = getInt("game-mechanics.shield-blocking-delay", 5);
@@ -509,6 +438,27 @@ public class PaperWorldConfig {
log("Water over lava flow speed: " + waterOverLavaFlowSpeed);
}
+ public boolean armorStandTick = true;
+ private void armorStandTick() {
+ this.armorStandTick = this.getBoolean("armor-stands-tick", this.armorStandTick);
+ log("ArmorStand ticking is " + (this.armorStandTick ? "enabled" : "disabled") + " by default");
+ }
+
+ public boolean preventMovingIntoUnloadedChunks = false;
+ private void preventMovingIntoUnloadedChunks() {
+ preventMovingIntoUnloadedChunks = getBoolean("prevent-moving-into-unloaded-chunks", false);
+ }
+
+ public boolean useEigencraftRedstone = false;
+ private void useEigencraftRedstone() {
+ useEigencraftRedstone = this.getBoolean("use-faster-eigencraft-redstone", false);
+ if (useEigencraftRedstone) {
+ log("Using Eigencraft redstone algorithm by theosib.");
+ } else {
+ log("Using vanilla redstone algorithm.");
+ }
+ }
+
public enum DuplicateUUIDMode {
SAFE_REGEN, DELETE, NOTHING, WARN
}
@@ -548,10 +498,35 @@ public class PaperWorldConfig {
}
}
- public boolean armorStandTick = true;
- private void armorStandTick() {
- this.armorStandTick = this.getBoolean("armor-stands-tick", this.armorStandTick);
- log("ArmorStand ticking is " + (this.armorStandTick ? "enabled" : "disabled") + " by default");
+ public short keepLoadedRange;
+ private void keepLoadedRange() {
+ keepLoadedRange = (short) (getInt("keep-spawn-loaded-range", Math.min(spigotConfig.viewDistance, 10)) * 16);
+ log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16));
+ }
+
+ public int autoSavePeriod = -1;
+ private void autoSavePeriod() {
+ autoSavePeriod = getInt("auto-save-interval", -1);
+ if (autoSavePeriod > 0) {
+ log("Auto Save Interval: " +autoSavePeriod + " (" + (autoSavePeriod / 20) + "s)");
+ } else if (autoSavePeriod < 0) {
+ autoSavePeriod = net.minecraft.server.MinecraftServer.getServer().autosavePeriod;
+ }
+ }
+
+ public int maxAutoSaveChunksPerTick = 24;
+ private void maxAutoSaveChunksPerTick() {
+ maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24);
+ }
+
+ public boolean countAllMobsForSpawning = false;
+ private void countAllMobsForSpawning() {
+ countAllMobsForSpawning = getBoolean("count-all-mobs-for-spawning", false);
+ if (countAllMobsForSpawning) {
+ log("Counting all mobs for spawning. Mob farms may reduce natural spawns elsewhere in world.");
+ } else {
+ log("Using improved mob spawn limits (Only Natural Spawns impact spawn limits for more natural spawns)");
+ }
}
public boolean antiXray;
@@ -579,33 +554,85 @@ public class PaperWorldConfig {
maxChunkSectionIndex = getInt("anti-xray.max-chunk-section-index", 3);
maxChunkSectionIndex = maxChunkSectionIndex > 15 ? 15 : maxChunkSectionIndex;
updateRadius = getInt("anti-xray.update-radius", 2);
- hiddenBlocks = getList("anti-xray.hidden-blocks", Arrays.asList("gold_ore", "iron_ore", "coal_ore", "lapis_ore", "mossy_cobblestone", "obsidian", "chest", "diamond_ore", "redstone_ore", "lit_redstone_ore", "clay", "emerald_ore", "ender_chest"));
- replacementBlocks = getList("anti-xray.replacement-blocks", Arrays.asList("stone", "planks"));
+ hiddenBlocks = getList("anti-xray.hidden-blocks", Arrays.asList("gold_ore", "iron_ore", "coal_ore", "lapis_ore", "mossy_cobblestone", "obsidian", "chest", "diamond_ore", "redstone_ore", "clay", "emerald_ore", "ender_chest"));
+ replacementBlocks = getList("anti-xray.replacement-blocks", Arrays.asList("stone", "oak_planks"));
+ if (PaperConfig.version < 19) {
+ hiddenBlocks.remove("lit_redstone_ore");
+ int index = replacementBlocks.indexOf("planks");
+ if (index != -1) {
+ replacementBlocks.set(index, "oak_planks");
+ }
+ set("anti-xray.hidden-blocks", hiddenBlocks);
+ set("anti-xray.replacement-blocks", replacementBlocks);
+ }
log("Anti-Xray: " + (antiXray ? "enabled" : "disabled") + " / Engine Mode: " + engineMode.getDescription() + " / Chunk Edge Mode: " + chunkEdgeMode.getDescription() + " / Up to " + ((maxChunkSectionIndex + 1) * 16) + " blocks / Update Radius: " + updateRadius);
}
- public boolean preventMovingIntoUnloadedChunks = false;
- private void preventMovingIntoUnloadedChunks() {
- preventMovingIntoUnloadedChunks = getBoolean("prevent-moving-into-unloaded-chunks", false);
+ public boolean disableRelativeProjectileVelocity;
+ private void disableRelativeProjectileVelocity() {
+ disableRelativeProjectileVelocity = getBoolean("game-mechanics.disable-relative-projectile-velocity", false);
}
- public boolean useEigencraftRedstone = false;
- private void useEigencraftRedstone() {
- useEigencraftRedstone = this.getBoolean("use-faster-eigencraft-redstone", false);
- if (useEigencraftRedstone) {
- log("Using Eigencraft redstone algorithm by theosib.");
- } else {
- log("Using vanilla redstone algorithm.");
+ public boolean fixZeroTickInstantGrowFarms = true;
+ private void fixZeroTickInstantGrowFarms() {
+ fixZeroTickInstantGrowFarms = getBoolean("fix-zero-tick-instant-grow-farms", fixZeroTickInstantGrowFarms);
+ }
+
+ public boolean altItemDespawnRateEnabled;
+ public Map altItemDespawnRateMap;
+ private void altItemDespawnRate() {
+ String path = "alt-item-despawn-rate";
+
+ altItemDespawnRateEnabled = getBoolean(path + ".enabled", false);
+
+ Map altItemDespawnRateMapDefault = new EnumMap<>(Material.class);
+ altItemDespawnRateMapDefault.put(Material.COBBLESTONE, 300);
+ for (Material key : altItemDespawnRateMapDefault.keySet()) {
+ config.addDefault("world-settings.default." + path + ".items." + key, altItemDespawnRateMapDefault.get(key));
+ }
+
+ Map rawMap = new HashMap<>();
+ try {
+ ConfigurationSection mapSection = config.getConfigurationSection("world-settings." + worldName + "." + path + ".items");
+ if (mapSection == null) {
+ mapSection = config.getConfigurationSection("world-settings.default." + path + ".items");
+ }
+ for (String key : mapSection.getKeys(false)) {
+ int val = mapSection.getInt(key);
+ rawMap.put(key, val);
+ }
+ }
+ catch (Exception e) {
+ logError("alt-item-despawn-rate was malformatted");
+ altItemDespawnRateEnabled = false;
+ }
+
+ altItemDespawnRateMap = new EnumMap<>(Material.class);
+ if (!altItemDespawnRateEnabled) {
+ return;
+ }
+
+ for(String key : rawMap.keySet()) {
+ try {
+ altItemDespawnRateMap.put(Material.valueOf(key), rawMap.get(key));
+ } catch (Exception e) {
+ logError("Could not add item " + key + " to altItemDespawnRateMap: " + e.getMessage());
+ }
+ }
+ if(altItemDespawnRateEnabled) {
+ for(Material key : altItemDespawnRateMap.keySet()) {
+ log("Alternative item despawn rate of " + key + ": " + altItemDespawnRateMap.get(key));
+ }
}
}
- public boolean countAllMobsForSpawning = false;
- private void countAllMobsForSpawning() {
- countAllMobsForSpawning = getBoolean("count-all-mobs-for-spawning", false);
- if (countAllMobsForSpawning) {
- log("Counting all mobs for spawning. Mob farms may reduce natural spawns elsewhere in world.");
- } else {
- log("Using improved mob spawn limits (Only Natural Spawns impact spawn limits for more natural spawns)");
- }
+ public boolean perPlayerMobSpawns = false;
+ private void perPlayerMobSpawns() {
+ perPlayerMobSpawns = getBoolean("per-player-mob-spawns", false);
+ }
+
+ public boolean generateFlatBedrock;
+ private void generatorSettings() {
+ generateFlatBedrock = getBoolean("generator-settings.flat-bedrock", false);
}
}
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldEntityList.java b/src/main/java/com/destroystokyo/paper/PaperWorldEntityList.java
deleted file mode 100644
index a5a63f800..000000000
--- a/src/main/java/com/destroystokyo/paper/PaperWorldEntityList.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.destroystokyo.paper;
-
-import net.minecraft.server.Entity;
-import net.minecraft.server.EntityInsentient;
-import net.minecraft.server.EnumCreatureType;
-import net.minecraft.server.IAnimal;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.World;
-import net.minecraft.server.WorldServer;
-import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class PaperWorldEntityList extends ArrayList {
-
- private final WorldServer world;
- private final int[] entityCounts = new int[EnumCreatureType.values().length];
-
-
- public PaperWorldEntityList(World world) {
- this.world = (WorldServer) world;
- }
-
- @Override
- public boolean addAll(Collection extends Entity> c) {
- for (Entity e : c) {
- updateEntityCount(e, 1);
- }
-
- return super.addAll(c);
- }
-
- @Override
- public boolean removeAll(Collection> c) {
- for (Object e : c) {
- if (e instanceof Entity && ((Entity) e).getWorld() == world) {
- updateEntityCount((Entity) e, -1);
- }
- }
-
- return super.removeAll(c);
- }
-
- @Override
- public boolean add(Entity e) {
- updateEntityCount(e, 1);
-
- return super.add(e);
- }
-
- @Override
- public Entity remove(int index) {
- guard();
- Entity entity = super.remove(index);
- if (entity != null) updateEntityCount(entity, -1);
- return entity;
- }
-
- @Override
- public boolean remove(Object o) {
- guard();
- if (super.remove(o)) {
- updateEntityCount((Entity) o, -1);
- return true;
- }
- return false;
- }
-
- private void guard() {
- if (world.guardEntityList) {
- throw new java.util.ConcurrentModificationException();
- }
- }
-
- public int getCreatureCount(EnumCreatureType type) {
- return entityCounts[type.ordinal()];
- }
-
- private void updateEntityCount(EnumCreatureType type, int amt) {
- int count = entityCounts[type.ordinal()];
-
- count += amt;
-
- if (count < 0) {
- MinecraftServer.LOGGER.error("Paper - Entity count cache has gone negative");
- count = 0;
- }
-
- entityCounts[type.ordinal()] = count;
- }
-
- public void updateEntityCount(Entity entity, int amt) {
- // Only count natural spawns so that mob
- if (!(entity instanceof IAnimal) || (
- !world.paperConfig.countAllMobsForSpawning &&
- entity.spawnReason != SpawnReason.NATURAL &&
- entity.spawnReason != SpawnReason.CHUNK_GEN
- )) return;
-
- if (entity instanceof EntityInsentient) {
- EntityInsentient entityinsentient = (EntityInsentient) entity;
- if (amt > 0 && entityinsentient.isTypeNotPersistent() && entityinsentient.isPersistent()) {
- return;
- }
- }
- if (amt < 0) {
- if (!entity.hasBeenCounted) {
- return;
- }
- // Only remove once, we remove from if the entity list is guarded, but may be called later
- entity.hasBeenCounted = false;
- } else {
- if (entity.hasBeenCounted) {
- return;
- }
- entity.hasBeenCounted = true;
- }
-
- for (EnumCreatureType type : EnumCreatureType.values()) {
- if (type.matches(entity)) {
- updateEntityCount(type, amt);
- break;
- }
- }
- }
-}
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldMap.java b/src/main/java/com/destroystokyo/paper/PaperWorldMap.java
index 449f620a3..6bb2f98b4 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldMap.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldMap.java
@@ -12,11 +12,10 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.CopyOnWriteArrayList;
public class PaperWorldMap extends HashMap {
- private final List worlds = new CopyOnWriteArrayList<>(); // Akarin
- private final List worldsIterable = new CopyOnWriteArrayList() { // Akarin
+ private final List worlds = new ArrayList<>();
+ private final List worldsIterable = new ArrayList() {
@Override
public Iterator iterator() {
Iterator iterator = super.iterator();
@@ -36,7 +35,7 @@ public class PaperWorldMap extends HashMap {
@Override
public void remove() {
- worlds.set(last.dimension.getDimensionID()+1, null);
+ worlds.set(last.worldProvider.getDimensionManager().getDimensionID() + 1, null);
}
};
}
@@ -116,7 +115,7 @@ public class PaperWorldMap extends HashMap {
@Override
public boolean containsValue(Object value) {
- return value instanceof WorldServer && get(((WorldServer) value).dimension) != null;
+ return value instanceof WorldServer && get(((WorldServer) value).worldProvider.getDimensionManager()) != null;
}
@Nonnull
@@ -135,7 +134,7 @@ public class PaperWorldMap extends HashMap {
@Override
public DimensionManager next() {
- return iterator.next().dimension;
+ return iterator.next().worldProvider.getDimensionManager();
}
@Override
@@ -173,7 +172,7 @@ public class PaperWorldMap extends HashMap {
@Override
public Entry next() {
WorldServer entry = iterator.next();
- return new SimpleEntry<>(entry.dimension, entry);
+ return new SimpleEntry<>(entry.worldProvider.getDimensionManager(), entry);
}
@Override
diff --git a/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
new file mode 100644
index 000000000..aac3f66cb
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
@@ -0,0 +1,145 @@
+package com.destroystokyo.paper;
+
+import com.google.common.base.MoreObjects;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.bukkit.Bukkit;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public enum VersionHistoryManager {
+ INSTANCE;
+
+ private final Gson gson = new Gson();
+
+ private final Logger logger = Bukkit.getLogger();
+
+ private VersionData currentData = null;
+
+ VersionHistoryManager() {
+ final Path path = Paths.get("version_history.json");
+
+ if (Files.exists(path)) {
+ // Basic file santiy checks
+ if (!Files.isRegularFile(path)) {
+ if (Files.isDirectory(path)) {
+ logger.severe(path + " is a directory, cannot be used for version history");
+ } else {
+ logger.severe(path + " is not a regular file, cannot be used for version history");
+ }
+ // We can't continue
+ return;
+ }
+
+ try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ currentData = gson.fromJson(reader, VersionData.class);
+ } catch (final IOException e) {
+ logger.log(Level.SEVERE, "Failed to read version history file '" + path + "'", e);
+ return;
+ } catch (final JsonSyntaxException e) {
+ logger.log(Level.SEVERE, "Invalid json syntax for file '" + path + "'", e);
+ return;
+ }
+
+ final String version = Bukkit.getVersion();
+ if (version == null) {
+ logger.severe("Failed to retrieve current version");
+ return;
+ }
+
+ if (!version.equals(currentData.getCurrentVersion())) {
+ // The version appears to have changed
+ currentData.setOldVersion(currentData.getCurrentVersion());
+ currentData.setCurrentVersion(version);
+ writeFile(path);
+ }
+ } else {
+ // File doesn't exist, start fresh
+ currentData = new VersionData();
+ // oldVersion is null
+ currentData.setCurrentVersion(Bukkit.getVersion());
+ writeFile(path);
+ }
+ }
+
+ private void writeFile(@Nonnull final Path path) {
+ try (final BufferedWriter writer = Files.newBufferedWriter(
+ path,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING
+ )) {
+ gson.toJson(currentData, writer);
+ } catch (final IOException e) {
+ logger.log(Level.SEVERE, "Failed to write to version history file", e);
+ }
+ }
+
+ @Nullable
+ public VersionData getVersionData() {
+ return currentData;
+ }
+
+ public static class VersionData {
+ private String oldVersion;
+
+ private String currentVersion;
+
+ @Nullable
+ public String getOldVersion() {
+ return oldVersion;
+ }
+
+ public void setOldVersion(@Nullable String oldVersion) {
+ this.oldVersion = oldVersion;
+ }
+
+ @Nullable
+ public String getCurrentVersion() {
+ return currentVersion;
+ }
+
+ public void setCurrentVersion(@Nullable String currentVersion) {
+ this.currentVersion = currentVersion;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("oldVersion", oldVersion)
+ .add("currentVersion", currentVersion)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final VersionData versionData = (VersionData) o;
+ return Objects.equals(oldVersion, versionData.oldVersion) &&
+ Objects.equals(currentVersion, versionData.currentVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oldVersion, currentVersion);
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
index 1ba8477bf..f7e376ce6 100644
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
@@ -19,7 +19,7 @@ public class ChunkPacketBlockController {
}
- public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean skyLight, boolean initializeBlocks) {
+ public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean initializeBlocks) {
return null;
}
@@ -27,11 +27,12 @@ public class ChunkPacketBlockController {
return true;
}
- public ChunkPacketInfo getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector) {
+ public ChunkPacketInfo getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk,
+ int chunkSectionSelector, boolean forceLoad) {
return null;
}
- public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo chunkPacketInfo) {
+ public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo chunkPacketInfo, boolean loadChunks, Integer ticketHold) {
packetPlayOutMapChunk.setReady(true);
}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
index e3da35b6b..1edcecd2e 100644
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -1,33 +1,21 @@
package com.destroystokyo.paper.antixray;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
-import net.minecraft.server.IRegistry;
-import net.minecraft.server.MinecraftKey;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import net.minecraft.server.*;
+import org.bukkit.Bukkit;
import org.bukkit.World.Environment;
import com.destroystokyo.paper.PaperWorldConfig;
-import net.minecraft.server.Block;
-import net.minecraft.server.BlockPosition;
-import net.minecraft.server.Blocks;
-import net.minecraft.server.Chunk;
-import net.minecraft.server.ChunkSection;
-import net.minecraft.server.DataPalette;
-import net.minecraft.server.EnumDirection;
-import net.minecraft.server.GeneratorAccess;
-import net.minecraft.server.IBlockData;
-import net.minecraft.server.IChunkAccess;
-import net.minecraft.server.IWorldReader;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.PacketPlayOutMapChunk;
-import net.minecraft.server.PlayerInteractManager;
-import net.minecraft.server.World;
-import net.minecraft.server.WorldServer;
-
public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
private static ExecutorService executorServiceInstance = null;
@@ -63,7 +51,10 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
executorService = null;
}
+ List toObfuscate;
+
if (engineMode == EngineMode.HIDE) {
+ toObfuscate = paperWorldConfig.hiddenBlocks;
predefinedBlockData = null;
predefinedBlockDataStone = new IBlockData[] {Blocks.STONE.getBlockData()};
predefinedBlockDataNetherrack = new IBlockData[] {Blocks.NETHERRACK.getBlockData()};
@@ -73,17 +64,19 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
predefinedBlockDataBitsNetherrackGlobal = new int[] {ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(Blocks.NETHERRACK.getBlockData())};
predefinedBlockDataBitsEndStoneGlobal = new int[] {ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(Blocks.END_STONE.getBlockData())};
} else {
+ toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
Set predefinedBlockDataSet = new HashSet();
for (String id : paperWorldConfig.hiddenBlocks) {
- Block block = IRegistry.BLOCK.get(new MinecraftKey(id));
+ Block block = IRegistry.BLOCK.getOptional(new MinecraftKey(id)).orElse(null);
if (block != null && !block.isTileEntity()) {
+ toObfuscate.add(id);
predefinedBlockDataSet.add(block.getBlockData());
}
}
- predefinedBlockData = predefinedBlockDataSet.size() == 0 ? new IBlockData[] {Blocks.DIAMOND_ORE.getBlockData()} : predefinedBlockDataSet.toArray(new IBlockData[predefinedBlockDataSet.size()]);
+ predefinedBlockData = predefinedBlockDataSet.size() == 0 ? new IBlockData[] {Blocks.DIAMOND_ORE.getBlockData()} : predefinedBlockDataSet.toArray(new IBlockData[0]);
predefinedBlockDataStone = null;
predefinedBlockDataNetherrack = null;
predefinedBlockDataEndStone = null;
@@ -98,19 +91,24 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
predefinedBlockDataBitsEndStoneGlobal = null;
}
- for (String id : (engineMode == EngineMode.HIDE) ? paperWorldConfig.hiddenBlocks : paperWorldConfig.replacementBlocks) {
- Block block = IRegistry.BLOCK.get(new MinecraftKey(id));
+ for (String id : toObfuscate) {
+ Block block = IRegistry.BLOCK.getOptional(new MinecraftKey(id)).orElse(null);
if (block != null) {
obfuscateGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(block.getBlockData())] = true;
}
}
+ ChunkEmpty emptyChunk = new ChunkEmpty(null, new ChunkCoordIntPair(0, 0));
+ BlockPosition zeroPos = new BlockPosition(0, 0, 0);
+
for (int i = 0; i < solidGlobal.length; i++) {
IBlockData blockData = ChunkSection.GLOBAL_PALETTE.getObject(i);
if (blockData != null) {
- solidGlobal[i] = blockData.getBlock().isOccluding(blockData) && blockData.getBlock() != Blocks.SPAWNER && blockData.getBlock() != Blocks.BARRIER;
+ solidGlobal[i] = blockData.getBlock().isOccluding(blockData, emptyChunk, zeroPos)
+ && blockData.getBlock() != Blocks.SPAWNER && blockData.getBlock() != Blocks.BARRIER && blockData.getBlock() != Blocks.SHULKER_BOX;
+ // shulker box checks TE.
}
}
@@ -126,7 +124,7 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
}
@Override
- public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean skyLight, boolean initializeBlocks) {
+ public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean initializeBlocks) {
//Return the block data which should be added to the data palettes so that they can be used for the obfuscation
if (chunkSection.getYPosition() >> 4 <= maxChunkSectionIndex) {
switch (engineMode) {
@@ -151,28 +149,103 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
return null;
}
+ private final AtomicInteger xrayRequests = new AtomicInteger();
+
+ // Paper start - async chunk api
+ private Integer nextTicketHold() {
+ return Integer.valueOf(this.xrayRequests.getAndIncrement());
+ }
+ // Paper end
+
+ private Integer addXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider) {
+ final Integer hold = Integer.valueOf(this.xrayRequests.getAndIncrement());
+
+ // Add at ticket level 33, which is just enough to keep chunks loaded
+ chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z), 0, hold);
+ chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x - 1, z), 0, hold);
+ chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x + 1, z), 0, hold);
+ chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z - 1), 0, hold);
+ chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z + 1), 0, hold);
+
+ return hold;
+ }
+
+ private void removeXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider, final Integer hold) {
+ // Remove at ticket level 33 (same one we added as), which is just enough to keep chunks loaded
+ chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z), 0, hold);
+ chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x - 1, z), 0, hold);
+ chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x + 1, z), 0, hold);
+ chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z - 1), 0, hold);
+ chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z + 1), 0, hold);
+ }
+
+ private void loadNeighbours(Chunk chunk) {
+ int locX = chunk.getPos().x;
+ int locZ = chunk.getPos().z;
+ chunk.world.getChunkAt(locX - 1, locZ);
+ chunk.world.getChunkAt(locX + 1, locZ);
+ chunk.world.getChunkAt(locX, locZ - 1);
+ chunk.world.getChunkAt(locX, locZ + 1);
+ }
+
+ // Paper start - async chunk api
+ private void loadNeighbourAsync(ChunkProviderServer chunkProvider, WorldServer world, int chunkX, int chunkZ, int[] counter, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) {
+ chunkProvider.getChunkAtAsynchronously(chunkX, chunkZ, true, (Chunk neighbour) -> {
+ onNeighourLoad.accept(neighbour);
+ if (++counter[0] == 4) {
+ onAllNeighboursLoad.run();
+ }
+ });
+ world.asyncChunkTaskManager.raisePriority(chunkX, chunkZ, PrioritizedTaskQueue.HIGHER_PRIORITY);
+ }
+
+ private void loadNeighboursAsync(Chunk chunk, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) {
+ int[] loaded = new int[1];
+
+ int locX = chunk.getPos().x;
+ int locZ = chunk.getPos().z;
+ WorldServer world = ((WorldServer)chunk.world);
+
+ onNeighourLoad.accept(chunk);
+
+ ChunkProviderServer chunkProvider = world.getChunkProvider();
+
+ this.loadNeighbourAsync(chunkProvider, world, locX - 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad);
+ this.loadNeighbourAsync(chunkProvider, world, locX + 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad);
+ this.loadNeighbourAsync(chunkProvider, world, locX, locZ - 1, loaded, onNeighourLoad, onAllNeighboursLoad);
+ this.loadNeighbourAsync(chunkProvider, world, locX, locZ + 1, loaded, onNeighourLoad, onAllNeighboursLoad);
+ }
+ // Paper end
+
@Override
public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) {
+ int locX = chunk.getPos().x;
+ int locZ = chunk.getPos().z;
+ WorldServer world = (WorldServer)chunk.world;
+ ChunkProviderServer chunkProvider = world.getChunkProvider();
+
//Load nearby chunks if necessary
- if (force) {
+ if (force || chunkEdgeMode == ChunkEdgeMode.LOAD) { // TODO temporary
// if forced, load NOW;
- chunk.world.getChunkAt(chunk.locX - 1, chunk.locZ);
- chunk.world.getChunkAt(chunk.locX + 1, chunk.locZ);
- chunk.world.getChunkAt(chunk.locX, chunk.locZ - 1);
- chunk.world.getChunkAt(chunk.locX, chunk.locZ + 1);
- } else if (chunkEdgeMode == ChunkEdgeMode.WAIT && !force) {
- if (chunk.world.getChunkIfLoaded(chunk.locX - 1, chunk.locZ) == null || chunk.world.getChunkIfLoaded(chunk.locX + 1, chunk.locZ) == null || chunk.world.getChunkIfLoaded(chunk.locX, chunk.locZ - 1) == null || chunk.world.getChunkIfLoaded(chunk.locX, chunk.locZ + 1) == null) {
+ this.loadNeighbours(chunk);
+ } else if (chunkEdgeMode == ChunkEdgeMode.WAIT) {
+ if (chunkProvider.getChunkAtIfCachedImmediately(locX - 1, locZ) == null ||
+ chunkProvider.getChunkAtIfCachedImmediately(locX + 1, locZ) == null ||
+ chunkProvider.getChunkAtIfCachedImmediately(locX, locZ - 1) == null ||
+ chunkProvider.getChunkAtIfCachedImmediately(locX, locZ + 1) == null) {
//Don't create the chunk packet now, wait until nearby chunks are loaded and create it later
return false;
}
- } else if (chunkEdgeMode == ChunkEdgeMode.LOAD) {
+ } else if (false && chunkEdgeMode == ChunkEdgeMode.LOAD) {
+ // TODO Note: These should be asynchronous loads; however we have no such thing in 1.14.
boolean missingChunk = false;
//noinspection ConstantConditions
+ /*
missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX - 1, chunk.locZ, true, true, c -> {}) == null;
missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX + 1, chunk.locZ, true, true, c -> {}) == null;
missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX, chunk.locZ - 1, true, true, c -> {}) == null;
missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX, chunk.locZ + 1, true, true, c -> {}) == null;
-
+ */
if (missingChunk) {
return false;
}
@@ -183,15 +256,61 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
}
@Override
- public ChunkPacketInfoAntiXray getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector) {
- //Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
+ public ChunkPacketInfoAntiXray getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk,
+ int chunkSectionSelector, boolean forceLoad) {
+ // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
+ // Note: As of 1.14 this has to be moved later due to the chunk system.
+
ChunkPacketInfoAntiXray chunkPacketInfoAntiXray = new ChunkPacketInfoAntiXray(packetPlayOutMapChunk, chunk, chunkSectionSelector, this);
- chunkPacketInfoAntiXray.setNearbyChunks(chunk.world.getChunkIfLoaded(chunk.locX - 1, chunk.locZ), chunk.world.getChunkIfLoaded(chunk.locX + 1, chunk.locZ), chunk.world.getChunkIfLoaded(chunk.locX, chunk.locZ - 1), chunk.world.getChunkIfLoaded(chunk.locX, chunk.locZ + 1));
return chunkPacketInfoAntiXray;
}
@Override
- public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo chunkPacketInfo) {
+ public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo chunkPacketInfo, boolean loadChunks, Integer hold) {
+ if (!Bukkit.isPrimaryThread()) {
+ // plugins?
+ final Integer finalHold = hold;
+ MinecraftServer.getServer().scheduleOnMain(() -> {
+ this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, loadChunks, finalHold);
+ });
+ return;
+ }
+ Chunk chunk = chunkPacketInfo.getChunk();
+ int locX = chunk.getPos().x;
+ int locZ = chunk.getPos().z;
+ WorldServer world = (WorldServer)chunk.world;
+
+ Chunk[] chunks = new Chunk[] {
+ (Chunk)world.getChunkIfLoadedImmediately(locX - 1, locZ),
+ (Chunk)world.getChunkIfLoadedImmediately(locX + 1, locZ),
+ (Chunk)world.getChunkIfLoadedImmediately(locX, locZ - 1),
+ (Chunk)world.getChunkIfLoadedImmediately(locX, locZ + 1)
+ };
+
+ if (loadChunks) {
+ // Note: This ugly hack is to get us out of the general chunk load/unload queue to prevent deadlock
+
+ if (chunks[0] == null || chunks[1] == null || chunks[2] == null || chunks[3] == null) {
+ // we need to load
+ // Paper start - async chunk api
+ Integer ticketHold = this.nextTicketHold();
+ this.loadNeighboursAsync(chunk, (Chunk neighbour) -> { // when a neighbour is loaded
+ ((WorldServer)neighbour.world).getChunkProvider().addTicket(TicketType.ANTIXRAY, neighbour.getPos(), 0, ticketHold);
+ },
+ () -> { // once neighbours get loaded
+ this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, false, ticketHold);
+ });
+ // Paper end
+ return;
+ }
+
+ hold = this.addXrayTickets(locX, locZ, world.getChunkProvider());
+ // fall through to normal behavior, our chunks are now loaded & have a ticket
+ }
+
+ ((ChunkPacketInfoAntiXray)chunkPacketInfo).setNearbyChunks(chunks);
+ ((ChunkPacketInfoAntiXray)chunkPacketInfo).ticketHold = hold;
+
if (asynchronous) {
executorService.submit((ChunkPacketInfoAntiXray) chunkPacketInfo);
} else {
@@ -212,108 +331,126 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
private final ChunkSection[] nearbyChunkSections = new ChunkSection[4];
public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
- boolean[] solidTemp = null;
- boolean[] obfuscateTemp = null;
- dataBitsReader.setDataBits(chunkPacketInfoAntiXray.getData());
- dataBitsWriter.setDataBits(chunkPacketInfoAntiXray.getData());
- int counter = 0;
+ try {
+ boolean[] solidTemp = null;
+ boolean[] obfuscateTemp = null;
+ dataBitsReader.setDataBits(chunkPacketInfoAntiXray.getData());
+ dataBitsWriter.setDataBits(chunkPacketInfoAntiXray.getData());
+ int counter = 0;
- for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
- if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex) != null) {
- int[] predefinedBlockDataBitsTemp;
+ for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
+ if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex) != null) {
+ int[] predefinedBlockDataBitsTemp;
- if (chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex) == ChunkSection.GLOBAL_PALETTE) {
- predefinedBlockDataBitsTemp = engineMode == EngineMode.HIDE ? chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.NETHER ? predefinedBlockDataBitsNetherrackGlobal : chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.THE_END ? predefinedBlockDataBitsEndStoneGlobal : predefinedBlockDataBitsStoneGlobal : predefinedBlockDataBitsGlobal;
- } else {
- predefinedBlockDataBitsTemp = predefinedBlockDataBits == null ? predefinedBlockDataBits = engineMode == EngineMode.HIDE ? new int[1] : new int[predefinedBlockData.length] : predefinedBlockDataBits;
+ if (chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex) == ChunkSection.GLOBAL_PALETTE) {
+ predefinedBlockDataBitsTemp = engineMode == EngineMode.HIDE ? chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.NETHER ? predefinedBlockDataBitsNetherrackGlobal : chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.THE_END ? predefinedBlockDataBitsEndStoneGlobal : predefinedBlockDataBitsStoneGlobal : predefinedBlockDataBitsGlobal;
+ } else {
+ predefinedBlockDataBitsTemp = predefinedBlockDataBits == null ? predefinedBlockDataBits = engineMode == EngineMode.HIDE ? new int[1] : new int[predefinedBlockData.length] : predefinedBlockDataBits;
- for (int i = 0; i < predefinedBlockDataBitsTemp.length; i++) {
- predefinedBlockDataBitsTemp[i] = chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex).getOrCreateIdFor(chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex)[i]);
- }
- }
-
- dataBitsWriter.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
-
- //Check if the chunk section below was not obfuscated
- if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex - 1) == null) {
- //If so, initialize some stuff
- dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
- dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
- solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), solid, solidGlobal);
- obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
- //Read the blocks of the upper layer of the chunk section below if it exists
- ChunkSection belowChunkSection = null;
- boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex - 1]) == Chunk.EMPTY_CHUNK_SECTION;
-
- for (int z = 0; z < 16; z++) {
- for (int x = 0; x < 16; x++) {
- current[z][x] = true;
- next[z][x] = skipFirstLayer || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(belowChunkSection.getType(x, 15, z))];
+ for (int i = 0; i < predefinedBlockDataBitsTemp.length; i++) {
+ predefinedBlockDataBitsTemp[i] = chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex).getOrCreateIdFor(chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex)[i]);
}
}
- //Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
- dataBitsWriter.setBitsPerObject(0);
- obfuscateLayer(-1, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, emptyNearbyChunkSections, counter);
- }
+ dataBitsWriter.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
- dataBitsWriter.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
- nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
- nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
- nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
- nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
+ //Check if the chunk section below was not obfuscated
+ if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex - 1) == null) {
+ //If so, initialize some stuff
+ dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
+ dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
+ solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), solid, solidGlobal);
+ obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
+ //Read the blocks of the upper layer of the chunk section below if it exists
+ ChunkSection belowChunkSection = null;
+ boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex - 1]) == Chunk.EMPTY_CHUNK_SECTION;
- //Obfuscate all layers of the current chunk section except the upper one
- for (int y = 0; y < 15; y++) {
- boolean[][] temp = current;
- current = next;
- next = nextNext;
- nextNext = temp;
- counter = obfuscateLayer(y, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
- }
+ for (int z = 0; z < 16; z++) {
+ for (int x = 0; x < 16; x++) {
+ current[z][x] = true;
+ next[z][x] = skipFirstLayer || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(belowChunkSection.getType(x, 15, z))];
+ }
+ }
- //Check if the chunk section above doesn't need obfuscation
- if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex + 1) == null) {
- //If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
- ChunkSection aboveChunkSection;
+ //Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
+ dataBitsWriter.setBitsPerObject(0);
+ obfuscateLayer(-1, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, emptyNearbyChunkSections, counter);
+ }
- if (chunkSectionIndex != 15 && (aboveChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex + 1]) != Chunk.EMPTY_CHUNK_SECTION) {
+ dataBitsWriter.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
+ nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
+ nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
+ nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
+ nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
+
+ //Obfuscate all layers of the current chunk section except the upper one
+ for (int y = 0; y < 15; y++) {
boolean[][] temp = current;
current = next;
next = nextNext;
nextNext = temp;
+ counter = obfuscateLayer(y, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
+ }
- for (int z = 0; z < 16; z++) {
- for (int x = 0; x < 16; x++) {
- if (!solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(aboveChunkSection.getType(x, 0, z))]) {
- current[z][x] = true;
+ //Check if the chunk section above doesn't need obfuscation
+ if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex + 1) == null) {
+ //If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
+ ChunkSection aboveChunkSection;
+
+ if (chunkSectionIndex != 15 && (aboveChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex + 1]) != Chunk.EMPTY_CHUNK_SECTION) {
+ boolean[][] temp = current;
+ current = next;
+ next = nextNext;
+ nextNext = temp;
+
+ for (int z = 0; z < 16; z++) {
+ for (int x = 0; x < 16; x++) {
+ if (!solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(aboveChunkSection.getType(x, 0, z))]) {
+ current[z][x] = true;
+ }
}
}
+
+ //There is nothing to read anymore
+ dataBitsReader.setBitsPerObject(0);
+ solid[0] = true;
+ counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solid, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
}
-
- //There is nothing to read anymore
- dataBitsReader.setBitsPerObject(0);
- solid[0] = true;
- counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solid, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
+ } else {
+ //If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
+ dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex + 1));
+ dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex + 1));
+ solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), solid, solidGlobal);
+ obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
+ boolean[][] temp = current;
+ current = next;
+ next = nextNext;
+ nextNext = temp;
+ counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
}
- } else {
- //If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
- dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex + 1));
- dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex + 1));
- solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), solid, solidGlobal);
- obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
- boolean[][] temp = current;
- current = next;
- next = nextNext;
- nextNext = temp;
- counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
- }
- dataBitsWriter.finish();
+ dataBitsWriter.finish();
+ }
+ }
+
+ chunkPacketInfoAntiXray.getPacketPlayOutMapChunk().setReady(true);
+
+ } finally {
+ if (chunkPacketInfoAntiXray.ticketHold != null) {
+ Runnable runnable = () -> {
+ Chunk chunk = chunkPacketInfoAntiXray.getChunk();
+ ChunkCoordIntPair chunkPos = chunk.getPos();
+
+ ChunkPacketBlockControllerAntiXray.this.removeXrayTickets(chunkPos.x, chunkPos.z, (ChunkProviderServer) chunk.world.getChunkProvider(),
+ chunkPacketInfoAntiXray.ticketHold);
+ };
+ if (MinecraftServer.getServer().isMainThread()) {
+ runnable.run();
+ } else {
+ MinecraftServer.getServer().scheduleOnMain(runnable);
+ }
}
}
-
- chunkPacketInfoAntiXray.getPacketPlayOutMapChunk().setReady(true);
}
private int obfuscateLayer(int y, DataBitsReader dataBitsReader, DataBitsWriter dataBitsWriter, boolean[] solid, boolean[] obfuscate, int[] predefinedBlockDataBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, ChunkSection[] nearbyChunkSections, int counter) {
@@ -613,7 +750,8 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
IBlockData blockData = world.getTypeIfLoaded(blockPosition);
if (blockData != null && obfuscateGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(blockData)]) {
- world.notify(blockPosition, blockData, blockData, 3);
+ //world.notify(blockPosition, blockData, blockData, 3);
+ ((WorldServer)world).getChunkProvider().flagDirty(blockPosition); // We only need to re-send to client
}
}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
index e255a45fa..067dfb2f1 100644
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
@@ -8,8 +8,10 @@ public class ChunkPacketInfoAntiXray extends ChunkPacketInfo impleme
private Chunk[] nearbyChunks;
private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
+ public Integer ticketHold;
- public ChunkPacketInfoAntiXray(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
+ public ChunkPacketInfoAntiXray(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector,
+ ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
super(packetPlayOutMapChunk, chunk, chunkSectionSelector);
this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
}
diff --git a/src/main/java/com/destroystokyo/paper/block/CraftBlockSoundGroup.java b/src/main/java/com/destroystokyo/paper/block/CraftBlockSoundGroup.java
new file mode 100644
index 000000000..99f99330d
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/block/CraftBlockSoundGroup.java
@@ -0,0 +1,38 @@
+package com.destroystokyo.paper.block;
+
+import net.minecraft.server.SoundEffectType;
+import org.bukkit.Sound;
+import org.bukkit.craftbukkit.CraftSound;
+
+public class CraftBlockSoundGroup implements BlockSoundGroup {
+ private final SoundEffectType soundEffectType;
+
+ public CraftBlockSoundGroup(SoundEffectType soundEffectType) {
+ this.soundEffectType = soundEffectType;
+ }
+
+ @Override
+ public Sound getBreakSound() {
+ return CraftSound.getSoundByEffect(soundEffectType.getBreakSound());
+ }
+
+ @Override
+ public Sound getStepSound() {
+ return CraftSound.getSoundByEffect(soundEffectType.getStepSound());
+ }
+
+ @Override
+ public Sound getPlaceSound() {
+ return CraftSound.getSoundByEffect(soundEffectType.getPlaceSound());
+ }
+
+ @Override
+ public Sound getHitSound() {
+ return CraftSound.getSoundByEffect(soundEffectType.getHitSound());
+ }
+
+ @Override
+ public Sound getFallSound() {
+ return CraftSound.getSoundByEffect(soundEffectType.getFallSound());
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
index 688b4715e..cd6e25923 100644
--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
@@ -34,7 +34,7 @@ public final class PaperConsole extends SimpleTerminalConsole {
@Override
protected void shutdown() {
- this.server.safeShutdown();
+ this.server.safeShutdown(false);
}
}
diff --git a/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java b/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
index ed3d86ddd..f68a07cb9 100644
--- a/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
+++ b/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
@@ -83,11 +83,9 @@ public class PaperPathfinder implements com.destroystokyo.paper.entity.Pathfinde
@Override
public List getPoints() {
- int pathCount = path.getPathCount();
List points = new ArrayList<>();
- PathPoint[] pathPoints = path.getPoints();
- for (int i = 0; i < pathCount; i++) {
- points.add(toLoc(pathPoints[i]));
+ for (PathPoint point : path.getPoints()) {
+ points.add(toLoc(point));
}
return points;
}
@@ -103,7 +101,7 @@ public class PaperPathfinder implements com.destroystokyo.paper.entity.Pathfinde
if (!path.hasNext()) {
return null;
}
- return toLoc(path.getPoints()[path.getNextIndex()]);
+ return toLoc(path.getPoints().get(path.getNextIndex()));
}
}
diff --git a/src/main/java/com/destroystokyo/paper/io/IOUtil.java b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
new file mode 100644
index 000000000..5af0ac3d9
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
@@ -0,0 +1,62 @@
+package com.destroystokyo.paper.io;
+
+import org.bukkit.Bukkit;
+
+public final class IOUtil {
+
+ /* Copied from concrete or concurrentutil */
+
+ public static long getCoordinateKey(final int x, final int z) {
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public static int getCoordinateX(final long key) {
+ return (int)key;
+ }
+
+ public static int getCoordinateZ(final long key) {
+ return (int)(key >>> 32);
+ }
+
+ public static int getRegionCoordinate(final int chunkCoordinate) {
+ return chunkCoordinate >> 5;
+ }
+
+ public static int getChunkInRegion(final int chunkCoordinate) {
+ return chunkCoordinate & 31;
+ }
+
+ public static String genericToString(final Object object) {
+ return object == null ? "null" : object.getClass().getName() + ":" + object.toString();
+ }
+
+ public static T notNull(final T obj) {
+ if (obj == null) {
+ throw new NullPointerException();
+ }
+ return obj;
+ }
+
+ public static T notNull(final T obj, final String msgIfNull) {
+ if (obj == null) {
+ throw new NullPointerException(msgIfNull);
+ }
+ return obj;
+ }
+
+ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
+ if (off < 0 || len < 0 || (arrayLength - off) < len) {
+ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
+ }
+ }
+
+ public static int getPriorityForCurrentThread() {
+ return Bukkit.isPrimaryThread() ? PrioritizedTaskQueue.HIGHEST_PRIORITY : PrioritizedTaskQueue.NORMAL_PRIORITY;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void rethrow(final Throwable throwable) throws T {
+ throw (T)throwable;
+ }
+
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
new file mode 100644
index 000000000..4f10a8311
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
@@ -0,0 +1,661 @@
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ExceptionWorldConflict;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.NBTTagCompound;
+import net.minecraft.server.RegionFile;
+import net.minecraft.server.WorldServer;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Prioritized singleton thread responsible for all chunk IO that occurs in a minecraft server.
+ *
+ *
+ * Singleton access: {@link Holder#INSTANCE}
+ *
+ *
+ *
+ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced):
+ *
+ * Chunk saves may not occur for unloaded chunks.
+ *
+ *
+ * Tasks must be scheduled on the main thread.
+ *
+ *
+ *
+ * @see Holder#INSTANCE
+ * @see #scheduleSave(WorldServer, int, int, NBTTagCompound, NBTTagCompound, int)
+ * @see #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)
+ */
+public final class PaperFileIOThread extends QueueExecutorThread {
+
+ public static final Logger LOGGER = MinecraftServer.LOGGER;
+ public static final NBTTagCompound FAILURE_VALUE = new NBTTagCompound();
+
+ public static final class Holder {
+
+ public static final PaperFileIOThread INSTANCE = new PaperFileIOThread();
+
+ static {
+ INSTANCE.start();
+ }
+ }
+
+ private final AtomicLong writeCounter = new AtomicLong();
+
+ private PaperFileIOThread() {
+ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time
+ this.setName("Paper RegionFile IO Thread");
+ this.setPriority(Thread.NORM_PRIORITY - 1); // we keep priority close to normal because threads can wait on us
+ this.setUncaughtExceptionHandler((final Thread unused, final Throwable thr) -> {
+ LOGGER.fatal("Uncaught exception thrown from IO thread, report this!", thr);
+ });
+ }
+
+ /* run() is implemented by superclass */
+
+ /*
+ *
+ * IO thread will perform reads before writes
+ *
+ * How reads/writes are scheduled:
+ *
+ * If read in progress while scheduling write, ignore read and schedule write
+ * If read in progress while scheduling read (no write in progress), chain the read task
+ *
+ *
+ * If write in progress while scheduling read, use the pending write data and ret immediately
+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
+ *
+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
+ * it fails to properly propagate write failures. When writes fail the data is kept so future reads will actually
+ * read the failed write data. This should hopefully act as a way to prevent data loss for spurious fails for writing data.
+ *
+ */
+
+ /**
+ * Attempts to bump the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued.
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority level to try to bump to
+ */
+ public void bumpPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
+
+ if (poiTask != null) {
+ poiTask.raisePriority(priority);
+ }
+ if (chunkTask != null) {
+ chunkTask.raisePriority(priority);
+ }
+ }
+
+ // Hack start
+ /**
+ * if {@code waitForRead} is true, then this task will wait on an available read task, else it will wait on an available
+ * write task
+ * if {@code poiTask} is true, then this task will wait on a poi task, else it will wait on chunk data task
+ * @deprecated API is garbage and will only work for main thread queueing of tasks (which is vanilla), plugins messing
+ * around asynchronously will give unexpected results
+ * @return whether the task succeeded, or {@code null} if there is no task
+ */
+ @Deprecated
+ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, final boolean waitForRead,
+ final boolean poiTask) {
+ final ChunkDataTask task;
+
+ final Long key = IOUtil.getCoordinateKey(chunkX, chunkZ);
+ if (poiTask) {
+ task = world.poiDataController.tasks.get(key);
+ } else {
+ task = world.chunkDataController.tasks.get(key);
+ }
+
+ if (task == null) {
+ return null;
+ }
+
+ if (waitForRead) {
+ ChunkDataController.InProgressRead read = task.inProgressRead;
+ if (read == null) {
+ return null;
+ }
+ return Boolean.valueOf(read.readFuture.join() != PaperFileIOThread.FAILURE_VALUE);
+ }
+
+ // wait for write
+ ChunkDataController.InProgressWrite write = task.inProgressWrite;
+ if (write == null) {
+ return null;
+ }
+ return Boolean.valueOf(write.wrote.join() != PaperFileIOThread.FAILURE_VALUE);
+ }
+ // Hack end
+
+ public NBTTagCompound getPendingWrite(final WorldServer world, final int chunkX, final int chunkZ, final boolean poiData) {
+ final ChunkDataController taskController = poiData ? world.poiDataController : world.chunkDataController;
+
+ final ChunkDataTask dataTask = taskController.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
+
+ if (dataTask == null) {
+ return null;
+ }
+
+ final ChunkDataController.InProgressWrite write = dataTask.inProgressWrite;
+
+ if (write == null) {
+ return null;
+ }
+
+ return write.data;
+ }
+
+ /**
+ * Sets the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued.
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority level to set to
+ */
+ public void setPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
+
+ if (poiTask != null) {
+ poiTask.updatePriority(priority);
+ }
+ if (chunkTask != null) {
+ chunkTask.updatePriority(priority);
+ }
+ }
+
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ *
+ * Impl notes:
+ *
+ *
+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
+ * saves must be scheduled before a chunk is unloaded.
+ *
+ *
+ * Writes may be called concurrently, although only the "later" write will go through.
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param poiData Chunk point of interest data. If {@code null}, then no poi data is saved.
+ * @param chunkData Chunk data. If {@code null}, then no chunk data is saved.
+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
+ * @throws IllegalArgumentException If both {@code poiData} and {@code chunkData} are {@code null}.
+ * @throws IllegalStateException If the file io thread has shutdown.
+ */
+ public void scheduleSave(final WorldServer world, final int chunkX, final int chunkZ,
+ final NBTTagCompound poiData, final NBTTagCompound chunkData,
+ final int priority) throws IllegalArgumentException {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final long writeCounter = this.writeCounter.getAndIncrement();
+
+ if (poiData != null) {
+ this.scheduleWrite(world.poiDataController, world, chunkX, chunkZ, poiData, priority, writeCounter);
+ }
+ if (chunkData != null) {
+ this.scheduleWrite(world.chunkDataController, world, chunkX, chunkZ, chunkData, priority, writeCounter);
+ }
+ }
+
+ private void scheduleWrite(final ChunkDataController dataController, final WorldServer world,
+ final int chunkX, final int chunkZ, final NBTTagCompound data, final int priority, final long writeCounter) {
+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask taskRunning) -> {
+ if (taskRunning == null) {
+ // no task is scheduled
+
+ // create task
+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
+ newTask.inProgressWrite = new ChunkDataController.InProgressWrite();
+ newTask.inProgressWrite.writeCounter = writeCounter;
+ newTask.inProgressWrite.data = data;
+
+ PaperFileIOThread.this.queueTask(newTask); // schedule
+ return newTask;
+ }
+
+ taskRunning.raisePriority(priority);
+
+ if (taskRunning.inProgressWrite == null) {
+ taskRunning.inProgressWrite = new ChunkDataController.InProgressWrite();
+ }
+
+ boolean reschedule = taskRunning.inProgressWrite.writeCounter == -1L;
+
+ // synchronize for readers
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (taskRunning) {
+ taskRunning.inProgressWrite.data = data;
+ taskRunning.inProgressWrite.writeCounter = writeCounter;
+ }
+
+ if (reschedule) {
+ // We need to reschedule this task since the previous one is not currently scheduled since it failed
+ taskRunning.reschedule(priority);
+ }
+
+ return taskRunning;
+ });
+ }
+
+ /**
+ * Same as {@link #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns
+ * a {@link CompletableFuture} which is potentially completed ASYNCHRONOUSLY ON THE FILE IO THREAD when the load task
+ * has completed.
+ *
+ * Note that if the chunk fails to load the returned future is completed with {@code null}.
+ *
+ */
+ public CompletableFuture loadChunkDataAsyncFuture(final WorldServer world, final int chunkX, final int chunkZ,
+ final int priority, final boolean readPoiData, final boolean readChunkData,
+ final boolean intendingToBlock) {
+ final CompletableFuture future = new CompletableFuture<>();
+ this.loadChunkDataAsync(world, chunkX, chunkZ, priority, future::complete, readPoiData, readChunkData, intendingToBlock);
+ return future;
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously.
+ *
+ * Impl notes:
+ *
+ *
+ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}.
+ *
+ *
+ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data
+ * this call did not request.
+ *
+ *
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
+ * @param onComplete Consumer to execute once this task has completed
+ * @param readPoiData Whether to read point of interest data. If {@code false}, the {@code NBTTagCompound} will be {@code null}.
+ * @param readChunkData Whether to read chunk data. If {@code false}, the {@code NBTTagCompound} will be {@code null}.
+ * @return The {@link PrioritizedTaskQueue.PrioritizedTask} associated with this task. Note that this task does not support
+ * cancellation.
+ */
+ public void loadChunkDataAsync(final WorldServer world, final int chunkX, final int chunkZ,
+ final int priority, final Consumer onComplete,
+ final boolean readPoiData, final boolean readChunkData,
+ final boolean intendingToBlock) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ if (!(readPoiData | readChunkData)) {
+ throw new IllegalArgumentException("Must read chunk data or poi data");
+ }
+
+ final ChunkData complete = new ChunkData();
+ final boolean[] requireCompletion = new boolean[] { readPoiData, readChunkData };
+
+ if (readPoiData) {
+ this.scheduleRead(world.poiDataController, world, chunkX, chunkZ, (final NBTTagCompound poiData) -> {
+ complete.poiData = poiData;
+
+ final boolean finished;
+
+ // avoid a race condition where the file io thread completes and we complete synchronously
+ // Note: Synchronization can be elided if both of the accesses are volatile
+ synchronized (requireCompletion) {
+ requireCompletion[0] = false; // 0 -> poi data
+ finished = !requireCompletion[1]; // 1 -> chunk data
+ }
+
+ if (finished) {
+ onComplete.accept(complete);
+ }
+ }, priority, intendingToBlock);
+ }
+
+ if (readChunkData) {
+ this.scheduleRead(world.chunkDataController, world, chunkX, chunkZ, (final NBTTagCompound chunkData) -> {
+ complete.chunkData = chunkData;
+
+ final boolean finished;
+
+ // avoid a race condition where the file io thread completes and we complete synchronously
+ // Note: Synchronization can be elided if both of the accesses are volatile
+ synchronized (requireCompletion) {
+ requireCompletion[1] = false; // 1 -> chunk data
+ finished = !requireCompletion[0]; // 0 -> poi data
+ }
+
+ if (finished) {
+ onComplete.accept(complete);
+ }
+ }, priority, intendingToBlock);
+ }
+
+ }
+
+ // Note: the onComplete may be called asynchronously or synchronously here.
+ private void scheduleRead(final ChunkDataController dataController, final WorldServer world,
+ final int chunkX, final int chunkZ, final Consumer onComplete, final int priority,
+ final boolean intendingToBlock) {
+
+ Function tryLoadFunction = (final RegionFile file) -> {
+ if (file == null) {
+ return Boolean.TRUE;
+ }
+ return Boolean.valueOf(file.chunkExists(new ChunkCoordIntPair(chunkX, chunkZ)));
+ };
+
+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask running) -> {
+ if (running == null) {
+ // not scheduled
+
+ final Boolean shouldSchedule = intendingToBlock ? dataController.computeForRegionFile(chunkX, chunkZ, tryLoadFunction) :
+ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction);
+
+ if (shouldSchedule == Boolean.FALSE) {
+ // not on disk
+ onComplete.accept(null);
+ return null;
+ }
+
+ // set up task
+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
+ newTask.inProgressRead = new ChunkDataController.InProgressRead();
+ newTask.inProgressRead.readFuture.thenAccept(onComplete);
+
+ PaperFileIOThread.this.queueTask(newTask); // schedule task
+ return newTask;
+ }
+
+ running.raisePriority(priority);
+
+ if (running.inProgressWrite == null) {
+ // chain to the read future
+ running.inProgressRead.readFuture.thenAccept(onComplete);
+ return running;
+ }
+
+ // at this stage we have to use the in progress write's data to avoid an order issue
+ // we don't synchronize since all writes to data occur in the compute() call
+ onComplete.accept(running.inProgressWrite.data);
+ return running;
+ });
+ }
+
+ /**
+ * Same as {@link #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns
+ * the {@link ChunkData} associated with the specified chunk when the task is complete.
+ * @return The chunk data, or {@code null} if the chunk failed to load.
+ */
+ public ChunkData loadChunkData(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final boolean readPoiData, final boolean readChunkData) {
+ return this.loadChunkDataAsyncFuture(world, chunkX, chunkZ, priority, readPoiData, readChunkData, true).join();
+ }
+
+ /**
+ * Schedules the given task at the specified priority to be executed on the IO thread.
+ *
+ * Internal api. Do not use.
+ *
+ */
+ public void runTask(final int priority, final Runnable runnable) {
+ this.queueTask(new GeneralTask(priority, runnable));
+ }
+
+ static final class GeneralTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ private final Runnable run;
+
+ public GeneralTask(final int priority, final Runnable run) {
+ super(priority);
+ this.run = IOUtil.notNull(run, "Task may not be null");
+ }
+
+ @Override
+ public void run() {
+ try {
+ this.run.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable);
+ }
+ }
+ }
+
+ public static final class ChunkData {
+
+ public NBTTagCompound poiData;
+ public NBTTagCompound chunkData;
+
+ public ChunkData() {}
+
+ public ChunkData(final NBTTagCompound poiData, final NBTTagCompound chunkData) {
+ this.poiData = poiData;
+ this.chunkData = chunkData;
+ }
+ }
+
+ public static abstract class ChunkDataController {
+
+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
+ public final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f);
+
+ public abstract void writeData(final int x, final int z, final NBTTagCompound compound) throws IOException;
+ public abstract NBTTagCompound readData(final int x, final int z) throws IOException;
+
+ public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function);
+ public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function);
+
+ public static final class InProgressWrite {
+ public long writeCounter;
+ public NBTTagCompound data;
+
+ // Hack start
+ @Deprecated
+ public CompletableFuture wrote = new CompletableFuture<>();
+ // Hack end
+ }
+
+ public static final class InProgressRead {
+ public final CompletableFuture readFuture = new CompletableFuture<>();
+ }
+ }
+
+ public static final class ChunkDataTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ public ChunkDataController.InProgressWrite inProgressWrite;
+ public ChunkDataController.InProgressRead inProgressRead;
+
+ private final WorldServer world;
+ private final int x;
+ private final int z;
+ private final ChunkDataController taskController;
+
+ public ChunkDataTask(final int priority, final WorldServer world, final int x, final int z, final ChunkDataController taskController) {
+ super(priority);
+ this.world = world;
+ this.x = x;
+ this.z = z;
+ this.taskController = taskController;
+ }
+
+ @Override
+ public String toString() {
+ return "Task for world: '" + this.world.getWorld().getName() + "' at " + this.x + "," + this.z +
+ " poi: " + (this.taskController == this.world.poiDataController) + ", hash: " + this.hashCode();
+ }
+
+ /*
+ *
+ * IO thread will perform reads before writes
+ *
+ * How reads/writes are scheduled:
+ *
+ * If read in progress while scheduling write, ignore read and schedule write
+ * If read in progress while scheduling read (no write in progress), chain the read task
+ *
+ *
+ * If write in progress while scheduling read, use the pending write data and ret immediately
+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
+ *
+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
+ * it fails to properly propagate write failures
+ *
+ */
+
+ void reschedule(final int priority) {
+ // priority is checked before this stage // TODO what
+ this.queue.lazySet(null);
+ this.inProgressWrite.wrote = new CompletableFuture<>(); // Hack
+ this.priority.lazySet(priority);
+ PaperFileIOThread.Holder.INSTANCE.queueTask(this);
+ }
+
+ @Override
+ public void run() {
+ ChunkDataController.InProgressRead read = this.inProgressRead;
+ if (read != null) {
+ NBTTagCompound compound = PaperFileIOThread.FAILURE_VALUE;
+ try {
+ compound = this.taskController.readData(this.x, this.z);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to read chunk data for task: " + this.toString(), thr);
+ // fall through to complete with null data
+ }
+ read.readFuture.complete(compound);
+ }
+
+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(this.x, this.z));
+
+ ChunkDataController.InProgressWrite write = this.inProgressWrite;
+
+ if (write == null) {
+ // IntelliJ warns this is invalid, however it does not consider that writes to the task map & the inProgress field can occur concurrently.
+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ return valueInMap.inProgressWrite == null ? null : valueInMap;
+ });
+
+ if (inMap == null) {
+ return; // set the task value to null, indicating we're done
+ }
+
+ // not null, which means there was a concurrent write
+ write = this.inProgressWrite;
+ }
+
+ // check if another process is writing
+ try {
+ this.world.checkSession();
+ } catch (final ExceptionWorldConflict ex) {
+ LOGGER.fatal("Couldn't save chunk; already in use by another instance of Minecraft?", ex);
+ // we don't need to set the write counter to -1 as we know at this stage there's no point in re-scheduling
+ // writes since they'll fail anyways.
+ write.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack - However we need to fail the write
+ return;
+ }
+
+ for (;;) {
+ final long writeCounter;
+ final NBTTagCompound data;
+
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (write) {
+ writeCounter = write.writeCounter;
+ data = write.data;
+ }
+
+ boolean failedWrite = false;
+
+ try {
+ this.taskController.writeData(this.x, this.z, data);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to write chunk data for task: " + this.toString(), thr);
+ failedWrite = true;
+ }
+
+ boolean finalFailWrite = failedWrite;
+
+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ ChunkDataTask.this.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ ChunkDataTask.this.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ if (valueInMap.inProgressWrite.writeCounter == writeCounter) {
+ if (finalFailWrite) {
+ valueInMap.inProgressWrite.writeCounter = -1L;
+ valueInMap.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE);
+ } else {
+ valueInMap.inProgressWrite.wrote.complete(data);
+ }
+
+ return null;
+ }
+ return valueInMap;
+ // Hack end
+ });
+
+ if (inMap == null) {
+ // write counter matched, so we wrote the most up-to-date pending data, we're done here
+ // or we failed to write and successfully set the write counter to -1
+ return; // we're done here
+ }
+
+ // fetch & write new data
+ continue;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
new file mode 100644
index 000000000..78bd238f4
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
@@ -0,0 +1,276 @@
+package com.destroystokyo.paper.io;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class PrioritizedTaskQueue {
+
+ // lower numbers are a higher priority (except < 0)
+ // higher priorities are always executed before lower priorities
+
+ /**
+ * Priority value indicating the task has completed or is being completed.
+ */
+ public static final int COMPLETING_PRIORITY = -1;
+
+ /**
+ * Highest priority, should only be used for main thread tasks or tasks that are blocking the main thread.
+ */
+ public static final int HIGHEST_PRIORITY = 0;
+
+ /**
+ * Should be only used in an IO task so that chunk loads do not wait on other IO tasks.
+ * This only exists because IO tasks are scheduled before chunk load tasks to decrease IO waiting times.
+ */
+ public static final int HIGHER_PRIORITY = 1;
+
+ /**
+ * Should be used for scheduling chunk loads/generation that would increase response times to users.
+ */
+ public static final int HIGH_PRIORITY = 2;
+
+ /**
+ * Default priority.
+ */
+ public static final int NORMAL_PRIORITY = 3;
+
+ /**
+ * Use for tasks not at all critical and can potentially be delayed.
+ */
+ public static final int LOW_PRIORITY = 4;
+
+ /**
+ * Use for tasks that should "eventually" execute.
+ */
+ public static final int LOWEST_PRIORITY = 5;
+
+ private static final int TOTAL_PRIORITIES = 6;
+
+ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[])new ConcurrentLinkedQueue[TOTAL_PRIORITIES];
+
+ private final AtomicBoolean shutdown = new AtomicBoolean();
+
+ {
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ this.queues[i] = new ConcurrentLinkedQueue<>();
+ }
+ }
+
+ /**
+ * Returns whether the specified priority is valid
+ */
+ public static boolean validPriority(final int priority) {
+ return priority >= 0 && priority < TOTAL_PRIORITIES;
+ }
+
+ /**
+ * Queues a task.
+ * @throws IllegalStateException If the task has already been queued. Use {@link PrioritizedTask#raisePriority(int)} to
+ * raise a task's priority.
+ * This can also be thrown if the queue has shutdown.
+ */
+ public void add(final T task) throws IllegalStateException {
+ task.onQueue(this);
+ this.queues[task.getPriority()].add(task);
+ if (this.shutdown.get()) {
+ // note: we're not actually sure at this point if our task will go through
+ throw new IllegalStateException("Queue has shutdown, refusing to execute task " + IOUtil.genericToString(task));
+ }
+ }
+
+ /**
+ * Polls the highest priority task currently available. {@code null} if none.
+ */
+ public T poll() {
+ T task;
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ final ConcurrentLinkedQueue queue = this.queues[i];
+
+ while ((task = queue.poll()) != null) {
+ final int prevPriority = task.tryComplete(i);
+ if (prevPriority != COMPLETING_PRIORITY && prevPriority <= i) {
+ // if the prev priority was greater-than or equal to our current priority
+ return task;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether this queue may have tasks queued.
+ *
+ * This operation is not atomic, but is MT-Safe.
+ *
+ * @return {@code true} if tasks may be queued, {@code false} otherwise
+ */
+ public boolean hasTasks() {
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ final ConcurrentLinkedQueue queue = this.queues[i];
+
+ if (queue.peek() != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
+ * result in {@link IllegalStateException} being thrown.
+ *
+ * This operation is atomic with respect to other shutdown calls
+ *
+ *
+ * After this call has completed, regardless of return value, this queue will be shutdown.
+ *
+ * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
+ */
+ public boolean shutdown() {
+ return this.shutdown.getAndSet(false);
+ }
+
+ public abstract static class PrioritizedTask {
+
+ protected final AtomicReference queue = new AtomicReference<>();
+
+ protected final AtomicInteger priority;
+
+ protected PrioritizedTask() {
+ this(PrioritizedTaskQueue.NORMAL_PRIORITY);
+ }
+
+ protected PrioritizedTask(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.priority = new AtomicInteger(priority);
+ }
+
+ /**
+ * Returns the current priority. Note that {@link PrioritizedTaskQueue#COMPLETING_PRIORITY} will be returned
+ * if this task is completing or has completed.
+ */
+ public final int getPriority() {
+ return this.priority.get();
+ }
+
+ /**
+ * Returns whether this task is scheduled to execute, or has been already executed.
+ */
+ public boolean isScheduled() {
+ return this.queue.get() != null;
+ }
+
+ final int tryComplete(final int minPriority) {
+ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return COMPLETING_PRIORITY;
+ }
+ if (curr > minPriority) {
+ // curr is lower priority
+ return curr;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, COMPLETING_PRIORITY))) {
+ return curr;
+ }
+ continue;
+ }
+ }
+
+ /**
+ * Forces this task to be completed.
+ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
+ */
+ public boolean cancel() {
+ return this.exchangePriorityVolatile(PrioritizedTaskQueue.COMPLETING_PRIORITY) != PrioritizedTaskQueue.COMPLETING_PRIORITY;
+ }
+
+ /**
+ * Attempts to raise the priority to the priority level specified.
+ * @param priority Priority specified
+ * @return {@code true} if successful, {@code false} otherwise.
+ */
+ public boolean raisePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
+ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
+ if (priority >= curr) {
+ return true;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
+ PrioritizedTaskQueue queue = this.queue.get();
+ if (queue != null) {
+ //noinspection unchecked
+ queue.queues[priority].add(this); // silently fail on shutdown
+ }
+ return true;
+ }
+ continue;
+ }
+ }
+
+ /**
+ * Attempts to set this task's priority level to the level specified.
+ * @param priority Specified priority level.
+ * @return {@code true} if successful, {@code false} if this task is completing or has completed.
+ */
+ public boolean updatePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
+ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
+ if (curr == priority) {
+ return true;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
+ PrioritizedTaskQueue queue = this.queue.get();
+ if (queue != null) {
+ //noinspection unchecked
+ queue.queues[priority].add(this); // silently fail on shutdown
+ }
+ return true;
+ }
+ continue;
+ }
+ }
+
+ void onQueue(final PrioritizedTaskQueue queue) {
+ if (this.queue.getAndSet(queue) != null) {
+ throw new IllegalStateException("Already queued!");
+ }
+ }
+
+ /* priority */
+
+ protected final int getPriorityVolatile() {
+ return this.priority.get();
+ }
+
+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ if (this.priority.compareAndSet(expect, update)) {
+ return expect;
+ }
+ return this.priority.get();
+ }
+
+ protected final int exchangePriorityVolatile(final int value) {
+ return this.priority.getAndSet(value);
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
new file mode 100644
index 000000000..ee906b594
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
@@ -0,0 +1,241 @@
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.MinecraftServer;
+import org.apache.logging.log4j.Logger;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+public class QueueExecutorThread extends Thread {
+
+ private static final Logger LOGGER = MinecraftServer.LOGGER;
+
+ protected final PrioritizedTaskQueue queue;
+ protected final long spinWaitTime;
+
+ protected volatile boolean closed;
+
+ protected final AtomicBoolean parked = new AtomicBoolean();
+
+ protected volatile ConcurrentLinkedQueue flushQueue = new ConcurrentLinkedQueue<>();
+ protected volatile long flushCycles;
+
+ public QueueExecutorThread(final PrioritizedTaskQueue queue) {
+ this(queue, (int)(1.e6)); // 1.0ms
+ }
+
+ public QueueExecutorThread(final PrioritizedTaskQueue queue, final long spinWaitTime) { // in ms
+ this.queue = queue;
+ this.spinWaitTime = spinWaitTime;
+ }
+
+ @Override
+ public void run() {
+ final long spinWaitTime = this.spinWaitTime;
+ main_loop:
+ for (;;) {
+ this.pollTasks(true);
+
+ // spinwait
+
+ final long start = System.nanoTime();
+
+ for (;;) {
+ // If we are interrpted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
+ Thread.interrupted();
+ LockSupport.parkNanos("Spinwaiting on tasks", 1000L); // 1us
+
+ if (this.pollTasks(true)) {
+ // restart loop, found tasks
+ continue main_loop;
+ }
+
+ if (this.handleClose()) {
+ return; // we're done
+ }
+
+ if ((System.nanoTime() - start) >= spinWaitTime) {
+ break;
+ }
+ }
+
+ if (this.handleClose()) {
+ return;
+ }
+
+ this.parked.set(true);
+
+ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
+ // (i.e it will not notify us)
+ if (this.pollTasks(true)) {
+ this.parked.set(false);
+ continue;
+ }
+
+ if (this.handleClose()) {
+ return;
+ }
+
+ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
+ // LockSupport.park() can fail for any reason
+ do {
+ Thread.interrupted();
+ LockSupport.park("Waiting on tasks");
+ } while (this.parked.get());
+ }
+ }
+
+ protected boolean handleClose() {
+ if (this.closed) {
+ this.pollTasks(true); // this ensures we've emptied the queue
+ this.handleFlushThreads(true);
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean pollTasks(boolean flushTasks) {
+ Runnable task;
+ boolean ret = false;
+
+ while ((task = this.queue.poll()) != null) {
+ ret = true;
+ try {
+ task.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Exception thrown from prioritized runnable task in thread '" + this.getName() + "': " + IOUtil.genericToString(task), throwable);
+ }
+ }
+
+ if (flushTasks) {
+ this.handleFlushThreads(false);
+ }
+
+ return ret;
+ }
+
+ protected void handleFlushThreads(final boolean shutdown) {
+ Thread parking;
+ ConcurrentLinkedQueue flushQueue = this.flushQueue;
+ do {
+ ++flushCycles; // may be plain read opaque write
+ while ((parking = flushQueue.poll()) != null) {
+ LockSupport.unpark(parking);
+ }
+ } while (this.pollTasks(false));
+
+ if (shutdown) {
+ this.flushQueue = null;
+
+ // defend against a race condition where a flush thread double-checks right before we set to null
+ while ((parking = flushQueue.poll()) != null) {
+ LockSupport.unpark(parking);
+ }
+ }
+ }
+
+ /**
+ * Notify's this thread that a task has been added to its queue
+ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
+ */
+ public boolean notifyTasks() {
+ if (this.parked.get() && this.parked.getAndSet(false)) {
+ LockSupport.unpark(this);
+ return true;
+ }
+ return false;
+ }
+
+ protected void queueTask(final T task) {
+ this.queue.add(task);
+ this.notifyTasks();
+ }
+
+ /**
+ * Waits until this thread's queue is empty.
+ *
+ * @throws IllegalStateException If the current thread is {@code this} thread.
+ */
+ public void flush() {
+ final Thread currentThread = Thread.currentThread();
+
+ if (currentThread == this) {
+ // avoid deadlock
+ throw new IllegalStateException("Cannot flush the queue executor thread while on the queue executor thread");
+ }
+
+ // order is important
+
+ int successes = 0;
+ long lastCycle = -1L;
+
+ do {
+ final ConcurrentLinkedQueue flushQueue = this.flushQueue;
+ if (flushQueue == null) {
+ return;
+ }
+
+ flushQueue.add(currentThread);
+
+ // double check flush queue
+ if (this.flushQueue == null) {
+ return;
+ }
+
+ final long currentCycle = this.flushCycles; // may be opaque read
+
+ if (currentCycle == lastCycle) {
+ Thread.yield();
+ continue;
+ }
+
+ // force response
+ this.parked.set(false);
+ LockSupport.unpark(this);
+
+ LockSupport.park("flushing queue executor thread");
+
+ // returns whether there are tasks queued, does not return whether there are tasks executing
+ // this is why we cycle twice twice through flush (we know a pollTask call is made after a flush cycle)
+ // we really only need to guarantee that the tasks this thread has queued has gone through, and can leave
+ // tasks queued concurrently that are unsychronized with this thread as undefined behavior
+ if (this.queue.hasTasks()) {
+ successes = 0;
+ } else {
+ ++successes;
+ }
+
+ } while (successes != 2);
+
+ }
+
+ /**
+ * Closes this queue executor's queue and optionally waits for it to empty.
+ *
+ * If wait is {@code true}, then the queue will be empty by the time this call completes.
+ *
+ *
+ * This function is MT-Safe.
+ *
+ * @param wait If this call is to wait until the queue is empty
+ * @param killQueue Whether to shutdown this thread's queue
+ * @return whether this thread shut down the queue
+ */
+ public boolean close(final boolean wait, final boolean killQueue) {
+ boolean ret = !killQueue ? false : this.queue.shutdown();
+ this.closed = true;
+
+ // force thread to respond to the shutdown
+ this.parked.set(false);
+ LockSupport.unpark(this);
+
+ if (wait) {
+ this.flush();
+ }
+ return ret;
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java
new file mode 100644
index 000000000..59aec1032
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java
@@ -0,0 +1,172 @@
+package com.destroystokyo.paper.io;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mojang.datafixers.util.Pair;
+import it.unimi.dsi.fastutil.longs.Long2IntMap;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.minecraft.server.World;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+public class SyncLoadFinder {
+
+ public static final boolean ENABLED = Boolean.getBoolean("paper.debug-sync-loads");
+
+ private static final WeakHashMap> SYNC_LOADS = new WeakHashMap<>();
+
+ private static final class SyncLoadInformation {
+
+ public int times;
+
+ public final Long2IntOpenHashMap coordinateTimes = new Long2IntOpenHashMap();
+ }
+
+ public static void logSyncLoad(final World world, final int chunkX, final int chunkZ) {
+ if (!ENABLED) {
+ return;
+ }
+
+ final ThrowableWithEquals stacktrace = new ThrowableWithEquals(Thread.currentThread().getStackTrace());
+
+ SYNC_LOADS.compute(world, (final World keyInMap, Object2ObjectOpenHashMap map) -> {
+ if (map == null) {
+ map = new Object2ObjectOpenHashMap<>();
+ }
+
+ map.compute(stacktrace, (ThrowableWithEquals keyInMap0, SyncLoadInformation valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new SyncLoadInformation();
+ }
+
+ ++valueInMap.times;
+
+ valueInMap.coordinateTimes.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (Long keyInMap1, Integer valueInMap1) -> {
+ return valueInMap1 == null ? Integer.valueOf(1) : Integer.valueOf(valueInMap1.intValue() + 1);
+ });
+
+ return valueInMap;
+ });
+
+ return map;
+ });
+ }
+
+ public static JsonObject serialize() {
+ final JsonObject ret = new JsonObject();
+
+ final JsonArray worldsData = new JsonArray();
+
+ for (final Map.Entry> entry : SYNC_LOADS.entrySet()) {
+ final World world = entry.getKey();
+
+ final JsonObject worldData = new JsonObject();
+
+ worldData.addProperty("name", world.getWorld().getName());
+
+ final List> data = new ArrayList<>();
+
+ entry.getValue().forEach((ThrowableWithEquals stacktrace, SyncLoadInformation times) -> {
+ data.add(new Pair<>(stacktrace, times));
+ });
+
+ data.sort((Pair pair1, Pair pair2) -> {
+ return Integer.compare(pair2.getSecond().times, pair1.getSecond().times); // reverse order
+ });
+
+ final JsonArray stacktraces = new JsonArray();
+
+ for (Pair pair : data) {
+ final JsonObject stacktrace = new JsonObject();
+
+ stacktrace.addProperty("times", pair.getSecond().times);
+
+ final JsonArray traces = new JsonArray();
+
+ for (StackTraceElement element : pair.getFirst().stacktrace) {
+ traces.add(String.valueOf(element));
+ }
+
+ stacktrace.add("stacktrace", traces);
+
+ final JsonArray coordinates = new JsonArray();
+
+ for (Long2IntMap.Entry coordinate : pair.getSecond().coordinateTimes.long2IntEntrySet()) {
+ final long key = coordinate.getLongKey();
+ final int times = coordinate.getIntValue();
+ coordinates.add("(" + IOUtil.getCoordinateX(key) + "," + IOUtil.getCoordinateZ(key) + "): " + times);
+ }
+
+ stacktrace.add("coordinates", coordinates);
+
+ stacktraces.add(stacktrace);
+ }
+
+
+ worldData.add("stacktraces", stacktraces);
+ worldsData.add(worldData);
+ }
+
+ ret.add("worlds", worldsData);
+
+ return ret;
+ }
+
+ static final class ThrowableWithEquals {
+
+ private final StackTraceElement[] stacktrace;
+ private final int hash;
+
+ public ThrowableWithEquals(final StackTraceElement[] stacktrace) {
+ this.stacktrace = stacktrace;
+ this.hash = ThrowableWithEquals.hash(stacktrace);
+ }
+
+ public static int hash(final StackTraceElement[] stacktrace) {
+ int hash = 0;
+
+ for (int i = 0; i < stacktrace.length; ++i) {
+ hash *= 31;
+ hash += stacktrace[i].hashCode();
+ }
+
+ return hash;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hash;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+
+ final ThrowableWithEquals other = (ThrowableWithEquals)obj;
+ final StackTraceElement[] otherStackTrace = other.stacktrace;
+
+ if (this.stacktrace.length != otherStackTrace.length) {
+ return false;
+ }
+
+ if (this == obj) {
+ return true;
+ }
+
+ for (int i = 0; i < this.stacktrace.length; ++i) {
+ if (!this.stacktrace[i].equals(otherStackTrace[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
new file mode 100644
index 000000000..305da4786
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
@@ -0,0 +1,149 @@
+package com.destroystokyo.paper.io.chunk;
+
+import co.aikar.timings.Timing;
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.IOUtil;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ChunkRegionLoader;
+import net.minecraft.server.PlayerChunkMap;
+import net.minecraft.server.WorldServer;
+
+import java.util.ArrayDeque;
+import java.util.function.Consumer;
+
+public final class ChunkLoadTask extends ChunkTask {
+
+ public boolean cancelled;
+
+ Consumer onComplete;
+ public PaperFileIOThread.ChunkData chunkData;
+
+ private boolean hasCompleted;
+
+ public ChunkLoadTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager,
+ final Consumer onComplete) {
+ super(world, chunkX, chunkZ, priority, taskManager);
+ this.onComplete = onComplete;
+ }
+
+ private static final ArrayDeque EMPTY_QUEUE = new ArrayDeque<>();
+
+ private static ChunkRegionLoader.InProgressChunkHolder createEmptyHolder() {
+ return new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE);
+ }
+
+ @Override
+ public void run() {
+ try {
+ this.executeTask();
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Failed to execute chunk load task: " + this.toString(), ex);
+ if (!this.hasCompleted) {
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ }
+ }
+ }
+
+ private boolean checkCancelled() {
+ if (this.cancelled) {
+ // IntelliJ does not understand writes may occur to cancelled concurrently.
+ return this.taskManager.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != ChunkLoadTask.this) {
+ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other: " + valueInMap + ", current: " + ChunkLoadTask.this);
+ }
+
+ if (valueInMap.cancelled) {
+ return null;
+ }
+ return valueInMap;
+ }) == null;
+ }
+ return false;
+ }
+
+ public void executeTask() {
+ if (this.checkCancelled()) {
+ return;
+ }
+
+ // either executed synchronously or asynchronously
+ final PaperFileIOThread.ChunkData chunkData = this.chunkData;
+
+ if (chunkData.poiData == PaperFileIOThread.FAILURE_VALUE || chunkData.chunkData == PaperFileIOThread.FAILURE_VALUE) {
+ PaperFileIOThread.LOGGER.error("Could not load chunk for task: " + this.toString() + ", file IO thread has dumped the relevant exception above");
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ if (chunkData.chunkData == null) {
+ // not on disk
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(this.chunkX, this.chunkZ);
+
+ final PlayerChunkMap chunkManager = this.world.getChunkProvider().playerChunkMap;
+
+ try (Timing ignored = this.world.timings.chunkIOStage1.startTimingIfSync()) {
+ final ChunkRegionLoader.InProgressChunkHolder chunkHolder;
+
+ // apply fixes
+
+ try {
+ if (chunkData.poiData != null) {
+ chunkData.poiData = chunkData.poiData.clone(); // clone data for safety, file IO thread does not clone
+ }
+ chunkData.chunkData = chunkManager.getChunkData(this.world.getWorldProvider().getDimensionManager(),
+ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData.clone(), chunkPos, this.world); // clone data for safety, file IO thread does not clone
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Could not apply datafixers for chunk task: " + this.toString(), ex);
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ }
+
+ if (this.checkCancelled()) {
+ return;
+ }
+
+ try {
+ this.world.getChunkProvider().playerChunkMap.updateChunkStatusOnDisk(chunkPos, chunkData.chunkData);
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.warn("Failed to update chunk status cache for task: " + this.toString(), ex);
+ // non-fatal, continue
+ }
+
+ try {
+ chunkHolder = ChunkRegionLoader.loadChunk(this.world,
+ chunkManager.definedStructureManager, chunkManager.getVillagePlace(), chunkPos,
+ chunkData.chunkData, true);
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Could not de-serialize chunk data for task: " + this.toString(), ex);
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ this.complete(chunkHolder);
+ }
+ }
+
+ private void complete(final ChunkRegionLoader.InProgressChunkHolder holder) {
+ this.hasCompleted = true;
+ holder.poiData = this.chunkData == null ? null : this.chunkData.poiData;
+
+ this.taskManager.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != ChunkLoadTask.this) {
+ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other: " + valueInMap + ", current: " + ChunkLoadTask.this);
+ }
+ if (valueInMap.cancelled) {
+ return null;
+ }
+ try {
+ ChunkLoadTask.this.onComplete.accept(holder);
+ } catch (final Throwable thr) {
+ PaperFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
+ }
+ return null;
+ });
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
new file mode 100644
index 000000000..60312b85f
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
@@ -0,0 +1,112 @@
+package com.destroystokyo.paper.io.chunk;
+
+import co.aikar.timings.Timing;
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.IOUtil;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import net.minecraft.server.ChunkRegionLoader;
+import net.minecraft.server.IAsyncTaskHandler;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.NBTTagCompound;
+import net.minecraft.server.WorldServer;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class ChunkSaveTask extends ChunkTask {
+
+ public final ChunkRegionLoader.AsyncSaveData asyncSaveData;
+ public final IChunkAccess chunk;
+ public final CompletableFuture onComplete = new CompletableFuture<>();
+
+ private final AtomicInteger attemptedPriority;
+
+ public ChunkSaveTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager, final ChunkRegionLoader.AsyncSaveData asyncSaveData,
+ final IChunkAccess chunk) {
+ super(world, chunkX, chunkZ, priority, taskManager);
+ this.chunk = chunk;
+ this.asyncSaveData = asyncSaveData;
+ this.attemptedPriority = new AtomicInteger(priority);
+ }
+
+ @Override
+ public void run() {
+ // can be executed asynchronously or synchronously
+ final NBTTagCompound compound;
+
+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTimingIfSync()) {
+ compound = ChunkRegionLoader.saveChunk(this.world, this.chunk, this.asyncSaveData);
+ } catch (final Throwable ex) {
+ // has a plugin modified something it should not have and made us CME?
+ PaperFileIOThread.LOGGER.error("Failed to serialize unloading chunk data for task: " + this.toString() + ", falling back to a synchronous execution", ex);
+
+ // Note: We add to the server thread queue here since this is what the server will drain tasks from
+ // when waiting for chunks
+ ChunkTaskManager.queueChunkWaitTask(() -> {
+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTiming()) {
+ NBTTagCompound data = PaperFileIOThread.FAILURE_VALUE;
+
+ try {
+ data = ChunkRegionLoader.saveChunk(this.world, this.chunk, this.asyncSaveData);
+ PaperFileIOThread.LOGGER.info("Successfully serialized chunk data for task: " + this.toString() + " synchronously");
+ } catch (final Throwable ex1) {
+ PaperFileIOThread.LOGGER.fatal("Failed to synchronously serialize unloading chunk data for task: " + this.toString() + "! Chunk data will be lost", ex1);
+ }
+
+ ChunkSaveTask.this.complete(data);
+ }
+ });
+
+ return; // the main thread will now complete the data
+ }
+
+ this.complete(compound);
+ }
+
+ @Override
+ public boolean raisePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalStateException("Invalid priority: " + priority);
+ }
+
+ // we know priority is valid here
+ for (int curr = this.attemptedPriority.get();;) {
+ if (curr <= priority) {
+ break; // curr is higher/same priority
+ }
+ if (this.attemptedPriority.compareAndSet(curr, priority)) {
+ break;
+ }
+ curr = this.attemptedPriority.get();
+ }
+
+ return super.raisePriority(priority);
+ }
+
+ @Override
+ public boolean updatePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalStateException("Invalid priority: " + priority);
+ }
+ this.attemptedPriority.set(priority);
+ return super.updatePriority(priority);
+ }
+
+ private void complete(final NBTTagCompound compound) {
+ try {
+ this.onComplete.complete(compound);
+ } catch (final Throwable thr) {
+ PaperFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
+ }
+ if (compound != PaperFileIOThread.FAILURE_VALUE) {
+ PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, this.chunkX, this.chunkZ, null, compound, this.attemptedPriority.get());
+ }
+ this.taskManager.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
+ if (valueInMap != ChunkSaveTask.this) {
+ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other: " + valueInMap + ", this: " + ChunkSaveTask.this);
+ }
+ return null;
+ });
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
new file mode 100644
index 000000000..1dfa8abfd
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
@@ -0,0 +1,40 @@
+package com.destroystokyo.paper.io.chunk;
+
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import net.minecraft.server.WorldServer;
+
+abstract class ChunkTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ public final WorldServer world;
+ public final int chunkX;
+ public final int chunkZ;
+ public final ChunkTaskManager taskManager;
+
+ public ChunkTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager) {
+ super(priority);
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.taskManager = taskManager;
+ }
+
+ @Override
+ public String toString() {
+ return "Chunk task: class:" + this.getClass().getName() + ", for world '" + this.world.getWorld().getName() +
+ "', (" + this.chunkX + "," + this.chunkZ + "), hashcode:" + this.hashCode() + ", priority: " + this.getPriority();
+ }
+
+ @Override
+ public boolean raisePriority(final int priority) {
+ PaperFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority);
+ return super.raisePriority(priority);
+ }
+
+ @Override
+ public boolean updatePriority(final int priority) {
+ PaperFileIOThread.Holder.INSTANCE.setPriority(this.world, this.chunkX, this.chunkZ, priority);
+ return super.updatePriority(priority);
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
new file mode 100644
index 000000000..59d73bfad
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
@@ -0,0 +1,453 @@
+package com.destroystokyo.paper.io.chunk;
+
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.IOUtil;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import com.destroystokyo.paper.io.QueueExecutorThread;
+import net.minecraft.server.ChunkRegionLoader;
+import net.minecraft.server.IAsyncTaskHandler;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.NBTTagCompound;
+import net.minecraft.server.WorldServer;
+import org.apache.logging.log4j.Level;
+import org.bukkit.Bukkit;
+import org.spigotmc.AsyncCatcher;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Consumer;
+
+public final class ChunkTaskManager {
+
+ private final QueueExecutorThread[] workers;
+ private final WorldServer world;
+
+ private final PrioritizedTaskQueue queue;
+ private final boolean perWorldQueue;
+
+ final ConcurrentHashMap chunkLoadTasks = new ConcurrentHashMap<>(64, 0.5f);
+ final ConcurrentHashMap chunkSaveTasks = new ConcurrentHashMap<>(64, 0.5f);
+
+ private final PrioritizedTaskQueue chunkTasks = new PrioritizedTaskQueue<>(); // used if async chunks are disabled in config
+
+ protected static QueueExecutorThread[] globalWorkers;
+ protected static PrioritizedTaskQueue globalQueue;
+
+ protected static final ConcurrentLinkedQueue CHUNK_WAIT_QUEUE = new ConcurrentLinkedQueue<>();
+
+ public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack
+
+ private static final class ChunkInfo {
+
+ public final int chunkX;
+ public final int chunkZ;
+ public final WorldServer world;
+
+ public ChunkInfo(final int chunkX, final int chunkZ, final WorldServer world) {
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+ }
+
+ @Override
+ public String toString() {
+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']";
+ }
+ }
+
+ public static void pushChunkWait(final WorldServer world, final int chunkX, final int chunkZ) {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
+ }
+ }
+
+ public static void popChunkWait() {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.pop();
+ }
+ }
+
+ public static String getChunkWaitInfo() {
+ synchronized (WAITING_CHUNKS) {
+ return WAITING_CHUNKS.toString();
+ }
+ }
+
+ public static void dumpAllChunkLoadInfo() {
+ synchronized (WAITING_CHUNKS) {
+ if (WAITING_CHUNKS.isEmpty()) {
+ return;
+ }
+
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk wait task info below: ");
+
+ for (final ChunkInfo chunkInfo : WAITING_CHUNKS) {
+ final long key = IOUtil.getCoordinateKey(chunkInfo.chunkX, chunkInfo.chunkZ);
+ final ChunkLoadTask loadTask = chunkInfo.world.asyncChunkTaskManager.chunkLoadTasks.get(key);
+ final ChunkSaveTask saveTask = chunkInfo.world.asyncChunkTaskManager.chunkSaveTasks.get(key);
+
+ PaperFileIOThread.LOGGER.log(Level.ERROR, chunkInfo.chunkX + "," + chunkInfo.chunkZ + " in '" + chunkInfo.world.getWorld().getName() + ":");
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Load Task - " + (loadTask == null ? "none" : loadTask.toString()));
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Save Task - " + (saveTask == null ? "none" : saveTask.toString()));
+ // log current status of chunk to indicate whether we're waiting on generation or loading
+ net.minecraft.server.PlayerChunk chunkHolder = chunkInfo.world.getChunkProvider().playerChunkMap.getVisibleChunk(key);
+
+ if (chunkHolder == null) {
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk Holder - null");
+ } else {
+ IChunkAccess chunk = chunkHolder.getAvailableChunkNow();
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk Holder - non-null");
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getChunkStatus().toString()));
+ }
+
+ }
+ }
+ }
+
+ public static void initGlobalLoadThreads(int threads) {
+ if (threads <= 0 || globalWorkers != null) {
+ return;
+ }
+
+ globalWorkers = new QueueExecutorThread[threads];
+ globalQueue = new PrioritizedTaskQueue<>();
+
+ for (int i = 0; i < threads; ++i) {
+ globalWorkers[i] = new QueueExecutorThread<>(globalQueue, (long)0.10e6); //0.1ms
+ globalWorkers[i].setName("Paper Async Chunk Task Thread #" + i);
+ globalWorkers[i].setPriority(Thread.NORM_PRIORITY - 1);
+ globalWorkers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
+ });
+
+ globalWorkers[i].start();
+ }
+ }
+
+ /**
+ * Creates this chunk task manager to operate off the specified number of threads. If the specified number of threads is
+ * less-than or equal to 0, then this chunk task manager will operate off of the world's chunk task queue.
+ * @param world Specified world.
+ * @param threads Specified number of threads.
+ * @see net.minecraft.server.ChunkProviderServer#serverThreadQueue
+ */
+ public ChunkTaskManager(final WorldServer world, final int threads) {
+ this.world = world;
+ this.workers = threads <= 0 ? null : new QueueExecutorThread[threads];
+ this.queue = new PrioritizedTaskQueue<>();
+ this.perWorldQueue = true;
+
+ for (int i = 0; i < threads; ++i) {
+ this.workers[i] = new QueueExecutorThread<>(this.queue, (long)0.10e6); //0.1ms
+ this.workers[i].setName("Async chunk loader thread #" + i + " for world: " + world.getWorldData().getName());
+ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1);
+ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
+ });
+
+ this.workers[i].start();
+ }
+ }
+
+ /**
+ * Creates the chunk task manager to work from the global workers. When {@link #close(boolean)} is invoked,
+ * the global queue is not shutdown. If the global workers is configured to be disabled or use 0 threads, then
+ * this chunk task manager will operate off of the world's chunk task queue.
+ * @param world The world that this task manager is responsible for
+ * @see net.minecraft.server.ChunkProviderServer#serverThreadQueue
+ */
+ public ChunkTaskManager(final WorldServer world) {
+ this.world = world;
+ this.workers = globalWorkers;
+ this.queue = globalQueue;
+ this.perWorldQueue = false;
+ }
+
+ public boolean pollNextChunkTask() {
+ final ChunkTask task = this.chunkTasks.poll();
+
+ if (task != null) {
+ task.run();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Polls and runs the next available chunk wait queue task. This is to be used when the server is waiting on a chunk queue.
+ * (per-world can cause issues if all the worker threads are blocked waiting for a response from the main thread)
+ */
+ public static boolean pollChunkWaitQueue() {
+ final Runnable run = CHUNK_WAIT_QUEUE.poll();
+ if (run != null) {
+ run.run();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Queues a chunk wait task. Note that this will execute out of order with respect to tasks scheduled on a world's
+ * chunk task queue, since this is the global chunk wait queue.
+ */
+ public static void queueChunkWaitTask(final Runnable runnable) {
+ CHUNK_WAIT_QUEUE.add(runnable);
+ }
+
+ private static void drainChunkWaitQueue() {
+ Runnable run;
+ while ((run = CHUNK_WAIT_QUEUE.poll()) != null) {
+ run.run();
+ }
+ }
+
+ /**
+ * The exact same as {@link #scheduleChunkLoad(int, int, int, Consumer, boolean)}, except that the chunk data is provided as
+ * the {@code data} parameter.
+ */
+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
+ final Consumer onComplete,
+ final boolean intendingToBlock, final CompletableFuture dataFuture) {
+ final WorldServer world = this.world;
+
+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != null) {
+ if (!valueInMap.cancelled) {
+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
+ }
+ valueInMap.cancelled = false;
+ valueInMap.onComplete = onComplete;
+ return valueInMap;
+ }
+
+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
+
+ dataFuture.thenAccept((final NBTTagCompound data) -> {
+ final boolean failed = data == PaperFileIOThread.FAILURE_VALUE;
+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
+ ret.chunkData = chunkData;
+ if (!failed) {
+ chunkData.chunkData = data;
+ }
+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
+ }, true, failed, intendingToBlock); // read data off disk if the future fails
+ });
+
+ return ret;
+ });
+ }
+
+ public void cancelChunkLoad(final int chunkX, final int chunkZ) {
+ this.chunkLoadTasks.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap == null) {
+ return null;
+ }
+
+ if (valueInMap.cancelled) {
+ PaperFileIOThread.LOGGER.warn("Task " + valueInMap.toString() + " is already cancelled!");
+ }
+ valueInMap.cancelled = true;
+ if (valueInMap.cancel()) {
+ return null;
+ }
+
+ return valueInMap;
+ });
+ }
+
+ /**
+ * Schedules an asynchronous chunk load for the specified coordinates. The onComplete parameter may be invoked asynchronously
+ * on a worker thread or on the world's chunk executor queue. As such the code that is executed for the parameter should be
+ * carefully chosen.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority for this task
+ * @param onComplete The consumer to invoke with the {@link net.minecraft.server.ChunkRegionLoader.InProgressChunkHolder} object once this task is complete
+ * @param intendingToBlock Whether the caller is intending to block on this task completing (this is a performance tune, and has no adverse side-effects)
+ * @return The {@link ChunkLoadTask} associated with
+ */
+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
+ final Consumer onComplete,
+ final boolean intendingToBlock) {
+ final WorldServer world = this.world;
+
+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != null) {
+ if (!valueInMap.cancelled) {
+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
+ }
+ valueInMap.cancelled = false;
+ valueInMap.onComplete = onComplete;
+ return valueInMap;
+ }
+
+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
+
+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
+ ret.chunkData = chunkData;
+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
+ }, true, true, intendingToBlock);
+
+ return ret;
+ });
+ }
+
+ /**
+ * Schedules an async save for the specified chunk. The chunk, at the beginning of this call, must be completely unloaded
+ * from the world.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority for this task
+ * @param asyncSaveData Async save data. See {@link ChunkRegionLoader#getAsyncSaveData(WorldServer, IChunkAccess)}
+ * @param chunk Chunk to save
+ * @return The {@link ChunkSaveTask} associated with the save task.
+ */
+ public ChunkSaveTask scheduleChunkSave(final int chunkX, final int chunkZ, final int priority,
+ final ChunkRegionLoader.AsyncSaveData asyncSaveData,
+ final IChunkAccess chunk) {
+ AsyncCatcher.catchOp("chunk save schedule");
+
+ final WorldServer world = this.world;
+
+ return this.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
+ if (valueInMap != null) {
+ throw new IllegalStateException("Double scheduling chunk save for task: " + valueInMap.toString());
+ }
+
+ final ChunkSaveTask ret = new ChunkSaveTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, asyncSaveData, chunk);
+
+ ChunkTaskManager.this.internalSchedule(ret);
+
+ return ret;
+ });
+ }
+
+ /**
+ * Returns a completable future which will be completed with the un-copied chunk data for an in progress async save.
+ * Returns {@code null} if no save is in progress.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ */
+ public CompletableFuture getChunkSaveFuture(final int chunkX, final int chunkZ) {
+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
+ if (chunkSaveTask == null) {
+ return null;
+ }
+ return chunkSaveTask.onComplete;
+ }
+
+ /**
+ * Returns the chunk object being used to serialize data async for an unloaded chunk. Note that modifying this chunk
+ * is not safe to do as another thread is handling its save. The chunk is also not loaded into the world.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @return Chunk object for an in-progress async save, or {@code null} if no save is in progress
+ */
+ public IChunkAccess getChunkInSaveProgress(final int chunkX, final int chunkZ) {
+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
+ if (chunkSaveTask == null) {
+ return null;
+ }
+ return chunkSaveTask.chunk;
+ }
+
+ public void flush() {
+ // flush here since we schedule tasks on the IO thread that can schedule tasks here
+ drainChunkWaitQueue();
+ PaperFileIOThread.Holder.INSTANCE.flush();
+ drainChunkWaitQueue();
+
+ if (this.workers == null) {
+ if (Bukkit.isPrimaryThread()) {
+ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).executeAll();
+ } else {
+ CompletableFuture wait = new CompletableFuture<>();
+ MinecraftServer.getServer().scheduleOnMain(() -> {
+ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).executeAll();
+ });
+ wait.join();
+ }
+ } else {
+ for (final QueueExecutorThread worker : this.workers) {
+ worker.flush();
+ }
+ }
+
+ // flush again since tasks we execute async saves
+ drainChunkWaitQueue();
+ PaperFileIOThread.Holder.INSTANCE.flush();
+ }
+
+ public void close(final boolean wait) {
+ // flush here since we schedule tasks on the IO thread that can schedule tasks to this task manager
+ // we do this regardless of the wait param since after we invoke close no tasks can be queued
+ PaperFileIOThread.Holder.INSTANCE.flush();
+
+ if (this.workers == null) {
+ if (wait) {
+ this.flush();
+ }
+ return;
+ }
+
+ if (this.workers != globalWorkers) {
+ for (final QueueExecutorThread worker : this.workers) {
+ worker.close(false, this.perWorldQueue);
+ }
+ }
+
+ if (wait) {
+ this.flush();
+ }
+ }
+
+ public void raisePriority(final int chunkX, final int chunkZ, final int priority) {
+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
+ ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(chunkKey);
+ if (chunkSaveTask != null) {
+ final boolean raised = chunkSaveTask.raisePriority(priority);
+ if (chunkSaveTask.isScheduled() && raised) {
+ // only notify if we're in queue to be executed
+ this.internalScheduleNotify();
+ }
+ }
+
+ ChunkLoadTask chunkLoadTask = this.chunkLoadTasks.get(chunkKey);
+ if (chunkLoadTask != null) {
+ final boolean raised = chunkLoadTask.raisePriority(priority);
+ if (chunkLoadTask.isScheduled() && raised) {
+ // only notify if we're in queue to be executed
+ this.internalScheduleNotify();
+ }
+ }
+ }
+
+ protected void internalSchedule(final ChunkTask task) {
+ if (this.workers == null) {
+ this.chunkTasks.add(task);
+ return;
+ }
+
+ // It's important we order the task to be executed before notifying. Avoid a race condition where the worker thread
+ // wakes up and goes to sleep before we actually schedule (or it's just about to sleep)
+ this.queue.add(task);
+ this.internalScheduleNotify();
+ }
+
+ protected void internalScheduleNotify() {
+ if (this.workers == null) {
+ return;
+ }
+ for (final QueueExecutorThread worker : this.workers) {
+ if (worker.notifyTasks()) {
+ // break here since we only want to wake up one worker for scheduling one task
+ break;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/destroystokyo/paper/log/LogFullPolicy.java b/src/main/java/com/destroystokyo/paper/log/LogFullPolicy.java
new file mode 100644
index 000000000..db652a1f7
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/log/LogFullPolicy.java
@@ -0,0 +1,17 @@
+package com.destroystokyo.paper.log;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.async.AsyncQueueFullPolicy;
+import org.apache.logging.log4j.core.async.EventRoute;
+
+public final class LogFullPolicy implements AsyncQueueFullPolicy {
+
+ /*
+ * Prevents log calls being logged out of order when the log queue is full.
+ */
+
+ @Override
+ public EventRoute getRoute(final long backgroundThreadId, final Level level) {
+ return EventRoute.ENQUEUE;
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java
index a7dbe11cb..b151a13c1 100644
--- a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java
+++ b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java
@@ -1,18 +1,17 @@
package com.destroystokyo.paper.profile;
import com.destroystokyo.paper.PaperConfig;
+import com.google.common.base.Charsets;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
-
-import net.minecraft.server.AkarinUserCache;
import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.UserCache;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.spigotmc.SpigotConfig;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
@@ -144,9 +143,7 @@ public class CraftPlayerProfile implements PlayerProfile {
}
MinecraftServer server = MinecraftServer.getServer();
String name = profile.getName();
- // Akarin start
- AkarinUserCache userCache = server.getModernUserCache();
- /*
+ UserCache userCache = server.getUserCache();
if (profile.getId() == null) {
final GameProfile profile;
boolean isOnlineMode = server.getOnlineMode() || (SpigotConfig.bungee && PaperConfig.bungeeOnlineMode);
@@ -168,24 +165,7 @@ public class CraftPlayerProfile implements PlayerProfile {
this.profile = profile;
}
}
- */
-
- if (profile.getId() == null) {
- if (lookupName) {
- profile = userCache.acquire(name);
- } else {
- GameProfile peeked = userCache.peek(name);
-
- if (peeked != null)
- profile = peeked;
- }
- }
-
- if (profile.getName() == null)
- profile = userCache.acquire(name);
-
- return profile.isComplete();
- // Akarin end
+ return this.profile.isComplete();
}
public boolean complete(boolean textures) {
diff --git a/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java
new file mode 100644
index 000000000..9ebd7ecb7
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java
@@ -0,0 +1,253 @@
+package com.destroystokyo.paper.util;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.EntityPlayer;
+import net.minecraft.server.SectionPosition;
+import org.spigotmc.AsyncCatcher;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** @author Spottedleaf */
+public final class PlayerMobDistanceMap {
+
+ private static final PooledHashSets.PooledObjectLinkedOpenHashSet EMPTY_SET = new PooledHashSets.PooledObjectLinkedOpenHashSet<>();
+
+ private final Map players = new HashMap<>();
+ // we use linked for better iteration.
+ private final Long2ObjectOpenHashMap> playerMap = new Long2ObjectOpenHashMap<>(32, 0.5f);
+ private int viewDistance;
+
+ private final PooledHashSets pooledHashSets = new PooledHashSets<>();
+
+ public PooledHashSets.PooledObjectLinkedOpenHashSet getPlayersInRange(final ChunkCoordIntPair chunkPos) {
+ return this.getPlayersInRange(chunkPos.x, chunkPos.z);
+ }
+
+ public PooledHashSets.PooledObjectLinkedOpenHashSet getPlayersInRange(final int chunkX, final int chunkZ) {
+ return this.playerMap.getOrDefault(ChunkCoordIntPair.pair(chunkX, chunkZ), EMPTY_SET);
+ }
+
+ public void update(final List currentPlayers, final int newViewDistance) {
+ AsyncCatcher.catchOp("Distance map update");
+ final ObjectLinkedOpenHashSet gone = new ObjectLinkedOpenHashSet<>(this.players.keySet());
+
+ final int oldViewDistance = this.viewDistance;
+ this.viewDistance = newViewDistance;
+
+ for (final EntityPlayer player : currentPlayers) {
+ if (player.isSpectator() || !player.affectsSpawning) {
+ continue; // will be left in 'gone' (or not added at all)
+ }
+
+ gone.remove(player);
+
+ final SectionPosition newPosition = player.getPlayerMapSection();
+ final SectionPosition oldPosition = this.players.put(player, newPosition);
+
+ if (oldPosition == null) {
+ this.addNewPlayer(player, newPosition, newViewDistance);
+ } else {
+ this.updatePlayer(player, oldPosition, newPosition, oldViewDistance, newViewDistance);
+ }
+ //this.validatePlayer(player, newViewDistance); // debug only
+ }
+
+ for (final EntityPlayer player : gone) {
+ final SectionPosition oldPosition = this.players.remove(player);
+ if (oldPosition != null) {
+ this.removePlayer(player, oldPosition, oldViewDistance);
+ }
+ }
+ }
+
+ // expensive op, only for debug
+ private void validatePlayer(final EntityPlayer player, final int viewDistance) {
+ int entiesGot = 0;
+ int expectedEntries = (2 * viewDistance + 1);
+ expectedEntries *= expectedEntries;
+
+ final SectionPosition currPosition = player.getPlayerMapSection();
+
+ final int centerX = currPosition.getX();
+ final int centerZ = currPosition.getZ();
+
+ for (final Long2ObjectLinkedOpenHashMap.Entry> entry : this.playerMap.long2ObjectEntrySet()) {
+ final long key = entry.getLongKey();
+ final PooledHashSets.PooledObjectLinkedOpenHashSet map = entry.getValue();
+
+ if (map.referenceCount == 0) {
+ throw new IllegalStateException("Invalid map");
+ }
+
+ if (map.set.contains(player)) {
+ ++entiesGot;
+
+ final int chunkX = ChunkCoordIntPair.getX(key);
+ final int chunkZ = ChunkCoordIntPair.getZ(key);
+
+ final int dist = Math.max(Math.abs(chunkX - centerX), Math.abs(chunkZ - centerZ));
+
+ if (dist > viewDistance) {
+ throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist);
+ }
+ }
+ }
+
+ if (entiesGot != expectedEntries) {
+ throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot);
+ }
+ }
+
+ private void addPlayerTo(final EntityPlayer player, final int chunkX, final int chunkZ) {
+ this.playerMap.compute(ChunkCoordIntPair.pair(chunkX, chunkZ), (final Long key, final PooledHashSets.PooledObjectLinkedOpenHashSet players) -> {
+ if (players == null) {
+ return player.cachedSingleMobDistanceMap;
+ } else {
+ return PlayerMobDistanceMap.this.pooledHashSets.findMapWith(players, player);
+ }
+ });
+ }
+
+ private void removePlayerFrom(final EntityPlayer player, final int chunkX, final int chunkZ) {
+ this.playerMap.compute(ChunkCoordIntPair.pair(chunkX, chunkZ), (final Long keyInMap, final PooledHashSets.PooledObjectLinkedOpenHashSet players) -> {
+ return PlayerMobDistanceMap.this.pooledHashSets.findMapWithout(players, player); // rets null instead of an empty map
+ });
+ }
+
+ private void updatePlayer(final EntityPlayer player, final SectionPosition oldPosition, final SectionPosition newPosition, final int oldViewDistance, final int newViewDistance) {
+ final int toX = newPosition.getX();
+ final int toZ = newPosition.getZ();
+ final int fromX = oldPosition.getX();
+ final int fromZ = oldPosition.getZ();
+
+ final int dx = toX - fromX;
+ final int dz = toZ - fromZ;
+
+ final int totalX = Math.abs(fromX - toX);
+ final int totalZ = Math.abs(fromZ - toZ);
+
+ if (Math.max(totalX, totalZ) > (2 * oldViewDistance)) {
+ // teleported?
+ this.removePlayer(player, oldPosition, oldViewDistance);
+ this.addNewPlayer(player, newPosition, newViewDistance);
+ return;
+ }
+
+ // x axis is width
+ // z axis is height
+ // right refers to the x axis of where we moved
+ // top refers to the z axis of where we moved
+
+ if (oldViewDistance == newViewDistance) {
+ // same view distance
+
+ // used for relative positioning
+ final int up = 1 | (dz >> (Integer.SIZE - 1)); // 1 if dz >= 0, -1 otherwise
+ final int right = 1 | (dx >> (Integer.SIZE - 1)); // 1 if dx >= 0, -1 otherwise
+
+ // The area excluded by overlapping the two view distance squares creates four rectangles:
+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
+ // and on the right the "added" section.
+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
+ // exclusive to the regions they surround.
+
+ // 4 points of the rectangle
+ int maxX; // exclusive
+ int minX; // inclusive
+ int maxZ; // exclusive
+ int minZ; // inclusive
+
+ if (dx != 0) {
+ // handle right addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX + (oldViewDistance * right) + right; // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addPlayerTo(player, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle up addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = toX - (oldViewDistance * right); // inclusive
+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addPlayerTo(player, currX, currZ);
+ }
+ }
+ }
+
+ if (dx != 0) {
+ // handle left removal
+
+ maxX = toX - (oldViewDistance * right); // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removePlayerFrom(player, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle down removal
+
+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = toZ - (oldViewDistance * up); // exclusive
+ minZ = fromZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removePlayerFrom(player, currX, currZ);
+ }
+ }
+ }
+ } else {
+ // different view distance
+ // for now :)
+ this.removePlayer(player, oldPosition, oldViewDistance);
+ this.addNewPlayer(player, newPosition, newViewDistance);
+ }
+ }
+
+ private void removePlayer(final EntityPlayer player, final SectionPosition position, final int viewDistance) {
+ final int x = position.getX();
+ final int z = position.getZ();
+
+ for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) {
+ for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) {
+ this.removePlayerFrom(player, x + xoff, z + zoff);
+ }
+ }
+ }
+
+ private void addNewPlayer(final EntityPlayer player, final SectionPosition position, final int viewDistance) {
+ final int x = position.getX();
+ final int z = position.getZ();
+
+ for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) {
+ for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) {
+ this.addPlayerTo(player, x + xoff, z + zoff);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
new file mode 100644
index 000000000..4f13d3ff8
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
@@ -0,0 +1,241 @@
+package com.destroystokyo.paper.util;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import java.lang.ref.WeakReference;
+import java.util.Iterator;
+
+/** @author Spottedleaf */
+public class PooledHashSets {
+
+ // we really want to avoid that equals() check as much as possible...
+ protected final Object2ObjectOpenHashMap, PooledObjectLinkedOpenHashSet> mapPool = new Object2ObjectOpenHashMap<>(64, 0.25f);
+
+ protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet current) {
+ if (current.referenceCount == 0) {
+ throw new IllegalStateException("Cannot decrement reference count for " + current);
+ }
+ if (current.referenceCount == -1 || --current.referenceCount > 0) {
+ return;
+ }
+
+ this.mapPool.remove(current);
+ return;
+ }
+
+ public PooledObjectLinkedOpenHashSet findMapWith(final PooledObjectLinkedOpenHashSet current, final E object) {
+ final PooledObjectLinkedOpenHashSet cached = current.getAddCache(object);
+
+ if (cached != null) {
+ if (cached.referenceCount != -1) {
+ ++cached.referenceCount;
+ }
+
+ decrementReferenceCount(current);
+
+ return cached;
+ }
+
+ if (!current.add(object)) {
+ return current;
+ }
+
+ // we use get/put since we use a different key on put
+ PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current);
+
+ if (ret == null) {
+ ret = new PooledObjectLinkedOpenHashSet<>(current);
+ current.remove(object);
+ this.mapPool.put(ret, ret);
+ ret.referenceCount = 1;
+ } else {
+ if (ret.referenceCount != -1) {
+ ++ret.referenceCount;
+ }
+ current.remove(object);
+ }
+
+ current.updateAddCache(object, ret);
+
+ decrementReferenceCount(current);
+ return ret;
+ }
+
+ // rets null if current.size() == 1
+ public PooledObjectLinkedOpenHashSet findMapWithout(final PooledObjectLinkedOpenHashSet current, final E object) {
+ if (current.set.size() == 1) {
+ decrementReferenceCount(current);
+ return null;
+ }
+
+ final PooledObjectLinkedOpenHashSet cached = current.getRemoveCache(object);
+
+ if (cached != null) {
+ if (cached.referenceCount != -1) {
+ ++cached.referenceCount;
+ }
+
+ decrementReferenceCount(current);
+
+ return cached;
+ }
+
+ if (!current.remove(object)) {
+ return current;
+ }
+
+ // we use get/put since we use a different key on put
+ PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current);
+
+ if (ret == null) {
+ ret = new PooledObjectLinkedOpenHashSet<>(current);
+ current.add(object);
+ this.mapPool.put(ret, ret);
+ ret.referenceCount = 1;
+ } else {
+ if (ret.referenceCount != -1) {
+ ++ret.referenceCount;
+ }
+ current.add(object);
+ }
+
+ current.updateRemoveCache(object, ret);
+
+ decrementReferenceCount(current);
+ return ret;
+ }
+
+ public static final class PooledObjectLinkedOpenHashSet implements Iterable {
+
+ private static final WeakReference NULL_REFERENCE = new WeakReference(null);
+
+ final ObjectLinkedOpenHashSet set;
+ int referenceCount; // -1 if special
+ int hash; // optimize hashcode
+
+ // add cache
+ WeakReference lastAddObject = NULL_REFERENCE;
+ WeakReference> lastAddMap = NULL_REFERENCE;
+
+ // remove cache
+ WeakReference lastRemoveObject = NULL_REFERENCE;
+ WeakReference> lastRemoveMap = NULL_REFERENCE;
+
+ public PooledObjectLinkedOpenHashSet() {
+ this.set = new ObjectLinkedOpenHashSet<>(2, 0.6f);
+ }
+
+ public PooledObjectLinkedOpenHashSet(final E single) {
+ this();
+ this.referenceCount = -1;
+ this.add(single);
+ }
+
+ public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet other) {
+ this.set = other.set.clone();
+ this.hash = other.hash;
+ }
+
+ // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+ // generated by https://github.com/skeeto/hash-prospector
+ static int hash0(int x) {
+ x *= 0x36935555;
+ x ^= x >>> 16;
+ return x;
+ }
+
+ public PooledObjectLinkedOpenHashSet getAddCache(final E element) {
+ final E currentAdd = this.lastAddObject.get();
+
+ if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
+ return null;
+ }
+
+ final PooledObjectLinkedOpenHashSet map = this.lastAddMap.get();
+ if (map == null || map.referenceCount == 0) {
+ // we need to ret null if ref count is zero as calling code will assume the map is in use
+ return null;
+ }
+
+ return map;
+ }
+
+ public PooledObjectLinkedOpenHashSet getRemoveCache(final E element) {
+ final E currentRemove = this.lastRemoveObject.get();
+
+ if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
+ return null;
+ }
+
+ final PooledObjectLinkedOpenHashSet map = this.lastRemoveMap.get();
+ if (map == null || map.referenceCount == 0) {
+ // we need to ret null if ref count is zero as calling code will assume the map is in use
+ return null;
+ }
+
+ return map;
+ }
+
+ public void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet map) {
+ this.lastAddObject = new WeakReference<>(element);
+ this.lastAddMap = new WeakReference<>(map);
+ }
+
+ public void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet map) {
+ this.lastRemoveObject = new WeakReference<>(element);
+ this.lastRemoveMap = new WeakReference<>(map);
+ }
+
+ boolean add(final E element) {
+ boolean added = this.set.add(element);
+
+ if (added) {
+ this.hash += hash0(element.hashCode());
+ }
+
+ return added;
+ }
+
+ boolean remove(Object element) {
+ boolean removed = this.set.remove(element);
+
+ if (removed) {
+ this.hash -= hash0(element.hashCode());
+ }
+
+ return removed;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this.set.iterator();
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hash;
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (!(other instanceof PooledObjectLinkedOpenHashSet)) {
+ return false;
+ }
+ if (this.referenceCount == 0) {
+ return other == this;
+ } else {
+ if (other == this) {
+ // Unfortunately we are never equal to our own instance while in use!
+ return false;
+ }
+ return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " +
+ this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java b/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java
deleted file mode 100644
index 8f18c2869..000000000
--- a/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java
+++ /dev/null
@@ -1,347 +0,0 @@
-package com.destroystokyo.paper.util;
-
-import javax.annotation.Nonnull;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
-
-/**
- * Implements an Executor Service that allows specifying Task Priority
- * and bumping of task priority.
- *
- * This is a non blocking executor with 3 priority levels.
- *
- * URGENT: Rarely used, something that is critical to take action now.
- * HIGH: Something with more importance than the base tasks
- *
- * @author Daniel Ennis <aikar@aikar.co>
- */
-@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"})
-public class PriorityQueuedExecutor extends AbstractExecutorService {
-
- private final ConcurrentLinkedQueue urgent = new ConcurrentLinkedQueue<>();
- private final ConcurrentLinkedQueue high = new ConcurrentLinkedQueue<>();
- private final ConcurrentLinkedQueue normal = new ConcurrentLinkedQueue<>();
- private final List threads = new ArrayList<>();
- private final RejectionHandler handler;
-
- private volatile boolean shuttingDown = false;
- private volatile boolean shuttingDownNow = false;
-
- public PriorityQueuedExecutor(String name) {
- this(name, Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
- }
-
- public PriorityQueuedExecutor(String name, int threads) {
- this(name, threads, Thread.NORM_PRIORITY, null);
- }
-
- public PriorityQueuedExecutor(String name, int threads, int threadPriority) {
- this(name, threads, threadPriority, null);
- }
-
- public PriorityQueuedExecutor(String name, int threads, RejectionHandler handler) {
- this(name, threads, Thread.NORM_PRIORITY, handler);
- }
-
- public PriorityQueuedExecutor(String name, int threads, int threadPriority, RejectionHandler handler) {
- for (int i = 0; i < threads; i++) {
- ExecutorThread thread = new ExecutorThread(this::processQueues);
- thread.setDaemon(true);
- thread.setName(threads == 1 ? name : name + "-" + (i + 1));
- thread.setPriority(threadPriority);
- thread.start();
- this.threads.add(thread);
- }
- if (handler == null) {
- handler = ABORT_POLICY;
- }
- this.handler = handler;
- }
-
- /**
- * If the Current thread belongs to a PriorityQueuedExecutor, return that Executro
- * @return The executor that controls this thread
- */
- public static PriorityQueuedExecutor getExecutor() {
- if (!(Thread.currentThread() instanceof ExecutorThread)) {
- return null;
- }
- return ((ExecutorThread) Thread.currentThread()).getExecutor();
- }
-
- public void shutdown() {
- shuttingDown = true;
- synchronized (this) {
- this.notifyAll();
- }
- }
-
- @Nonnull
- @Override
- public List shutdownNow() {
- shuttingDown = true;
- shuttingDownNow = true;
- List tasks = new ArrayList<>(high.size() + normal.size());
- Runnable run;
- while ((run = getTask()) != null) {
- tasks.add(run);
- }
-
- return tasks;
- }
-
- @Override
- public boolean isShutdown() {
- return shuttingDown;
- }
-
- @Override
- public boolean isTerminated() {
- if (!shuttingDown) {
- return false;
- }
- return high.isEmpty() && normal.isEmpty();
- }
-
- @Override
- public boolean awaitTermination(long timeout, @Nonnull TimeUnit unit) {
- synchronized (this) {
- this.notifyAll();
- }
- final long wait = unit.toNanos(timeout);
- final long max = System.nanoTime() + wait;
- for (;!threads.isEmpty() && System.nanoTime() < max;) {
- threads.removeIf(thread -> !thread.isAlive());
- }
- return isTerminated();
- }
-
-
- public PendingTask createPendingTask(Runnable task) {
- return createPendingTask(task, Priority.NORMAL);
- }
- public PendingTask createPendingTask(Runnable task, Priority priority) {
- return createPendingTask(() -> {
- task.run();
- return null;
- }, priority);
- }
-
- public PendingTask createPendingTask(Supplier task) {
- return createPendingTask(task, Priority.NORMAL);
- }
-
- public PendingTask createPendingTask(Supplier task, Priority priority) {
- return new PendingTask<>(task, priority);
- }
-
- public PendingTask submitTask(Runnable run) {
- return createPendingTask(run).submit();
- }
-
- public PendingTask submitTask(Runnable run, Priority priority) {
- return createPendingTask(run, priority).submit();
- }
-
- public PendingTask submitTask(Supplier run) {
- return createPendingTask(run).submit();
- }
-
- public PendingTask submitTask(Supplier run, Priority priority) {
- PendingTask task = createPendingTask(run, priority);
- return task.submit();
- }
-
- @Override
- public void execute(@Nonnull Runnable command) {
- submitTask(command);
- }
-
- public boolean isCurrentThread() {
- final Thread thread = Thread.currentThread();
- if (!(thread instanceof ExecutorThread)) {
- return false;
- }
- return ((ExecutorThread) thread).getExecutor() == this;
- }
-
- public Runnable getUrgentTask() {
- return urgent.poll();
- }
-
- public Runnable getTask() {
- Runnable run = urgent.poll();
- if (run != null) {
- return run;
- }
- run = high.poll();
- if (run != null) {
- return run;
- }
- return normal.poll();
- }
-
- private void processQueues() {
- Runnable run = null;
- while (true) {
- if (run != null) {
- run.run();
- }
- if (shuttingDownNow) {
- return;
- }
- if ((run = getTask()) != null) {
- continue;
- }
- synchronized (PriorityQueuedExecutor.this) {
- if ((run = getTask()) != null) {
- continue;
- }
-
- if (shuttingDown || shuttingDownNow) {
- return;
- }
- try {
- PriorityQueuedExecutor.this.wait();
- } catch (InterruptedException ignored) {
- }
- }
- }
- }
-
- public boolean processUrgentTasks() {
- Runnable run;
- boolean hadTask = false;
- while ((run = getUrgentTask()) != null) {
- run.run();
- hadTask = true;
- }
- return hadTask;
- }
-
- public enum Priority {
- NORMAL, HIGH, URGENT
- }
-
- public class ExecutorThread extends Thread {
- public ExecutorThread(Runnable runnable) {
- super(runnable);
- }
-
- public PriorityQueuedExecutor getExecutor() {
- return PriorityQueuedExecutor.this;
- }
- }
-
- public class PendingTask implements Runnable {
-
- private final AtomicBoolean hasRan = new AtomicBoolean();
- private final AtomicInteger submitted = new AtomicInteger(-1);
- private final AtomicInteger priority;
- private final Supplier run;
- private final CompletableFuture future = new CompletableFuture<>();
- private volatile PriorityQueuedExecutor executor;
-
- public PendingTask(Supplier run) {
- this(run, Priority.NORMAL);
- }
-
- public PendingTask(Supplier run, Priority priority) {
- this.priority = new AtomicInteger(priority.ordinal());
- this.run = run;
- }
-
- public boolean cancel() {
- return hasRan.compareAndSet(false, true);
- }
-
- @Override
- public void run() {
- if (!hasRan.compareAndSet(false, true)) {
- return;
- }
-
- try {
- future.complete(run.get());
- } catch (Throwable e) {
- future.completeExceptionally(e);
- }
- }
-
- public void bumpPriority() {
- bumpPriority(Priority.HIGH);
- }
-
- public void bumpPriority(Priority newPriority) {
- for (;;) {
- int current = this.priority.get();
- int ordinal = newPriority.ordinal();
- if (current >= ordinal || priority.compareAndSet(current, ordinal)) {
- break;
- }
- }
-
-
- if (this.submitted.get() == -1 || this.hasRan.get()) {
- return;
- }
-
- // Only resubmit if it hasnt ran yet and has been submitted
- submit();
- }
-
- public CompletableFuture onDone() {
- return future;
- }
-
- public PendingTask submit() {
- if (shuttingDown) {
- handler.onRejection(this, PriorityQueuedExecutor.this);
- return this;
- }
- for (;;) {
- final int submitted = this.submitted.get();
- final int priority = this.priority.get();
- if (submitted == priority) {
- return this;
- }
- if (this.submitted.compareAndSet(submitted, priority)) {
- if (priority == Priority.URGENT.ordinal()) {
- urgent.add(this);
- } else if (priority == Priority.HIGH.ordinal()) {
- high.add(this);
- } else {
- normal.add(this);
- }
-
- break;
- }
- }
-
- synchronized (PriorityQueuedExecutor.this) {
- // Wake up a thread to take this work
- PriorityQueuedExecutor.this.notify();
- }
- return this;
- }
- }
- public interface RejectionHandler {
- void onRejection(Runnable run, PriorityQueuedExecutor executor);
- }
-
- public static final RejectionHandler ABORT_POLICY = (run, executor) -> {
- throw new RejectedExecutionException("Executor has been shutdown");
- };
- public static final RejectionHandler CALLER_RUNS_POLICY = (run, executor) -> {
- run.run();
- };
-
-}
diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
index 4b3d62ab8..cf5661f1c 100644
--- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
@@ -13,6 +13,8 @@ import net.minecraft.server.Block;
import net.minecraft.server.BlockPosition;
import net.minecraft.server.BlockRedstoneWire;
import net.minecraft.server.IBlockData;
+import net.minecraft.server.Items;
+import net.minecraft.server.ItemStack;
import net.minecraft.server.World;
/**
@@ -320,7 +322,7 @@ public class RedstoneWireTurbo {
// be replicated here.
if (!wire.canPlace(null, worldIn, pos)) {
// Pop off the redstone dust
- oldState.dropNaturally(worldIn, pos, 0);
+ Block.a(worldIn, pos, new ItemStack(Items.REDSTONE)); // TODO
worldIn.setAir(pos);
// Mark this position as not being redstone wire
@@ -843,7 +845,7 @@ public class RedstoneWireTurbo {
// position directly above the node being calculated is always
// at index 1.
UpdateNode center_up = upd.neighbor_nodes[1];
- boolean center_up_is_cube = center_up.currentState.isOccluding();
+ boolean center_up_is_cube = center_up.currentState.isOccluding(worldIn, center_up.self); // TODO
for (int m = 0; m < 4; m++) {
// Get the neighbor array index of each of the four cardinal
@@ -857,7 +859,7 @@ public class RedstoneWireTurbo {
// Also check the positions above and below the cardinal
// neighbors
- boolean neighbor_is_cube = neighbor.currentState.isOccluding();
+ boolean neighbor_is_cube = neighbor.currentState.isOccluding(worldIn, neighbor.self); // TODO
if (!neighbor_is_cube) {
UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]];
l = getMaxCurrentStrength(neighbor_down, l);
@@ -881,7 +883,7 @@ public class RedstoneWireTurbo {
// egg82's amendment
// Adding Bukkit's BlockRedstoneEvent - er.. event.
if (i != j) {
- BlockRedstoneEvent event = new BlockRedstoneEvent(worldIn.getWorld().getBlockAt(upd.self), i, j); // Akarin
+ BlockRedstoneEvent event = new BlockRedstoneEvent(worldIn.getWorld().getBlockAt(upd.self.getX(), upd.self.getY(), upd.self.getZ()), i, j);
worldIn.getServer().getPluginManager().callEvent(event);
j = event.getNewCurrent();
}
diff --git a/src/main/java/com/destroystokyo/paper/util/ReentrantLockWithGetOwner.java b/src/main/java/com/destroystokyo/paper/util/ReentrantLockWithGetOwner.java
new file mode 100644
index 000000000..a3b174618
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/ReentrantLockWithGetOwner.java
@@ -0,0 +1,11 @@
+package com.destroystokyo.paper.util;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+public class ReentrantLockWithGetOwner extends ReentrantLock {
+
+ @Override
+ public Thread getOwner() {
+ return super.getOwner();
+ }
+}
diff --git a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
index 370c8716d..23f1447cf 100644
--- a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
+++ b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
@@ -18,10 +18,7 @@ import java.util.Set;
public class YggdrasilGameProfileRepository implements GameProfileRepository {
private static final Logger LOGGER = LogManager.getLogger();
- // Akarin Start
- //private static final String BASE_URL = "https://api.mojang.com/";
- private static final String BASE_URL = io.akarin.server.core.AkarinGlobalConfig.yggdrasilServerURL; //Akarin
- // Akarin End
+ private static final String BASE_URL = "https://api.mojang.com/";
private static final String SEARCH_PAGE_URL = BASE_URL + "profiles/";
private static final int ENTRIES_PER_PAGE = 2;
private static final int MAX_FAIL_COUNT = 3;
diff --git a/src/main/java/io/akarin/server/api/structure/CraftVillage.java b/src/main/java/io/akarin/server/api/structure/CraftVillage.java
deleted file mode 100644
index 121e77e28..000000000
--- a/src/main/java/io/akarin/server/api/structure/CraftVillage.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.akarin.server.api.structure;
-
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-public class CraftVillage implements Village {
- private final net.minecraft.server.Village village;
-
- public Village getHandle() {
- return (Village) village;
- }
-
- @Override
- public String toString() {
- return "CraftVillage";
- }
-}
diff --git a/src/main/java/io/akarin/server/core/AkarinAsyncExecutor.java b/src/main/java/io/akarin/server/core/AkarinAsyncExecutor.java
deleted file mode 100644
index 386919e27..000000000
--- a/src/main/java/io/akarin/server/core/AkarinAsyncExecutor.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package io.akarin.server.core;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-import co.aikar.timings.ThreadAssertion;
-
-public class AkarinAsyncExecutor {
- private static final ExecutorService singleExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("Akarin Single Async Executor Thread - %1$d").build());
- private static final ExecutorService asyncExecutor = Executors.newFixedThreadPool(getNThreads(), new ThreadFactoryBuilder().setNameFormat("Akarin Async Executor Thread - %1$d").build());
-
- private static int getNThreads() {
- int processors = Runtime.getRuntime().availableProcessors() / 2;
- if (processors < 2)
- return 2;
- if (processors > 8)
- return 8;
- return processors;
- }
-
- /**
- * Posts a task to be executed asynchronously in a single thread
- * @param run
- */
- public static void scheduleSingleAsyncTask(Runnable run) {
- ThreadAssertion.close();
- singleExecutor.execute(run);
- }
-
- /**
- * Posts a task to be executed asynchronously
- * @param run
- */
- public static void scheduleAsyncTask(Runnable run) {
- ThreadAssertion.close();
- asyncExecutor.execute(run);
- }
-
- /**
- * Posts a task to be executed asynchronously in a single thread
- * @param run
- * @return
- */
- public static Future scheduleSingleAsyncTask(Callable run) {
- ThreadAssertion.close();
- return singleExecutor.submit(run);
- }
-
- /**
- * Posts a task to be executed asynchronously
- * @param run
- */
- public static Future scheduleAsyncTask(Callable run) {
- ThreadAssertion.close();
- return asyncExecutor.submit(run);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/core/AkarinAsyncScheduler.java b/src/main/java/io/akarin/server/core/AkarinAsyncScheduler.java
deleted file mode 100644
index 253eb85e3..000000000
--- a/src/main/java/io/akarin/server/core/AkarinAsyncScheduler.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package io.akarin.server.core;
-
-import java.util.List;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
-
-import net.minecraft.server.EntityHuman;
-import net.minecraft.server.EntityPlayer;
-import net.minecraft.server.EnumDifficulty;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.NetworkManager;
-import net.minecraft.server.PacketPlayOutPlayerInfo;
-import net.minecraft.server.PacketPlayOutUpdateTime;
-import net.minecraft.server.WorldServer;
-
-public class AkarinAsyncScheduler extends Thread {
- private final static Logger LOGGER = LogManager.getLogger("Akarin");
- private final static int STD_TICK_TIME = 50;
-
- public static AkarinAsyncScheduler initalise() {
- return Singleton.instance;
- }
-
- private static class Singleton {
- private static final AkarinAsyncScheduler instance;
-
- static {
- instance = new AkarinAsyncScheduler();
- instance.setName("Akarin Async Scheduler Thread");
- instance.setDaemon(true);
- instance.setPriority(MIN_PRIORITY);
- instance.start();
- LOGGER.info("Async executor started");
- }
- }
-
- private int playerListTick;
-
- @Override
- public void run() {
- MinecraftServer server = MinecraftServer.getServer();
-
- while (server.isRunning()) {
- long currentLoop = System.currentTimeMillis();
-
- // Send pending chunk packets
- List networkManagers = server.getServerConnection().getNetworkManagers();
- if (!networkManagers.isEmpty()) {
- synchronized (networkManagers) {
- for (NetworkManager player : networkManagers)
- player.sendPacketQueue();
- }
- }
-
- for (WorldServer world : server.getWorlds()) {
- // Send time updates to everyone, it will get the right time from the world the player is in.
- boolean doDaylight = world.getGameRules().getBoolean("doDaylightCycle");
- long dayTime = world.getDayTime();
- long worldTime = world.getTime();
- final PacketPlayOutUpdateTime worldPacket = new PacketPlayOutUpdateTime(worldTime, dayTime, doDaylight);
- for (EntityHuman entityhuman : world.players) {
- if (!(entityhuman instanceof EntityPlayer) || (server.currentTick() + entityhuman.getId()) % 20 != 0) {
- continue;
- }
- EntityPlayer entityplayer = (EntityPlayer) entityhuman;
- long playerTime = entityplayer.getPlayerTime();
- PacketPlayOutUpdateTime packet = (playerTime == dayTime) ? worldPacket :
- new PacketPlayOutUpdateTime(worldTime, playerTime, doDaylight);
- entityplayer.playerConnection.sendPacket(packet); // Add support for per player time
- }
-
- // Hardcore difficulty lock
- if (world.getWorldData().isHardcore() && world.getDifficulty() != EnumDifficulty.HARD) {
- world.getWorldData().setDifficulty(EnumDifficulty.HARD);
- }
-
- // Sleeping time management
- if (world.everyoneDeeplySleeping()) {
- if (world.getGameRules().getBoolean("doDaylightCycle")) {
- long i = world.worldData.getDayTime() + 24000L;
-
- world.worldData.setDayTime(i - i % 24000L);
- }
-
- if (world.getGameRules().getBoolean("doWeatherCycle")) {
- world.clearWeather();
- }
- }
-
- // Random light updates
- world.randomLightUpdates();
- }
-
- // Send player latency update packets
- if (++playerListTick > 600) {
- List players = server.getPlayerList().players;
- for (EntityPlayer target : players) {
- target.playerConnection.sendPacket(new PacketPlayOutPlayerInfo(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY, Iterables.filter(players, new Predicate() {
- @Override
- public boolean apply(EntityPlayer input) {
- return target.getBukkitEntity().canSee(input.getBukkitEntity());
- }
- })));
- }
- playerListTick = 0;
- }
-
- // Save players data
- int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate;
- if (playerSaveInterval < 0) {
- playerSaveInterval = server.autosavePeriod;
- }
- if (playerSaveInterval > 0) {
- server.getPlayerList().savePlayers(playerSaveInterval);
- }
-
- try {
- long sleepFixed = STD_TICK_TIME - (System.currentTimeMillis() - currentLoop);
- if (sleepFixed > 0) Thread.sleep(sleepFixed);
- } catch (InterruptedException interrupted) {
- continue;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/core/AkarinCreatureSpanwner.java b/src/main/java/io/akarin/server/core/AkarinCreatureSpanwner.java
deleted file mode 100644
index c2acc5554..000000000
--- a/src/main/java/io/akarin/server/core/AkarinCreatureSpanwner.java
+++ /dev/null
@@ -1,275 +0,0 @@
-package io.akarin.server.core;
-
-import java.util.EnumMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.ThreadLocalRandom;
-
-import javax.annotation.Nullable;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
-
-import com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent;
-import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent;
-import com.destroystokyo.paper.exception.ServerInternalException;
-import com.google.common.collect.Maps;
-import com.koloboke.collect.set.hash.HashObjSets;
-
-import io.akarin.server.misc.ChunkCoordOrdinalInt3Tuple;
-import net.minecraft.server.BiomeBase;
-import net.minecraft.server.BlockPosition;
-import net.minecraft.server.ChunkCoordIntPair;
-import net.minecraft.server.EntityHuman;
-import net.minecraft.server.EntityInsentient;
-import net.minecraft.server.EntityPositionTypes;
-import net.minecraft.server.EntityTypes;
-import net.minecraft.server.EnumCreatureType;
-import net.minecraft.server.GroupDataEntity;
-import net.minecraft.server.MCUtil;
-import net.minecraft.server.MathHelper;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.SpawnerCreature;
-import net.minecraft.server.WorldServer;
-
-/*
- * Reference on spawning mechanics by Colin Godsey
- * https://github.com/yesdog/Paper/blob/0de3dd84b7e6688feb42af4fe6b4f323ce7e3013/Spigot-Server-Patches/0433-alternate-mob-spawning-mechanic.patch
- */
-public class AkarinCreatureSpanwner {
- private static final Map rangeChunks = Maps.newConcurrentMap();
-
- public static void increment(ChunkCoordIntPair chunk, EnumCreatureType type) {
- int[] values = rangeChunks.get(chunk);
- if (values == null) {
- values = new int[EnumCreatureType.values().length];;
- values[type.ordinal()]++;
- rangeChunks.put(chunk, values);
- } else {
- values[type.ordinal()]++;
- }
- }
-
- public static void decrement(ChunkCoordIntPair chunk, EnumCreatureType type) {
- int[] values = rangeChunks.get(chunk);
- if (values == null) {
- values = new int[EnumCreatureType.values().length];;
- int count = values[type.ordinal()];
- values[type.ordinal()] = count > 1 ? --count : 0;
- rangeChunks.put(chunk, values);
- } else {
- int count = values[type.ordinal()];
- values[type.ordinal()] = count > 1 ? --count : 0;
- }
- }
-
- private static int getSpawnRange(WorldServer world, EntityHuman player) {
- byte mobSpawnRange = world.spigotConfig.mobSpawnRange;
-
- mobSpawnRange = (mobSpawnRange > world.spigotConfig.viewDistance) ? (byte) world.spigotConfig.viewDistance : mobSpawnRange;
- mobSpawnRange = (mobSpawnRange > 8) ? 8 : mobSpawnRange;
-
- if (PlayerNaturallySpawnCreaturesEvent.getHandlerList().getRegisteredListeners().length != 0) {
- PlayerNaturallySpawnCreaturesEvent event = new PlayerNaturallySpawnCreaturesEvent((Player) player.getBukkitEntity(), mobSpawnRange);
- // prevent concurrent handling, at least
- synchronized (PlayerNaturallySpawnCreaturesEvent.class) {
- Bukkit.getPluginManager().callEvent(event);
- }
-
- return event.isCancelled() ? 0 : event.getSpawnRadius();
- }
-
- return mobSpawnRange;
- }
-
- private static int getCreatureLimit(WorldServer world, EnumCreatureType type) {
- switch (type) {
- case MONSTER:
- return world.getWorld().getMonsterSpawnLimit();
- case CREATURE:
- return world.getWorld().getAnimalSpawnLimit();
- case WATER_CREATURE:
- return world.getWorld().getWaterAnimalSpawnLimit();
- case AMBIENT:
- return world.getWorld().getAmbientSpawnLimit();
- }
- return type.spawnLimit();
- }
-
- @Nullable
- private static EntityInsentient createMob(WorldServer world, EnumCreatureType type, BlockPosition pos, BiomeBase.BiomeMeta biomeMeta) {
- if (!world.isBiomeMetaValidAt(type, biomeMeta, pos)) return null;
-
- EntityTypes extends EntityInsentient> entityType = biomeMeta.entityType();
- org.bukkit.entity.EntityType bType = EntityTypes.clsToTypeMap.get(entityType.entityClass());
- if (bType != null) {
- PreCreatureSpawnEvent event = new PreCreatureSpawnEvent(
- MCUtil.toLocation(world, pos),
- bType, SpawnReason.NATURAL
- );
-
- if (!event.callEvent() || event.shouldAbortSpawn())
- return null;
- }
-
- EntityInsentient entity = null;
-
- try {
- entity = entityType.create(world);
- } catch (Exception exception) {
- MinecraftServer.LOGGER.warn("Failed to create mob", exception);
- ServerInternalException.reportInternalException(exception);
- }
-
- return entity;
- }
-
- private static void spawnMob0(WorldServer world, Set chunks, EnumCreatureType type, int amount) {
- if (chunks.isEmpty()) return;
-
- Random rand = ThreadLocalRandom.current();
- final int maxPackIterations = 10; // X attempts per pack, 1 pack per chunk
- Iterator iterator = chunks.iterator();
- BlockPosition worldSpawn = world.getSpawn();
-
- int spawned = 0;
-
- while (spawned < amount && iterator.hasNext()) {
- ChunkCoordIntPair chunkCoord = iterator.next();
- int packSize = rand.nextInt(4) + 1;
- BlockPosition packCenter = SpawnerCreature.getRandomPosition(world, chunkCoord.x, chunkCoord.z);
-
- if (world.getType(packCenter).isOccluding()) continue;
-
- int x = packCenter.getX();
- int y = packCenter.getY();
- int z = packCenter.getZ();
- BlockPosition.MutableBlockPosition blockPointer = new BlockPosition.MutableBlockPosition();
- BiomeBase.BiomeMeta biomeMeta = null;
- GroupDataEntity group = null;
- EntityPositionTypes.Surface surfaceType = null;
- int iter = 0;
- int packSpawned = 0;
-
- while (packSpawned < packSize && iter < maxPackIterations) {
- iter++;
-
- // random walk
- x += rand.nextInt(12) - 6;
- y += rand.nextInt(2) - 1;
- z += rand.nextInt(12) - 6;
- blockPointer.setValues(x, y, z);
-
- if (worldSpawn.distanceSquared(x + 0.5, y, z + 0.5) < (24 * 24)) continue;
-
- if (biomeMeta == null) {
- biomeMeta = world.getBiomeMetaAt(type, blockPointer);
-
- if (biomeMeta == null) break;
-
- int packRange = 1 + biomeMeta.getMaxPackSize() - biomeMeta.getMinPackSize();
- packSize = biomeMeta.getMinPackSize() + rand.nextInt(packRange);
- surfaceType = EntityPositionTypes.a(biomeMeta.entityType());
- }
-
- EntityInsentient entity = createMob(world, type, blockPointer, biomeMeta);
-
- if (entity == null) continue;
-
- entity.setPositionRotation(x + 0.5, y, z + 0.5, rand.nextFloat() * 360.0F, 0.0F);
-
- if (entity.canSpawnHere() && surfaceType != null
- && SpawnerCreature.isValidSpawnSurface(surfaceType, world, blockPointer, biomeMeta.entityType())
- && entity.isNotColliding(world) && !world.isPlayerNearby(x + 0.5, y, z + 0.5, 24)) {
- group = entity.prepare(world.getDamageScaler(new BlockPosition(entity)), group, null);
-
- if (entity.isNotColliding(world) && world.addEntity(entity, SpawnReason.NATURAL))
- packSpawned++;
-
- if (packSpawned >= entity.maxPackSize()) break;
- if ((packSpawned + spawned) >= amount) break;
- } else {
- entity.die();
- }
- }
-
- spawned += packSpawned;
- }
- }
-
- public static void spawnMobs(WorldServer world, boolean spawnMonsters, boolean spawnPassives, boolean spawnRare) {
- if(!spawnMonsters && !spawnPassives) return;
-
- AkarinAsyncExecutor.scheduleAsyncTask(() -> {
- Random rand = ThreadLocalRandom.current();
- int hashOrdinal = rand.nextInt();
-
- //Set rangeChunks = HashObjSets.newUpdatableSet();
- Map> creatureChunks = new EnumMap<>(EnumCreatureType.class);
- int[] typeNumSpawn = new int[EnumCreatureType.values().length];
-
- for (EnumCreatureType type : EnumCreatureType.values()) {
- if (type.passive() && !spawnPassives) continue;
- if (!type.passive() && !spawnMonsters) continue;
- if (type.rare() && !spawnRare) continue;
- if (getCreatureLimit(world, type) <= 0) continue;
-
- creatureChunks.put(type, HashObjSets.newUpdatableSet());
- }
-
- if (creatureChunks.isEmpty()) return;
-
- for (EntityHuman player : world.players) {
- if (!player.affectsSpawning || player.isSpectator()) continue;
-
- int spawnRange = getSpawnRange(world, player);
- if (spawnRange <= 0) continue;
-
- int playerChunkX = MathHelper.floor(player.locX / 16.0);
- int playerChunkZ = MathHelper.floor(player.locZ / 16.0);
-
- rangeChunks.clear();
-
- for (int dX = -spawnRange; dX <= spawnRange; ++dX) {
- for (int dZ = -spawnRange; dZ <= spawnRange; ++dZ) {
- ChunkCoordIntPair chunkCoord = new ChunkCoordOrdinalInt3Tuple(dX + playerChunkX, dZ + playerChunkZ, hashOrdinal);
-
- if (!world.getWorldBorder().isInBounds(chunkCoord)) continue;
-
- ChunkCoordIntPair pair = new ChunkCoordIntPair(chunkCoord.x, chunkCoord.z);
- int[] cached = rangeChunks.get(pair);
- if (cached == null)
- rangeChunks.put(pair, new int[EnumCreatureType.values().length]);
- else
- for (int i = 0; i < cached.length; i++)
- cached[i] = 0;
- }
- }
-
- for (EnumCreatureType type : creatureChunks.keySet()) {
- int limit = getCreatureLimit(world, type);
- int creatureTotal = 0;
-
- for (int[] chunk : rangeChunks.values())
- creatureTotal += chunk[type.ordinal()];
-
- // if our local count is above the limit, dont qualify our chunks
- if (creatureTotal >= limit) continue;
-
- // expect number is rather meaningless, just a ceil
- int expect = limit - creatureTotal;
- typeNumSpawn[type.ordinal()] = Math.max(typeNumSpawn[type.ordinal()], expect);
- }
- }
-
- for (EnumCreatureType type : creatureChunks.keySet()) {
- Set chunks = creatureChunks.get(type);
-
- if (!chunks.isEmpty())
- MinecraftServer.getServer().ensuresMainThread(() -> spawnMob0(world, chunks, type, typeNumSpawn[type.ordinal()]));
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/core/AkarinWorldAccessor.java b/src/main/java/io/akarin/server/core/AkarinWorldAccessor.java
deleted file mode 100644
index a0350329a..000000000
--- a/src/main/java/io/akarin/server/core/AkarinWorldAccessor.java
+++ /dev/null
@@ -1,146 +0,0 @@
-package io.akarin.server.core;
-
-import java.util.List;
-
-import javax.annotation.Nullable;
-
-import com.google.common.collect.Lists;
-
-import net.minecraft.server.BlockPosition;
-import net.minecraft.server.Entity;
-import net.minecraft.server.EntityHuman;
-import net.minecraft.server.IBlockAccess;
-import net.minecraft.server.IBlockData;
-import net.minecraft.server.IWorldAccess;
-import net.minecraft.server.NavigationListener;
-import net.minecraft.server.ParticleParam;
-import net.minecraft.server.SoundCategory;
-import net.minecraft.server.SoundEffect;
-import net.minecraft.server.WorldManager;
-
-public class AkarinWorldAccessor implements IWorldAccess {
- private @Nullable WorldManager worldManager;
- private final NavigationListener navigationListener;
- private IWorldAccess[] customAccessors = new IWorldAccess[0];
- private boolean hasCustomAccessor;
-
- public AkarinWorldAccessor(WorldManager worldManager, NavigationListener navigationListener) {
- this.worldManager = worldManager;
- this.navigationListener = navigationListener;
- }
-
- public AkarinWorldAccessor(NavigationListener navigationListener) {
- this.navigationListener = navigationListener;
- }
-
- public void add(IWorldAccess worldAccessor) {
- if (worldManager == null && worldAccessor instanceof WorldManager) {
- worldManager = (WorldManager) worldAccessor;
- return;
- }
- List accessors = Lists.newArrayList(customAccessors);
- accessors.add(worldAccessor);
- customAccessors = accessors.toArray(new IWorldAccess[accessors.size()]);
- hasCustomAccessor = true;
- }
-
- @Override
- public void a(Entity arg0) {
- worldManager.a(arg0);
- navigationListener.a(arg0);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0);
- }
-
- @Override
- public void a(int arg0, BlockPosition arg1, int arg2) {
- worldManager.a(arg0, arg1, arg2);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2);
- }
-
- @Override
- public void a(EntityHuman arg0, int arg1, BlockPosition arg2, int arg3) {
- worldManager.a(arg0, arg1, arg2, arg3);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3);
- }
-
- @Override
- public void a(IBlockAccess arg0, BlockPosition arg1, IBlockData arg2, IBlockData arg3, int arg4) {
- worldManager.a(arg0, arg1, arg2, arg3, arg4);
- navigationListener.a(arg0, arg1, arg2, arg3, arg4);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3, arg4);
- }
-
- @Override
- public void a(EntityHuman arg0, SoundEffect arg1, SoundCategory arg2, double arg3, double arg4, double arg5, float arg6, float arg7) {
- worldManager.a(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
- }
-
- @Override
- public void b(Entity arg0) {
- worldManager.b(arg0);
- navigationListener.b(arg0);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.b(arg0);
- }
-
- @Override
- public void b(int arg0, BlockPosition arg1, int arg2) {
- worldManager.b(arg0, arg1, arg2);
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.b(arg0, arg1, arg2);
- }
-
- // unused
- @Override
- @Deprecated
- public void a(BlockPosition arg0) {
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0);
- }
-
- @Override
- @Deprecated
- public void a(SoundEffect arg0, BlockPosition arg1) {
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1);
- }
-
- @Override
- @Deprecated
- public void a(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3, arg4, arg5);
- }
-
- @Override
- @Deprecated
- public void a(ParticleParam arg0, boolean arg1, double arg2, double arg3, double arg4, double arg5, double arg6, double arg7) {
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
- }
-
- @Override
- @Deprecated
- public void a(ParticleParam arg0, boolean arg1, boolean arg2, double arg3, double arg4, double arg5, double arg6, double arg7, double arg8) {
- if (hasCustomAccessor)
- for (IWorldAccess accessor : customAccessors)
- accessor.a(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/core/PacketType.java b/src/main/java/io/akarin/server/core/PacketType.java
deleted file mode 100644
index 49dfacf08..000000000
--- a/src/main/java/io/akarin/server/core/PacketType.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.akarin.server.core;
-
-public enum PacketType {
- PLAY_OUT_MAP_CHUNK,
- PLAY_OUT_SPAWN_POSITION,
- PLAY_OUT_CHAT,
-
- UNKNOWN;
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/misc/ChunkCoordOrdinalInt3Tuple.java b/src/main/java/io/akarin/server/misc/ChunkCoordOrdinalInt3Tuple.java
deleted file mode 100644
index de10f5bcf..000000000
--- a/src/main/java/io/akarin/server/misc/ChunkCoordOrdinalInt3Tuple.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.akarin.server.misc;
-
-import net.minecraft.server.ChunkCoordIntPair;
-
-import com.google.common.hash.Hashing;
-import com.google.common.hash.HashFunction;
-
-/*
- * Reference on spawning mechanics by Colin Godsey
- * https://github.com/yesdog/Paper/blob/0de3dd84b7e6688feb42af4fe6b4f323ce7e3013/Spigot-Server-Patches/0433-alternate-mob-spawning-mechanic.patch
- */
-public class ChunkCoordOrdinalInt3Tuple extends ChunkCoordIntPair {
- public static final HashFunction hashFunc = Hashing.murmur3_32("akarin".hashCode());
-
- public final int ordinal;
- public final int cachedHashCode;
-
- public ChunkCoordOrdinalInt3Tuple(int x, int z, int ord) {
- super(x, z);
-
- this.ordinal = ord;
-
- cachedHashCode = hashFunc.newHasher()
- .putInt(ordinal)
- .putInt(x)
- .putInt(z)
- .hash().asInt();
- }
-
- @Override
- public int hashCode() {
- return cachedHashCode;
- }
-
- @Override
- public boolean equals(Object object) {
- if (this == object) {
- return true;
- } else if (!(object instanceof ChunkCoordOrdinalInt3Tuple)) {
- return false;
- } else {
- ChunkCoordOrdinalInt3Tuple pair = (ChunkCoordOrdinalInt3Tuple) object;
-
- return this.x == pair.x && this.z == pair.z && this.ordinal == pair.ordinal;
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/akarin/server/misc/CopyOnWriteHashMap.java b/src/main/java/io/akarin/server/misc/CopyOnWriteHashMap.java
deleted file mode 100644
index ca9c60ea7..000000000
--- a/src/main/java/io/akarin/server/misc/CopyOnWriteHashMap.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package io.akarin.server.misc;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A thread-safe version of {@link Map} in which all operations that change the
- * Map are implemented by making a new copy of the underlying Map.
- *
- * While the creation of a new Map can be expensive, this class is designed for
- * cases in which the primary function is to read data from the Map, not to
- * modify the Map. Therefore the operations that do not cause a change to this
- * class happen quickly and concurrently.
- *
- * @author Kuzma Deretuke
- */
-public class CopyOnWriteHashMap implements Map, Serializable, Cloneable {
- private static final long serialVersionUID = 5481095911554321115L;
- private AtomicReference