diff --git a/README.md b/README.md
index 3320bb4af..d61e758d6 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ Maven
org.purpurmc.purpur
purpur-api
- 1.18.2-R0.1-SNAPSHOT
+ 1.19-R0.1-SNAPSHOT
provided
```
@@ -77,7 +77,7 @@ repositories {
```
```kotlin
dependencies {
- compileOnly("org.purpurmc.purpur", "purpur-api", "1.18.2-R0.1-SNAPSHOT")
+ compileOnly("org.purpurmc.purpur", "purpur-api", "1.19-R0.1-SNAPSHOT")
}
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 3794af36b..a1f93f1da 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,7 +18,7 @@ repositories {
dependencies {
remapper("net.fabricmc:tiny-remapper:0.8.2:fat")
- decompiler("net.minecraftforge:forgeflower:1.5.498.29")
+ decompiler("net.minecraftforge:forgeflower:1.5.605.7")
paperclip("io.papermc:paperclip:3.0.2")
}
diff --git a/gradle.properties b/gradle.properties
index 0e3e9476a..36dfbe297 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,7 @@
group = org.purpurmc.purpur
-version = 1.18.2-R0.1-SNAPSHOT
+version = 1.19-R0.1-SNAPSHOT
-paperCommit = 276d830d223ddf68611beacc248285ae5a4e8a1f
+paperCommit = a9c507310b02836e37d5d1e3ed9ba9620289fdb1
org.gradle.caching = true
org.gradle.parallel = true
diff --git a/patches/api/0002-Add-pufferfish-added-classes-to-junit-exemptions.patch b/patches/api/0001-Add-pufferfish-added-classes-to-junit-exemptions.patch
similarity index 100%
rename from patches/api/0002-Add-pufferfish-added-classes-to-junit-exemptions.patch
rename to patches/api/0001-Add-pufferfish-added-classes-to-junit-exemptions.patch
diff --git a/patches/api/0001-Pufferfish-API-Changes.patch b/patches/api/0001-Pufferfish-API-Changes.patch
deleted file mode 100644
index 1473bd727..000000000
--- a/patches/api/0001-Pufferfish-API-Changes.patch
+++ /dev/null
@@ -1,514 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Kevin Raneri
-Date: Tue, 9 Nov 2021 14:01:56 -0500
-Subject: [PATCH] Pufferfish API Changes
-
-Pufferfish
-Copyright (C) 2022 Pufferfish Studios LLC
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-diff --git a/build.gradle.kts b/build.gradle.kts
-index 01798255d45f2a642df00156f11dd2bcd8108079..9d7bd0f965c7dc3a60246310688aa5f93a4594a4 100644
---- a/build.gradle.kts
-+++ b/build.gradle.kts
-@@ -41,6 +41,7 @@ dependencies {
- apiAndDocs("net.kyori:adventure-text-serializer-plain")
- api("org.apache.logging.log4j:log4j-api:2.17.1")
- api("org.slf4j:slf4j-api:1.8.0-beta4")
-+ api("io.sentry:sentry:5.4.0") // Pufferfish
-
- implementation("org.ow2.asm:asm:9.2")
- implementation("org.ow2.asm:asm-commons:9.2")
-@@ -82,6 +83,13 @@ val generateApiVersioningFile by tasks.registering {
- }
- }
-
-+// Pufferfish Start
-+tasks.withType {
-+ val compilerArgs = options.compilerArgs
-+ compilerArgs.add("--add-modules=jdk.incubator.vector")
-+}
-+// Pufferfish End
-+
- tasks.jar {
- from(generateApiVersioningFile.map { it.outputs.files.singleFile }) {
- into("META-INF/maven/${project.group}/${project.name}")
-diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..10310fdd53de28efb8a8250f6d3b0c8eb08fb68a
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java
-@@ -0,0 +1,161 @@
-+package gg.pufferfish.pufferfish.sentry;
-+
-+import com.google.gson.Gson;
-+import java.lang.reflect.Field;
-+import java.lang.reflect.Modifier;
-+import java.util.Map;
-+import java.util.TreeMap;
-+import org.apache.logging.log4j.ThreadContext;
-+import org.bukkit.command.Command;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.Event;
-+import org.bukkit.event.player.PlayerEvent;
-+import org.bukkit.plugin.Plugin;
-+import org.bukkit.plugin.RegisteredListener;
-+import org.jetbrains.annotations.Nullable;
-+
-+public class SentryContext {
-+
-+ private static final Gson GSON = new Gson();
-+
-+ public static void setPluginContext(@Nullable Plugin plugin) {
-+ if (plugin != null) {
-+ ThreadContext.put("pufferfishsentry_pluginname", plugin.getName());
-+ ThreadContext.put("pufferfishsentry_pluginversion", plugin.getDescription().getVersion());
-+ }
-+ }
-+
-+ public static void removePluginContext() {
-+ ThreadContext.remove("pufferfishsentry_pluginname");
-+ ThreadContext.remove("pufferfishsentry_pluginversion");
-+ }
-+
-+ public static void setSenderContext(@Nullable CommandSender sender) {
-+ if (sender != null) {
-+ ThreadContext.put("pufferfishsentry_playername", sender.getName());
-+ if (sender instanceof Player player) {
-+ ThreadContext.put("pufferfishsentry_playerid", player.getUniqueId().toString());
-+ }
-+ }
-+ }
-+
-+ public static void removeSenderContext() {
-+ ThreadContext.remove("pufferfishsentry_playername");
-+ ThreadContext.remove("pufferfishsentry_playerid");
-+ }
-+
-+ public static void setEventContext(Event event, RegisteredListener registration) {
-+ setPluginContext(registration.getPlugin());
-+
-+ try {
-+ // Find the player that was involved with this event
-+ Player player = null;
-+ if (event instanceof PlayerEvent) {
-+ player = ((PlayerEvent) event).getPlayer();
-+ } else {
-+ Class extends Event> eventClass = event.getClass();
-+
-+ Field playerField = null;
-+
-+ for (Field field : eventClass.getDeclaredFields()) {
-+ if (field.getType().equals(Player.class)) {
-+ playerField = field;
-+ break;
-+ }
-+ }
-+
-+ if (playerField != null) {
-+ playerField.setAccessible(true);
-+ player = (Player) playerField.get(event);
-+ }
-+ }
-+
-+ if (player != null) {
-+ setSenderContext(player);
-+ }
-+ } catch (Exception e) {} // We can't really safely log exceptions.
-+
-+ ThreadContext.put("pufferfishsentry_eventdata", GSON.toJson(serializeFields(event)));
-+ }
-+
-+ public static void removeEventContext() {
-+ removePluginContext();
-+ removeSenderContext();
-+ ThreadContext.remove("pufferfishsentry_eventdata");
-+ }
-+
-+ private static Map serializeFields(Object object) {
-+ Map fields = new TreeMap<>();
-+ fields.put("_class", object.getClass().getName());
-+ for (Field declaredField : object.getClass().getDeclaredFields()) {
-+ try {
-+ if (Modifier.isStatic(declaredField.getModifiers())) {
-+ continue;
-+ }
-+
-+ String fieldName = declaredField.getName();
-+ if (fieldName.equals("handlers")) {
-+ continue;
-+ }
-+ declaredField.setAccessible(true);
-+ Object value = declaredField.get(object);
-+ if (value != null) {
-+ fields.put(fieldName, value.toString());
-+ } else {
-+ fields.put(fieldName, "");
-+ }
-+ } catch (Exception e) {} // We can't really safely log exceptions.
-+ }
-+ return fields;
-+ }
-+
-+ public static class State {
-+
-+ private Plugin plugin;
-+ private Command command;
-+ private String commandLine;
-+ private Event event;
-+ private RegisteredListener registeredListener;
-+
-+ public Plugin getPlugin() {
-+ return plugin;
-+ }
-+
-+ public void setPlugin(Plugin plugin) {
-+ this.plugin = plugin;
-+ }
-+
-+ public Command getCommand() {
-+ return command;
-+ }
-+
-+ public void setCommand(Command command) {
-+ this.command = command;
-+ }
-+
-+ public String getCommandLine() {
-+ return commandLine;
-+ }
-+
-+ public void setCommandLine(String commandLine) {
-+ this.commandLine = commandLine;
-+ }
-+
-+ public Event getEvent() {
-+ return event;
-+ }
-+
-+ public void setEvent(Event event) {
-+ this.event = event;
-+ }
-+
-+ public RegisteredListener getRegisteredListener() {
-+ return registeredListener;
-+ }
-+
-+ public void setRegisteredListener(RegisteredListener registeredListener) {
-+ this.registeredListener = registeredListener;
-+ }
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..93f5d7ca36e043e6c0f959450d38e6946b348eaf
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java
-@@ -0,0 +1,40 @@
-+package gg.pufferfish.pufferfish.simd;
-+
-+import java.util.logging.Level;
-+import java.util.logging.Logger;
-+import jdk.incubator.vector.FloatVector;
-+import jdk.incubator.vector.IntVector;
-+import jdk.incubator.vector.VectorSpecies;
-+
-+/**
-+ * Basically, java is annoying and we have to push this out to its own class.
-+ */
-+@Deprecated
-+public class SIMDChecker {
-+
-+ @Deprecated
-+ public static boolean canEnable(Logger logger) {
-+ try {
-+ if (SIMDDetection.getJavaVersion() != 17 && SIMDDetection.getJavaVersion() != 18) {
-+ return false;
-+ } else {
-+ SIMDDetection.testRun = true;
-+
-+ VectorSpecies ISPEC = IntVector.SPECIES_PREFERRED;
-+ VectorSpecies FSPEC = FloatVector.SPECIES_PREFERRED;
-+
-+ logger.log(Level.INFO, "Max SIMD vector size on this system is " + ISPEC.vectorBitSize() + " bits (int)");
-+ logger.log(Level.INFO, "Max SIMD vector size on this system is " + FSPEC.vectorBitSize() + " bits (float)");
-+
-+ if (ISPEC.elementSize() < 2 || FSPEC.elementSize() < 2) {
-+ logger.log(Level.WARNING, "SIMD is not properly supported on this system!");
-+ return false;
-+ }
-+
-+ return true;
-+ }
-+ } catch (NoClassDefFoundError | Exception ignored) {} // Basically, we don't do anything. This lets us detect if it's not functional and disable it.
-+ return false;
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a84889d3e9cfc4d7ab5f867820a6484c6070711b
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java
-@@ -0,0 +1,35 @@
-+package gg.pufferfish.pufferfish.simd;
-+
-+import java.util.logging.Logger;
-+
-+@Deprecated
-+public class SIMDDetection {
-+
-+ public static boolean isEnabled = false;
-+ public static boolean versionLimited = false;
-+ public static boolean testRun = false;
-+
-+ @Deprecated
-+ public static boolean canEnable(Logger logger) {
-+ try {
-+ return SIMDChecker.canEnable(logger);
-+ } catch (NoClassDefFoundError | Exception ignored) {
-+ return false;
-+ }
-+ }
-+
-+ @Deprecated
-+ public static int getJavaVersion() {
-+ // https://stackoverflow.com/a/2591122
-+ String version = System.getProperty("java.version");
-+ if(version.startsWith("1.")) {
-+ version = version.substring(2, 3);
-+ } else {
-+ int dot = version.indexOf(".");
-+ if(dot != -1) { version = version.substring(0, dot); }
-+ }
-+ version = version.split("-")[0]; // Azul is stupid
-+ return Integer.parseInt(version);
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java b/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ae2464920c9412ac90b819a540ee58be0741465f
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java
-@@ -0,0 +1,83 @@
-+package gg.pufferfish.pufferfish.simd;
-+
-+import java.awt.Color;
-+import jdk.incubator.vector.FloatVector;
-+import jdk.incubator.vector.IntVector;
-+import jdk.incubator.vector.VectorMask;
-+import jdk.incubator.vector.VectorSpecies;
-+import org.bukkit.map.MapPalette;
-+
-+@Deprecated
-+public class VectorMapPalette {
-+
-+ private static final VectorSpecies I_SPEC = IntVector.SPECIES_PREFERRED;
-+ private static final VectorSpecies F_SPEC = FloatVector.SPECIES_PREFERRED;
-+
-+ @Deprecated
-+ public static void matchColorVectorized(int[] in, byte[] out) {
-+ int speciesLength = I_SPEC.length();
-+ int i;
-+ for (i = 0; i < in.length - speciesLength; i += speciesLength) {
-+ float[] redsArr = new float[speciesLength];
-+ float[] bluesArr = new float[speciesLength];
-+ float[] greensArr = new float[speciesLength];
-+ int[] alphasArr = new int[speciesLength];
-+
-+ for (int j = 0; j < speciesLength; j++) {
-+ alphasArr[j] = (in[i + j] >> 24) & 0xFF;
-+ redsArr[j] = (in[i + j] >> 16) & 0xFF;
-+ greensArr[j] = (in[i + j] >> 8) & 0xFF;
-+ bluesArr[j] = (in[i + j] >> 0) & 0xFF;
-+ }
-+
-+ IntVector alphas = IntVector.fromArray(I_SPEC, alphasArr, 0);
-+ FloatVector reds = FloatVector.fromArray(F_SPEC, redsArr, 0);
-+ FloatVector greens = FloatVector.fromArray(F_SPEC, greensArr, 0);
-+ FloatVector blues = FloatVector.fromArray(F_SPEC, bluesArr, 0);
-+ IntVector resultIndex = IntVector.zero(I_SPEC);
-+ VectorMask modificationMask = VectorMask.fromLong(I_SPEC, 0xffffffff);
-+
-+ modificationMask = modificationMask.and(alphas.lt(128).not());
-+ FloatVector bestDistances = FloatVector.broadcast(F_SPEC, Float.MAX_VALUE);
-+
-+ for (int c = 4; c < MapPalette.colors.length; c++) {
-+ // We're using 32-bit floats here because it's 2x faster and nobody will know the difference.
-+ // For correctness, the original algorithm uses 64-bit floats instead. Completely unnecessary.
-+ FloatVector compReds = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getRed());
-+ FloatVector compGreens = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getGreen());
-+ FloatVector compBlues = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getBlue());
-+
-+ FloatVector rMean = reds.add(compReds).div(2.0f);
-+ FloatVector rDiff = reds.sub(compReds);
-+ FloatVector gDiff = greens.sub(compGreens);
-+ FloatVector bDiff = blues.sub(compBlues);
-+
-+ FloatVector weightR = rMean.div(256.0f).add(2);
-+ FloatVector weightG = FloatVector.broadcast(F_SPEC, 4.0f);
-+ FloatVector weightB = FloatVector.broadcast(F_SPEC, 255.0f).sub(rMean).div(256.0f).add(2.0f);
-+
-+ FloatVector distance = weightR.mul(rDiff).mul(rDiff).add(weightG.mul(gDiff).mul(gDiff)).add(weightB.mul(bDiff).mul(bDiff));
-+
-+ // Now we compare to the best distance we've found.
-+ // This mask contains a "1" if better, and a "0" otherwise.
-+ VectorMask bestDistanceMask = distance.lt(bestDistances);
-+ bestDistances = bestDistances.blend(distance, bestDistanceMask); // Update the best distances
-+
-+ // Update the result array
-+ // We also AND with the modification mask because we don't want to interfere if the alpha value isn't large enough.
-+ resultIndex = resultIndex.blend(c, bestDistanceMask.cast(I_SPEC).and(modificationMask)); // Update the results
-+ }
-+
-+ for (int j = 0; j < speciesLength; j++) {
-+ int index = resultIndex.lane(j);
-+ out[i + j] = (byte) (index < 128 ? index : -129 + (index - 127));
-+ }
-+ }
-+
-+ // For the final ones, fall back to the regular method
-+ for (; i < in.length; i++) {
-+ out[i] = MapPalette.matchColor(new Color(in[i], true));
-+ }
-+ }
-+
-+}
-diff --git a/src/main/java/org/bukkit/map/MapPalette.java b/src/main/java/org/bukkit/map/MapPalette.java
-index b937441d2fb46b108644c49fcf073859765aa02e..d95b01bfd0657cf089c0f5412453cca08e36c02f 100644
---- a/src/main/java/org/bukkit/map/MapPalette.java
-+++ b/src/main/java/org/bukkit/map/MapPalette.java
-@@ -1,5 +1,6 @@
- package org.bukkit.map;
-
-+import gg.pufferfish.pufferfish.simd.SIMDDetection;
- import java.awt.Color;
- import java.awt.Graphics2D;
- import java.awt.Image;
-@@ -34,7 +35,7 @@ public final class MapPalette {
- }
-
- @NotNull
-- static final Color[] colors = {
-+ public static final Color[] colors = { // Pufferfish - public access
- c(0, 0, 0), c(0, 0, 0), c(0, 0, 0), c(0, 0, 0),
- c(89, 125, 39), c(109, 153, 48), c(127, 178, 56), c(67, 94, 29),
- c(174, 164, 115), c(213, 201, 140), c(247, 233, 163), c(130, 123, 86),
-@@ -205,9 +206,15 @@ public final class MapPalette {
- temp.getRGB(0, 0, temp.getWidth(), temp.getHeight(), pixels, 0, temp.getWidth());
-
- byte[] result = new byte[temp.getWidth() * temp.getHeight()];
-+ // Pufferfish start
-+ if (!SIMDDetection.isEnabled) {
- for (int i = 0; i < pixels.length; i++) {
- result[i] = matchColor(new Color(pixels[i], true));
- }
-+ } else {
-+ gg.pufferfish.pufferfish.simd.VectorMapPalette.matchColorVectorized(pixels, result);
-+ }
-+ // Pufferfish end
- return result;
- }
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index bab8bb3a52cdeef5f7052d4e3f404c42f37d117d..dba9041784e7d3051b5248cbc24e4879e60103c1 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -622,7 +622,9 @@ public final class SimplePluginManager implements PluginManager {
-
- // Paper start
- private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
-+ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish
- server.getLogger().log(Level.SEVERE, msg, ex);
-+ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish
- callEvent(new ServerExceptionEvent(new ServerPluginEnableDisableException(msg, ex, plugin)));
- }
- // Paper end
-@@ -681,9 +683,11 @@ public final class SimplePluginManager implements PluginManager {
- ));
- }
- } catch (Throwable ex) {
-+ gg.pufferfish.pufferfish.sentry.SentryContext.setEventContext(event, registration); // Pufferfish
- // Paper start - error reporting
- String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getDescription().getFullName();
- server.getLogger().log(Level.SEVERE, msg, ex);
-+ gg.pufferfish.pufferfish.sentry.SentryContext.removeEventContext(); // Pufferfish
- if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
- callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
- }
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index c8b11793c6a3baabc1c9566e0463ab1d6e293827..2b9218ddd262e89180588c3014dad328317dd8db 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -369,7 +369,9 @@ public final class JavaPluginLoader implements PluginLoader {
- try {
- jPlugin.setEnabled(true);
- } catch (Throwable ex) {
-+ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish
- server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
-+ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish
- // Paper start - Disable plugins that fail to load
- this.server.getPluginManager().disablePlugin(jPlugin);
- return;
-@@ -398,7 +400,9 @@ public final class JavaPluginLoader implements PluginLoader {
- try {
- jPlugin.setEnabled(false);
- } catch (Throwable ex) {
-+ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish
- server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
-+ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish
- }
-
- if (cloader instanceof PluginClassLoader) {
-diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-index 9938ebb38353f4aa2adf1bb08cd1c347ddd9fc88..dc76cdbe93a0229a8ff552e4048613e3d8e050ce 100644
---- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -46,6 +46,8 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
- private final Set seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
- private java.util.logging.Logger logger; // Paper - add field
-
-+ private boolean closed = false; // Pufferfish
-+
- static {
- ClassLoader.registerAsParallelCapable();
- }
-@@ -151,6 +153,7 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
- throw new ClassNotFoundException(name);
- }
-
-+ public boolean _airplane_hasClass(@NotNull String name) { return this.classes.containsKey(name); } // Pufferfish
- @Override
- protected Class> findClass(String name) throws ClassNotFoundException {
- if (name.startsWith("org.bukkit.") || name.startsWith("net.minecraft.")) {
-@@ -158,7 +161,7 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
- }
- Class> result = classes.get(name);
-
-- if (result == null) {
-+ if (result == null && !this.closed) { // Pufferfish
- String path = name.replace('.', '/').concat(".class");
- JarEntry entry = jar.getJarEntry(path);
-
-@@ -213,6 +216,7 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
- try {
- super.close();
- } finally {
-+ this.closed = true; // Pufferfish
- jar.close();
- }
- }
diff --git a/patches/api/0003-Build-System-Changes.patch b/patches/api/0002-Build-System-Changes.patch
similarity index 85%
rename from patches/api/0003-Build-System-Changes.patch
rename to patches/api/0002-Build-System-Changes.patch
index 44d5b02e9..d79b12a71 100644
--- a/patches/api/0003-Build-System-Changes.patch
+++ b/patches/api/0002-Build-System-Changes.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Build System Changes
diff --git a/build.gradle.kts b/build.gradle.kts
-index 0007c6b1d81373cb6592c8b9e02f464405050f68..30d3086fae6aaac493a3ad536447911ef5b2c1b1 100644
+index ac0a4aea48436201b5712b166413bb7dc0d0b9d8..6326d99ed2fb157da200bb53d67a639a2c3d6cc1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
-@@ -102,6 +102,8 @@ tasks.jar {
+@@ -93,6 +93,8 @@ tasks.jar {
}
tasks.withType {
diff --git a/patches/api/0004-Purpur-config-files.patch b/patches/api/0003-Purpur-config-files.patch
similarity index 100%
rename from patches/api/0004-Purpur-config-files.patch
rename to patches/api/0003-Purpur-config-files.patch
diff --git a/patches/api/0005-Purpur-client-support.patch b/patches/api/0004-Purpur-client-support.patch
similarity index 100%
rename from patches/api/0005-Purpur-client-support.patch
rename to patches/api/0004-Purpur-client-support.patch
diff --git a/patches/api/0006-Default-permissions.patch b/patches/api/0005-Default-permissions.patch
similarity index 100%
rename from patches/api/0006-Default-permissions.patch
rename to patches/api/0005-Default-permissions.patch
diff --git a/patches/api/0007-Ridables.patch b/patches/api/0006-Ridables.patch
similarity index 100%
rename from patches/api/0007-Ridables.patch
rename to patches/api/0006-Ridables.patch
diff --git a/patches/api/0008-Allow-inventory-resizing.patch b/patches/api/0007-Allow-inventory-resizing.patch
similarity index 100%
rename from patches/api/0008-Allow-inventory-resizing.patch
rename to patches/api/0007-Allow-inventory-resizing.patch
diff --git a/patches/api/0009-Llama-API.patch b/patches/api/0008-Llama-API.patch
similarity index 100%
rename from patches/api/0009-Llama-API.patch
rename to patches/api/0008-Llama-API.patch
diff --git a/patches/api/0010-AFK-API.patch b/patches/api/0009-AFK-API.patch
similarity index 100%
rename from patches/api/0010-AFK-API.patch
rename to patches/api/0009-AFK-API.patch
diff --git a/patches/api/0011-Bring-back-server-name.patch b/patches/api/0010-Bring-back-server-name.patch
similarity index 93%
rename from patches/api/0011-Bring-back-server-name.patch
rename to patches/api/0010-Bring-back-server-name.patch
index bef4594d7..400583e20 100644
--- a/patches/api/0011-Bring-back-server-name.patch
+++ b/patches/api/0010-Bring-back-server-name.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Bring back server name
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
-index d8666481f9a407403d0114ff02024fd3c50c27c4..a1374b608c20d47fde15135089bf8aceb98d6129 100644
+index a87399fa4838d4b2c1ff9cc35d433ae76cc149bf..2d9a065aa8c6a835e49eee76acdf8cfa6af420bf 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -2368,4 +2368,15 @@ public final class Bukkit {
diff --git a/patches/api/0012-ExecuteCommandEvent.patch b/patches/api/0011-ExecuteCommandEvent.patch
similarity index 92%
rename from patches/api/0012-ExecuteCommandEvent.patch
rename to patches/api/0011-ExecuteCommandEvent.patch
index 8b83d17e9..741382c89 100644
--- a/patches/api/0012-ExecuteCommandEvent.patch
+++ b/patches/api/0011-ExecuteCommandEvent.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] ExecuteCommandEvent
diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java
-index 74252236b138969560e6513f24e7ecc6dc4a4127..9c3d02b949fe806e17200d0ff6127fc367ef2abc 100644
+index b8623575b1c1b565560c2dd6438190716845a652..0bc24d0effe9b2e44c41a1c00060b0ebf7395c4a 100644
--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
-@@ -147,6 +147,19 @@ public class SimpleCommandMap implements CommandMap {
+@@ -143,6 +143,19 @@ public class SimpleCommandMap implements CommandMap {
return false;
}
@@ -28,7 +28,7 @@ index 74252236b138969560e6513f24e7ecc6dc4a4127..9c3d02b949fe806e17200d0ff6127fc3
// Paper start - Plugins do weird things to workaround normal registration
if (target.timings == null) {
target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target);
-@@ -156,7 +169,7 @@ public class SimpleCommandMap implements CommandMap {
+@@ -152,7 +165,7 @@ public class SimpleCommandMap implements CommandMap {
try {
try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources
// Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false)
@@ -36,7 +36,7 @@ index 74252236b138969560e6513f24e7ecc6dc4a4127..9c3d02b949fe806e17200d0ff6127fc3
+ target.execute(sender, sentCommandLabel, parsedArgs); // Purpur
} // target.timings.stopTiming(); // Spigot // Paper
} catch (CommandException ex) {
- server.getPluginManager().callEvent(new ServerExceptionEvent(new ServerCommandException(ex, target, sender, args))); // Paper
+ server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper
diff --git a/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java b/src/main/java/org/purpurmc/purpur/event/ExecuteCommandEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..103d5699d0993f358749b3d288b924e48b734693
diff --git a/patches/api/0013-LivingEntity-safeFallDistance.patch b/patches/api/0012-LivingEntity-safeFallDistance.patch
similarity index 100%
rename from patches/api/0013-LivingEntity-safeFallDistance.patch
rename to patches/api/0012-LivingEntity-safeFallDistance.patch
diff --git a/patches/api/0014-Lagging-threshold.patch b/patches/api/0013-Lagging-threshold.patch
similarity index 93%
rename from patches/api/0014-Lagging-threshold.patch
rename to patches/api/0013-Lagging-threshold.patch
index 43338ea34..6abe7fbbe 100644
--- a/patches/api/0014-Lagging-threshold.patch
+++ b/patches/api/0013-Lagging-threshold.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Lagging threshold
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
-index a1374b608c20d47fde15135089bf8aceb98d6129..e7bca7fc74a6a2914c966183c9f83340dd510bf0 100644
+index 2d9a065aa8c6a835e49eee76acdf8cfa6af420bf..cd7e04aa1de8f051ff4bb23f36912830ab573987 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -2378,5 +2378,14 @@ public final class Bukkit {
diff --git a/patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch b/patches/api/0014-PlayerSetSpawnerTypeWithEggEvent.patch
similarity index 100%
rename from patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch
rename to patches/api/0014-PlayerSetSpawnerTypeWithEggEvent.patch
diff --git a/patches/api/0016-EMC-MonsterEggSpawnEvent.patch b/patches/api/0015-EMC-MonsterEggSpawnEvent.patch
similarity index 100%
rename from patches/api/0016-EMC-MonsterEggSpawnEvent.patch
rename to patches/api/0015-EMC-MonsterEggSpawnEvent.patch
diff --git a/patches/api/0017-Player-invulnerabilities.patch b/patches/api/0016-Player-invulnerabilities.patch
similarity index 100%
rename from patches/api/0017-Player-invulnerabilities.patch
rename to patches/api/0016-Player-invulnerabilities.patch
diff --git a/patches/api/0018-Anvil-API.patch b/patches/api/0017-Anvil-API.patch
similarity index 100%
rename from patches/api/0018-Anvil-API.patch
rename to patches/api/0017-Anvil-API.patch
diff --git a/patches/api/0019-ItemStack-convenience-methods.patch b/patches/api/0018-ItemStack-convenience-methods.patch
similarity index 98%
rename from patches/api/0019-ItemStack-convenience-methods.patch
rename to patches/api/0018-ItemStack-convenience-methods.patch
index 311f7c0b0..b49fd6353 100644
--- a/patches/api/0019-ItemStack-convenience-methods.patch
+++ b/patches/api/0018-ItemStack-convenience-methods.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] ItemStack convenience methods
diff --git a/src/main/java/org/bukkit/Material.java b/src/main/java/org/bukkit/Material.java
-index a39e7af2529fb8e65641d76f78e2d8eb12900853..8418eecabcb5319ef4db0785f4350920fa433278 100644
+index 2d39ecea67cd033858eaa713e405260a87c718a3..b20b3d54342a8c81d1ff4062028c7b7d2eec4fe5 100644
--- a/src/main/java/org/bukkit/Material.java
+++ b/src/main/java/org/bukkit/Material.java
-@@ -9871,4 +9871,39 @@ public enum Material implements Keyed, net.kyori.adventure.translation.Translata
+@@ -10219,4 +10219,39 @@ public enum Material implements Keyed, net.kyori.adventure.translation.Translata
return Bukkit.getUnsafe().getCreativeCategory(this);
}
@@ -49,10 +49,10 @@ index a39e7af2529fb8e65641d76f78e2d8eb12900853..8418eecabcb5319ef4db0785f4350920
+ // Purpur end
}
diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java
-index 62841846ec3e14daa46564509671cab146984cc6..6ebfa971cf5bd8dac84eb29402b764feb5e6b974 100644
+index b8a344fd900dcbd4b28085a54b85b16c742e9c6f..346788b5524248e5dd431927ab206f073db01c38 100644
--- a/src/main/java/org/bukkit/inventory/ItemStack.java
+++ b/src/main/java/org/bukkit/inventory/ItemStack.java
-@@ -17,6 +17,18 @@ import org.bukkit.inventory.meta.ItemMeta;
+@@ -16,6 +16,18 @@ import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -71,7 +71,7 @@ index 62841846ec3e14daa46564509671cab146984cc6..6ebfa971cf5bd8dac84eb29402b764fe
/**
* Represents a stack of items.
-@@ -979,4 +991,626 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, net.kyor
+@@ -978,4 +990,626 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, net.kyor
return Bukkit.getUnsafe().isValidRepairItemStack(toBeRepaired, this);
}
// Paper end
diff --git a/patches/api/0020-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch b/patches/api/0019-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch
similarity index 100%
rename from patches/api/0020-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch
rename to patches/api/0019-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch
diff --git a/patches/api/0021-ChatColor-conveniences.patch b/patches/api/0020-ChatColor-conveniences.patch
similarity index 93%
rename from patches/api/0021-ChatColor-conveniences.patch
rename to patches/api/0020-ChatColor-conveniences.patch
index 77b1b7d02..e2497dc96 100644
--- a/patches/api/0021-ChatColor-conveniences.patch
+++ b/patches/api/0020-ChatColor-conveniences.patch
@@ -5,17 +5,17 @@ Subject: [PATCH] ChatColor conveniences
diff --git a/src/main/java/org/bukkit/ChatColor.java b/src/main/java/org/bukkit/ChatColor.java
-index 4594701d77c5d0f744bece871b98d9f6f73eb5a7..924af5982b1990492cafe6ef8d9f284f7933e7c4 100644
+index f6eb30f53dad684f156102cf7147b2f00c82c71e..f1239a2618b08fa92e0e20692d1c3d20d1558502 100644
--- a/src/main/java/org/bukkit/ChatColor.java
+++ b/src/main/java/org/bukkit/ChatColor.java
-@@ -2,6 +2,7 @@ package org.bukkit;
-
+@@ -3,6 +3,7 @@ package org.bukkit;
+ import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.util.Map;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
- import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.Contract;
+ import org.jetbrains.annotations.NotNull;
@@ -413,4 +414,77 @@ public enum ChatColor {
BY_CHAR.put(color.code, color);
}
diff --git a/patches/api/0022-LivingEntity-broadcastItemBreak.patch b/patches/api/0021-LivingEntity-broadcastItemBreak.patch
similarity index 100%
rename from patches/api/0022-LivingEntity-broadcastItemBreak.patch
rename to patches/api/0021-LivingEntity-broadcastItemBreak.patch
diff --git a/patches/api/0023-Item-entity-immunities.patch b/patches/api/0022-Item-entity-immunities.patch
similarity index 100%
rename from patches/api/0023-Item-entity-immunities.patch
rename to patches/api/0022-Item-entity-immunities.patch
diff --git a/patches/api/0024-Spigot-Improve-output-of-plugins-command.patch b/patches/api/0023-Spigot-Improve-output-of-plugins-command.patch
similarity index 100%
rename from patches/api/0024-Spigot-Improve-output-of-plugins-command.patch
rename to patches/api/0023-Spigot-Improve-output-of-plugins-command.patch
diff --git a/patches/api/0025-Add-option-to-disable-zombie-aggressiveness-towards-.patch b/patches/api/0024-Add-option-to-disable-zombie-aggressiveness-towards-.patch
similarity index 100%
rename from patches/api/0025-Add-option-to-disable-zombie-aggressiveness-towards-.patch
rename to patches/api/0024-Add-option-to-disable-zombie-aggressiveness-towards-.patch
diff --git a/patches/api/0026-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch b/patches/api/0025-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
similarity index 100%
rename from patches/api/0026-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
rename to patches/api/0025-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
diff --git a/patches/api/0027-Alphabetize-in-game-plugins-list.patch b/patches/api/0026-Alphabetize-in-game-plugins-list.patch
similarity index 100%
rename from patches/api/0027-Alphabetize-in-game-plugins-list.patch
rename to patches/api/0026-Alphabetize-in-game-plugins-list.patch
diff --git a/patches/api/0028-Rabid-Wolf-API.patch b/patches/api/0027-Rabid-Wolf-API.patch
similarity index 100%
rename from patches/api/0028-Rabid-Wolf-API.patch
rename to patches/api/0027-Rabid-Wolf-API.patch
diff --git a/patches/api/0029-PlayerBookTooLargeEvent.patch b/patches/api/0028-PlayerBookTooLargeEvent.patch
similarity index 100%
rename from patches/api/0029-PlayerBookTooLargeEvent.patch
rename to patches/api/0028-PlayerBookTooLargeEvent.patch
diff --git a/patches/api/0030-Full-netherite-armor-grants-fire-resistance.patch b/patches/api/0029-Full-netherite-armor-grants-fire-resistance.patch
similarity index 100%
rename from patches/api/0030-Full-netherite-armor-grants-fire-resistance.patch
rename to patches/api/0029-Full-netherite-armor-grants-fire-resistance.patch
diff --git a/patches/api/0031-Add-EntityTeleportHinderedEvent.patch b/patches/api/0030-Add-EntityTeleportHinderedEvent.patch
similarity index 100%
rename from patches/api/0031-Add-EntityTeleportHinderedEvent.patch
rename to patches/api/0030-Add-EntityTeleportHinderedEvent.patch
diff --git a/patches/api/0032-Conflict-on-change-for-adventure-deprecations.patch b/patches/api/0031-Conflict-on-change-for-adventure-deprecations.patch
similarity index 99%
rename from patches/api/0032-Conflict-on-change-for-adventure-deprecations.patch
rename to patches/api/0031-Conflict-on-change-for-adventure-deprecations.patch
index 760cba3a6..7c6ef90c9 100644
--- a/patches/api/0032-Conflict-on-change-for-adventure-deprecations.patch
+++ b/patches/api/0031-Conflict-on-change-for-adventure-deprecations.patch
@@ -591,7 +591,7 @@ index 943d324435350d3f16fad3e21cb472a01a3ff60b..9f66a44977ab3248ce41733e424b5b71
this.message = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(message); // Paper
}
diff --git a/src/main/java/org/bukkit/event/server/ServerListPingEvent.java b/src/main/java/org/bukkit/event/server/ServerListPingEvent.java
-index 172697ac5dc0ea3551a61b5589416ac68f372cd1..fffe2ab5a9f282b60d5d7ae316c063d8945a65c0 100644
+index 12cdae95c338d21684991d34aea5a643f4b4bcd3..f88356e5be4d33f403617b004e5cd76fe17f2e24 100644
--- a/src/main/java/org/bukkit/event/server/ServerListPingEvent.java
+++ b/src/main/java/org/bukkit/event/server/ServerListPingEvent.java
@@ -113,7 +113,7 @@ public class ServerListPingEvent extends ServerEvent implements Iterable
diff --git a/patches/api/0033-Add-enchantment-target-for-bows-and-crossbows.patch b/patches/api/0032-Add-enchantment-target-for-bows-and-crossbows.patch
similarity index 100%
rename from patches/api/0033-Add-enchantment-target-for-bows-and-crossbows.patch
rename to patches/api/0032-Add-enchantment-target-for-bows-and-crossbows.patch
diff --git a/patches/api/0034-Iron-golem-poppy-calms-anger.patch b/patches/api/0033-Iron-golem-poppy-calms-anger.patch
similarity index 100%
rename from patches/api/0034-Iron-golem-poppy-calms-anger.patch
rename to patches/api/0033-Iron-golem-poppy-calms-anger.patch
diff --git a/patches/api/0035-API-for-any-mob-to-burn-daylight.patch b/patches/api/0034-API-for-any-mob-to-burn-daylight.patch
similarity index 100%
rename from patches/api/0035-API-for-any-mob-to-burn-daylight.patch
rename to patches/api/0034-API-for-any-mob-to-burn-daylight.patch
diff --git a/patches/api/0036-Flying-Fall-Damage-API.patch b/patches/api/0035-Flying-Fall-Damage-API.patch
similarity index 100%
rename from patches/api/0036-Flying-Fall-Damage-API.patch
rename to patches/api/0035-Flying-Fall-Damage-API.patch
diff --git a/patches/api/0037-Add-back-player-spawned-endermite-API.patch b/patches/api/0036-Add-back-player-spawned-endermite-API.patch
similarity index 100%
rename from patches/api/0037-Add-back-player-spawned-endermite-API.patch
rename to patches/api/0036-Add-back-player-spawned-endermite-API.patch
diff --git a/patches/api/0038-Fix-default-permission-system.patch b/patches/api/0037-Fix-default-permission-system.patch
similarity index 100%
rename from patches/api/0038-Fix-default-permission-system.patch
rename to patches/api/0037-Fix-default-permission-system.patch
diff --git a/patches/api/0039-Summoner-API.patch b/patches/api/0038-Summoner-API.patch
similarity index 100%
rename from patches/api/0039-Summoner-API.patch
rename to patches/api/0038-Summoner-API.patch
diff --git a/patches/api/0040-Clean-up-version-command-output.patch b/patches/api/0039-Clean-up-version-command-output.patch
similarity index 59%
rename from patches/api/0040-Clean-up-version-command-output.patch
rename to patches/api/0039-Clean-up-version-command-output.patch
index 3954833b1..279ab404e 100644
--- a/patches/api/0040-Clean-up-version-command-output.patch
+++ b/patches/api/0039-Clean-up-version-command-output.patch
@@ -22,30 +22,30 @@ index a736d7bcdc5861a01b66ba36158db1c716339346..22fc165fd9c95f0f3ae1be7a0857e48c
@Override
diff --git a/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/src/main/java/org/bukkit/command/defaults/VersionCommand.java
-index 57a21495843f3a144cd73473cdc8781d6129b7ca..b7fa160a305ee89004c11a3d8a01ac3b721f59b6 100644
+index e40f017f87d6b6b4770501b106c76dc69ec69abb..eac5830986cd0638950bbb1e6f10a30e246e09a7 100644
--- a/src/main/java/org/bukkit/command/defaults/VersionCommand.java
+++ b/src/main/java/org/bukkit/command/defaults/VersionCommand.java
-@@ -199,7 +199,7 @@ public class VersionCommand extends BukkitCommand {
+@@ -198,7 +198,7 @@ public class VersionCommand extends BukkitCommand {
String version = Bukkit.getVersion();
// Paper start
if (version.startsWith("null")) { // running from ide?
-- setVersionMessage(Component.text("Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.YELLOW));
-+ setVersionMessage(Component.text("* Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.RED)); // Purpur
+- setVersionMessage(net.kyori.adventure.text.Component.text("Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.YELLOW));
++ setVersionMessage(net.kyori.adventure.text.Component.text("* Unknown version, custom build?", net.kyori.adventure.text.format.NamedTextColor.RED)); // Purpur
return;
}
setVersionMessage(getVersionFetcher().getVersionMessage(version));
-@@ -240,9 +240,11 @@ public class VersionCommand extends BukkitCommand {
+@@ -239,9 +239,11 @@ public class VersionCommand extends BukkitCommand {
// Paper start
- private void setVersionMessage(final @NotNull Component msg) {
+ private void setVersionMessage(final @NotNull net.kyori.adventure.text.Component msg) {
lastCheck = System.currentTimeMillis();
-- final Component message = net.kyori.adventure.text.TextComponent.ofChildren(
-- Component.text(Bukkit.getVersionMessage(), net.kyori.adventure.text.format.NamedTextColor.WHITE),
-- Component.newline(),
+- final net.kyori.adventure.text.Component message = net.kyori.adventure.text.TextComponent.ofChildren(
+- net.kyori.adventure.text.Component.text(Bukkit.getVersionMessage(), net.kyori.adventure.text.format.NamedTextColor.WHITE),
+- net.kyori.adventure.text.Component.newline(),
+ // Purpur start
+ int distance = getVersionFetcher().distance();
-+ final Component message = Component.join(net.kyori.adventure.text.JoinConfiguration.separator(Component.newline()),
++ final net.kyori.adventure.text.Component message = net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.separator(net.kyori.adventure.text.Component.newline()),
+ ChatColor.parseMM("Current: %s%s*", distance == 0 ? "" : distance > 0 ? "" : "", Bukkit.getVersion()),
+ // Purpur end
msg
);
- this.versionMessage = Component.text()
+ this.versionMessage = net.kyori.adventure.text.Component.text()
diff --git a/patches/api/0041-Extended-OfflinePlayer-API.patch b/patches/api/0040-Extended-OfflinePlayer-API.patch
similarity index 100%
rename from patches/api/0041-Extended-OfflinePlayer-API.patch
rename to patches/api/0040-Extended-OfflinePlayer-API.patch
diff --git a/patches/api/0042-Added-the-ability-to-add-combustible-items.patch b/patches/api/0041-Added-the-ability-to-add-combustible-items.patch
similarity index 95%
rename from patches/api/0042-Added-the-ability-to-add-combustible-items.patch
rename to patches/api/0041-Added-the-ability-to-add-combustible-items.patch
index e0225ab90..87e352001 100644
--- a/patches/api/0042-Added-the-ability-to-add-combustible-items.patch
+++ b/patches/api/0041-Added-the-ability-to-add-combustible-items.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Added the ability to add combustible items
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
-index 9189ada0644a226038eeb7967d45c72ddfc89085..8c37680ca16adbf030cfd3098330d2592d4aaa23 100644
+index 9ff99a1ea5183042d7eea89bb310386c6630663a..c17e49f1eea2a70c70eea84820fc17fd5e263304 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -2387,5 +2387,24 @@ public final class Bukkit {
diff --git a/patches/api/0043-Potion-NamespacedKey.patch b/patches/api/0042-Potion-NamespacedKey.patch
similarity index 97%
rename from patches/api/0043-Potion-NamespacedKey.patch
rename to patches/api/0042-Potion-NamespacedKey.patch
index 97873f4d0..00e7fc1b7 100644
--- a/patches/api/0043-Potion-NamespacedKey.patch
+++ b/patches/api/0042-Potion-NamespacedKey.patch
@@ -5,12 +5,12 @@ Subject: [PATCH] Potion NamespacedKey
diff --git a/src/main/java/org/bukkit/potion/PotionEffect.java b/src/main/java/org/bukkit/potion/PotionEffect.java
-index 74767751199bce03d63f2a9524712656193f850c..52a31aa6d77e6c9afac78e76829d0265e903b029 100644
+index 24e36cdf580da885ac64002673a786b9c5a3f787..d20cc4d4f5b37a3de9cb3cf47af7a908e9dbc2fc 100644
--- a/src/main/java/org/bukkit/potion/PotionEffect.java
+++ b/src/main/java/org/bukkit/potion/PotionEffect.java
-@@ -5,6 +5,7 @@ import java.util.Map;
+@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap;
+ import java.util.Map;
import java.util.NoSuchElementException;
- import org.apache.commons.lang.Validate;
import org.bukkit.Color;
+import org.bukkit.NamespacedKey;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
@@ -65,7 +65,7 @@ index 74767751199bce03d63f2a9524712656193f850c..52a31aa6d77e6c9afac78e76829d0265
+ */
+ public PotionEffect(@NotNull PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon, @Nullable NamespacedKey key) {
+ // Purpur end
- Validate.notNull(type, "effect type cannot be null");
+ Preconditions.checkArgument(type != null, "effect type cannot be null");
this.type = type;
this.duration = duration;
@@ -51,6 +84,7 @@ public class PotionEffect implements ConfigurationSerializable {
diff --git a/patches/api/0044-Grindstone-API.patch b/patches/api/0043-Grindstone-API.patch
similarity index 100%
rename from patches/api/0044-Grindstone-API.patch
rename to patches/api/0043-Grindstone-API.patch
diff --git a/patches/api/0045-Shears-can-have-looting-enchantment.patch b/patches/api/0044-Shears-can-have-looting-enchantment.patch
similarity index 100%
rename from patches/api/0045-Shears-can-have-looting-enchantment.patch
rename to patches/api/0044-Shears-can-have-looting-enchantment.patch
diff --git a/patches/api/0046-Lobotomize-stuck-villagers.patch b/patches/api/0045-Lobotomize-stuck-villagers.patch
similarity index 100%
rename from patches/api/0046-Lobotomize-stuck-villagers.patch
rename to patches/api/0045-Lobotomize-stuck-villagers.patch
diff --git a/patches/server/.keep b/patches/server/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/patches/server/0001-Pufferfish-Server-Changes.patch b/patches/server/0001-Pufferfish-Server-Changes.patch
deleted file mode 100644
index 9a9b1b8de..000000000
--- a/patches/server/0001-Pufferfish-Server-Changes.patch
+++ /dev/null
@@ -1,4331 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Kevin Raneri
-Date: Wed, 3 Feb 2021 23:02:38 -0600
-Subject: [PATCH] Pufferfish Server Changes
-
-Pufferfish
-Copyright (C) 2022 Pufferfish Studios LLC
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-diff --git a/build.gradle.kts b/build.gradle.kts
-index 0282e3b75470e1a68ea1fc228082483514ba432e..b04e9ab317fbce9c090b61076eb07c40f069dc59 100644
---- a/build.gradle.kts
-+++ b/build.gradle.kts
-@@ -9,8 +9,12 @@ plugins {
- }
-
- dependencies {
-- implementation(project(":paper-api"))
-- implementation(project(":paper-mojangapi"))
-+ implementation(project(":pufferfish-api")) // Pufferfish // Paper
-+ // Pufferfish start
-+ implementation("io.papermc.paper:paper-mojangapi:1.18.2-R0.1-SNAPSHOT") {
-+ exclude("io.papermc.paper", "paper-api")
-+ }
-+ // Pufferfish end
- // Paper start
- implementation("org.jline:jline-terminal-jansi:3.21.0")
- implementation("net.minecrell:terminalconsoleappender:1.3.0")
-@@ -42,12 +46,28 @@ dependencies {
- }
- // Paper end
-
-+ // Pufferfish start
-+ implementation("org.yaml:snakeyaml:1.30")
-+ implementation ("me.carleslc.Simple-YAML:Simple-Yaml:1.8") {
-+ exclude(group="org.yaml", module="snakeyaml")
-+ }
-+ // Pufferfish end
-+ implementation("com.github.technove:Flare:34637f3f87") // Pufferfish - flare
-+
- testImplementation("io.github.classgraph:classgraph:4.8.47") // Paper - mob goal test
- testImplementation("junit:junit:4.13.2")
- testImplementation("org.hamcrest:hamcrest-library:1.3")
- }
-
- val craftbukkitPackageVersion = "1_18_R2" // Paper
-+
-+// Pufferfish Start
-+tasks.withType {
-+ val compilerArgs = options.compilerArgs
-+ compilerArgs.add("--add-modules=jdk.incubator.vector")
-+}
-+// Pufferfish End
-+
- tasks.jar {
- archiveClassifier.set("dev")
-
-@@ -60,7 +80,7 @@ tasks.jar {
- attributes(
- "Main-Class" to "org.bukkit.craftbukkit.Main",
- "Implementation-Title" to "CraftBukkit",
-- "Implementation-Version" to "git-Paper-$implementationVersion",
-+ "Implementation-Version" to "git-Pufferfish-$implementationVersion", // Pufferfish
- "Implementation-Vendor" to date, // Paper
- "Specification-Title" to "Bukkit",
- "Specification-Version" to project.version,
-diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
-index f3bf9df8c0bd56cad461210ce8551ade3a220b6b..088585a1075cd8790e2599eb6a8372adcff050e3 100644
---- a/src/main/java/co/aikar/timings/TimingsExport.java
-+++ b/src/main/java/co/aikar/timings/TimingsExport.java
-@@ -240,7 +240,8 @@ public class TimingsExport extends Thread {
- parent.put("config", createObject(
- pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)),
- pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)),
-- pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
-+ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Pufferfish
-+ pair("pufferfish", mapAsJSON(gg.pufferfish.pufferfish.PufferfishConfig.getConfigCopy(), null)) // Pufferfish
- ));
-
- new TimingsExport(listeners, parent, history).start();
-diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
-index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..f2fe6ea3719ff8b2913b7a3a939d7a5b75cb8b28 100644
---- a/src/main/java/com/destroystokyo/paper/Metrics.java
-+++ b/src/main/java/com/destroystokyo/paper/Metrics.java
-@@ -593,7 +593,7 @@ public class Metrics {
- boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
- // Only start Metrics, if it's enabled in the config
- if (config.getBoolean("enabled", true)) {
-- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
-+ Metrics metrics = new Metrics("Pufferfish", serverUUID, logFailedRequests, Bukkit.getLogger()); // Pufferfish
-
- metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
- String minecraftVersion = Bukkit.getVersion();
-@@ -603,7 +603,7 @@ public class Metrics {
-
- metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size()));
- metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline"));
-- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown"));
-+ metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown"));
-
- metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
- Map> map = new HashMap<>();
-diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-index 8379c6313f06ab3eeaf02bad41d8b835d50e093f..7a7d0566611aafafba30b7b25c2f1f3e78b054fa 100644
---- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
-+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-@@ -241,6 +241,15 @@ public class PaperConfig {
- public static String timingsServerName;
- private static void timings() {
- boolean timings = getBoolean("timings.enabled", true);
-+ // Pufferfish start
-+ boolean reallyEnableTimings = getBoolean("timings.really-enabled", false);
-+ if (timings && !reallyEnableTimings) {
-+ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] To improve performance, timings have been disabled by default");
-+ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] You can still use timings by using /timings on, but they will not start on server startup unless you set timings.really-enabled to true in paper.yml");
-+ Bukkit.getLogger().log(Level.WARNING, "[Pufferfish] If you would like to disable this message, either set timings.really-enabled to true or timings.enabled to false.");
-+ }
-+ timings = reallyEnableTimings;
-+ // Pufferfish end
- boolean verboseTimings = getBoolean("timings.verbose", true);
- TimingsManager.url = getString("timings.url", "https://timings.aikar.co/");
- if (!TimingsManager.url.endsWith("/")) {
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
-index c89f6986eda5a132a948732ea1b6923370685317..a69c13e20040c1561d9c2d4d89ec7d4e635134fc 100644
---- a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
-@@ -26,7 +26,7 @@ public abstract class AreaMap {
-
- // we use linked for better iteration.
- // map of: coordinate to set of objects in coordinate
-- protected final Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f);
-+ protected Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); // Pufferfish - not actually final
- protected final PooledLinkedHashSets pooledHashSets;
-
- protected final ChangeCallback addCallback;
-@@ -160,7 +160,8 @@ public abstract class AreaMap {
- protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final E object);
-
- // expensive op, only for debug
-- protected void validate(final E object, final int viewDistance) {
-+ protected void validate0(final E object, final int viewDistance) { // Pufferfish - rename this thing just in case it gets used I'd rather a compile time error.
-+ if (true) throw new UnsupportedOperationException(); // Pufferfish - not going to put in the effort to fix this if it doesn't ever get used.
- int entiesGot = 0;
- int expectedEntries = (2 * viewDistance + 1);
- expectedEntries *= expectedEntries;
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-index 46954db7ecd35ac4018fdf476df7c8020d7ce6c8..1ad890a244bdf6df48a8db68cb43450e08c788a6 100644
---- a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-@@ -5,7 +5,7 @@ import net.minecraft.server.level.ServerPlayer;
- /**
- * @author Spottedleaf
- */
--public final class PlayerAreaMap extends AreaMap {
-+public class PlayerAreaMap extends AreaMap { // Pufferfish - not actually final
-
- public PlayerAreaMap() {
- super();
-diff --git a/src/main/java/gg/airplane/structs/FluidDirectionCache.java b/src/main/java/gg/airplane/structs/FluidDirectionCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..aa8467b9dda1f7707e41f50ac7b3e9d7343723ec
---- /dev/null
-+++ b/src/main/java/gg/airplane/structs/FluidDirectionCache.java
-@@ -0,0 +1,136 @@
-+package gg.airplane.structs;
-+
-+import it.unimi.dsi.fastutil.HashCommon;
-+
-+/**
-+ * This is a replacement for the cache used in FluidTypeFlowing.
-+ * The requirements for the previous cache were:
-+ * - Store 200 entries
-+ * - Look for the flag in the cache
-+ * - If it exists, move to front of cache
-+ * - If it doesn't exist, remove last entry in cache and insert in front
-+ *
-+ * This class accomplishes something similar, however has a few different
-+ * requirements put into place to make this more optimize:
-+ *
-+ * - maxDistance is the most amount of entries to be checked, instead
-+ * of having to check the entire list.
-+ * - In combination with that, entries are all tracked by age and how
-+ * frequently they're used. This enables us to remove old entries,
-+ * without constantly shifting any around.
-+ *
-+ * Usage of the previous map would have to reset the head every single usage,
-+ * shifting the entire map. Here, nothing happens except an increment when
-+ * the cache is hit, and when it needs to replace an old element only a single
-+ * element is modified.
-+ */
-+public class FluidDirectionCache {
-+
-+ private static class FluidDirectionEntry {
-+ private final T data;
-+ private final boolean flag;
-+ private int uses = 0;
-+ private int age = 0;
-+
-+ private FluidDirectionEntry(T data, boolean flag) {
-+ this.data = data;
-+ this.flag = flag;
-+ }
-+
-+ public int getValue() {
-+ return this.uses - (this.age >> 1); // age isn't as important as uses
-+ }
-+
-+ public void incrementUses() {
-+ this.uses = this.uses + 1 & Integer.MAX_VALUE;
-+ }
-+
-+ public void incrementAge() {
-+ this.age = this.age + 1 & Integer.MAX_VALUE;
-+ }
-+ }
-+
-+ private final FluidDirectionEntry[] entries;
-+ private final int mask;
-+ private final int maxDistance; // the most amount of entries to check for a value
-+
-+ public FluidDirectionCache(int size) {
-+ int arraySize = HashCommon.nextPowerOfTwo(size);
-+ this.entries = new FluidDirectionEntry[arraySize];
-+ this.mask = arraySize - 1;
-+ this.maxDistance = Math.min(arraySize, 4);
-+ }
-+
-+ public Boolean getValue(T data) {
-+ FluidDirectionEntry curr;
-+ int pos;
-+
-+ if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) {
-+ return null;
-+ } else if (data.equals(curr.data)) {
-+ curr.incrementUses();
-+ return curr.flag;
-+ }
-+
-+ int checked = 1; // start at 1 because we already checked the first spot above
-+
-+ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
-+ if (data.equals(curr.data)) {
-+ curr.incrementUses();
-+ return curr.flag;
-+ } else if (++checked >= this.maxDistance) {
-+ break;
-+ }
-+ }
-+
-+ return null;
-+ }
-+
-+ public void putValue(T data, boolean flag) {
-+ FluidDirectionEntry curr;
-+ int pos;
-+
-+ if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) {
-+ this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add
-+ return;
-+ } else if (data.equals(curr.data)) {
-+ curr.incrementUses();
-+ return;
-+ }
-+
-+ int checked = 1; // start at 1 because we already checked the first spot above
-+
-+ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
-+ if (data.equals(curr.data)) {
-+ curr.incrementUses();
-+ return;
-+ } else if (++checked >= this.maxDistance) {
-+ this.forceAdd(data, flag);
-+ return;
-+ }
-+ }
-+
-+ this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add
-+ }
-+
-+ private void forceAdd(T data, boolean flag) {
-+ int expectedPos = HashCommon.mix(data.hashCode()) & this.mask;
-+
-+ int toRemovePos = expectedPos;
-+ FluidDirectionEntry entryToRemove = this.entries[toRemovePos];
-+
-+ for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) {
-+ int pos = i & this.mask;
-+ FluidDirectionEntry entry = this.entries[pos];
-+ if (entry.getValue() < entryToRemove.getValue()) {
-+ toRemovePos = pos;
-+ entryToRemove = entry;
-+ }
-+
-+ entry.incrementAge(); // use this as a mechanism to age the other entries
-+ }
-+
-+ // remove the least used/oldest entry
-+ this.entries[toRemovePos] = new FluidDirectionEntry(data, flag);
-+ }
-+}
-diff --git a/src/main/java/gg/airplane/structs/ItemListWithBitset.java b/src/main/java/gg/airplane/structs/ItemListWithBitset.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1b7a4ee47f4445d7f2ac91d3a73ae113edbdddb2
---- /dev/null
-+++ b/src/main/java/gg/airplane/structs/ItemListWithBitset.java
-@@ -0,0 +1,114 @@
-+package gg.airplane.structs;
-+
-+import net.minecraft.core.NonNullList;
-+import net.minecraft.world.item.ItemStack;
-+import org.apache.commons.lang.Validate;
-+import org.jetbrains.annotations.NotNull;
-+import org.jetbrains.annotations.Nullable;
-+
-+import java.util.AbstractList;
-+import java.util.Arrays;
-+import java.util.List;
-+
-+public class ItemListWithBitset extends AbstractList {
-+ public static ItemListWithBitset fromList(List list) {
-+ if (list instanceof ItemListWithBitset ours) {
-+ return ours;
-+ }
-+ return new ItemListWithBitset(list);
-+ }
-+
-+ private static ItemStack[] createArray(int size) {
-+ ItemStack[] array = new ItemStack[size];
-+ Arrays.fill(array, ItemStack.EMPTY);
-+ return array;
-+ }
-+
-+ private final ItemStack[] items;
-+
-+ private long bitSet = 0;
-+ private final long allBits;
-+
-+ private static class OurNonNullList extends NonNullList {
-+ protected OurNonNullList(List delegate) {
-+ super(delegate, ItemStack.EMPTY);
-+ }
-+ }
-+
-+ public final NonNullList nonNullList = new OurNonNullList(this);
-+
-+ private ItemListWithBitset(List list) {
-+ this(list.size());
-+
-+ for (int i = 0; i < list.size(); i++) {
-+ this.set(i, list.get(i));
-+ }
-+ }
-+
-+ public ItemListWithBitset(int size) {
-+ Validate.isTrue(size < Long.BYTES * 8, "size is too large");
-+
-+ this.items = createArray(size);
-+ this.allBits = ((1L << size) - 1);
-+ }
-+
-+ public boolean isCompletelyEmpty() {
-+ return this.bitSet == 0;
-+ }
-+
-+ public boolean hasFullStacks() {
-+ return (this.bitSet & this.allBits) == allBits;
-+ }
-+
-+ @Override
-+ public ItemStack set(int index, @NotNull ItemStack itemStack) {
-+ ItemStack existing = this.items[index];
-+
-+ this.items[index] = itemStack;
-+
-+ if (itemStack == ItemStack.EMPTY) {
-+ this.bitSet &= ~(1L << index);
-+ } else {
-+ this.bitSet |= 1L << index;
-+ }
-+
-+ return existing;
-+ }
-+
-+ @NotNull
-+ @Override
-+ public ItemStack get(int var0) {
-+ return this.items[var0];
-+ }
-+
-+ @Override
-+ public int size() {
-+ return this.items.length;
-+ }
-+
-+ @Override
-+ public void clear() {
-+ Arrays.fill(this.items, ItemStack.EMPTY);
-+ }
-+
-+ // these are unsupported for block inventories which have a static size
-+ @Override
-+ public void add(int var0, ItemStack var1) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public ItemStack remove(int var0) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "ItemListWithBitset{" +
-+ "items=" + Arrays.toString(items) +
-+ ", bitSet=" + Long.toString(bitSet, 2) +
-+ ", allBits=" + Long.toString(allBits, 2) +
-+ ", size=" + this.items.length +
-+ '}';
-+ }
-+}
-diff --git a/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a7f297ebb569f7c1f205e967ca485be70013a714
---- /dev/null
-+++ b/src/main/java/gg/airplane/structs/Long2FloatAgingCache.java
-@@ -0,0 +1,119 @@
-+package gg.airplane.structs;
-+
-+import it.unimi.dsi.fastutil.HashCommon;
-+
-+/**
-+ * A replacement for the cache used in Biome.
-+ */
-+public class Long2FloatAgingCache {
-+
-+ private static class AgingEntry {
-+ private long data;
-+ private float value;
-+ private int uses = 0;
-+ private int age = 0;
-+
-+ private AgingEntry(long data, float value) {
-+ this.data = data;
-+ this.value = value;
-+ }
-+
-+ public void replace(long data, float flag) {
-+ this.data = data;
-+ this.value = flag;
-+ }
-+
-+ public int getValue() {
-+ return this.uses - (this.age >> 1); // age isn't as important as uses
-+ }
-+
-+ public void incrementUses() {
-+ this.uses = this.uses + 1 & Integer.MAX_VALUE;
-+ }
-+
-+ public void incrementAge() {
-+ this.age = this.age + 1 & Integer.MAX_VALUE;
-+ }
-+ }
-+
-+ private final AgingEntry[] entries;
-+ private final int mask;
-+ private final int maxDistance; // the most amount of entries to check for a value
-+
-+ public Long2FloatAgingCache(int size) {
-+ int arraySize = HashCommon.nextPowerOfTwo(size);
-+ this.entries = new AgingEntry[arraySize];
-+ this.mask = arraySize - 1;
-+ this.maxDistance = Math.min(arraySize, 4);
-+ }
-+
-+ public float getValue(long data) {
-+ AgingEntry curr;
-+ int pos;
-+
-+ if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) {
-+ return Float.NaN;
-+ } else if (data == curr.data) {
-+ curr.incrementUses();
-+ return curr.value;
-+ }
-+
-+ int checked = 1; // start at 1 because we already checked the first spot above
-+
-+ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
-+ if (data == curr.data) {
-+ curr.incrementUses();
-+ return curr.value;
-+ } else if (++checked >= this.maxDistance) {
-+ break;
-+ }
-+ }
-+
-+ return Float.NaN;
-+ }
-+
-+ public void putValue(long data, float value) {
-+ AgingEntry curr;
-+ int pos;
-+
-+ if ((curr = this.entries[pos = HashCommon.mix(HashCommon.long2int(data)) & this.mask]) == null) {
-+ this.entries[pos] = new AgingEntry(data, value); // add
-+ return;
-+ } else if (data == curr.data) {
-+ curr.incrementUses();
-+ return;
-+ }
-+
-+ int checked = 1; // start at 1 because we already checked the first spot above
-+
-+ while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
-+ if (data == curr.data) {
-+ curr.incrementUses();
-+ return;
-+ } else if (++checked >= this.maxDistance) {
-+ this.forceAdd(data, value);
-+ return;
-+ }
-+ }
-+
-+ this.entries[pos] = new AgingEntry(data, value); // add
-+ }
-+
-+ private void forceAdd(long data, float value) {
-+ int expectedPos = HashCommon.mix(HashCommon.long2int(data)) & this.mask;
-+ AgingEntry entryToRemove = this.entries[expectedPos];
-+
-+ for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) {
-+ int pos = i & this.mask;
-+ AgingEntry entry = this.entries[pos];
-+ if (entry.getValue() < entryToRemove.getValue()) {
-+ entryToRemove = entry;
-+ }
-+
-+ entry.incrementAge(); // use this as a mechanism to age the other entries
-+ }
-+
-+ // remove the least used/oldest entry
-+ entryToRemove.replace(data, value);
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e164237e749bcc43466d4ed7aeada5ab9fddf8a6
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishCommand.java
-@@ -0,0 +1,68 @@
-+package gg.pufferfish.pufferfish;
-+
-+import java.io.IOException;
-+import java.util.Collections;
-+import java.util.List;
-+import java.util.stream.Collectors;
-+import java.util.stream.Stream;
-+import net.kyori.adventure.text.Component;
-+import net.kyori.adventure.text.format.NamedTextColor;
-+import net.md_5.bungee.api.ChatColor;
-+import net.minecraft.server.MinecraftServer;
-+import org.bukkit.Bukkit;
-+import org.bukkit.Location;
-+import org.bukkit.command.Command;
-+import org.bukkit.command.CommandSender;
-+
-+public class PufferfishCommand extends Command {
-+
-+ public PufferfishCommand() {
-+ super("pufferfish");
-+ this.description = "Pufferfish related commands";
-+ this.usageMessage = "/pufferfish [reload | version]";
-+ this.setPermission("bukkit.command.pufferfish");
-+ }
-+
-+ public static void init() {
-+ MinecraftServer.getServer().server.getCommandMap().register("pufferfish", "Pufferfish", new PufferfishCommand());
-+ }
-+
-+ @Override
-+ public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException {
-+ if (args.length == 1) {
-+ return Stream.of("reload", "version")
-+ .filter(arg -> arg.startsWith(args[0].toLowerCase()))
-+ .collect(Collectors.toList());
-+ }
-+ return Collections.emptyList();
-+ }
-+
-+ @Override
-+ public boolean execute(CommandSender sender, String commandLabel, String[] args) {
-+ if (!testPermission(sender)) return true;
-+ String prefix = ChatColor.of("#12fff6") + "" + ChatColor.BOLD + "Pufferfish » " + ChatColor.of("#e8f9f9");
-+
-+ if (args.length != 1) {
-+ sender.sendMessage(prefix + "Usage: " + usageMessage);
-+ args = new String[]{"version"};
-+ }
-+
-+ if (args[0].equalsIgnoreCase("reload")) {
-+ MinecraftServer console = MinecraftServer.getServer();
-+ try {
-+ PufferfishConfig.load();
-+ } catch (IOException e) {
-+ sender.sendMessage(Component.text("Failed to reload.", NamedTextColor.RED));
-+ e.printStackTrace();
-+ return true;
-+ }
-+ console.server.reloadCount++;
-+
-+ Command.broadcastCommandMessage(sender, prefix + "Pufferfish configuration has been reloaded.");
-+ } else if (args[0].equalsIgnoreCase("version")) {
-+ Command.broadcastCommandMessage(sender, prefix + "This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")");
-+ }
-+
-+ return true;
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7572cbc662a5b824435d75e1b3b7ea0e58144c9c
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java
-@@ -0,0 +1,313 @@
-+package gg.pufferfish.pufferfish;
-+
-+import gg.pufferfish.pufferfish.simd.SIMDDetection;
-+import java.io.File;
-+import java.io.IOException;
-+import java.util.Collections;
-+import java.util.Locale;
-+import java.util.Map;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.tags.TagKey;
-+import org.apache.logging.log4j.Level;
-+import org.bukkit.configuration.ConfigurationSection;
-+import net.minecraft.core.Registry;
-+import net.minecraft.world.entity.EntityType;
-+import java.lang.reflect.Method;
-+import java.lang.reflect.Modifier;
-+import java.util.List;
-+import gg.pufferfish.pufferfish.flare.FlareCommand;
-+import net.minecraft.server.MinecraftServer;
-+import org.apache.logging.log4j.Level;
-+import org.bukkit.configuration.ConfigurationSection;
-+import org.bukkit.configuration.MemoryConfiguration;
-+import org.jetbrains.annotations.Nullable;
-+import org.simpleyaml.configuration.comments.CommentType;
-+import org.simpleyaml.configuration.file.YamlFile;
-+import org.simpleyaml.exceptions.InvalidConfigurationException;
-+import org.bukkit.command.SimpleCommandMap;
-+
-+import java.lang.reflect.Method;
-+import java.lang.reflect.Modifier;
-+import java.util.List;
-+import java.net.URI;
-+import java.util.Collections;
-+
-+public class PufferfishConfig {
-+
-+ private static final YamlFile config = new YamlFile();
-+ private static int updates = 0;
-+
-+ private static ConfigurationSection convertToBukkit(org.simpleyaml.configuration.ConfigurationSection section) {
-+ ConfigurationSection newSection = new MemoryConfiguration();
-+ for (String key : section.getKeys(false)) {
-+ if (section.isConfigurationSection(key)) {
-+ newSection.set(key, convertToBukkit(section.getConfigurationSection(key)));
-+ } else {
-+ newSection.set(key, section.get(key));
-+ }
-+ }
-+ return newSection;
-+ }
-+
-+ public static ConfigurationSection getConfigCopy() {
-+ return convertToBukkit(config);
-+ }
-+
-+ public static int getUpdates() {
-+ return updates;
-+ }
-+
-+ public static void load() throws IOException {
-+ File configFile = new File("pufferfish.yml");
-+
-+ if (configFile.exists()) {
-+ try {
-+ config.load(configFile);
-+ } catch (InvalidConfigurationException e) {
-+ throw new IOException(e);
-+ }
-+ }
-+
-+ getString("info.version", "1.0");
-+ setComment("info",
-+ "Pufferfish Configuration",
-+ "Check out Pufferfish Host for maximum performance server hosting: https://pufferfish.host",
-+ "Join our Discord for support: https://discord.gg/reZw4vQV9H",
-+ "Download new builds at https://ci.pufferfish.host/job/Pufferfish");
-+
-+ for (Method method : PufferfishConfig.class.getDeclaredMethods()) {
-+ if (Modifier.isStatic(method.getModifiers()) && Modifier.isPrivate(method.getModifiers()) && method.getParameterCount() == 0 &&
-+ method.getReturnType() == Void.TYPE && !method.getName().startsWith("lambda")) {
-+ method.setAccessible(true);
-+ try {
-+ method.invoke(null);
-+ } catch (Throwable t) {
-+ MinecraftServer.LOGGER.warn("Failed to load configuration option from " + method.getName(), t);
-+ }
-+ }
-+ }
-+
-+ updates++;
-+
-+ config.save(configFile);
-+
-+ // Attempt to detect vectorization
-+ try {
-+ SIMDDetection.isEnabled = SIMDDetection.canEnable(PufferfishLogger.LOGGER);
-+ SIMDDetection.versionLimited = SIMDDetection.getJavaVersion() != 17 && SIMDDetection.getJavaVersion() != 18;
-+ } catch (NoClassDefFoundError | Exception ignored) {}
-+
-+ if (SIMDDetection.isEnabled) {
-+ PufferfishLogger.LOGGER.info("SIMD operations detected as functional. Will replace some operations with faster versions.");
-+ } else if (SIMDDetection.versionLimited) {
-+ PufferfishLogger.LOGGER.warning("Will not enable SIMD! These optimizations are only safely supported on Java 17 and Java 18.");
-+ } else {
-+ PufferfishLogger.LOGGER.warning("SIMD operations are available for your server, but are not configured!");
-+ PufferfishLogger.LOGGER.warning("To enable additional optimizations, add \"--add-modules=jdk.incubator.vector\" to your startup flags, BEFORE the \"-jar\".");
-+ PufferfishLogger.LOGGER.warning("If you have already added this flag, then SIMD operations are not supported on your JVM or CPU.");
-+ PufferfishLogger.LOGGER.warning("Debug: Java: " + System.getProperty("java.version") + ", test run: " + SIMDDetection.testRun);
-+ }
-+ }
-+
-+ private static void setComment(String key, String... comment) {
-+ if (config.contains(key)) {
-+ config.setComment(key, String.join("\n", comment), CommentType.BLOCK);
-+ }
-+ }
-+
-+ private static void ensureDefault(String key, Object defaultValue, String... comment) {
-+ if (!config.contains(key)) {
-+ config.set(key, defaultValue);
-+ config.setComment(key, String.join("\n", comment), CommentType.BLOCK);
-+ }
-+ }
-+
-+ private static boolean getBoolean(String key, boolean defaultValue, String... comment) {
-+ return getBoolean(key, null, defaultValue, comment);
-+ }
-+
-+ private static boolean getBoolean(String key, @Nullable String oldKey, boolean defaultValue, String... comment) {
-+ ensureDefault(key, defaultValue, comment);
-+ return config.getBoolean(key, defaultValue);
-+ }
-+
-+ private static int getInt(String key, int defaultValue, String... comment) {
-+ return getInt(key, null, defaultValue, comment);
-+ }
-+
-+ private static int getInt(String key, @Nullable String oldKey, int defaultValue, String... comment) {
-+ ensureDefault(key, defaultValue, comment);
-+ return config.getInt(key, defaultValue);
-+ }
-+
-+ private static double getDouble(String key, double defaultValue, String... comment) {
-+ return getDouble(key, null, defaultValue, comment);
-+ }
-+
-+ private static double getDouble(String key, @Nullable String oldKey, double defaultValue, String... comment) {
-+ ensureDefault(key, defaultValue, comment);
-+ return config.getDouble(key, defaultValue);
-+ }
-+
-+ private static String getString(String key, String defaultValue, String... comment) {
-+ return getOldString(key, null, defaultValue, comment);
-+ }
-+
-+ private static String getOldString(String key, @Nullable String oldKey, String defaultValue, String... comment) {
-+ ensureDefault(key, defaultValue, comment);
-+ return config.getString(key, defaultValue);
-+ }
-+
-+ private static List getStringList(String key, List defaultValue, String... comment) {
-+ return getStringList(key, null, defaultValue, comment);
-+ }
-+
-+ private static List getStringList(String key, @Nullable String oldKey, List defaultValue, String... comment) {
-+ ensureDefault(key, defaultValue, comment);
-+ return config.getStringList(key);
-+ }
-+
-+ public static String sentryDsn;
-+ private static void sentry() {
-+ String sentryEnvironment = System.getenv("SENTRY_DSN");
-+ String sentryConfig = getString("sentry-dsn", "", "Sentry DSN for improved error logging, leave blank to disable", "Obtain from https://sentry.io/");
-+
-+ sentryDsn = sentryEnvironment == null ? sentryConfig : sentryEnvironment;
-+ if (sentryDsn != null && !sentryDsn.isBlank()) {
-+ gg.pufferfish.pufferfish.sentry.SentryManager.init();
-+ }
-+ }
-+
-+ public static boolean enableBooks;
-+ private static void books() {
-+ enableBooks = getBoolean("enable-books", true,
-+ "Whether or not books should be writeable.",
-+ "Servers that anticipate being a target for duping may want to consider",
-+ "disabling this option.",
-+ "This can be overridden per-player with the permission pufferfish.usebooks");
-+ }
-+
-+ public static boolean enableSuffocationOptimization;
-+ private static void suffocationOptimization() {
-+ enableSuffocationOptimization = getBoolean("enable-suffocation-optimization", true,
-+ "Optimizes the suffocation check by selectively skipping",
-+ "the check in a way that still appears vanilla. This should",
-+ "be left enabled on most servers, but is provided as a",
-+ "configuration option if the vanilla deviation is undesirable.");
-+ }
-+
-+ public static boolean enableAsyncMobSpawning;
-+ public static boolean asyncMobSpawningInitialized;
-+ private static void asyncMobSpawning() {
-+ boolean temp = getBoolean("enable-async-mob-spawning", true,
-+ "Whether or not asynchronous mob spawning should be enabled.",
-+ "On servers with many entities, this can improve performance by up to 15%. You must have",
-+ "paper's per-player-mob-spawns setting set to true for this to work.",
-+ "One quick note - this does not actually spawn mobs async (that would be very unsafe).",
-+ "This just offloads some expensive calculations that are required for mob spawning.");
-+
-+ // This prevents us from changing the value during a reload.
-+ if (!asyncMobSpawningInitialized) {
-+ asyncMobSpawningInitialized = true;
-+ enableAsyncMobSpawning = temp;
-+ }
-+ }
-+
-+ public static int maxProjectileLoadsPerTick;
-+ public static int maxProjectileLoadsPerProjectile;
-+ private static void projectileLoading() {
-+ maxProjectileLoadsPerTick = getInt("projectile.max-loads-per-tick", 10, "Controls how many chunks are allowed", "to be sync loaded by projectiles in a tick.");
-+ maxProjectileLoadsPerProjectile = getInt("projectile.max-loads-per-projectile", 10, "Controls how many chunks a projectile", "can load in its lifetime before it gets", "automatically removed.");
-+
-+ setComment("projectile", "Optimizes projectile settings");
-+ }
-+
-+
-+ public static boolean dearEnabled;
-+ public static int startDistance;
-+ public static int startDistanceSquared;
-+ public static int maximumActivationPrio;
-+ public static int activationDistanceMod;
-+
-+ private static void dynamicActivationOfBrains() throws IOException {
-+ dearEnabled = getBoolean("dab.enabled", "activation-range.enabled", true);
-+ startDistance = getInt("dab.start-distance", "activation-range.start-distance", 12,
-+ "This value determines how far away an entity has to be",
-+ "from the player to start being effected by DEAR.");
-+ startDistanceSquared = startDistance * startDistance;
-+ maximumActivationPrio = getInt("dab.max-tick-freq", "activation-range.max-tick-freq", 20,
-+ "This value defines how often in ticks, the furthest entity",
-+ "will get their pathfinders and behaviors ticked. 20 = 1s");
-+ activationDistanceMod = getInt("dab.activation-dist-mod", "activation-range.activation-dist-mod", 8,
-+ "This value defines how much distance modifies an entity's",
-+ "tick frequency. freq = (distanceToPlayer^2) / (2^value)",
-+ "If you want further away entities to tick less often, use 7.",
-+ "If you want further away entities to tick more often, try 9.");
-+
-+ for (EntityType> entityType : Registry.ENTITY_TYPE) {
-+ entityType.dabEnabled = true; // reset all, before setting the ones to true
-+ }
-+ getStringList("dab.blacklisted-entities", "activation-range.blacklisted-entities", Collections.emptyList(), "A list of entities to ignore for activation")
-+ .forEach(name -> EntityType.byString(name).ifPresentOrElse(entityType -> {
-+ entityType.dabEnabled = false;
-+ }, () -> MinecraftServer.LOGGER.warn("Unknown entity \"" + name + "\"")));
-+
-+ setComment("dab", "Optimizes entity brains when", "they're far away from the player");
-+ }
-+
-+ public static Map projectileTimeouts;
-+ private static void projectileTimeouts() {
-+ // Set some defaults
-+ getInt("entity_timeouts.SNOWBALL", -1);
-+ getInt("entity_timeouts.LLAMA_SPIT", -1);
-+ setComment("entity_timeouts",
-+ "These values define a entity's maximum lifespan. If an",
-+ "entity is in this list and it has survived for longer than",
-+ "that number of ticks, then it will be removed. Setting a value to",
-+ "-1 disables this feature.");
-+
-+ for (EntityType> entityType : Registry.ENTITY_TYPE) {
-+ String type = EntityType.getKey(entityType).getPath().toUpperCase(Locale.ROOT);
-+ entityType.ttl = config.getInt("entity_timeouts." + type, -1);
-+ }
-+ }
-+
-+ public static boolean throttleInactiveGoalSelectorTick;
-+ private static void inactiveGoalSelectorThrottle() {
-+ getBoolean("inactive-goal-selector-throttle", "inactive-goal-selector-disable", true,
-+ "Throttles the AI goal selector in entity inactive ticks.",
-+ "This can improve performance by a few percent, but has minor gameplay implications.");
-+ }
-+
-+ public static URI profileWebUrl;
-+ private static void profilerOptions() {
-+ profileWebUrl = URI.create(getString("flare.url", "https://flare.airplane.gg", "Sets the server to use for profiles."));
-+
-+ setComment("flare", "Configures Flare, the built-in profiler");
-+ }
-+
-+
-+ public static String accessToken;
-+ private static void airplaneWebServices() {
-+ accessToken = getString("web-services.token", "");
-+ // todo lookup token (off-thread) and let users know if their token is valid
-+ if (accessToken.length() > 0) {
-+ gg.pufferfish.pufferfish.flare.FlareSetup.init(); // Pufferfish
-+ SimpleCommandMap commandMap = MinecraftServer.getServer().server.getCommandMap();
-+ if (commandMap.getCommand("flare") == null) {
-+ commandMap.register("flare", "Pufferfish", new FlareCommand());
-+ }
-+ }
-+
-+ setComment("web-services", "Options for connecting to Pufferfish/Airplane's online utilities");
-+
-+ }
-+
-+
-+ public static boolean disableMethodProfiler;
-+ private static void miscSettings() {
-+ disableMethodProfiler = getBoolean("misc.disable-method-profiler", true);
-+ setComment("misc", "Settings for things that don't belong elsewhere");
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..53f2df00c6809618a9ee3d2ea72e85e8052fbcf1
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java
-@@ -0,0 +1,16 @@
-+package gg.pufferfish.pufferfish;
-+
-+import java.util.logging.Level;
-+import java.util.logging.Logger;
-+import org.bukkit.Bukkit;
-+
-+public class PufferfishLogger extends Logger {
-+ public static final PufferfishLogger LOGGER = new PufferfishLogger();
-+
-+ private PufferfishLogger() {
-+ super("Pufferfish", null);
-+
-+ setParent(Bukkit.getLogger());
-+ setLevel(Level.ALL);
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..461022af9ad85fe00329678f0f61d684d291c628
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishVersionFetcher.java
-@@ -0,0 +1,136 @@
-+package gg.pufferfish.pufferfish;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+import com.destroystokyo.paper.VersionHistoryManager;
-+import com.destroystokyo.paper.util.VersionFetcher;
-+import com.google.gson.Gson;
-+import com.google.gson.JsonObject;
-+import java.io.IOException;
-+import java.net.URI;
-+import java.net.http.HttpClient;
-+import java.net.http.HttpRequest;
-+import java.net.http.HttpResponse;
-+import java.nio.charset.StandardCharsets;
-+import java.util.concurrent.TimeUnit;
-+import java.util.logging.Level;
-+import java.util.logging.Logger;
-+import net.kyori.adventure.text.Component;
-+import net.kyori.adventure.text.JoinConfiguration;
-+import net.kyori.adventure.text.format.NamedTextColor;
-+import net.kyori.adventure.text.format.TextDecoration;
-+import org.bukkit.craftbukkit.CraftServer;
-+import org.jetbrains.annotations.NotNull;
-+import org.jetbrains.annotations.Nullable;
-+
-+public class PufferfishVersionFetcher implements VersionFetcher {
-+
-+ private static final Logger LOGGER = Logger.getLogger("PufferfishVersionFetcher");
-+ private static final HttpClient client = HttpClient.newHttpClient();
-+
-+ private static final URI JENKINS_URI = URI.create("https://ci.pufferfish.host/job/Pufferfish-1.18/lastSuccessfulBuild/buildNumber");
-+ private static final String GITHUB_FORMAT = "https://api.github.com/repos/pufferfish-gg/Pufferfish/compare/ver/1.18...%s";
-+
-+ private static final HttpResponse.BodyHandler JSON_OBJECT_BODY_HANDLER = responseInfo -> HttpResponse.BodySubscribers
-+ .mapping(
-+ HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8),
-+ string -> new Gson().fromJson(string, JsonObject.class)
-+ );
-+
-+ @Override
-+ public long getCacheTime() {
-+ return TimeUnit.MINUTES.toMillis(30);
-+ }
-+
-+ @Override
-+ public @NotNull Component getVersionMessage(final @NotNull String serverVersion) {
-+ final String[] parts = CraftServer.class.getPackage().getImplementationVersion().split("-");
-+ @NotNull Component component;
-+
-+ if (parts.length != 3) {
-+ component = text("Unknown server version.", RED);
-+ } else {
-+ final String versionString = parts[2];
-+
-+ try {
-+ component = this.fetchJenkinsVersion(Integer.parseInt(versionString));
-+ } catch (NumberFormatException e) {
-+ component = this.fetchGithubVersion(versionString.substring(1, versionString.length() - 1));
-+ }
-+ }
-+
-+ final @Nullable Component history = this.getHistory();
-+ return history != null ? Component
-+ .join(JoinConfiguration.noSeparators(), component, Component.newline(), this.getHistory()) : component;
-+ }
-+
-+ private @NotNull Component fetchJenkinsVersion(final int versionNumber) {
-+ final HttpRequest request = HttpRequest.newBuilder(JENKINS_URI).build();
-+ try {
-+ final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-+ if (response.statusCode() != 200) {
-+ return text("Received invalid status code (" + response.statusCode() + ") from server.", RED);
-+ }
-+
-+ int latestVersionNumber;
-+ try {
-+ latestVersionNumber = Integer.parseInt(response.body());
-+ } catch (NumberFormatException e) {
-+ LOGGER.log(Level.WARNING, "Received invalid response from Jenkins \"" + response.body() + "\".");
-+ return text("Received invalid response from server.", RED);
-+ }
-+
-+ final int versionDiff = latestVersionNumber - versionNumber;
-+ return this.getResponseMessage(versionDiff);
-+ } catch (IOException | InterruptedException e) {
-+ LOGGER.log(Level.WARNING, "Failed to look up version from Jenkins", e);
-+ return text("Failed to retrieve version from server.", RED);
-+ }
-+ }
-+
-+ // Based off code contributed by Techcable in Paper/GH-65
-+ private @NotNull Component fetchGithubVersion(final @NotNull String hash) {
-+ final URI uri = URI.create(String.format(GITHUB_FORMAT, hash));
-+ final HttpRequest request = HttpRequest.newBuilder(uri).build();
-+ try {
-+ final HttpResponse response = client.send(request, JSON_OBJECT_BODY_HANDLER);
-+ if (response.statusCode() != 200) {
-+ return text("Received invalid status code (" + response.statusCode() + ") from server.", RED);
-+ }
-+
-+ final JsonObject obj = response.body();
-+ final int versionDiff = obj.get("behind_by").getAsInt();
-+
-+ return this.getResponseMessage(versionDiff);
-+ } catch (IOException | InterruptedException e) {
-+ LOGGER.log(Level.WARNING, "Failed to look up version from GitHub", e);
-+ return text("Failed to retrieve version from server.", RED);
-+ }
-+ }
-+
-+ private @NotNull Component getResponseMessage(final int versionDiff) {
-+ return switch (Math.max(-1, Math.min(1, versionDiff))) {
-+ case -1 -> text("You are running an unsupported version of Pufferfish.", RED);
-+ case 0 -> text("You are on the latest version!", GREEN);
-+ default -> text("You are running " + versionDiff + " version" + (versionDiff == 1 ? "" : "s") + " beyond. " +
-+ "Please update your server when possible to maintain stability, security, and receive the latest optimizations.",
-+ RED);
-+ };
-+ }
-+
-+ private @Nullable Component getHistory() {
-+ final VersionHistoryManager.VersionData data = VersionHistoryManager.INSTANCE.getVersionData();
-+ if (data == null) {
-+ return null;
-+ }
-+
-+ final String oldVersion = data.getOldVersion();
-+ if (oldVersion == null) {
-+ return null;
-+ }
-+
-+ return Component.text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC);
-+ }
-+}
-\ No newline at end of file
-diff --git a/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java b/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f0ec1f3944c00d38537f6f3b3f13236b22562458
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/compat/ServerConfigurations.java
-@@ -0,0 +1,78 @@
-+package gg.pufferfish.pufferfish.compat;
-+
-+import co.aikar.timings.TimingsManager;
-+import com.google.common.io.Files;
-+import org.bukkit.configuration.InvalidConfigurationException;
-+import org.bukkit.configuration.file.YamlConfiguration;
-+
-+import java.io.ByteArrayOutputStream;
-+import java.io.File;
-+import java.io.FileInputStream;
-+import java.io.IOException;
-+import java.nio.charset.StandardCharsets;
-+import java.util.Arrays;
-+import java.util.HashMap;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.Properties;
-+import java.util.stream.Collectors;
-+
-+public class ServerConfigurations {
-+
-+ public static final String[] configurationFiles = new String[]{
-+ "server.properties",
-+ "bukkit.yml",
-+ "spigot.yml",
-+ "paper.yml",
-+ "pufferfish.yml"
-+ };
-+
-+ public static Map getCleanCopies() throws IOException {
-+ Map files = new HashMap<>(configurationFiles.length);
-+ for (String file : configurationFiles) {
-+ files.put(file, getCleanCopy(file));
-+ }
-+ return files;
-+ }
-+
-+ public static String getCleanCopy(String configName) throws IOException {
-+ File file = new File(configName);
-+ List hiddenConfigs = TimingsManager.hiddenConfigs;
-+
-+ switch (Files.getFileExtension(configName)) {
-+ case "properties": {
-+ Properties properties = new Properties();
-+ try (FileInputStream inputStream = new FileInputStream(file)) {
-+ properties.load(inputStream);
-+ }
-+ for (String hiddenConfig : hiddenConfigs) {
-+ properties.remove(hiddenConfig);
-+ }
-+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-+ properties.store(outputStream, "");
-+ return Arrays.stream(outputStream.toString()
-+ .split("\n"))
-+ .filter(line -> !line.startsWith("#"))
-+ .collect(Collectors.joining("\n"));
-+ }
-+ case "yml": {
-+ YamlConfiguration configuration = new YamlConfiguration();
-+ try {
-+ configuration.load(file);
-+ } catch (InvalidConfigurationException e) {
-+ throw new IOException(e);
-+ }
-+ configuration.options().header(null);
-+ for (String key : configuration.getKeys(true)) {
-+ if (hiddenConfigs.contains(key)) {
-+ configuration.set(key, null);
-+ }
-+ }
-+ return configuration.saveToString();
-+ }
-+ default:
-+ throw new IllegalArgumentException("Bad file type " + configName);
-+ }
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java b/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..401b42e29bccb5251684062f10b2e0f8b091bc95
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/CustomCategories.java
-@@ -0,0 +1,8 @@
-+package gg.pufferfish.pufferfish.flare;
-+
-+import co.technove.flare.live.category.GraphCategory;
-+
-+public class CustomCategories {
-+ public static final GraphCategory MC_PERF = new GraphCategory("MC Performance");
-+ public static final GraphCategory ENTITIES_AND_CHUNKS = new GraphCategory("Entities & Chunks");
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java b/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3785d1512eb650f91d58903672c059e7449598fc
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/FlareCommand.java
-@@ -0,0 +1,136 @@
-+package gg.pufferfish.pufferfish.flare;
-+
-+import co.technove.flare.exceptions.UserReportableException;
-+import co.technove.flare.internal.profiling.ProfileType;
-+import gg.pufferfish.pufferfish.PufferfishConfig;
-+import net.kyori.adventure.text.Component;
-+import net.kyori.adventure.text.event.ClickEvent;
-+import net.kyori.adventure.text.format.NamedTextColor;
-+import net.kyori.adventure.text.format.TextColor;
-+import net.kyori.adventure.text.format.TextDecoration;
-+import net.minecraft.server.MinecraftServer;
-+import org.apache.logging.log4j.Level;
-+import org.bukkit.Bukkit;
-+import org.bukkit.command.Command;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.command.ConsoleCommandSender;
-+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
-+import org.bukkit.util.StringUtil;
-+import org.jetbrains.annotations.NotNull;
-+
-+import java.time.Duration;
-+import java.util.ArrayList;
-+import java.util.Collections;
-+import java.util.List;
-+
-+public class FlareCommand extends Command {
-+
-+ private static final String BASE_URL = "https://blog.airplane.gg/flare-tutorial/#setting-the-access-token";
-+ private static final TextColor HEX = TextColor.fromHexString("#e3eaea");
-+ private static final Component PREFIX = Component.text()
-+ .append(Component.text("Flare ✈")
-+ .color(TextColor.fromHexString("#6a7eda"))
-+ .decoration(TextDecoration.BOLD, true)
-+ .append(Component.text(" ", HEX)
-+ .decoration(TextDecoration.BOLD, false)))
-+ .asComponent();
-+
-+ public FlareCommand() {
-+ super("flare", "Profile your server with Flare", "/flare", Collections.singletonList("profile"));
-+ this.setPermission("airplane.flare");
-+ }
-+
-+ @Override
-+ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String @NotNull [] args) {
-+ if (!testPermission(sender)) return true;
-+ if (PufferfishConfig.accessToken.length() == 0) {
-+ Component clickable = Component.text(BASE_URL, HEX, TextDecoration.UNDERLINED).clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, BASE_URL));
-+
-+ sender.sendMessage(PREFIX.append(Component.text("Flare currently requires an access token to use. To learn more, visit ").color(HEX).append(clickable)));
-+ return true;
-+ }
-+
-+ if (!FlareSetup.isSupported()) {
-+ sender.sendMessage(PREFIX.append(
-+ Component.text("Profiling is not supported in this environment, check your startup logs for the error.", NamedTextColor.RED)));
-+ return true;
-+ }
-+ if (ProfilingManager.isProfiling()) {
-+ if (args.length == 1 && args[0].equalsIgnoreCase("status")) {
-+ sender.sendMessage(PREFIX.append(Component.text("Current profile has been ran for " + ProfilingManager.getTimeRan().toString(), HEX)));
-+ return true;
-+ }
-+ if (ProfilingManager.stop()) {
-+ if (!(sender instanceof ConsoleCommandSender)) {
-+ sender.sendMessage(PREFIX.append(Component.text("Profiling has been stopped.", HEX)));
-+ }
-+ } else {
-+ sender.sendMessage(PREFIX.append(Component.text("Profiling has already been stopped.", HEX)));
-+ }
-+ } else {
-+ ProfileType profileType = ProfileType.ITIMER;
-+ if (args.length > 0) {
-+ try {
-+ profileType = ProfileType.valueOf(args[0].toUpperCase());
-+ } catch (Exception e) {
-+ sender.sendMessage(PREFIX.append(Component
-+ .text("Invalid profile type ", HEX)
-+ .append(Component.text(args[0], HEX, TextDecoration.BOLD)
-+ .append(Component.text("!", HEX)))
-+ ));
-+ }
-+ }
-+ ProfileType finalProfileType = profileType;
-+ Bukkit.getScheduler().runTaskAsynchronously(new MinecraftInternalPlugin(), () -> {
-+ try {
-+ if (ProfilingManager.start(finalProfileType)) {
-+ if (!(sender instanceof ConsoleCommandSender)) {
-+ sender.sendMessage(PREFIX.append(Component
-+ .text("Flare has been started: " + ProfilingManager.getProfilingUri(), HEX)
-+ .clickEvent(ClickEvent.openUrl(ProfilingManager.getProfilingUri()))
-+ ));
-+ sender.sendMessage(PREFIX.append(Component.text(" Run /" + commandLabel + " to stop the Flare.", HEX)));
-+ }
-+ } else {
-+ sender.sendMessage(PREFIX.append(Component
-+ .text("Flare has already been started: " + ProfilingManager.getProfilingUri(), HEX)
-+ .clickEvent(ClickEvent.openUrl(ProfilingManager.getProfilingUri()))
-+ ));
-+ }
-+ } catch (UserReportableException e) {
-+ sender.sendMessage(Component.text("Flare failed to start: " + e.getUserError(), NamedTextColor.RED));
-+ if (e.getCause() != null) {
-+ MinecraftServer.LOGGER.warn("Flare failed to start", e);
-+ }
-+ }
-+ });
-+ }
-+ return true;
-+ }
-+
-+ @Override
-+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args) throws IllegalArgumentException {
-+ List list = new ArrayList<>();
-+ if (ProfilingManager.isProfiling()) {
-+ if (args.length == 1) {
-+ String lastWord = args[0];
-+ if (StringUtil.startsWithIgnoreCase("status", lastWord)) {
-+ list.add("status");
-+ }
-+ if (StringUtil.startsWithIgnoreCase("stop", lastWord)) {
-+ list.add("stop");
-+ }
-+ }
-+ } else {
-+ if (args.length <= 1) {
-+ String lastWord = args.length == 0 ? "" : args[0];
-+ for (ProfileType value : ProfileType.values()) {
-+ if (StringUtil.startsWithIgnoreCase(value.getInternalName(), lastWord)) {
-+ list.add(value.name().toLowerCase());
-+ }
-+ }
-+ }
-+ }
-+ return list;
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java b/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..cd22e4dcc8b7b57b10a95ef084637249a98e524f
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/FlareSetup.java
-@@ -0,0 +1,33 @@
-+package gg.pufferfish.pufferfish.flare;
-+
-+import co.technove.flare.FlareInitializer;
-+import co.technove.flare.internal.profiling.InitializationException;
-+import net.minecraft.server.MinecraftServer;
-+import org.apache.logging.log4j.Level;
-+
-+public class FlareSetup {
-+
-+ private static boolean initialized = false;
-+ private static boolean supported = false;
-+
-+ public static void init() {
-+ if (initialized) {
-+ return;
-+ }
-+
-+ initialized = true;
-+ try {
-+ for (String warning : FlareInitializer.initialize()) {
-+ MinecraftServer.LOGGER.warn("Flare warning: " + warning);
-+ }
-+ supported = true;
-+ } catch (InitializationException e) {
-+ MinecraftServer.LOGGER.warn("Failed to enable Flare:", e);
-+ }
-+ }
-+
-+ public static boolean isSupported() {
-+ return supported;
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java b/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..74aab5eb4b54ffbaf19b8976ffb8ca4a64584006
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/PluginLookup.java
-@@ -0,0 +1,44 @@
-+package gg.pufferfish.pufferfish.flare;
-+
-+import com.google.common.cache.Cache;
-+import com.google.common.cache.CacheBuilder;
-+import org.bukkit.Bukkit;
-+import org.bukkit.plugin.Plugin;
-+import org.bukkit.plugin.java.PluginClassLoader;
-+
-+import java.util.Optional;
-+import java.util.concurrent.TimeUnit;
-+
-+public class PluginLookup {
-+ private static final Cache pluginNameCache = CacheBuilder.newBuilder()
-+ .expireAfterAccess(1, TimeUnit.MINUTES)
-+ .maximumSize(1024)
-+ .build();
-+
-+ public static Optional getPluginForClass(String name) {
-+ if (name.startsWith("net.minecraft") || name.startsWith("java.") || name.startsWith("com.mojang") ||
-+ name.startsWith("com.google") || name.startsWith("it.unimi") || name.startsWith("sun")) {
-+ return Optional.empty();
-+ }
-+
-+ String existing = pluginNameCache.getIfPresent(name);
-+ if (existing != null) {
-+ return Optional.ofNullable(existing.isEmpty() ? null : existing);
-+ }
-+
-+ String newValue = "";
-+
-+ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
-+ ClassLoader classLoader = plugin.getClass().getClassLoader();
-+ if (classLoader instanceof PluginClassLoader) {
-+ if (((PluginClassLoader) classLoader)._airplane_hasClass(name)) {
-+ newValue = plugin.getName();
-+ break;
-+ }
-+ }
-+ }
-+
-+ pluginNameCache.put(name, newValue);
-+ return Optional.ofNullable(newValue.isEmpty() ? null : newValue);
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java b/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e3f76eb11a261c3347f0cd89b5da309bc2dc82f9
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/ProfilingManager.java
-@@ -0,0 +1,151 @@
-+package gg.pufferfish.pufferfish.flare;
-+
-+import co.technove.flare.Flare;
-+import co.technove.flare.FlareAuth;
-+import co.technove.flare.FlareBuilder;
-+import co.technove.flare.exceptions.UserReportableException;
-+import co.technove.flare.internal.profiling.ProfileType;
-+import gg.pufferfish.pufferfish.PufferfishConfig;
-+import gg.pufferfish.pufferfish.PufferfishLogger;
-+import gg.pufferfish.pufferfish.compat.ServerConfigurations;
-+import gg.pufferfish.pufferfish.flare.collectors.GCEventCollector;
-+import gg.pufferfish.pufferfish.flare.collectors.StatCollector;
-+import gg.pufferfish.pufferfish.flare.collectors.TPSCollector;
-+import gg.pufferfish.pufferfish.flare.collectors.WorldCountCollector;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
-+import org.bukkit.scheduler.BukkitTask;
-+import oshi.SystemInfo;
-+import oshi.hardware.CentralProcessor;
-+import oshi.hardware.GlobalMemory;
-+import oshi.hardware.HardwareAbstractionLayer;
-+import oshi.hardware.VirtualMemory;
-+import oshi.software.os.OperatingSystem;
-+
-+import java.io.IOException;
-+import java.net.URI;
-+import java.time.Duration;
-+import java.util.Objects;
-+import java.util.logging.Level;
-+
-+public class ProfilingManager {
-+
-+ private static Flare currentFlare;
-+ private static BukkitTask currentTask = null;
-+
-+ public static synchronized boolean isProfiling() {
-+ return currentFlare != null && currentFlare.isRunning();
-+ }
-+
-+ public static synchronized String getProfilingUri() {
-+ return Objects.requireNonNull(currentFlare).getURI().map(URI::toString).orElse("Flare is not running");
-+ }
-+
-+ public static Duration getTimeRan() {
-+ Flare flare = currentFlare; // copy reference so no need to sync
-+ if (flare == null) {
-+ return Duration.ofMillis(0);
-+ }
-+ return flare.getCurrentDuration();
-+ }
-+
-+ public static synchronized boolean start(ProfileType profileType) throws UserReportableException {
-+ if (currentFlare != null && !currentFlare.isRunning()) {
-+ currentFlare = null; // errored out
-+ }
-+ if (isProfiling()) {
-+ return false;
-+ }
-+ if (Bukkit.isPrimaryThread()) {
-+ throw new UserReportableException("Profiles should be started off-thread");
-+ }
-+
-+ try {
-+ OperatingSystem os = new SystemInfo().getOperatingSystem();
-+
-+ SystemInfo systemInfo = new SystemInfo();
-+ HardwareAbstractionLayer hardware = systemInfo.getHardware();
-+
-+ CentralProcessor processor = hardware.getProcessor();
-+ CentralProcessor.ProcessorIdentifier processorIdentifier = processor.getProcessorIdentifier();
-+
-+ GlobalMemory memory = hardware.getMemory();
-+ VirtualMemory virtualMemory = memory.getVirtualMemory();
-+
-+ FlareBuilder builder = new FlareBuilder()
-+ .withProfileType(profileType)
-+ .withMemoryProfiling(true)
-+ .withAuth(FlareAuth.fromTokenAndUrl(PufferfishConfig.accessToken, PufferfishConfig.profileWebUrl))
-+
-+ .withFiles(ServerConfigurations.getCleanCopies())
-+ .withVersion("Primary Version", Bukkit.getVersion())
-+ .withVersion("Bukkit Version", Bukkit.getBukkitVersion())
-+ .withVersion("Minecraft Version", Bukkit.getMinecraftVersion())
-+
-+ .withGraphCategories(CustomCategories.ENTITIES_AND_CHUNKS, CustomCategories.MC_PERF)
-+ .withCollectors(new TPSCollector(), new WorldCountCollector(), new GCEventCollector(), new StatCollector())
-+ .withClassIdentifier(PluginLookup::getPluginForClass)
-+
-+ .withHardware(new FlareBuilder.HardwareBuilder()
-+ .setCoreCount(processor.getPhysicalProcessorCount())
-+ .setThreadCount(processor.getLogicalProcessorCount())
-+ .setCpuModel(processorIdentifier.getName())
-+ .setCpuFrequency(processor.getMaxFreq())
-+
-+ .setTotalMemory(memory.getTotal())
-+ .setTotalSwap(virtualMemory.getSwapTotal())
-+ .setTotalVirtual(virtualMemory.getVirtualMax())
-+ )
-+
-+ .withOperatingSystem(new FlareBuilder.OperatingSystemBuilder()
-+ .setManufacturer(os.getManufacturer())
-+ .setFamily(os.getFamily())
-+ .setVersion(os.getVersionInfo().toString())
-+ .setBitness(os.getBitness())
-+ );
-+
-+ currentFlare = builder.build();
-+ } catch (IOException e) {
-+ PufferfishLogger.LOGGER.log(Level.WARNING, "Failed to read configuration files:", e);
-+ throw new UserReportableException("Failed to load configuration files, check logs for further details.");
-+ }
-+
-+ try {
-+ currentFlare.start();
-+ } catch (IllegalStateException e) {
-+ PufferfishLogger.LOGGER.log(Level.WARNING, "Error starting Flare:", e);
-+ throw new UserReportableException("Failed to start Flare, check logs for further details.");
-+ }
-+
-+ currentTask = Bukkit.getScheduler().runTaskLater(new MinecraftInternalPlugin(), ProfilingManager::stop, 20 * 60 * 15);
-+ PufferfishLogger.LOGGER.log(Level.INFO, "Flare has been started: " + getProfilingUri());
-+ return true;
-+ }
-+
-+ public static synchronized boolean stop() {
-+ if (!isProfiling()) {
-+ return false;
-+ }
-+ if (!currentFlare.isRunning()) {
-+ currentFlare = null;
-+ return true;
-+ }
-+ PufferfishLogger.LOGGER.log(Level.INFO, "Flare has been stopped: " + getProfilingUri());
-+ try {
-+ currentFlare.stop();
-+ } catch (IllegalStateException e) {
-+ PufferfishLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", e);
-+ }
-+ currentFlare = null;
-+
-+ try {
-+ currentTask.cancel();
-+ } catch (Throwable t) {
-+ PufferfishLogger.LOGGER.log(Level.WARNING, "Error occurred stopping Flare", t);
-+ }
-+
-+ currentTask = null;
-+ return true;
-+ }
-+
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d426575c669020f369960107da1e2de2f11f082f
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/GCEventCollector.java
-@@ -0,0 +1,66 @@
-+package gg.pufferfish.pufferfish.flare.collectors;
-+
-+import co.technove.flare.Flare;
-+import co.technove.flare.internal.FlareInternal;
-+import co.technove.flare.live.CollectorData;
-+import co.technove.flare.live.EventCollector;
-+import co.technove.flare.live.LiveEvent;
-+import co.technove.flare.live.category.GraphCategory;
-+import co.technove.flare.live.formatter.DataFormatter;
-+import com.google.common.collect.ImmutableMap;
-+import com.sun.management.GarbageCollectionNotificationInfo;
-+
-+import javax.management.ListenerNotFoundException;
-+import javax.management.Notification;
-+import javax.management.NotificationEmitter;
-+import javax.management.NotificationListener;
-+import javax.management.openmbean.CompositeData;
-+import java.lang.management.GarbageCollectorMXBean;
-+import java.lang.management.ManagementFactory;
-+
-+public class GCEventCollector extends EventCollector implements NotificationListener {
-+
-+ private static final CollectorData MINOR_GC = new CollectorData("builtin:gc:minor", "Minor GC", "A small pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM);
-+ private static final CollectorData MAJOR_GC = new CollectorData("builtin:gc:major", "Major GC", "A large pause in the program to allow Garbage Collection to run.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM);
-+ private static final CollectorData UNKNOWN_GC = new CollectorData("builtin:gc:generic", "Major GC", "A run of the Garbage Collection.", DataFormatter.MILLISECONDS, GraphCategory.SYSTEM);
-+
-+ public GCEventCollector() {
-+ super(MINOR_GC, MAJOR_GC, UNKNOWN_GC);
-+ }
-+
-+ private static CollectorData fromString(String string) {
-+ if (string.endsWith("minor GC")) {
-+ return MINOR_GC;
-+ } else if (string.endsWith("major GC")) {
-+ return MAJOR_GC;
-+ }
-+ return UNKNOWN_GC;
-+ }
-+
-+ @Override
-+ public void start(Flare flare) {
-+ for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) {
-+ NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean;
-+ notificationEmitter.addNotificationListener(this, null, null);
-+ }
-+ }
-+
-+ @Override
-+ public void stop(Flare flare) {
-+ for (GarbageCollectorMXBean garbageCollectorBean : ManagementFactory.getGarbageCollectorMXBeans()) {
-+ NotificationEmitter notificationEmitter = (NotificationEmitter) garbageCollectorBean;
-+ try {
-+ notificationEmitter.removeNotificationListener(this);
-+ } catch (ListenerNotFoundException e) {
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public void handleNotification(Notification notification, Object o) {
-+ if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
-+ GarbageCollectionNotificationInfo gcInfo = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
-+ reportEvent(new LiveEvent(fromString(gcInfo.getGcAction()), System.currentTimeMillis(), (int) gcInfo.getGcInfo().getDuration(), ImmutableMap.of()));
-+ }
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a22c6dbae53667e4c72464fa27153aee30c7946e
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/StatCollector.java
-@@ -0,0 +1,41 @@
-+package gg.pufferfish.pufferfish.flare.collectors;
-+
-+import co.technove.flare.live.CollectorData;
-+import co.technove.flare.live.LiveCollector;
-+import co.technove.flare.live.category.GraphCategory;
-+import co.technove.flare.live.formatter.DataFormatter;
-+import com.sun.management.OperatingSystemMXBean;
-+import oshi.SystemInfo;
-+import oshi.hardware.CentralProcessor;
-+
-+import java.lang.management.ManagementFactory;
-+import java.time.Duration;
-+
-+public class StatCollector extends LiveCollector {
-+
-+ private static final CollectorData CPU = new CollectorData("builtin:stat:cpu", "CPU Load", "The total amount of CPU usage across all cores.", DataFormatter.PERCENT, GraphCategory.SYSTEM);
-+ private static final CollectorData CPU_PROCESS = new CollectorData("builtin:stat:cpu_process", "Process CPU", "The amount of CPU being used by this process.", DataFormatter.PERCENT, GraphCategory.SYSTEM);
-+ private static final CollectorData MEMORY = new CollectorData("builtin:stat:memory_used", "Memory", "The amount of memory being used currently.", DataFormatter.BYTES, GraphCategory.SYSTEM);
-+ private static final CollectorData MEMORY_TOTAL = new CollectorData("builtin:stat:memory_total", "Memory Total", "The total amount of memory allocated.", DataFormatter.BYTES, GraphCategory.SYSTEM);
-+
-+ private final OperatingSystemMXBean bean;
-+ private final CentralProcessor processor;
-+
-+ public StatCollector() {
-+ super(CPU, CPU_PROCESS, MEMORY, MEMORY_TOTAL);
-+ this.interval = Duration.ofSeconds(5);
-+
-+ this.bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
-+ this.processor = new SystemInfo().getHardware().getProcessor();
-+ }
-+
-+ @Override
-+ public void run() {
-+ Runtime runtime = Runtime.getRuntime();
-+
-+ this.report(CPU, this.processor.getSystemLoadAverage(1)[0] / 100); // percentage
-+ this.report(CPU_PROCESS, this.bean.getProcessCpuLoad());
-+ this.report(MEMORY, runtime.totalMemory() - runtime.freeMemory());
-+ this.report(MEMORY_TOTAL, runtime.totalMemory());
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..40447d00aefb5ffedb8a2ee87155a04088f0649f
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/TPSCollector.java
-@@ -0,0 +1,31 @@
-+package gg.pufferfish.pufferfish.flare.collectors;
-+
-+import co.technove.flare.live.CollectorData;
-+import co.technove.flare.live.LiveCollector;
-+import co.technove.flare.live.formatter.SuffixFormatter;
-+import gg.pufferfish.pufferfish.flare.CustomCategories;
-+import net.minecraft.server.MinecraftServer;
-+import org.bukkit.Bukkit;
-+
-+import java.time.Duration;
-+import java.util.Arrays;
-+
-+public class TPSCollector extends LiveCollector {
-+ private static final CollectorData TPS = new CollectorData("airplane:tps", "TPS", "Ticks per second, or how fast the server updates. For a smooth server this should be a constant 20TPS.", SuffixFormatter.of("TPS"), CustomCategories.MC_PERF);
-+ private static final CollectorData MSPT = new CollectorData("airplane:mspt", "MSPT", "Milliseconds per tick, which can show how well your server is performing. This value should always be under 50mspt.", SuffixFormatter.of("mspt"), CustomCategories.MC_PERF);
-+
-+ public TPSCollector() {
-+ super(TPS, MSPT);
-+
-+ this.interval = Duration.ofSeconds(5);
-+ }
-+
-+ @Override
-+ public void run() {
-+ long[] times = MinecraftServer.getServer().tickTimes5s.getTimes();
-+ double mspt = ((double) Arrays.stream(times).sum() / (double) times.length) * 1.0E-6D;
-+
-+ this.report(TPS, Math.min(20D, Math.round(Bukkit.getServer().getTPS()[0] * 100d) / 100d));
-+ this.report(MSPT, (double) Math.round(mspt * 100d) / 100d);
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..db15d3fbe2b65fc8035573f5fdbea382055db9b2
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/flare/collectors/WorldCountCollector.java
-@@ -0,0 +1,45 @@
-+package gg.pufferfish.pufferfish.flare.collectors;
-+
-+import co.technove.flare.live.CollectorData;
-+import co.technove.flare.live.LiveCollector;
-+import co.technove.flare.live.formatter.SuffixFormatter;
-+import gg.pufferfish.pufferfish.flare.CustomCategories;
-+import org.bukkit.Bukkit;
-+import org.bukkit.World;
-+import org.bukkit.craftbukkit.CraftWorld;
-+
-+import java.time.Duration;
-+
-+public class WorldCountCollector extends LiveCollector {
-+
-+ private static final CollectorData PLAYER_COUNT = new CollectorData("airplane:world:playercount", "Player Count", "The number of players currently on the server.", new SuffixFormatter(" Player", " Players"), CustomCategories.ENTITIES_AND_CHUNKS);
-+ private static final CollectorData ENTITY_COUNT = new CollectorData("airplane:world:entitycount", "Entity Count", "The number of entities in all worlds", new SuffixFormatter(" Entity", " Entities"), CustomCategories.ENTITIES_AND_CHUNKS);
-+ private static final CollectorData CHUNK_COUNT = new CollectorData("airplane:world:chunkcount", "Chunk Count", "The number of chunks currently loaded.", new SuffixFormatter(" Chunk", " Chunks"), CustomCategories.ENTITIES_AND_CHUNKS);
-+ private static final CollectorData TILE_ENTITY_COUNT = new CollectorData("airplane:world:blockentitycount", "Block Entity Count", "The number of block entities currently loaded.", new SuffixFormatter(" Block Entity", " Block Entities"), CustomCategories.ENTITIES_AND_CHUNKS);
-+
-+ public WorldCountCollector() {
-+ super(PLAYER_COUNT, ENTITY_COUNT, CHUNK_COUNT, TILE_ENTITY_COUNT);
-+
-+ this.interval = Duration.ofSeconds(5);
-+ }
-+
-+ @Override
-+ public void run() {
-+ int entities = 0;
-+ int chunkCount = 0;
-+ int tileEntityCount = 0;
-+
-+ if (!Bukkit.isStopping()) {
-+ for (World world : Bukkit.getWorlds()) {
-+ entities += ((CraftWorld) world).getHandle().entityManager.getEntityGetter().getCount();
-+ chunkCount += world.getChunkCount();
-+ tileEntityCount += world.getTileEntityCount();
-+ }
-+ }
-+
-+ this.report(PLAYER_COUNT, Bukkit.getOnlinePlayers().size());
-+ this.report(ENTITY_COUNT, entities);
-+ this.report(CHUNK_COUNT, chunkCount);
-+ this.report(TILE_ENTITY_COUNT, tileEntityCount);
-+ }
-+}
-diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d04a8a4336566dbe6e1b9ec0d574cff43e003fa8
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java
-@@ -0,0 +1,135 @@
-+package gg.pufferfish.pufferfish.sentry;
-+
-+import com.google.common.reflect.TypeToken;
-+import com.google.gson.Gson;
-+import io.sentry.Breadcrumb;
-+import io.sentry.Sentry;
-+import io.sentry.SentryEvent;
-+import io.sentry.SentryLevel;
-+import io.sentry.protocol.Message;
-+import io.sentry.protocol.User;
-+import java.util.Map;
-+import org.apache.logging.log4j.Level;
-+import org.apache.logging.log4j.LogManager;
-+import org.apache.logging.log4j.Marker;
-+import org.apache.logging.log4j.core.LogEvent;
-+import org.apache.logging.log4j.core.Logger;
-+import org.apache.logging.log4j.core.appender.AbstractAppender;
-+import org.apache.logging.log4j.core.filter.AbstractFilter;
-+
-+public class PufferfishSentryAppender extends AbstractAppender {
-+
-+ private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class);
-+ private static final Gson GSON = new Gson();
-+
-+ public PufferfishSentryAppender() {
-+ super("PufferfishSentryAdapter", new SentryFilter(), null);
-+ }
-+
-+ @Override
-+ public void append(LogEvent logEvent) {
-+ if (logEvent.getThrown() != null && logEvent.getLevel().isMoreSpecificThan(Level.WARN)) {
-+ try {
-+ logException(logEvent);
-+ } catch (Exception e) {
-+ logger.warn("Failed to log event with sentry", e);
-+ }
-+ } else {
-+ try {
-+ logBreadcrumb(logEvent);
-+ } catch (Exception e) {
-+ logger.warn("Failed to log event with sentry", e);
-+ }
-+ }
-+ }
-+
-+ private void logException(LogEvent e) {
-+ SentryEvent event = new SentryEvent(e.getThrown());
-+
-+ Message sentryMessage = new Message();
-+ sentryMessage.setMessage(e.getMessage().getFormattedMessage());
-+
-+ event.setThrowable(e.getThrown());
-+ event.setLevel(getLevel(e.getLevel()));
-+ event.setLogger(e.getLoggerName());
-+ event.setTransaction(e.getLoggerName());
-+ event.setExtra("thread_name", e.getThreadName());
-+
-+ boolean hasContext = e.getContextData() != null;
-+
-+ if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) {
-+ User user = new User();
-+ user.setId(e.getContextData().getValue("pufferfishsentry_playerid"));
-+ user.setUsername(e.getContextData().getValue("pufferfishsentry_playername"));
-+ event.setUser(user);
-+ }
-+
-+ if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) {
-+ event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname"));
-+ event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion"));
-+ event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname"));
-+ }
-+
-+ if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) {
-+ Map eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken