diff --git a/patches/server/0008-Paper-Metrics.patch b/patches/server/0008-Paper-Metrics.patch index 234f6cde4c..4a7b3d452b 100644 --- a/patches/server/0008-Paper-Metrics.patch +++ b/patches/server/0008-Paper-Metrics.patch @@ -13,39 +13,53 @@ tangible effect on server performance. The data is used to allow the authors of PaperMC to track version and platform usage so that we can make better management decisions on behalf of the project. +diff --git a/build.gradle.kts b/build.gradle.kts +index 4db0cc3f8505747e77d314320545eb71904b4eac..6baabc30a363d132ea3d8a7da54a40aaf918f15b 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -16,6 +16,7 @@ dependencies { + implementation("org.apache.logging.log4j:log4j-iostreams:2.14.1") // Paper + implementation("org.ow2.asm:asm:9.2") + implementation("org.ow2.asm:asm-commons:9.2") // Paper - ASM event executor generation ++ implementation("org.bstats:bstats-base:2.2.1") // Paper + runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3") + runtimeOnly("mysql:mysql-connector-java:8.0.27") + +@@ -65,6 +66,7 @@ relocation { + relocate("org.bukkit.craftbukkit" to "org.bukkit.craftbukkit.v$packageVersion") { + exclude("org.bukkit.craftbukkit.Main*") + } ++ relocate("org.bstats:bstats-base" to "org.bstats") // Paper + } + + tasks.shadowJar { diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java new file mode 100644 -index 0000000000000000000000000000000000000000..e3b74dbdf8e14219a56fab939f3174e0c2f66de6 +index 0000000000000000000000000000000000000000..6402516b8b1a04489184dc2adde83d6cbc5e83d5 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -0,0 +1,670 @@ +@@ -0,0 +1,236 @@ +package com.destroystokyo.paper; + ++import java.io.File; ++import java.io.IOException; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.UUID; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; +import net.minecraft.server.MinecraftServer; ++import org.bstats.MetricsBase; ++import org.bstats.charts.DrilldownPie; ++import org.bstats.charts.SimplePie; ++import org.bstats.charts.SingleLineChart; ++import org.bstats.json.JsonObjectBuilder; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.plugin.Plugin; -+ -+import org.json.simple.JSONArray; -+import org.json.simple.JSONObject; -+ -+import javax.net.ssl.HttpsURLConnection; -+import java.io.ByteArrayOutputStream; -+import java.io.DataOutputStream; -+import java.io.File; -+import java.io.IOException; -+import java.net.URL; -+import java.util.*; -+import java.util.concurrent.Callable; -+import java.util.concurrent.Executors; -+import java.util.concurrent.ScheduledExecutorService; -+import java.util.concurrent.TimeUnit; -+import java.util.logging.Level; -+import java.util.logging.Logger; -+import java.util.regex.Matcher; -+import java.util.regex.Pattern; -+import java.util.zip.GZIPOutputStream; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; + +/** + * bStats collects some data for plugin authors. @@ -54,643 +68,215 @@ index 0000000000000000000000000000000000000000..e3b74dbdf8e14219a56fab939f3174e0 + */ +public class Metrics { + -+ // Executor service for requests -+ // We use an executor service because the Bukkit scheduler is affected by server lags -+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); ++ private static Logger logger = LoggerFactory.getLogger("bStats"); + -+ // The version of this bStats class -+ public static final int B_STATS_VERSION = 1; ++ public Metrics() { ++ PaperMetricsConfig config = new PaperMetricsConfig(); ++ MetricsBase metricsBase = new MetricsBase( ++ "server-implementation", ++ config.getServerUUID(), ++ 580, ++ config.isEnabled(), ++ this::appendPlatformData, ++ jsonObjectBuilder -> { /* NOP */ }, ++ null, ++ () -> !MinecraftServer.getServer().hasStopped(), ++ logger::warn, ++ logger::info, ++ config.isLogErrorsEnabled(), ++ config.isLogSentDataEnabled(), ++ config.isLogResponseStatusTextEnabled() ++ ); + -+ // The url to which the data is sent -+ private static final String URL = "https://bStats.org/submitData/server-implementation"; ++ metricsBase.addCustomChart(new SimplePie("minecraft_version", () -> { ++ String minecraftVersion = Bukkit.getVersion(); ++ minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1); ++ return minecraftVersion; ++ })); + -+ // Should failed requests be logged? -+ private static boolean logFailedRequests = false; ++ metricsBase.addCustomChart(new SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); ++ metricsBase.addCustomChart(new SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); ++ metricsBase.addCustomChart(new SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); + -+ // The logger for the failed requests -+ private static Logger logger = Logger.getLogger("bStats"); ++ metricsBase.addCustomChart(new DrilldownPie("java_version", () -> { ++ Map> map = new HashMap<>(); ++ String javaVersion = System.getProperty("java.version"); ++ Map entry = new HashMap<>(); ++ entry.put(javaVersion, 1); + -+ // The name of the server software -+ private final String name; ++ // http://openjdk.java.net/jeps/223 ++ // Java decided to change their versioning scheme and in doing so modified the java.version system ++ // property to return $major[.$minor][.$secuity][-ea], as opposed to 1.$major.0_$identifier ++ // we can handle pre-9 by checking if the "major" is equal to "1", otherwise, 9+ ++ String majorVersion = javaVersion.split("\\.")[0]; ++ String release; + -+ // The uuid of the server -+ private final String serverUUID; ++ int indexOf = javaVersion.lastIndexOf('.'); + -+ // A list with all custom charts -+ private final List charts = new ArrayList<>(); -+ -+ /** -+ * Class constructor. -+ * -+ * @param name The name of the server software. -+ * @param serverUUID The uuid of the server. -+ * @param logFailedRequests Whether failed requests should be logged or not. -+ * @param logger The logger for the failed requests. -+ */ -+ public Metrics(String name, String serverUUID, boolean logFailedRequests, Logger logger) { -+ this.name = name; -+ this.serverUUID = serverUUID; -+ Metrics.logFailedRequests = logFailedRequests; -+ Metrics.logger = logger; -+ -+ // Start submitting the data -+ startSubmitting(); -+ } -+ -+ /** -+ * Adds a custom chart. -+ * -+ * @param chart The chart to add. -+ */ -+ public void addCustomChart(CustomChart chart) { -+ if (chart == null) { -+ throw new IllegalArgumentException("Chart cannot be null!"); -+ } -+ charts.add(chart); -+ } -+ -+ /** -+ * Starts the Scheduler which submits our data every 30 minutes. -+ */ -+ private void startSubmitting() { -+ final Runnable submitTask = this::submitData; -+ -+ // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the -+ // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. -+ // WARNING: You must not modify any part of this Metrics class, including the submit delay or frequency! -+ // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! -+ long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); -+ long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); -+ scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); -+ scheduler.scheduleAtFixedRate(submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); -+ } -+ -+ /** -+ * Gets the plugin specific data. -+ * -+ * @return The plugin specific data. -+ */ -+ private JSONObject getPluginData() { -+ JSONObject data = new JSONObject(); -+ -+ data.put("pluginName", name); // Append the name of the server software -+ JSONArray customCharts = new JSONArray(); -+ for (CustomChart customChart : charts) { -+ // Add the data of the custom charts -+ JSONObject chart = customChart.getRequestJsonObject(); -+ if (chart == null) { // If the chart is null, we skip it -+ continue; -+ } -+ customCharts.add(chart); -+ } -+ data.put("customCharts", customCharts); -+ -+ return data; -+ } -+ -+ /** -+ * Gets the server specific data. -+ * -+ * @return The server specific data. -+ */ -+ private JSONObject getServerData() { -+ // OS specific data -+ String osName = System.getProperty("os.name"); -+ String osArch = System.getProperty("os.arch"); -+ String osVersion = System.getProperty("os.version"); -+ int coreCount = Runtime.getRuntime().availableProcessors(); -+ -+ JSONObject data = new JSONObject(); -+ -+ data.put("serverUUID", serverUUID); -+ -+ data.put("osName", osName); -+ data.put("osArch", osArch); -+ data.put("osVersion", osVersion); -+ data.put("coreCount", coreCount); -+ -+ return data; -+ } -+ -+ /** -+ * Collects the data and sends it afterwards. -+ */ -+ private void submitData() { -+ final JSONObject data = getServerData(); -+ -+ JSONArray pluginData = new JSONArray(); -+ pluginData.add(getPluginData()); -+ data.put("plugins", pluginData); -+ -+ try { -+ // We are still in the Thread of the timer, so nothing get blocked :) -+ sendData(data); -+ } catch (Exception e) { -+ // Something went wrong! :( -+ if (logFailedRequests) { -+ logger.log(Level.WARNING, "Could not submit stats of " + name, e); -+ } -+ } -+ } -+ -+ /** -+ * Sends the data to the bStats server. -+ * -+ * @param data The data to send. -+ * @throws Exception If the request failed. -+ */ -+ private static void sendData(JSONObject data) throws Exception { -+ if (data == null) { -+ throw new IllegalArgumentException("Data cannot be null!"); -+ } -+ HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); -+ -+ // Compress the data to save bandwidth -+ byte[] compressedData = compress(data.toString()); -+ -+ // Add headers -+ connection.setRequestMethod("POST"); -+ connection.addRequestProperty("Accept", "application/json"); -+ connection.addRequestProperty("Connection", "close"); -+ connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request -+ connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); -+ connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format -+ connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); -+ -+ // Send data -+ connection.setDoOutput(true); -+ DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); -+ outputStream.write(compressedData); -+ outputStream.flush(); -+ outputStream.close(); -+ -+ connection.getInputStream().close(); // We don't care about the response - Just send our data :) -+ } -+ -+ /** -+ * Gzips the given String. -+ * -+ * @param str The string to gzip. -+ * @return The gzipped String. -+ * @throws IOException If the compression failed. -+ */ -+ private static byte[] compress(final String str) throws IOException { -+ if (str == null) { -+ return null; -+ } -+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); -+ GZIPOutputStream gzip = new GZIPOutputStream(outputStream); -+ gzip.write(str.getBytes("UTF-8")); -+ gzip.close(); -+ return outputStream.toByteArray(); -+ } -+ -+ /** -+ * Represents a custom chart. -+ */ -+ public static abstract class CustomChart { -+ -+ // The id of the chart -+ final String chartId; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ */ -+ CustomChart(String chartId) { -+ if (chartId == null || chartId.isEmpty()) { -+ throw new IllegalArgumentException("ChartId cannot be null or empty!"); -+ } -+ this.chartId = chartId; -+ } -+ -+ private JSONObject getRequestJsonObject() { -+ JSONObject chart = new JSONObject(); -+ chart.put("chartId", chartId); -+ try { -+ JSONObject data = getChartData(); -+ if (data == null) { -+ // If the data is null we don't send the chart. -+ return null; ++ if(majorVersion.equals("1")) { ++ release = "Java " + javaVersion.substring(0, indexOf); ++ } else { ++ // of course, it really wouldn't be all that simple if they didn't add a quirk, now would it ++ // valid strings for the major may potentially include values such as -ea to deannotate a pre release ++ Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); ++ if(versionMatcher.find()) { ++ majorVersion = versionMatcher.group(0); + } -+ chart.put("data", data); -+ } catch (Throwable t) { -+ if (logFailedRequests) { -+ logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); -+ } -+ return null; ++ release = "Java " + majorVersion; + } -+ return chart; -+ } ++ map.put(release, entry); + -+ protected abstract JSONObject getChartData() throws Exception; ++ return map; ++ })); + -+ } ++ metricsBase.addCustomChart(new DrilldownPie("legacy_plugins", () -> { ++ Map> map = new HashMap<>(); + -+ /** -+ * Represents a custom simple pie. -+ */ -+ public static class SimplePie extends CustomChart { -+ -+ private final Callable callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public SimplePie(String chartId, Callable callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ String value = callable.call(); -+ if (value == null || value.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ data.put("value", value); -+ return data; -+ } -+ } -+ -+ /** -+ * Represents a custom advanced pie. -+ */ -+ public static class AdvancedPie extends CustomChart { -+ -+ private final Callable> callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public AdvancedPie(String chartId, Callable> callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ JSONObject values = new JSONObject(); -+ Map map = callable.call(); -+ if (map == null || map.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ boolean allSkipped = true; -+ for (Map.Entry entry : map.entrySet()) { -+ if (entry.getValue() == 0) { -+ continue; // Skip this invalid -+ } -+ allSkipped = false; -+ values.put(entry.getKey(), entry.getValue()); -+ } -+ if (allSkipped) { -+ // Null = skip the chart -+ return null; -+ } -+ data.put("values", values); -+ return data; -+ } -+ } -+ -+ /** -+ * Represents a custom drilldown pie. -+ */ -+ public static class DrilldownPie extends CustomChart { -+ -+ private final Callable>> callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public DrilldownPie(String chartId, Callable>> callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ public JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ JSONObject values = new JSONObject(); -+ Map> map = callable.call(); -+ if (map == null || map.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ boolean reallyAllSkipped = true; -+ for (Map.Entry> entryValues : map.entrySet()) { -+ JSONObject value = new JSONObject(); -+ boolean allSkipped = true; -+ for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { -+ value.put(valueEntry.getKey(), valueEntry.getValue()); -+ allSkipped = false; -+ } -+ if (!allSkipped) { -+ reallyAllSkipped = false; -+ values.put(entryValues.getKey(), value); ++ // count legacy plugins ++ int legacy = 0; ++ for(Plugin plugin : Bukkit.getPluginManager().getPlugins()) { ++ if(CraftMagicNumbers.isLegacy(plugin.getDescription())) { ++ legacy++; + } + } -+ if (reallyAllSkipped) { -+ // Null = skip the chart -+ return null; ++ ++ // insert real value as lower dimension ++ Map entry = new HashMap<>(); ++ entry.put(String.valueOf(legacy), 1); ++ ++ // create buckets as higher dimension ++ if(legacy == 0) { ++ map.put("0 \uD83D\uDE0E", entry); // :sunglasses: ++ } else if(legacy <= 5) { ++ map.put("1-5", entry); ++ } else if(legacy <= 10) { ++ map.put("6-10", entry); ++ } else if(legacy <= 25) { ++ map.put("11-25", entry); ++ } else if(legacy <= 50) { ++ map.put("26-50", entry); ++ } else { ++ map.put("50+ \uD83D\uDE2D", entry); // :cry: + } -+ data.put("values", values); -+ return data; -+ } ++ ++ return map; ++ })); ++ ++ metricsBase.addCustomChart(new DrilldownPie("plugins", () -> { ++ Map> map = new HashMap<>(); ++ ++ int count = Bukkit.getPluginManager().getPlugins().length; ++ ++ // insert real value as lower dimension ++ Map entry = new HashMap<>(); ++ entry.put(String.valueOf(count), 1); ++ ++ // create buckets as higher dimension ++ if(count == 0) { ++ map.put("0", entry); ++ } else if(count <= 5) { ++ map.put("1-5", entry); ++ } else if(count <= 10) { ++ map.put("6-10", entry); ++ } else if(count <= 25) { ++ map.put("11-25", entry); ++ } else if(count <= 50) { ++ map.put("26-50", entry); ++ } else { ++ map.put("50+", entry); ++ } ++ ++ return map; ++ })); + } + -+ /** -+ * Represents a custom single line chart. -+ */ -+ public static class SingleLineChart extends CustomChart { -+ -+ private final Callable callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public SingleLineChart(String chartId, Callable callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ int value = callable.call(); -+ if (value == 0) { -+ // Null = skip the chart -+ return null; -+ } -+ data.put("value", value); -+ return data; -+ } -+ ++ private void appendPlatformData(JsonObjectBuilder builder) { ++ builder.appendField("osName", System.getProperty("os.name")); ++ builder.appendField("osArch", System.getProperty("os.arch")); ++ builder.appendField("osVersion", System.getProperty("os.version")); ++ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + -+ /** -+ * Represents a custom multi line chart. -+ */ -+ public static class MultiLineChart extends CustomChart { ++ static class PaperMetricsConfig { + -+ private final Callable> callable; ++ private String serverUUID; ++ private boolean enabled; ++ private boolean logErrors; ++ private boolean logSentData; ++ private boolean logResponseStatusText; + -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public MultiLineChart(String chartId, Callable> callable) { -+ super(chartId); -+ this.callable = callable; ++ public PaperMetricsConfig() { ++ setupConfig(); + } + -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ JSONObject values = new JSONObject(); -+ Map map = callable.call(); -+ if (map == null || map.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ boolean allSkipped = true; -+ for (Map.Entry entry : map.entrySet()) { -+ if (entry.getValue() == 0) { -+ continue; // Skip this invalid -+ } -+ allSkipped = false; -+ values.put(entry.getKey(), entry.getValue()); -+ } -+ if (allSkipped) { -+ // Null = skip the chart -+ return null; -+ } -+ data.put("values", values); -+ return data; -+ } -+ -+ } -+ -+ /** -+ * Represents a custom simple bar chart. -+ */ -+ public static class SimpleBarChart extends CustomChart { -+ -+ private final Callable> callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public SimpleBarChart(String chartId, Callable> callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ JSONObject values = new JSONObject(); -+ Map map = callable.call(); -+ if (map == null || map.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ for (Map.Entry entry : map.entrySet()) { -+ JSONArray categoryValues = new JSONArray(); -+ categoryValues.add(entry.getValue()); -+ values.put(entry.getKey(), categoryValues); -+ } -+ data.put("values", values); -+ return data; -+ } -+ -+ } -+ -+ /** -+ * Represents a custom advanced bar chart. -+ */ -+ public static class AdvancedBarChart extends CustomChart { -+ -+ private final Callable> callable; -+ -+ /** -+ * Class constructor. -+ * -+ * @param chartId The id of the chart. -+ * @param callable The callable which is used to request the chart data. -+ */ -+ public AdvancedBarChart(String chartId, Callable> callable) { -+ super(chartId); -+ this.callable = callable; -+ } -+ -+ @Override -+ protected JSONObject getChartData() throws Exception { -+ JSONObject data = new JSONObject(); -+ JSONObject values = new JSONObject(); -+ Map map = callable.call(); -+ if (map == null || map.isEmpty()) { -+ // Null = skip the chart -+ return null; -+ } -+ boolean allSkipped = true; -+ for (Map.Entry entry : map.entrySet()) { -+ if (entry.getValue().length == 0) { -+ continue; // Skip this invalid -+ } -+ allSkipped = false; -+ JSONArray categoryValues = new JSONArray(); -+ for (int categoryValue : entry.getValue()) { -+ categoryValues.add(categoryValue); -+ } -+ values.put(entry.getKey(), categoryValues); -+ } -+ if (allSkipped) { -+ // Null = skip the chart -+ return null; -+ } -+ data.put("values", values); -+ return data; -+ } -+ -+ } -+ -+ static class PaperMetrics { -+ static void startMetrics() { -+ // Get the config file -+ File configFile = new File(new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "bStats"), "config.yml"); ++ private void setupConfig() { ++ File bStatsFolder = new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "bStats"); ++ File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + -+ // Check if the config file exists -+ if (!config.isSet("serverUuid")) { -+ -+ // Add default values ++ if(!config.isSet("serverUuid")) { + config.addDefault("enabled", true); -+ // Every server gets it's unique random id. + config.addDefault("serverUuid", UUID.randomUUID().toString()); -+ // Should failed request be logged? + config.addDefault("logFailedRequests", false); ++ config.addDefault("logSentData", false); ++ config.addDefault("logResponseStatusText", false); + + // Inform the server owners about bStats + config.options().header( -+ "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + -+ "To honor their work, you should not disable it.\n" + -+ "This has nearly no effect on the server performance!\n" + -+ "Check out https://bStats.org/ to learn more :)" ++ "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + ++ "many people use their plugin and their total player count. It's recommended to keep bStats\n" + ++ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + ++ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + ++ "anonymous." + ).copyDefaults(true); + try { + config.save(configFile); -+ } catch (IOException ignored) { ++ } catch(IOException ignored) { + } ++ ++ // Send an info message when the bStats config file gets created for the first time ++ logger.info("Paper and some of its plugins collect metrics" ++ + " and send them to bStats (https://bStats.org)."); ++ logger.info("bStats collects some basic information for plugin" ++ + " authors, like how many people use"); ++ logger.info("their plugin and their total player count." ++ + " It's recommended to keep bStats enabled, but"); ++ logger.info("if you're not comfortable with this, you can opt-out" ++ + " by editing the config.yml file in"); ++ logger.info("the '{}' folder and setting enabled to false.", bStatsFolder.getPath()); + } ++ + // Load the data -+ String serverUUID = config.getString("serverUuid"); -+ 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()); ++ this.enabled = config.getBoolean("enabled", true); ++ this.serverUUID = config.getString("serverUuid"); ++ this.logErrors = config.getBoolean("logFailedRequests", false); ++ this.logSentData = config.getBoolean("logSentData", false); ++ this.logResponseStatusText = config.getBoolean("logResponseStatusText", false); ++ } + -+ metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { -+ String minecraftVersion = Bukkit.getVersion(); -+ minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1); -+ return minecraftVersion; -+ })); ++ public String getServerUUID() { ++ return this.serverUUID; ++ } + -+ metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); -+ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); -+ metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); ++ public boolean isEnabled() { ++ return this.enabled; ++ } + -+ metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { -+ Map> map = new HashMap<>(); -+ String javaVersion = System.getProperty("java.version"); -+ Map entry = new HashMap<>(); -+ entry.put(javaVersion, 1); ++ public boolean isLogErrorsEnabled() { ++ return this.logErrors; ++ } + -+ // http://openjdk.java.net/jeps/223 -+ // Java decided to change their versioning scheme and in doing so modified the java.version system -+ // property to return $major[.$minor][.$secuity][-ea], as opposed to 1.$major.0_$identifier -+ // we can handle pre-9 by checking if the "major" is equal to "1", otherwise, 9+ -+ String majorVersion = javaVersion.split("\\.")[0]; -+ String release; -+ -+ int indexOf = javaVersion.lastIndexOf('.'); -+ -+ if (majorVersion.equals("1")) { -+ release = "Java " + javaVersion.substring(0, indexOf); -+ } else { -+ // of course, it really wouldn't be all that simple if they didn't add a quirk, now would it -+ // valid strings for the major may potentially include values such as -ea to deannotate a pre release -+ Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); -+ if (versionMatcher.find()) { -+ majorVersion = versionMatcher.group(0); -+ } -+ release = "Java " + majorVersion; -+ } -+ map.put(release, entry); -+ -+ return map; -+ })); -+ -+ metrics.addCustomChart(new Metrics.DrilldownPie("legacy_plugins", () -> { -+ Map> map = new HashMap<>(); -+ -+ // count legacy plugins -+ int legacy = 0; -+ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { -+ if (CraftMagicNumbers.isLegacy(plugin.getDescription())) { -+ legacy++; -+ } -+ } -+ -+ // insert real value as lower dimension -+ Map entry = new HashMap<>(); -+ entry.put(String.valueOf(legacy), 1); -+ -+ // create buckets as higher dimension -+ if (legacy == 0) { -+ map.put("0 \uD83D\uDE0E", entry); // :sunglasses: -+ } else if (legacy <= 5) { -+ map.put("1-5", entry); -+ } else if (legacy <= 10) { -+ map.put("6-10", entry); -+ } else if (legacy <= 25) { -+ map.put("11-25", entry); -+ } else if (legacy <= 50) { -+ map.put("26-50", entry); -+ } else { -+ map.put("50+ \uD83D\uDE2D", entry); // :cry: -+ } -+ -+ return map; -+ })); -+ } ++ public boolean isLogSentDataEnabled() { ++ return this.logSentData; ++ } + ++ public boolean isLogResponseStatusTextEnabled() { ++ return this.logResponseStatusText; + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java -index e4368db074da7b5e48b47d41875c1e63b9745c2a..ed2627f76af277c9be23da3423542d6af0bff872 100644 +index e4368db074da7b5e48b47d41875c1e63b9745c2a..f86d64b8711b4a8ef7e666fca1c88acbfadb41e8 100644 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -42,6 +42,7 @@ public class PaperConfig { @@ -707,14 +293,14 @@ index e4368db074da7b5e48b47d41875c1e63b9745c2a..ed2627f76af277c9be23da3423542d6a } + + if (!metricsStarted) { -+ Metrics.PaperMetrics.startMetrics(); ++ new Metrics(); + metricsStarted = true; + } } static void readConfig(Class clazz, Object instance) { diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java -index 58c9ab2f6db97bfbf280efc56f9c9be791604a75..f8a9d6a394f796634e4663ef4078a4c98447e13c 100644 +index c90db55aadb1d16f6cc4e02e57a13a3c3fe6a420..5a912528f82e8f97229a412b0bf72e04a520b556 100644 --- a/src/main/java/org/spigotmc/SpigotConfig.java +++ b/src/main/java/org/spigotmc/SpigotConfig.java @@ -83,6 +83,7 @@ public class SpigotConfig diff --git a/patches/server/0223-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch b/patches/server/0223-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch index 9ae2453670..d922762036 100644 --- a/patches/server/0223-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch +++ b/patches/server/0223-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch @@ -5,11 +5,11 @@ Subject: [PATCH] Use AsyncAppender to keep logging IO off main thread diff --git a/build.gradle.kts b/build.gradle.kts -index 8cec9cdfc2708153e8d37a7d52a15fd47dc21dcd..125630037713c4790636ffd11b14b2c1d83a085a 100644 +index 09751abfae4cb9cff7e1cbf62b3cc5f81fb0d680..1494d794793c1cd45ef920bb02a7e7f98d51946a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts -@@ -28,6 +28,7 @@ dependencies { - implementation("org.ow2.asm:asm-commons:9.2") // Paper - ASM event executor generation +@@ -29,6 +29,7 @@ dependencies { + implementation("org.bstats:bstats-base:2.2.1") // Paper runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3") runtimeOnly("mysql:mysql-connector-java:8.0.27") + runtimeOnly("com.lmax:disruptor:3.4.4") // Paper diff --git a/patches/server/0395-Improved-Watchdog-Support.patch b/patches/server/0395-Improved-Watchdog-Support.patch index 42b646feff..19032b5dd8 100644 --- a/patches/server/0395-Improved-Watchdog-Support.patch +++ b/patches/server/0395-Improved-Watchdog-Support.patch @@ -40,24 +40,6 @@ This is to ensure that if main isn't truely stuck, it's not manipulating state w This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they are properly accounted for and wont trip watchdog on init. -diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index e3b74dbdf8e14219a56fab939f3174e0c2f66de6..218f5bafeed8551b55b91c7fccaf6935c8b631ca 100644 ---- a/src/main/java/com/destroystokyo/paper/Metrics.java -+++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -92,7 +92,12 @@ public class Metrics { - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { -- final Runnable submitTask = this::submitData; -+ final Runnable submitTask = () -> { -+ if (MinecraftServer.getServer().hasStopped()) { -+ return; -+ } -+ submitData(); -+ }; - - // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the - // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java index c54530d1c66845b190a9cb6d06f985943bb4dbe1..35c9b3e6c5a2d11b4dbd491b16647df105960d1a 100644 --- a/src/main/java/net/minecraft/CrashReport.java diff --git a/patches/server/0419-Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/0419-Deobfuscate-stacktraces-in-log-messages-crash-report.patch index 04d0cd8951..e9af8b5c49 100644 --- a/patches/server/0419-Deobfuscate-stacktraces-in-log-messages-crash-report.patch +++ b/patches/server/0419-Deobfuscate-stacktraces-in-log-messages-crash-report.patch @@ -6,7 +6,7 @@ Subject: [PATCH] Deobfuscate stacktraces in log messages, crash reports, and diff --git a/build.gradle.kts b/build.gradle.kts -index 898e2efb764e5bd97ab4e757e6c4c27fc4efdbef..055abcdfd779ce37d657845b3c6322f01fac989d 100644 +index 95e640be2f5bba403cbbf0102e94accfc225f866..6ad707e45cf612c8175061b074572374dfed17bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,6 @@ @@ -36,7 +36,7 @@ index 898e2efb764e5bd97ab4e757e6c4c27fc4efdbef..055abcdfd779ce37d657845b3c6322f0 // Paper end implementation("org.apache.logging.log4j:log4j-iostreams:2.14.1") // Paper implementation("org.ow2.asm:asm:9.2") -@@ -35,6 +43,8 @@ dependencies { +@@ -36,6 +44,8 @@ dependencies { runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.2") runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.2") @@ -45,7 +45,7 @@ index 898e2efb764e5bd97ab4e757e6c4c27fc4efdbef..055abcdfd779ce37d657845b3c6322f0 testImplementation("junit:junit:4.13.2") testImplementation("org.hamcrest:hamcrest-library:1.3") } -@@ -92,6 +102,45 @@ tasks.shadowJar { +@@ -94,6 +104,45 @@ tasks.shadowJar { } } @@ -92,7 +92,7 @@ index 898e2efb764e5bd97ab4e757e6c4c27fc4efdbef..055abcdfd779ce37d657845b3c6322f0 exclude("org/bukkit/craftbukkit/inventory/ItemStack*Test.class") } diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java -index c20fe23174d8a12bfc5acb4b0e947c6fca5ab897..6ee4d5377d3963c67c6da4e5b164e49949768f03 100644 +index d99d443657fcda26ca2941687b16d356de2d8731..5a4b25b16e8f515670b686bc1ab17e2e5fa0a6b7 100644 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -430,4 +430,9 @@ public class PaperConfig { diff --git a/patches/server/0420-Implement-Mob-Goal-API.patch b/patches/server/0420-Implement-Mob-Goal-API.patch index 3f50c4d346..e5213eabe4 100644 --- a/patches/server/0420-Implement-Mob-Goal-API.patch +++ b/patches/server/0420-Implement-Mob-Goal-API.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Implement Mob Goal API diff --git a/build.gradle.kts b/build.gradle.kts -index 055abcdfd779ce37d657845b3c6322f01fac989d..0ed1fa068da85543b161fe86869ad8c90e701b73 100644 +index 6ad707e45cf612c8175061b074572374dfed17bc..65d2353c948dea97acdfcdae0b83fba3f2b80b17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts -@@ -45,6 +45,7 @@ dependencies { +@@ -46,6 +46,7 @@ dependencies { implementation("net.fabricmc:mapping-io:0.3.0") // Paper - needed to read mappings for stacktrace deobfuscation diff --git a/patches/server/0779-Use-Velocity-compression-and-cipher-natives.patch b/patches/server/0779-Use-Velocity-compression-and-cipher-natives.patch index eb2878182d..4a0f2b4f43 100644 --- a/patches/server/0779-Use-Velocity-compression-and-cipher-natives.patch +++ b/patches/server/0779-Use-Velocity-compression-and-cipher-natives.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Use Velocity compression and cipher natives diff --git a/build.gradle.kts b/build.gradle.kts -index 0ed1fa068da85543b161fe86869ad8c90e701b73..17cde4eaf23e01710c131fbea5d171fd25725250 100644 +index 65d2353c948dea97acdfcdae0b83fba3f2b80b17..d4d0215b9b425d688f4630f1785a79ff69fb649f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts -@@ -44,6 +44,11 @@ dependencies { +@@ -45,6 +45,11 @@ dependencies { runtimeOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.2") implementation("net.fabricmc:mapping-io:0.3.0") // Paper - needed to read mappings for stacktrace deobfuscation @@ -268,7 +268,7 @@ index 792883afe53d2b7989c25a81c2f9a639d5e21d20..c04379ca8a4db0f4de46ad2b3b338431 return this.threshold; } diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index a1aafb037fd340dc93dd2afb758ffc7457d15f84..a7e7fe4be2784ff34f7f8d0b6c2f82d65de8c145 100644 +index c14294923a0c15aafe98bf5006866acaa9803280..81dde0efc1a06420c0791520b9e40b24dd1f0318 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java @@ -628,11 +628,28 @@ public class Connection extends SimpleChannelInboundHandler> { @@ -325,7 +325,7 @@ index a1aafb037fd340dc93dd2afb758ffc7457d15f84..a7e7fe4be2784ff34f7f8d0b6c2f82d6 } else { if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder) { diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -index f7aa0125e4724f1efddf28814f926289c1ae37d4..477aa83c3b342705a8a9b7ab41b2f77008e2e281 100644 +index 70b0c7fea72397f7bbdf02ce2794e7fb6fbd3742..bce781b645cc95f399eacf65edad5ad321fd68a4 100644 --- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java @@ -104,6 +104,11 @@ public class ServerConnectionListener { diff --git a/patches/server/0825-Update-Log4j.patch b/patches/server/0825-Update-Log4j.patch index 9f57674ddb..278699ec6d 100644 --- a/patches/server/0825-Update-Log4j.patch +++ b/patches/server/0825-Update-Log4j.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Update Log4j diff --git a/build.gradle.kts b/build.gradle.kts -index 17cde4eaf23e01710c131fbea5d171fd25725250..028f6a1795ceb99d1760c73b0980238677b4b8bc 100644 +index d4d0215b9b425d688f4630f1785a79ff69fb649f..e604c62ba7e82fee0028114fc7985f3077544c4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,10 +29,11 @@ dependencies { @@ -22,4 +22,4 @@ index 17cde4eaf23e01710c131fbea5d171fd25725250..028f6a1795ceb99d1760c73b09802386 + implementation("org.apache.logging.log4j:log4j-slf4j18-impl:2.17.1") // Paper implementation("org.ow2.asm:asm:9.2") implementation("org.ow2.asm:asm-commons:9.2") // Paper - ASM event executor generation - runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3") + implementation("org.bstats:bstats-base:2.2.1") // Paper