diff options
Diffstat (limited to 'patches/server/0018-Paper-Metrics.patch')
-rw-r--r-- | patches/server/0018-Paper-Metrics.patch | 731 |
1 files changed, 731 insertions, 0 deletions
diff --git a/patches/server/0018-Paper-Metrics.patch b/patches/server/0018-Paper-Metrics.patch new file mode 100644 index 0000000000..9ad0f5d70e --- /dev/null +++ b/patches/server/0018-Paper-Metrics.patch @@ -0,0 +1,731 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zach Brown <[email protected]> +Date: Fri, 24 Mar 2017 23:56:01 -0500 +Subject: [PATCH] Paper Metrics + +Removes Spigot's mcstats metrics in favor of a system using bStats + +To disable for privacy or other reasons go to the bStats folder in your plugins folder +and edit the config.yml file present there. + +Please keep in mind the data collected is anonymous and collection should have no +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/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6aaed8e8bf8c721fc834da5c76ac72a4c3e92458 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -0,0 +1,678 @@ ++package com.destroystokyo.paper; ++ ++import net.minecraft.server.MinecraftServer; ++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; ++ ++/** ++ * bStats collects some data for plugin authors. ++ * ++ * Check out https://bStats.org/ to learn more about bStats! ++ */ ++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); ++ ++ // The version of this bStats class ++ public static final int B_STATS_VERSION = 1; ++ ++ // The url to which the data is sent ++ private static final String URL = "https://bStats.org/submitData/server-implementation"; ++ ++ // Should failed requests be logged? ++ private static boolean logFailedRequests = false; ++ ++ // The logger for the failed requests ++ private static Logger logger = Logger.getLogger("bStats"); ++ ++ // The name of the server software ++ private final String name; ++ ++ // The uuid of the server ++ private final String serverUUID; ++ ++ // A list with all custom charts ++ private final List<CustomChart> 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; ++ } ++ 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; ++ } ++ return chart; ++ } ++ ++ protected abstract JSONObject getChartData() throws Exception; ++ ++ } ++ ++ /** ++ * Represents a custom simple pie. ++ */ ++ public static class SimplePie extends CustomChart { ++ ++ private final Callable<String> 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<String> 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<Map<String, Integer>> 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<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> 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<Map<String, Map<String, Integer>>> 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<Map<String, Map<String, Integer>>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ public JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Map<String, Integer>> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean reallyAllSkipped = true; ++ for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) { ++ JSONObject value = new JSONObject(); ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) { ++ value.put(valueEntry.getKey(), valueEntry.getValue()); ++ allSkipped = false; ++ } ++ if (!allSkipped) { ++ reallyAllSkipped = false; ++ values.put(entryValues.getKey(), value); ++ } ++ } ++ if (reallyAllSkipped) { ++ // Null = skip the chart ++ return null; ++ } ++ data.put("values", values); ++ return data; ++ } ++ } ++ ++ /** ++ * Represents a custom single line chart. ++ */ ++ public static class SingleLineChart extends CustomChart { ++ ++ private final Callable<Integer> 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<Integer> 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; ++ } ++ ++ } ++ ++ /** ++ * Represents a custom multi line chart. ++ */ ++ public static class MultiLineChart extends CustomChart { ++ ++ private final Callable<Map<String, Integer>> callable; ++ ++ /** ++ * 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<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, Integer> 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<Map<String, Integer>> 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<Map<String, Integer>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, Integer> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ for (Map.Entry<String, Integer> 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<Map<String, int[]>> 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<Map<String, int[]>> callable) { ++ super(chartId); ++ this.callable = callable; ++ } ++ ++ @Override ++ protected JSONObject getChartData() throws Exception { ++ JSONObject data = new JSONObject(); ++ JSONObject values = new JSONObject(); ++ Map<String, int[]> map = callable.call(); ++ if (map == null || map.isEmpty()) { ++ // Null = skip the chart ++ return null; ++ } ++ boolean allSkipped = true; ++ for (Map.Entry<String, int[]> 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; ++ } ++ ++ } ++ ++ public static class PaperMetrics { ++ public static void startMetrics() { ++ // Get the config file ++ File configFile = new File(new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "bStats"), "config.yml"); ++ YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); ++ ++ // Check if the config file exists ++ if (!config.isSet("serverUuid")) { ++ ++ // Add default values ++ 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); ++ ++ // 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 :)" ++ ).copyDefaults(true); ++ try { ++ config.save(configFile); ++ } catch (IOException ignored) { ++ } ++ } ++ // 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()); ++ ++ metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { ++ String minecraftVersion = Bukkit.getVersion(); ++ minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1); ++ return minecraftVersion; ++ })); ++ ++ metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); ++ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline")); ++ final String paperVersion; ++ 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); ++ } else { ++ paperVersion = "unknown"; ++ } ++ metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); ++ ++ metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { ++ Map<String, Map<String, Integer>> map = new HashMap<>(); ++ String javaVersion = System.getProperty("java.version"); ++ Map<String, Integer> entry = new HashMap<>(); ++ entry.put(javaVersion, 1); ++ ++ // 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<String, Map<String, Integer>> 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<String, Integer> 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; ++ })); ++ } ++ ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index cd9e4bfdb3f335213001ced27540bb7efbc04130..3b403e9edf4e860160dd230977870f21a0e32a7a 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -215,6 +215,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); + // Paper end - initialize global and world-defaults configuration + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command ++ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index 744edd40128c910c3ad2f3657bde995612e0a1e4..d9e73e37b54e2f6e13313977c76cb4212c240992 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -83,6 +83,7 @@ public class SpigotConfig + MinecraftServer.getServer().server.getCommandMap().register( entry.getKey(), "Spigot", entry.getValue() ); + } + ++ /* // Paper - Replace with our own + if ( SpigotConfig.metrics == null ) + { + try +@@ -94,6 +95,7 @@ public class SpigotConfig + Bukkit.getServer().getLogger().log( Level.SEVERE, "Could not start metrics service", ex ); + } + } ++ */ // Paper end + } + + public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public |