callable) {
+ super(chartId);
+ this.callable = callable;
+ }
+
+ @Override
+ protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
+ int value = callable.call();
+ if (value == 0) {
+ // Null = skip the chart
+ return null;
+ }
+ return new JsonObjectBuilder().appendField("value", value).build();
+ }
+ }
+
+ /**
+ * An extremely simple JSON builder.
+ *
+ * While this class is neither feature-rich nor the most performant one, it's sufficient enough
+ * for its use-case.
+ */
+ public static class JsonObjectBuilder {
+
+ private StringBuilder builder = new StringBuilder();
+
+ private boolean hasAtLeastOneField = false;
+
+ public JsonObjectBuilder() {
+ builder.append("{");
+ }
+
+ /**
+ * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
+ *
+ *
This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
+ * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
+ *
+ * @param value The value to escape.
+ * @return The escaped value.
+ */
+ private static String escape(String value) {
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '"') {
+ builder.append("\\\"");
+ } else if (c == '\\') {
+ builder.append("\\\\");
+ } else if (c <= '\u000F') {
+ builder.append("\\u000").append(Integer.toHexString(c));
+ } else if (c <= '\u001F') {
+ builder.append("\\u00").append(Integer.toHexString(c));
+ } else {
+ builder.append(c);
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Appends a null field to the JSON.
+ *
+ * @param key The key of the field.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendNull(String key) {
+ appendFieldUnescaped(key, "null");
+ return this;
+ }
+
+ /**
+ * Appends a string field to the JSON.
+ *
+ * @param key The key of the field.
+ * @param value The value of the field.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("JSON value must not be null");
+ }
+ appendFieldUnescaped(key, "\"" + escape(value) + "\"");
+ return this;
+ }
+
+ /**
+ * Appends an integer field to the JSON.
+ *
+ * @param key The key of the field.
+ * @param value The value of the field.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, int value) {
+ appendFieldUnescaped(key, String.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Appends an object to the JSON.
+ *
+ * @param key The key of the field.
+ * @param object The object.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, JsonObject object) {
+ if (object == null) {
+ throw new IllegalArgumentException("JSON object must not be null");
+ }
+ appendFieldUnescaped(key, object.toString());
+ return this;
+ }
+
+ /**
+ * Appends a string array to the JSON.
+ *
+ * @param key The key of the field.
+ * @param values The string array.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, String[] values) {
+ if (values == null) {
+ throw new IllegalArgumentException("JSON values must not be null");
+ }
+ String escapedValues =
+ Arrays.stream(values)
+ .map(value -> "\"" + escape(value) + "\"")
+ .collect(Collectors.joining(","));
+ appendFieldUnescaped(key, "[" + escapedValues + "]");
+ return this;
+ }
+
+ /**
+ * Appends an integer array to the JSON.
+ *
+ * @param key The key of the field.
+ * @param values The integer array.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, int[] values) {
+ if (values == null) {
+ throw new IllegalArgumentException("JSON values must not be null");
+ }
+ String escapedValues =
+ Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
+ appendFieldUnescaped(key, "[" + escapedValues + "]");
+ return this;
+ }
+
+ /**
+ * Appends an object array to the JSON.
+ *
+ * @param key The key of the field.
+ * @param values The integer array.
+ * @return A reference to this object.
+ */
+ public JsonObjectBuilder appendField(String key, JsonObject[] values) {
+ if (values == null) {
+ throw new IllegalArgumentException("JSON values must not be null");
+ }
+ String escapedValues =
+ Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
+ appendFieldUnescaped(key, "[" + escapedValues + "]");
+ return this;
+ }
+
+ /**
+ * Appends a field to the object.
+ *
+ * @param key The key of the field.
+ * @param escapedValue The escaped value of the field.
+ */
+ private void appendFieldUnescaped(String key, String escapedValue) {
+ if (builder == null) {
+ throw new IllegalStateException("JSON has already been built");
+ }
+ if (key == null) {
+ throw new IllegalArgumentException("JSON key must not be null");
+ }
+ if (hasAtLeastOneField) {
+ builder.append(",");
+ }
+ builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
+ hasAtLeastOneField = true;
+ }
+
+ /**
+ * Builds the JSON string and invalidates this builder.
+ *
+ * @return The built JSON string.
+ */
+ public JsonObject build() {
+ if (builder == null) {
+ throw new IllegalStateException("JSON has already been built");
+ }
+ JsonObject object = new JsonObject(builder.append("}").toString());
+ builder = null;
+ return object;
+ }
+
+ /**
+ * A super simple representation of a JSON object.
+ *
+ *
This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
+ * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
+ * JsonObject)}.
+ */
+ public static class JsonObject {
+
+ private final String value;
+
+ private JsonObject(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+ }
+ }
+}
diff --git a/src/main/java/me/oskar3123/staffchat/spigot/Main.java b/src/main/java/me/oskar3123/staffchat/spigot/Main.java
index 5bdf936..bd237a2 100644
--- a/src/main/java/me/oskar3123/staffchat/spigot/Main.java
+++ b/src/main/java/me/oskar3123/staffchat/spigot/Main.java
@@ -3,11 +3,13 @@ package me.oskar3123.staffchat.spigot;
import github.scarsz.discordsrv.DiscordSRV;
import java.util.Optional;
import me.clip.placeholderapi.PlaceholderAPI;
+import me.oskar3123.staffchat.bstats.folia.MetricsFolia;
import me.oskar3123.staffchat.spigot.command.StaffChatCommand;
import me.oskar3123.staffchat.spigot.handler.StaffChatHandler;
import me.oskar3123.staffchat.spigot.listener.ChatListener;
import me.oskar3123.staffchat.spigot.listener.DiscordSrvListener;
import me.oskar3123.staffchat.spigot.listener.StaffChatPml;
+import me.oskar3123.staffchat.spigot.util.FoliaUtils;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -29,7 +31,11 @@ public class Main extends JavaPlugin {
public void onEnable() {
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
getServer().getMessenger().registerIncomingPluginChannel(this, "BungeeCord", staffChatPml);
- new Metrics(this, BSTATS_PLUGIN_ID);
+ if (FoliaUtils.isFolia()) {
+ new MetricsFolia(this, BSTATS_PLUGIN_ID);
+ } else {
+ new Metrics(this, BSTATS_PLUGIN_ID);
+ }
saveDefaultConfig();
registerCommands();
registerEvents();
diff --git a/src/main/java/me/oskar3123/staffchat/spigot/listener/DiscordSrvListener.java b/src/main/java/me/oskar3123/staffchat/spigot/listener/DiscordSrvListener.java
index 5cd5c52..75ac5be 100644
--- a/src/main/java/me/oskar3123/staffchat/spigot/listener/DiscordSrvListener.java
+++ b/src/main/java/me/oskar3123/staffchat/spigot/listener/DiscordSrvListener.java
@@ -8,7 +8,7 @@ import github.scarsz.discordsrv.dependencies.jda.api.entities.Message;
import github.scarsz.discordsrv.dependencies.jda.api.entities.User;
import java.util.Optional;
import me.oskar3123.staffchat.spigot.Main;
-import org.bukkit.Bukkit;
+import me.oskar3123.staffchat.spigot.util.FoliaUtils;
import org.bukkit.configuration.Configuration;
public class DiscordSrvListener {
@@ -38,16 +38,15 @@ public class DiscordSrvListener {
Optional.ofNullable(event.getMessage())
.map(Message::getContentDisplay)
.orElse("");
- Bukkit.getScheduler()
- .runTask(
- plugin,
- () ->
- plugin.staffChatHandler.sendStaffChatMessage(
- plugin
- .getConfig()
- .getString("discordsrv.discord-to-minecraft-format", ""),
- () -> name,
- message));
+ FoliaUtils.execute(
+ plugin,
+ () ->
+ plugin.staffChatHandler.sendStaffChatMessage(
+ plugin
+ .getConfig()
+ .getString("discordsrv.discord-to-minecraft-format", ""),
+ () -> name,
+ message));
});
}
diff --git a/src/main/java/me/oskar3123/staffchat/spigot/util/FoliaUtils.java b/src/main/java/me/oskar3123/staffchat/spigot/util/FoliaUtils.java
new file mode 100644
index 0000000..15395b8
--- /dev/null
+++ b/src/main/java/me/oskar3123/staffchat/spigot/util/FoliaUtils.java
@@ -0,0 +1,52 @@
+package me.oskar3123.staffchat.spigot.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+public class FoliaUtils {
+
+ private static final String FOLIA_PROBE_CLASS =
+ "io.papermc.paper.threadedregions.RegionizedServer";
+ private static final boolean IS_FOLIA;
+
+ static {
+ boolean isFolia;
+ try {
+ Class.forName(FOLIA_PROBE_CLASS);
+ isFolia = true;
+ } catch (ClassNotFoundException e) {
+ isFolia = false;
+ }
+ IS_FOLIA = isFolia;
+ }
+
+ private FoliaUtils() {}
+
+ public static boolean isFolia() {
+ return IS_FOLIA;
+ }
+
+ public static void execute(Plugin plugin, Runnable runnable) {
+ if (isFolia()) {
+ try {
+ Method getGlobalRegionSchedulerMethod =
+ Class.forName("org.bukkit.Bukkit").getDeclaredMethod("getGlobalRegionScheduler");
+ Method executeMethod =
+ Class.forName("io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler")
+ .getDeclaredMethod("execute", Plugin.class, Runnable.class);
+ Object globalRegionScheduler = getGlobalRegionSchedulerMethod.invoke(null);
+ executeMethod.invoke(globalRegionScheduler, plugin, runnable);
+ } catch (ClassNotFoundException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | InvocationTargetException e) {
+ // Should not happen, rethrow as a runtime exception if it does
+ throw new RuntimeException(e);
+ }
+ } else {
+ Bukkit.getScheduler().runTask(plugin, runnable);
+ }
+ }
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 2688390..92f1a90 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -4,6 +4,7 @@ main: me.oskar3123.staffchat.spigot.Main
api-version: 1.13
version: SNAPSHOT
softdepend: [ PlaceholderAPI, DiscordSRV ]
+folia-supported: true
commands:
staffchat:
usage: /