diff --git a/README.md b/README.md
index 9ddb58167..9660ba25e 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
## Purpur
[](LICENSE)
-[](https://purpurmc.org/downloads/)
+[](https://purpurmc.org/downloads/)
[](https://www.codefactor.io/repository/github/PurpurMC/Purpur)
[](https://purpurmc.org/discord)
@@ -27,7 +27,7 @@ Join us on Discord:
## Downloads
Downloads can be obtained from the [downloads page](https://purpurmc.org/downloads/) or the [downloads API](https://api.purpurmc.org).
-[](https://purpurmc.org/downloads/)
+[](https://purpurmc.org/downloads/)
Downloads API endpoints:
* List versions of Minecraft with builds available:
@@ -67,7 +67,7 @@ Maven
org.purpurmc.purpur
purpur-api
- 1.19.4-R0.1-SNAPSHOT
+ 1.20-R0.1-SNAPSHOT
provided
```
@@ -80,7 +80,7 @@ repositories {
```
```kotlin
dependencies {
- compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
+ compileOnly("org.purpurmc.purpur:purpur-api:1.20-R0.1-SNAPSHOT")
}
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 659ea345c..724874df2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -116,7 +116,7 @@ tasks.register("printMinecraftVersion") {
}
}
-tasks.register("printPaperVersion") {
+tasks.register("printPurpurVersion") {
doLast {
println(project.version)
}
diff --git a/gradle.properties b/gradle.properties
index 9fc1b256d..701c42e51 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,8 @@
group = org.purpurmc.purpur
-version = 1.19.4-R0.1-SNAPSHOT
+version = 1.20-R0.1-SNAPSHOT
-paperCommit = bc4a6647c99ae98c52c1c81597834be8fec6aa0d
+mcVersion = 1.20
+paperCommit = ac1a62649313cc64dbc130b9399b79053d200c86
org.gradle.caching = true
org.gradle.parallel = true
diff --git a/patches/api/0003-Build-System-Changes.patch b/patches/api/0001-Build-System-Changes.patch
similarity index 85%
rename from patches/api/0003-Build-System-Changes.patch
rename to patches/api/0001-Build-System-Changes.patch
index dd4ffe1dc..5792549d2 100644
--- a/patches/api/0003-Build-System-Changes.patch
+++ b/patches/api/0001-Build-System-Changes.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Build System Changes
diff --git a/build.gradle.kts b/build.gradle.kts
-index b83e2c5a0a094002d12aee55ec0cf8d12bf33f3e..b5835fa536f90b7f88a5ee4df78733cf43e1cb23 100644
+index 7d6239855a84502de4eb3328b0dcf12ac671dce4..fc107bffc4efe56816d42c7f78d04897fe253729 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
-@@ -105,6 +105,8 @@ tasks.jar {
+@@ -99,6 +99,8 @@ tasks.jar {
}
tasks.withType {
diff --git a/patches/api/0001-Pufferfish-API-Changes.patch b/patches/api/0001-Pufferfish-API-Changes.patch
deleted file mode 100644
index b87bdf111..000000000
--- a/patches/api/0001-Pufferfish-API-Changes.patch
+++ /dev/null
@@ -1,527 +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 279a666e8ea2c07f41ee3f28b768e95dca5f0a10..a93b900889ddb56a2943c54a7fff6f60f42a78f1 100644
---- a/build.gradle.kts
-+++ b/build.gradle.kts
-@@ -42,6 +42,7 @@ dependencies {
- apiAndDocs("net.kyori:adventure-text-logger-slf4j")
- 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.4")
- implementation("org.ow2.asm:asm-commons:9.4")
-@@ -85,6 +86,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..ab5fea0b03224bf249352ce340e94704ff713345
---- /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 && SIMDDetection.getJavaVersion() != 19) {
-+ 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 3a9aaca2e76411a9c27f9f5e0f22d060d5a66d06..9584e245144b561b4f6745b2f26a4f69a6f92891 100644
---- a/src/main/java/org/bukkit/map/MapPalette.java
-+++ b/src/main/java/org/bukkit/map/MapPalette.java
-@@ -1,6 +1,7 @@
- package org.bukkit.map;
-
- import com.google.common.base.Preconditions;
-+import gg.pufferfish.pufferfish.simd.SIMDDetection; // Pufferfish
- import java.awt.Color;
- import java.awt.Graphics2D;
- import java.awt.Image;
-@@ -40,7 +41,7 @@ public final class MapPalette {
- }
-
- @NotNull
-- static final Color[] colors = {
-+ public static final Color[] colors = { // Pufferfish - public access
- c(0, 0, 0, 0), c(0, 0, 0, 0), c(0, 0, 0, 0), c(0, 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),
-@@ -211,9 +212,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 fc2dae69165776d08274e34a69962cc70445f411..899d67fa782fac639fe7fb096e05c551d75bd647 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -584,7 +584,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 com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
- }
- // Paper end
-@@ -654,9 +656,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 com.destroystokyo.paper.event.server.ServerExceptionEvent)) { // We don't want to cause an endless event loop
- callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.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 eaefbb00e9993d54906cc8cf35cf753c0d6c7707..301e82369603f3dd6e6c1bd380da4bacacd7ef6c 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -336,7 +336,13 @@ 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;
-+ // Paper end
- }
-
- // Perhaps abort here, rather than continue going, but as it stands,
-@@ -361,7 +367,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 13da387d3b59bc67c0d73e3fbd3a4034b1281527..7572a0bf6614b02be3cbccc7b86e52ee1b8df621 100644
---- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -48,6 +48,8 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
- private io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup classLoaderGroup; // Paper
- public io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext; // Paper
-
-+ private boolean closed = false; // Pufferfish
-+
- static {
- ClassLoader.registerAsParallelCapable();
- }
-@@ -183,6 +185,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
- 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.")) {
-@@ -190,7 +193,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
- }
- 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);
-
-@@ -237,6 +240,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
- this.setClass(name, result); // Paper
- }
-
-+ if (result == null) throw new ClassNotFoundException(name); // Pufferfish
- return result;
- }
-
-@@ -251,6 +255,7 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
- // Paper end
- super.close();
- } finally {
-+ this.closed = true; // Pufferfish
- jar.close();
- }
- }
diff --git a/patches/api/0004-Purpur-config-files.patch b/patches/api/0002-Purpur-config-files.patch
similarity index 100%
rename from patches/api/0004-Purpur-config-files.patch
rename to patches/api/0002-Purpur-config-files.patch
diff --git a/patches/api/0005-Purpur-client-support.patch b/patches/api/0003-Purpur-client-support.patch
similarity index 82%
rename from patches/api/0005-Purpur-client-support.patch
rename to patches/api/0003-Purpur-client-support.patch
index 755f857ac..b04a74c0e 100644
--- a/patches/api/0005-Purpur-client-support.patch
+++ b/patches/api/0003-Purpur-client-support.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Purpur client support
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 88c4885569d2b8b22fce55601d50608ac8e9388c..8d6353aa965474cf6bf333741abd7a059aa26d57 100644
+index def31cdbe5765f5d05386753868e51c424cbfe9e..d9e3157a9e4732f4a1cf87f206dceff48e853781 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -3068,4 +3068,13 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -3091,4 +3091,13 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
@Override
Spigot spigot();
// Spigot end
diff --git a/patches/api/0006-Default-permissions.patch b/patches/api/0004-Default-permissions.patch
similarity index 100%
rename from patches/api/0006-Default-permissions.patch
rename to patches/api/0004-Default-permissions.patch
diff --git a/patches/api/0007-Ridables.patch b/patches/api/0005-Ridables.patch
similarity index 100%
rename from patches/api/0007-Ridables.patch
rename to patches/api/0005-Ridables.patch
diff --git a/patches/api/0008-Allow-inventory-resizing.patch b/patches/api/0006-Allow-inventory-resizing.patch
similarity index 83%
rename from patches/api/0008-Allow-inventory-resizing.patch
rename to patches/api/0006-Allow-inventory-resizing.patch
index 64ada1cd6..f4ae288d5 100644
--- a/patches/api/0008-Allow-inventory-resizing.patch
+++ b/patches/api/0006-Allow-inventory-resizing.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Allow inventory resizing
diff --git a/src/main/java/org/bukkit/event/inventory/InventoryType.java b/src/main/java/org/bukkit/event/inventory/InventoryType.java
-index a8e631315f2da68895a258cf0ba9875bc88fc48c..d5648ec745e3530aecf18c3e1f3185a5f63f3d11 100644
+index b821fa535b23fe5af5884e536b1708460076ee40..a794beed383ec2beaa565eab372fb7e401c0379b 100644
--- a/src/main/java/org/bukkit/event/inventory/InventoryType.java
+++ b/src/main/java/org/bukkit/event/inventory/InventoryType.java
-@@ -155,7 +155,7 @@ public enum InventoryType {
+@@ -152,7 +152,7 @@ public enum InventoryType {
SMITHING_NEW(4, "Upgrade Gear"),
;
diff --git a/patches/api/0009-Llama-API.patch b/patches/api/0007-Llama-API.patch
similarity index 100%
rename from patches/api/0009-Llama-API.patch
rename to patches/api/0007-Llama-API.patch
diff --git a/patches/api/0010-AFK-API.patch b/patches/api/0008-AFK-API.patch
similarity index 94%
rename from patches/api/0010-AFK-API.patch
rename to patches/api/0008-AFK-API.patch
index d912bada6..cfecae16f 100644
--- a/patches/api/0010-AFK-API.patch
+++ b/patches/api/0008-AFK-API.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] AFK API
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 8d6353aa965474cf6bf333741abd7a059aa26d57..deda8a924742401c21d098406cbe7d07ab91faba 100644
+index d9e3157a9e4732f4a1cf87f206dceff48e853781..891bceaca672d23ccb179bc0c777aec9ca2dbdc6 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -3076,5 +3076,24 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -3099,5 +3099,24 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @return True if Player uses Purpur Client
*/
public boolean usesPurpurClient();
diff --git a/patches/api/0011-Bring-back-server-name.patch b/patches/api/0009-Bring-back-server-name.patch
similarity index 100%
rename from patches/api/0011-Bring-back-server-name.patch
rename to patches/api/0009-Bring-back-server-name.patch
diff --git a/patches/api/0012-ExecuteCommandEvent.patch b/patches/api/0010-ExecuteCommandEvent.patch
similarity index 100%
rename from patches/api/0012-ExecuteCommandEvent.patch
rename to patches/api/0010-ExecuteCommandEvent.patch
diff --git a/patches/api/0013-LivingEntity-safeFallDistance.patch b/patches/api/0011-LivingEntity-safeFallDistance.patch
similarity index 100%
rename from patches/api/0013-LivingEntity-safeFallDistance.patch
rename to patches/api/0011-LivingEntity-safeFallDistance.patch
diff --git a/patches/api/0014-Lagging-threshold.patch b/patches/api/0012-Lagging-threshold.patch
similarity index 100%
rename from patches/api/0014-Lagging-threshold.patch
rename to patches/api/0012-Lagging-threshold.patch
diff --git a/patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch b/patches/api/0013-PlayerSetSpawnerTypeWithEggEvent.patch
similarity index 100%
rename from patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch
rename to patches/api/0013-PlayerSetSpawnerTypeWithEggEvent.patch
diff --git a/patches/api/0016-EMC-MonsterEggSpawnEvent.patch b/patches/api/0014-EMC-MonsterEggSpawnEvent.patch
similarity index 100%
rename from patches/api/0016-EMC-MonsterEggSpawnEvent.patch
rename to patches/api/0014-EMC-MonsterEggSpawnEvent.patch
diff --git a/patches/api/0017-Player-invulnerabilities.patch b/patches/api/0015-Player-invulnerabilities.patch
similarity index 87%
rename from patches/api/0017-Player-invulnerabilities.patch
rename to patches/api/0015-Player-invulnerabilities.patch
index d7e208f33..a552a5606 100644
--- a/patches/api/0017-Player-invulnerabilities.patch
+++ b/patches/api/0015-Player-invulnerabilities.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Player invulnerabilities
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index deda8a924742401c21d098406cbe7d07ab91faba..53c536f0b828c2dbfc426c367d3b4e1d92944338 100644
+index 891bceaca672d23ccb179bc0c777aec9ca2dbdc6..c0e82ea4376ac29c99f42a9630d279480050fd0c 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -3095,5 +3095,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -3118,5 +3118,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* Reset the idle timer back to 0
*/
void resetIdleTimer();
diff --git a/patches/api/0018-Anvil-API.patch b/patches/api/0016-Anvil-API.patch
similarity index 100%
rename from patches/api/0018-Anvil-API.patch
rename to patches/api/0016-Anvil-API.patch
diff --git a/patches/api/0019-ItemStack-convenience-methods.patch b/patches/api/0017-ItemStack-convenience-methods.patch
similarity index 99%
rename from patches/api/0019-ItemStack-convenience-methods.patch
rename to patches/api/0017-ItemStack-convenience-methods.patch
index e16c33c11..0b0e94a62 100644
--- a/patches/api/0019-ItemStack-convenience-methods.patch
+++ b/patches/api/0017-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 9b290969b0e60f20450cd15e3fc6f37276f12ae6..77a885fd17f280649b95df758f1096fa38fe8d69 100644
+index 03b47012447430a350e152920f754d993d4023db..3ad843d519e239430c5f4f5754a8da3026ed0f8e 100644
--- a/src/main/java/org/bukkit/Material.java
+++ b/src/main/java/org/bukkit/Material.java
-@@ -11166,4 +11166,40 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla
+@@ -11047,4 +11047,40 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla
public boolean isEnabledByFeature(@NotNull World world) {
return Bukkit.getDataPackManager().isEnabledByFeature(this, world);
}
diff --git a/patches/api/0020-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch b/patches/api/0018-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/0018-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch
diff --git a/patches/api/0021-ChatColor-conveniences.patch b/patches/api/0019-ChatColor-conveniences.patch
similarity index 100%
rename from patches/api/0021-ChatColor-conveniences.patch
rename to patches/api/0019-ChatColor-conveniences.patch
diff --git a/patches/api/0022-LivingEntity-broadcastItemBreak.patch b/patches/api/0020-LivingEntity-broadcastItemBreak.patch
similarity index 100%
rename from patches/api/0022-LivingEntity-broadcastItemBreak.patch
rename to patches/api/0020-LivingEntity-broadcastItemBreak.patch
diff --git a/patches/api/0023-Item-entity-immunities.patch b/patches/api/0021-Item-entity-immunities.patch
similarity index 100%
rename from patches/api/0023-Item-entity-immunities.patch
rename to patches/api/0021-Item-entity-immunities.patch
diff --git a/patches/api/0024-Add-option-to-disable-zombie-aggressiveness-towards-.patch b/patches/api/0022-Add-option-to-disable-zombie-aggressiveness-towards-.patch
similarity index 100%
rename from patches/api/0024-Add-option-to-disable-zombie-aggressiveness-towards-.patch
rename to patches/api/0022-Add-option-to-disable-zombie-aggressiveness-towards-.patch
diff --git a/patches/api/0025-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch b/patches/api/0023-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
similarity index 100%
rename from patches/api/0025-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
rename to patches/api/0023-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch
diff --git a/patches/api/0026-Rabid-Wolf-API.patch b/patches/api/0024-Rabid-Wolf-API.patch
similarity index 100%
rename from patches/api/0026-Rabid-Wolf-API.patch
rename to patches/api/0024-Rabid-Wolf-API.patch
diff --git a/patches/api/0027-PlayerBookTooLargeEvent.patch b/patches/api/0025-PlayerBookTooLargeEvent.patch
similarity index 100%
rename from patches/api/0027-PlayerBookTooLargeEvent.patch
rename to patches/api/0025-PlayerBookTooLargeEvent.patch
diff --git a/patches/api/0028-Full-netherite-armor-grants-fire-resistance.patch b/patches/api/0026-Full-netherite-armor-grants-fire-resistance.patch
similarity index 91%
rename from patches/api/0028-Full-netherite-armor-grants-fire-resistance.patch
rename to patches/api/0026-Full-netherite-armor-grants-fire-resistance.patch
index 0ef9d9170..5082cfcd3 100644
--- a/patches/api/0028-Full-netherite-armor-grants-fire-resistance.patch
+++ b/patches/api/0026-Full-netherite-armor-grants-fire-resistance.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Full netherite armor grants fire resistance
diff --git a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java
-index 01c5e8b71338fbb4b1605e45bf2a2e705188f6b5..118d53ec9d1dc9c01cedfbedaf0b8edcbda7b3a5 100644
+index c9f395064656dd0126410eb3c6e197baa450c063..13156a12e5df50cdc1e465dc0bd9d94108275629 100644
--- a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java
+++ b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java
@@ -217,6 +217,12 @@ public class EntityPotionEffectEvent extends EntityEvent implements Cancellable
diff --git a/patches/api/0029-Add-EntityTeleportHinderedEvent.patch b/patches/api/0027-Add-EntityTeleportHinderedEvent.patch
similarity index 100%
rename from patches/api/0029-Add-EntityTeleportHinderedEvent.patch
rename to patches/api/0027-Add-EntityTeleportHinderedEvent.patch
diff --git a/patches/api/0030-Add-enchantment-target-for-bows-and-crossbows.patch b/patches/api/0028-Add-enchantment-target-for-bows-and-crossbows.patch
similarity index 87%
rename from patches/api/0030-Add-enchantment-target-for-bows-and-crossbows.patch
rename to patches/api/0028-Add-enchantment-target-for-bows-and-crossbows.patch
index 0b8df86e6..d9680f160 100644
--- a/patches/api/0030-Add-enchantment-target-for-bows-and-crossbows.patch
+++ b/patches/api/0028-Add-enchantment-target-for-bows-and-crossbows.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Add enchantment target for bows and crossbows
diff --git a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
-index bea786a8be4402f9384984e48390e745f2988dd6..db6375f481e0cb8c20e6417adfc435da6974acce 100644
+index 455ff52d90565838fe7640c3f045b27082a6c2f1..5831ffe24eed01311c71989dcb1830dbc395607b 100644
--- a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
+++ b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
-@@ -228,6 +228,18 @@ public enum EnchantmentTarget {
+@@ -227,6 +227,18 @@ public enum EnchantmentTarget {
public boolean includes(@NotNull Material item) {
return BREAKABLE.includes(item) || (WEARABLE.includes(item) && !item.equals(Material.ELYTRA)) || item.equals(Material.COMPASS);
}
diff --git a/patches/api/0031-Iron-golem-poppy-calms-anger.patch b/patches/api/0029-Iron-golem-poppy-calms-anger.patch
similarity index 100%
rename from patches/api/0031-Iron-golem-poppy-calms-anger.patch
rename to patches/api/0029-Iron-golem-poppy-calms-anger.patch
diff --git a/patches/api/0032-API-for-any-mob-to-burn-daylight.patch b/patches/api/0030-API-for-any-mob-to-burn-daylight.patch
similarity index 100%
rename from patches/api/0032-API-for-any-mob-to-burn-daylight.patch
rename to patches/api/0030-API-for-any-mob-to-burn-daylight.patch
diff --git a/patches/api/0033-Add-back-player-spawned-endermite-API.patch b/patches/api/0031-Add-back-player-spawned-endermite-API.patch
similarity index 100%
rename from patches/api/0033-Add-back-player-spawned-endermite-API.patch
rename to patches/api/0031-Add-back-player-spawned-endermite-API.patch
diff --git a/patches/api/0034-Fix-default-permission-system.patch b/patches/api/0032-Fix-default-permission-system.patch
similarity index 100%
rename from patches/api/0034-Fix-default-permission-system.patch
rename to patches/api/0032-Fix-default-permission-system.patch
diff --git a/patches/api/0035-Summoner-API.patch b/patches/api/0033-Summoner-API.patch
similarity index 100%
rename from patches/api/0035-Summoner-API.patch
rename to patches/api/0033-Summoner-API.patch
diff --git a/patches/api/0036-Clean-up-version-command-output.patch b/patches/api/0034-Clean-up-version-command-output.patch
similarity index 100%
rename from patches/api/0036-Clean-up-version-command-output.patch
rename to patches/api/0034-Clean-up-version-command-output.patch
diff --git a/patches/api/0037-Extended-OfflinePlayer-API.patch b/patches/api/0035-Extended-OfflinePlayer-API.patch
similarity index 100%
rename from patches/api/0037-Extended-OfflinePlayer-API.patch
rename to patches/api/0035-Extended-OfflinePlayer-API.patch
diff --git a/patches/api/0038-Added-the-ability-to-add-combustible-items.patch b/patches/api/0036-Added-the-ability-to-add-combustible-items.patch
similarity index 100%
rename from patches/api/0038-Added-the-ability-to-add-combustible-items.patch
rename to patches/api/0036-Added-the-ability-to-add-combustible-items.patch
diff --git a/patches/api/0039-Potion-NamespacedKey.patch b/patches/api/0037-Potion-NamespacedKey.patch
similarity index 100%
rename from patches/api/0039-Potion-NamespacedKey.patch
rename to patches/api/0037-Potion-NamespacedKey.patch
diff --git a/patches/api/0040-Grindstone-API.patch b/patches/api/0038-Grindstone-API.patch
similarity index 100%
rename from patches/api/0040-Grindstone-API.patch
rename to patches/api/0038-Grindstone-API.patch
diff --git a/patches/api/0041-Shears-can-have-looting-enchantment.patch b/patches/api/0039-Shears-can-have-looting-enchantment.patch
similarity index 86%
rename from patches/api/0041-Shears-can-have-looting-enchantment.patch
rename to patches/api/0039-Shears-can-have-looting-enchantment.patch
index d640b05df..53f2a3f93 100644
--- a/patches/api/0041-Shears-can-have-looting-enchantment.patch
+++ b/patches/api/0039-Shears-can-have-looting-enchantment.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Shears can have looting enchantment
diff --git a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
-index db6375f481e0cb8c20e6417adfc435da6974acce..5eb81fcc18b8fdec5a0e4c699525281fa6ad4d78 100644
+index 5831ffe24eed01311c71989dcb1830dbc395607b..45f5493eebfecf56b7c0ef4659c078dfc62c0612 100644
--- a/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
+++ b/src/main/java/org/bukkit/enchantments/EnchantmentTarget.java
-@@ -239,6 +239,16 @@ public enum EnchantmentTarget {
+@@ -238,6 +238,16 @@ public enum EnchantmentTarget {
public boolean includes(@NotNull Material item) {
return item.equals(Material.BOW) || item.equals(Material.CROSSBOW);
}
diff --git a/patches/api/0042-Lobotomize-stuck-villagers.patch b/patches/api/0040-Lobotomize-stuck-villagers.patch
similarity index 88%
rename from patches/api/0042-Lobotomize-stuck-villagers.patch
rename to patches/api/0040-Lobotomize-stuck-villagers.patch
index 5c60b6171..03b9bf50d 100644
--- a/patches/api/0042-Lobotomize-stuck-villagers.patch
+++ b/patches/api/0040-Lobotomize-stuck-villagers.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Lobotomize stuck villagers
diff --git a/src/main/java/org/bukkit/entity/Villager.java b/src/main/java/org/bukkit/entity/Villager.java
-index c61e7e41aeb3d4f5f4ac47da8890051d8e97340d..12b08318f78c8144cc809dbccf0feabdd31f0ee2 100644
+index 3bc24457d143449e6a338d79becf7c39b9f81054..4a5edf4e72e81b22c1abb2ade244f7f4292e993c 100644
--- a/src/main/java/org/bukkit/entity/Villager.java
+++ b/src/main/java/org/bukkit/entity/Villager.java
@@ -328,4 +328,14 @@ public interface Villager extends AbstractVillager {
diff --git a/patches/api/0043-Add-local-difficulty-api.patch b/patches/api/0041-Add-local-difficulty-api.patch
similarity index 100%
rename from patches/api/0043-Add-local-difficulty-api.patch
rename to patches/api/0041-Add-local-difficulty-api.patch
diff --git a/patches/api/0044-Remove-Timings.patch b/patches/api/0042-Remove-Timings.patch
similarity index 100%
rename from patches/api/0044-Remove-Timings.patch
rename to patches/api/0042-Remove-Timings.patch
diff --git a/patches/api/0045-Add-Bee-API.patch b/patches/api/0043-Add-Bee-API.patch
similarity index 100%
rename from patches/api/0045-Add-Bee-API.patch
rename to patches/api/0043-Add-Bee-API.patch
diff --git a/patches/api/0046-Debug-Marker-API.patch b/patches/api/0044-Debug-Marker-API.patch
similarity index 99%
rename from patches/api/0046-Debug-Marker-API.patch
rename to patches/api/0044-Debug-Marker-API.patch
index 7eb6a219b..78d492a64 100644
--- a/patches/api/0046-Debug-Marker-API.patch
+++ b/patches/api/0044-Debug-Marker-API.patch
@@ -260,10 +260,10 @@ index d07b6f40c111c9b131f2995e9796d66f5344c5df..adf8169d5baefa7a33c33ef066180a81
/**
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 53c536f0b828c2dbfc426c367d3b4e1d92944338..a003254281a07505cdd929fa9ee443749e48d5eb 100644
+index c0e82ea4376ac29c99f42a9630d279480050fd0c..49abe346f15f898efeff30dba66958eefc269926 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -3116,5 +3116,75 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -3139,5 +3139,75 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @param invulnerableTicks Invulnerable ticks remaining
*/
void setSpawnInvulnerableTicks(int invulnerableTicks);
diff --git a/patches/api/0047-Add-death-screen-API.patch b/patches/api/0045-Add-death-screen-API.patch
similarity index 87%
rename from patches/api/0047-Add-death-screen-API.patch
rename to patches/api/0045-Add-death-screen-API.patch
index 517414ce7..769ed9cb6 100644
--- a/patches/api/0047-Add-death-screen-API.patch
+++ b/patches/api/0045-Add-death-screen-API.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Add death screen API
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index a003254281a07505cdd929fa9ee443749e48d5eb..dc437885404ae147a06cac653e519a4674a9a951 100644
+index 49abe346f15f898efeff30dba66958eefc269926..185abf59b61f066a5961dd949d12469d9b4885f9 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -3186,5 +3186,21 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -3209,5 +3209,21 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* Clears all debug block highlights
*/
void clearBlockHighlights();
diff --git a/patches/api/0048-Add-item-packet-serialize-event.patch b/patches/api/0046-Add-item-packet-serialize-event.patch
similarity index 100%
rename from patches/api/0048-Add-item-packet-serialize-event.patch
rename to patches/api/0046-Add-item-packet-serialize-event.patch
diff --git a/patches/api/0049-Language-API.patch b/patches/api/0047-Language-API.patch
similarity index 100%
rename from patches/api/0049-Language-API.patch
rename to patches/api/0047-Language-API.patch
diff --git a/patches/api/0050-Add-log-suppression-for-LibraryLoader.patch b/patches/api/0048-Add-log-suppression-for-LibraryLoader.patch
similarity index 96%
rename from patches/api/0050-Add-log-suppression-for-LibraryLoader.patch
rename to patches/api/0048-Add-log-suppression-for-LibraryLoader.patch
index 95e4cd2ac..4948f9b89 100644
--- a/patches/api/0050-Add-log-suppression-for-LibraryLoader.patch
+++ b/patches/api/0048-Add-log-suppression-for-LibraryLoader.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Add log suppression for LibraryLoader
diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 301e82369603f3dd6e6c1bd380da4bacacd7ef6c..0c6ca7588fb3d6b6497ddf032fe75e5c6c9719e5 100644
+index eaefbb00e9993d54906cc8cf35cf753c0d6c7707..f1e58639213be0c43cd2ff090b625e7d0a67e8be 100644
--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -55,6 +55,7 @@ public final class JavaPluginLoader implements PluginLoader {
diff --git a/patches/api/0051-Fire-Immunity-API.patch b/patches/api/0049-Fire-Immunity-API.patch
similarity index 100%
rename from patches/api/0051-Fire-Immunity-API.patch
rename to patches/api/0049-Fire-Immunity-API.patch
diff --git a/patches/api/0052-Added-goat-ram-event.patch b/patches/api/0050-Added-goat-ram-event.patch
similarity index 100%
rename from patches/api/0052-Added-goat-ram-event.patch
rename to patches/api/0050-Added-goat-ram-event.patch
diff --git a/patches/api/0053-Add-PreExplodeEvents.patch b/patches/api/0051-Add-PreExplodeEvents.patch
similarity index 100%
rename from patches/api/0053-Add-PreExplodeEvents.patch
rename to patches/api/0051-Add-PreExplodeEvents.patch
diff --git a/patches/api/0002-Fix-pufferfish-issues.patch b/patches/removed/api/0002-Fix-pufferfish-issues.patch
similarity index 100%
rename from patches/api/0002-Fix-pufferfish-issues.patch
rename to patches/removed/api/0002-Fix-pufferfish-issues.patch
diff --git a/patches/server/0002-Fix-pufferfish-issues.patch b/patches/removed/server/0002-Fix-pufferfish-issues.patch
similarity index 100%
rename from patches/server/0002-Fix-pufferfish-issues.patch
rename to patches/removed/server/0002-Fix-pufferfish-issues.patch
diff --git a/patches/server/.gitkeep b/patches/server/.gitkeep
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 bcb3a9d6b..000000000
--- a/patches/server/0001-Pufferfish-Server-Changes.patch
+++ /dev/null
@@ -1,3581 +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 4f2fa65ade89c5703451dad4f80eeef162b277d1..3ee1160c796cc86db9bc9438055b307239e9a8f7 100644
---- a/build.gradle.kts
-+++ b/build.gradle.kts
-@@ -7,8 +7,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.19.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,6 +46,13 @@ dependencies {
- runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3")
- runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3")
-
-+ // Pufferfish start
-+ implementation("org.yaml:snakeyaml:1.32")
-+ implementation ("me.carleslc.Simple-YAML:Simple-Yaml:1.8.2") {
-+ exclude(group="org.yaml", module="snakeyaml")
-+ }
-+ // Pufferfish end
-+
- 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")
-@@ -50,6 +61,14 @@ dependencies {
- }
-
- val craftbukkitPackageVersion = "1_19_R3" // Paper
-+
-+// Pufferfish Start
-+tasks.withType {
-+ val compilerArgs = options.compilerArgs
-+ compilerArgs.add("--add-modules=jdk.incubator.vector")
-+}
-+// Pufferfish End
-+
- tasks.jar {
- archiveClassifier.set("dev")
-
-@@ -62,7 +81,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 c07eb451a576811a39021f6f97103c77488fd001..5af15c85fab72034b97ac210ff775e0a8fa0be78 100644
---- a/src/main/java/co/aikar/timings/TimingsExport.java
-+++ b/src/main/java/co/aikar/timings/TimingsExport.java
-@@ -242,7 +242,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 4b002e8b75d117b726b0de274a76d3596fce015b..692c962193cf9fcc6801fc93f3220bdc673d527b 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();
-@@ -607,11 +607,11 @@ public class Metrics {
- final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion();
- if (implVersion != null) {
- final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1);
-- paperVersion = "git-Paper-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash);
-+ paperVersion = "git-Pufferfish-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Pufferfish
- } else {
- paperVersion = "unknown";
- }
-- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion));
-+ metrics.addCustomChart(new Metrics.SimplePie("pufferfish_version", () -> paperVersion)); // Pufferfish
-
- metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
- Map> map = new HashMap<>();
-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 41b9405d6759d865e0d14dd4f95163e9690e967d..091b1ae822e1c0517e59572e7a9bda11e998c0ee 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..020368da69b9a492155f6de6297f74732f4ab6ea
---- /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..0dd3374468e05f7a312ba5856b9cf8a4787dfa59
---- /dev/null
-+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishConfig.java
-@@ -0,0 +1,293 @@
-+package gg.pufferfish.pufferfish;
-+
-+import gg.pufferfish.pufferfish.simd.SIMDDetection;
-+import java.io.File;
-+import java.io.IOException;
-+import java.util.Collections;
-+import net.minecraft.core.registries.BuiltInRegistries;
-+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.world.entity.EntityType;
-+import java.lang.reflect.Method;
-+import java.lang.reflect.Modifier;
-+import java.util.List;
-+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;
-+
-+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 && SIMDDetection.getJavaVersion() != 19;
-+ } catch (NoClassDefFoundError | Exception ignored) {
-+ ignored.printStackTrace();
-+ }
-+
-+ 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, Java 18, and Java 19.");
-+ } 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 : BuiltInRegistries.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 : BuiltInRegistries.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() {
-+ throttleInactiveGoalSelectorTick = 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 boolean allowEndCrystalRespawn;
-+ private static void allowEndCrystalRespawn() {
-+ allowEndCrystalRespawn = getBoolean("allow-end-crystal-respawn", true,
-+ "Allows end crystals to respawn the ender dragon.",
-+ "On servers that expect end crystal fights in the end dimension, disabling this",
-+ "will prevent the server from performing an expensive search to attempt respawning",
-+ "the ender dragon whenever a player places an end crystal.");
-+ }
-+
-+ public static boolean disableMethodProfiler;
-+ public static boolean disableOutOfOrderChat;
-+ private static void miscSettings() {
-+ disableMethodProfiler = getBoolean("misc.disable-method-profiler", true);
-+ disableOutOfOrderChat = getBoolean("misc.disable-out-of-order-chat", false);
-+ 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..e877921370f6009a4bd204d9b17d2d58834b8822
---- /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.19/lastSuccessfulBuild/buildNumber");
-+ private static final String GITHUB_FORMAT = "https://api.github.com/repos/pufferfish-gg/Pufferfish/compare/ver/1.19...%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/sentry/PufferfishSentryAppender.java b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..731ef11c7a025ae95ed8a757b530d834733d0621
---- /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