├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── settings.gradle └── src └── main ├── java └── dev │ └── unnm3d │ └── loadingscreenremover │ ├── LoadingScreenRemover.java │ ├── Metrics.java │ ├── PlayerListener.java │ └── PlayerManager.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'com.github.johnrengelman.shadow' version '8.1.1' 4 | } 5 | 6 | group = 'dev.unnm3d' 7 | version = '1.4' 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { 12 | url = "https://repo.papermc.io/repository/maven-public/" 13 | } 14 | maven { 15 | url = uri("https://repo.codemc.io/repository/maven-releases/") 16 | } 17 | maven { url 'https://jitpack.io' } 18 | } 19 | 20 | dependencies { 21 | compileOnly "io.papermc.paper:paper-api:1.18-R0.1-SNAPSHOT" 22 | compileOnly "org.projectlombok:lombok:1.18.30" 23 | 24 | compileOnly "com.github.retrooper:packetevents-spigot:2.6.0" 25 | implementation 'com.github.Anon8281:UniversalScheduler:0.1.6' 26 | annotationProcessor "org.projectlombok:lombok:1.18.30" 27 | } 28 | shadowJar { 29 | dependencies{ 30 | exclude(dependency('com.google.code.gson:gson:2.8.0')) 31 | } 32 | //relocate("com.github.retrooper.packetevents", "dev.unnm3d.loadingscreenremover.libraries.packetevents.api") 33 | //elocate("io.github.retrooper.packetevents", "dev.unnm3d.loadingscreenremover.libraries.packetevents.impl") 34 | relocate("net.kyori", "dev.unnm3d.loadingscreenremover.libraries.packetevents.kyori") 35 | relocate("com.github.Anon8281.universalScheduler", "dev.unnm3d.loadingscreenremover.libraries.universalScheduler") 36 | exclude 'assets/**' 37 | } 38 | def targetJavaVersion = 17 39 | java { 40 | def javaVersion = JavaVersion.toVersion(targetJavaVersion) 41 | sourceCompatibility = javaVersion 42 | targetCompatibility = javaVersion 43 | if (JavaVersion.current() < javaVersion) { 44 | toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) 45 | } 46 | } 47 | 48 | tasks.withType(JavaCompile).configureEach { 49 | options.encoding = 'UTF-8' 50 | 51 | if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { 52 | options.release.set(targetJavaVersion) 53 | } 54 | } 55 | 56 | processResources { 57 | def props = [version: version] 58 | inputs.properties props 59 | filteringCharset 'UTF-8' 60 | filesMatching('plugin.yml') { 61 | expand props 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Emibergo02/LoadingScreenRemover/3e052a07c936ac378354ea03ebf550915c6ba12a/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'LoadingScreenRemover' 2 | -------------------------------------------------------------------------------- /src/main/java/dev/unnm3d/loadingscreenremover/LoadingScreenRemover.java: -------------------------------------------------------------------------------- 1 | package dev.unnm3d.loadingscreenremover; 2 | 3 | import com.github.Anon8281.universalScheduler.UniversalScheduler; 4 | import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler; 5 | import com.github.retrooper.packetevents.PacketEvents; 6 | import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; 7 | import lombok.Getter; 8 | import org.bukkit.plugin.java.JavaPlugin; 9 | 10 | 11 | @Getter 12 | public final class LoadingScreenRemover extends JavaPlugin { 13 | 14 | private PlayerManager playerManager; 15 | private TaskScheduler taskScheduler; 16 | 17 | 18 | @Override 19 | public void onEnable() { 20 | this.playerManager = new PlayerManager(); 21 | this.taskScheduler = UniversalScheduler.getScheduler(this); 22 | 23 | final PlayerListener playerListener = new PlayerListener(this); 24 | 25 | getServer().getPluginManager().registerEvents(playerListener, this); 26 | 27 | PacketEvents.getAPI().getEventManager().registerListeners(playerListener); 28 | new Metrics(this, 20950); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/dev/unnm3d/loadingscreenremover/Metrics.java: -------------------------------------------------------------------------------- 1 | package dev.unnm3d.loadingscreenremover; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.DataOutputStream; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.lang.reflect.Method; 10 | import java.net.URL; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.HashSet; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | import java.util.UUID; 19 | import java.util.concurrent.Callable; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.ScheduledThreadPoolExecutor; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.function.BiConsumer; 24 | import java.util.function.Consumer; 25 | import java.util.function.Supplier; 26 | import java.util.logging.Level; 27 | import java.util.stream.Collectors; 28 | import java.util.zip.GZIPOutputStream; 29 | import javax.net.ssl.HttpsURLConnection; 30 | import org.bukkit.Bukkit; 31 | import org.bukkit.configuration.file.YamlConfiguration; 32 | import org.bukkit.entity.Player; 33 | import org.bukkit.plugin.Plugin; 34 | import org.bukkit.plugin.java.JavaPlugin; 35 | 36 | public class Metrics { 37 | 38 | private final Plugin plugin; 39 | 40 | private final MetricsBase metricsBase; 41 | 42 | /** 43 | * Creates a new Metrics instance. 44 | * 45 | * @param plugin Your plugin instance. 46 | * @param serviceId The id of the service. It can be found at What is my plugin id? 48 | */ 49 | public Metrics(JavaPlugin plugin, int serviceId) { 50 | this.plugin = plugin; 51 | // Get the config file 52 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 53 | File configFile = new File(bStatsFolder, "config.yml"); 54 | YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); 55 | if (!config.isSet("serverUuid")) { 56 | config.addDefault("enabled", true); 57 | config.addDefault("serverUuid", UUID.randomUUID().toString()); 58 | config.addDefault("logFailedRequests", false); 59 | config.addDefault("logSentData", false); 60 | config.addDefault("logResponseStatusText", false); 61 | // Inform the server owners about bStats 62 | config 63 | .options() 64 | .header( 65 | "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" 66 | + "many people use their plugin and their total player count. It's recommended to keep bStats\n" 67 | + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" 68 | + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" 69 | + "anonymous.") 70 | .copyDefaults(true); 71 | try { 72 | config.save(configFile); 73 | } catch (IOException ignored) { 74 | } 75 | } 76 | // Load the data 77 | boolean enabled = config.getBoolean("enabled", true); 78 | String serverUUID = config.getString("serverUuid"); 79 | boolean logErrors = config.getBoolean("logFailedRequests", false); 80 | boolean logSentData = config.getBoolean("logSentData", false); 81 | boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); 82 | metricsBase = 83 | new MetricsBase( 84 | "bukkit", 85 | serverUUID, 86 | serviceId, 87 | enabled, 88 | this::appendPlatformData, 89 | this::appendServiceData, 90 | submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), 91 | plugin::isEnabled, 92 | (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), 93 | (message) -> this.plugin.getLogger().log(Level.INFO, message), 94 | logErrors, 95 | logSentData, 96 | logResponseStatusText); 97 | } 98 | 99 | /** Shuts down the underlying scheduler service. */ 100 | public void shutdown() { 101 | metricsBase.shutdown(); 102 | } 103 | 104 | /** 105 | * Adds a custom chart. 106 | * 107 | * @param chart The chart to add. 108 | */ 109 | public void addCustomChart(CustomChart chart) { 110 | metricsBase.addCustomChart(chart); 111 | } 112 | 113 | private void appendPlatformData(JsonObjectBuilder builder) { 114 | builder.appendField("playerAmount", getPlayerAmount()); 115 | builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); 116 | builder.appendField("bukkitVersion", Bukkit.getVersion()); 117 | builder.appendField("bukkitName", Bukkit.getName()); 118 | builder.appendField("javaVersion", System.getProperty("java.version")); 119 | builder.appendField("osName", System.getProperty("os.name")); 120 | builder.appendField("osArch", System.getProperty("os.arch")); 121 | builder.appendField("osVersion", System.getProperty("os.version")); 122 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 123 | } 124 | 125 | private void appendServiceData(JsonObjectBuilder builder) { 126 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 127 | } 128 | 129 | private int getPlayerAmount() { 130 | try { 131 | // Around MC 1.8 the return type was changed from an array to a collection, 132 | // This fixes java.lang.NoSuchMethodError: 133 | // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; 134 | Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); 135 | return onlinePlayersMethod.getReturnType().equals(Collection.class) 136 | ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() 137 | : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; 138 | } catch (Exception e) { 139 | // Just use the new method if the reflection failed 140 | return Bukkit.getOnlinePlayers().size(); 141 | } 142 | } 143 | 144 | public static class MetricsBase { 145 | 146 | /** The version of the Metrics class. */ 147 | public static final String METRICS_VERSION = "3.0.2"; 148 | 149 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 150 | 151 | private final ScheduledExecutorService scheduler; 152 | 153 | private final String platform; 154 | 155 | private final String serverUuid; 156 | 157 | private final int serviceId; 158 | 159 | private final Consumer appendPlatformDataConsumer; 160 | 161 | private final Consumer appendServiceDataConsumer; 162 | 163 | private final Consumer submitTaskConsumer; 164 | 165 | private final Supplier checkServiceEnabledSupplier; 166 | 167 | private final BiConsumer errorLogger; 168 | 169 | private final Consumer infoLogger; 170 | 171 | private final boolean logErrors; 172 | 173 | private final boolean logSentData; 174 | 175 | private final boolean logResponseStatusText; 176 | 177 | private final Set customCharts = new HashSet<>(); 178 | 179 | private final boolean enabled; 180 | 181 | /** 182 | * Creates a new MetricsBase class instance. 183 | * 184 | * @param platform The platform of the service. 185 | * @param serviceId The id of the service. 186 | * @param serverUuid The server uuid. 187 | * @param enabled Whether or not data sending is enabled. 188 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 189 | * appends all platform-specific data. 190 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 191 | * appends all service-specific data. 192 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 193 | * used to delegate the data collection to a another thread to prevent errors caused by 194 | * concurrency. Can be {@code null}. 195 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 196 | * @param errorLogger A consumer that accepts log message and an error. 197 | * @param infoLogger A consumer that accepts info log messages. 198 | * @param logErrors Whether or not errors should be logged. 199 | * @param logSentData Whether or not the sent data should be logged. 200 | * @param logResponseStatusText Whether or not the response status text should be logged. 201 | */ 202 | public MetricsBase( 203 | String platform, 204 | String serverUuid, 205 | int serviceId, 206 | boolean enabled, 207 | Consumer appendPlatformDataConsumer, 208 | Consumer appendServiceDataConsumer, 209 | Consumer submitTaskConsumer, 210 | Supplier checkServiceEnabledSupplier, 211 | BiConsumer errorLogger, 212 | Consumer infoLogger, 213 | boolean logErrors, 214 | boolean logSentData, 215 | boolean logResponseStatusText) { 216 | ScheduledThreadPoolExecutor scheduler = 217 | new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics")); 218 | // We want delayed tasks (non-periodic) that will execute in the future to be 219 | // cancelled when the scheduler is shutdown. 220 | // Otherwise, we risk preventing the server from shutting down even when 221 | // MetricsBase#shutdown() is called 222 | scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 223 | this.scheduler = scheduler; 224 | this.platform = platform; 225 | this.serverUuid = serverUuid; 226 | this.serviceId = serviceId; 227 | this.enabled = enabled; 228 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 229 | this.appendServiceDataConsumer = appendServiceDataConsumer; 230 | this.submitTaskConsumer = submitTaskConsumer; 231 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 232 | this.errorLogger = errorLogger; 233 | this.infoLogger = infoLogger; 234 | this.logErrors = logErrors; 235 | this.logSentData = logSentData; 236 | this.logResponseStatusText = logResponseStatusText; 237 | checkRelocation(); 238 | if (enabled) { 239 | // WARNING: Removing the option to opt-out will get your plugin banned from 240 | // bStats 241 | startSubmitting(); 242 | } 243 | } 244 | 245 | public void addCustomChart(CustomChart chart) { 246 | this.customCharts.add(chart); 247 | } 248 | 249 | public void shutdown() { 250 | scheduler.shutdown(); 251 | } 252 | 253 | private void startSubmitting() { 254 | final Runnable submitTask = 255 | () -> { 256 | if (!enabled || !checkServiceEnabledSupplier.get()) { 257 | // Submitting data or service is disabled 258 | scheduler.shutdown(); 259 | return; 260 | } 261 | if (submitTaskConsumer != null) { 262 | submitTaskConsumer.accept(this::submitData); 263 | } else { 264 | this.submitData(); 265 | } 266 | }; 267 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven 268 | // distribution of requests on the 269 | // bStats backend. To circumvent this problem, we introduce some randomness into 270 | // the initial and second delay. 271 | // WARNING: You must not modify and part of this Metrics class, including the 272 | // submit delay or frequency! 273 | // WARNING: Modifying this code will get your plugin banned on bStats. Just 274 | // don't do it! 275 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 276 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 277 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 278 | scheduler.scheduleAtFixedRate( 279 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 280 | } 281 | 282 | private void submitData() { 283 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 284 | appendPlatformDataConsumer.accept(baseJsonBuilder); 285 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 286 | appendServiceDataConsumer.accept(serviceJsonBuilder); 287 | JsonObjectBuilder.JsonObject[] chartData = 288 | customCharts.stream() 289 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 290 | .filter(Objects::nonNull) 291 | .toArray(JsonObjectBuilder.JsonObject[]::new); 292 | serviceJsonBuilder.appendField("id", serviceId); 293 | serviceJsonBuilder.appendField("customCharts", chartData); 294 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 295 | baseJsonBuilder.appendField("serverUUID", serverUuid); 296 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 297 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 298 | scheduler.execute( 299 | () -> { 300 | try { 301 | // Send the data 302 | sendData(data); 303 | } catch (Exception e) { 304 | // Something went wrong! :( 305 | if (logErrors) { 306 | errorLogger.accept("Could not submit bStats metrics data", e); 307 | } 308 | } 309 | }); 310 | } 311 | 312 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 313 | if (logSentData) { 314 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 315 | } 316 | String url = String.format(REPORT_URL, platform); 317 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 318 | // Compress the data to save bandwidth 319 | byte[] compressedData = compress(data.toString()); 320 | connection.setRequestMethod("POST"); 321 | connection.addRequestProperty("Accept", "application/json"); 322 | connection.addRequestProperty("Connection", "close"); 323 | connection.addRequestProperty("Content-Encoding", "gzip"); 324 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 325 | connection.setRequestProperty("Content-Type", "application/json"); 326 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 327 | connection.setDoOutput(true); 328 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 329 | outputStream.write(compressedData); 330 | } 331 | StringBuilder builder = new StringBuilder(); 332 | try (BufferedReader bufferedReader = 333 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 334 | String line; 335 | while ((line = bufferedReader.readLine()) != null) { 336 | builder.append(line); 337 | } 338 | } 339 | if (logResponseStatusText) { 340 | infoLogger.accept("Sent data to bStats and received response: " + builder); 341 | } 342 | } 343 | 344 | /** Checks that the class was properly relocated. */ 345 | private void checkRelocation() { 346 | // You can use the property to disable the check in your test environment 347 | if (System.getProperty("bstats.relocatecheck") == null 348 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 349 | // Maven's Relocate is clever and changes strings, too. So we have to use this 350 | // little "trick" ... :D 351 | final String defaultPackage = 352 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 353 | final String examplePackage = 354 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 355 | // We want to make sure no one just copy & pastes the example and uses the wrong 356 | // package names 357 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 358 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 359 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 360 | } 361 | } 362 | } 363 | 364 | /** 365 | * Gzips the given string. 366 | * 367 | * @param str The string to gzip. 368 | * @return The gzipped string. 369 | */ 370 | private static byte[] compress(final String str) throws IOException { 371 | if (str == null) { 372 | return null; 373 | } 374 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 375 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 376 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 377 | } 378 | return outputStream.toByteArray(); 379 | } 380 | } 381 | 382 | public static class SimplePie extends CustomChart { 383 | 384 | private final Callable callable; 385 | 386 | /** 387 | * Class constructor. 388 | * 389 | * @param chartId The id of the chart. 390 | * @param callable The callable which is used to request the chart data. 391 | */ 392 | public SimplePie(String chartId, Callable callable) { 393 | super(chartId); 394 | this.callable = callable; 395 | } 396 | 397 | @Override 398 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 399 | String value = callable.call(); 400 | if (value == null || value.isEmpty()) { 401 | // Null = skip the chart 402 | return null; 403 | } 404 | return new JsonObjectBuilder().appendField("value", value).build(); 405 | } 406 | } 407 | 408 | public static class MultiLineChart extends CustomChart { 409 | 410 | private final Callable> callable; 411 | 412 | /** 413 | * Class constructor. 414 | * 415 | * @param chartId The id of the chart. 416 | * @param callable The callable which is used to request the chart data. 417 | */ 418 | public MultiLineChart(String chartId, Callable> callable) { 419 | super(chartId); 420 | this.callable = callable; 421 | } 422 | 423 | @Override 424 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 425 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 426 | Map map = callable.call(); 427 | if (map == null || map.isEmpty()) { 428 | // Null = skip the chart 429 | return null; 430 | } 431 | boolean allSkipped = true; 432 | for (Map.Entry entry : map.entrySet()) { 433 | if (entry.getValue() == 0) { 434 | // Skip this invalid 435 | continue; 436 | } 437 | allSkipped = false; 438 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 439 | } 440 | if (allSkipped) { 441 | // Null = skip the chart 442 | return null; 443 | } 444 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 445 | } 446 | } 447 | 448 | public static class AdvancedPie extends CustomChart { 449 | 450 | private final Callable> callable; 451 | 452 | /** 453 | * Class constructor. 454 | * 455 | * @param chartId The id of the chart. 456 | * @param callable The callable which is used to request the chart data. 457 | */ 458 | public AdvancedPie(String chartId, Callable> callable) { 459 | super(chartId); 460 | this.callable = callable; 461 | } 462 | 463 | @Override 464 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 465 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 466 | Map map = callable.call(); 467 | if (map == null || map.isEmpty()) { 468 | // Null = skip the chart 469 | return null; 470 | } 471 | boolean allSkipped = true; 472 | for (Map.Entry entry : map.entrySet()) { 473 | if (entry.getValue() == 0) { 474 | // Skip this invalid 475 | continue; 476 | } 477 | allSkipped = false; 478 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 479 | } 480 | if (allSkipped) { 481 | // Null = skip the chart 482 | return null; 483 | } 484 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 485 | } 486 | } 487 | 488 | public static class SimpleBarChart extends CustomChart { 489 | 490 | private final Callable> callable; 491 | 492 | /** 493 | * Class constructor. 494 | * 495 | * @param chartId The id of the chart. 496 | * @param callable The callable which is used to request the chart data. 497 | */ 498 | public SimpleBarChart(String chartId, Callable> callable) { 499 | super(chartId); 500 | this.callable = callable; 501 | } 502 | 503 | @Override 504 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 505 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 506 | Map map = callable.call(); 507 | if (map == null || map.isEmpty()) { 508 | // Null = skip the chart 509 | return null; 510 | } 511 | for (Map.Entry entry : map.entrySet()) { 512 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 513 | } 514 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 515 | } 516 | } 517 | 518 | public static class AdvancedBarChart extends CustomChart { 519 | 520 | private final Callable> callable; 521 | 522 | /** 523 | * Class constructor. 524 | * 525 | * @param chartId The id of the chart. 526 | * @param callable The callable which is used to request the chart data. 527 | */ 528 | public AdvancedBarChart(String chartId, Callable> callable) { 529 | super(chartId); 530 | this.callable = callable; 531 | } 532 | 533 | @Override 534 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 535 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 536 | Map map = callable.call(); 537 | if (map == null || map.isEmpty()) { 538 | // Null = skip the chart 539 | return null; 540 | } 541 | boolean allSkipped = true; 542 | for (Map.Entry entry : map.entrySet()) { 543 | if (entry.getValue().length == 0) { 544 | // Skip this invalid 545 | continue; 546 | } 547 | allSkipped = false; 548 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 549 | } 550 | if (allSkipped) { 551 | // Null = skip the chart 552 | return null; 553 | } 554 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 555 | } 556 | } 557 | 558 | public static class DrilldownPie extends CustomChart { 559 | 560 | private final Callable>> callable; 561 | 562 | /** 563 | * Class constructor. 564 | * 565 | * @param chartId The id of the chart. 566 | * @param callable The callable which is used to request the chart data. 567 | */ 568 | public DrilldownPie(String chartId, Callable>> callable) { 569 | super(chartId); 570 | this.callable = callable; 571 | } 572 | 573 | @Override 574 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 575 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 576 | Map> map = callable.call(); 577 | if (map == null || map.isEmpty()) { 578 | // Null = skip the chart 579 | return null; 580 | } 581 | boolean reallyAllSkipped = true; 582 | for (Map.Entry> entryValues : map.entrySet()) { 583 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 584 | boolean allSkipped = true; 585 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 586 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 587 | allSkipped = false; 588 | } 589 | if (!allSkipped) { 590 | reallyAllSkipped = false; 591 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 592 | } 593 | } 594 | if (reallyAllSkipped) { 595 | // Null = skip the chart 596 | return null; 597 | } 598 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 599 | } 600 | } 601 | 602 | public abstract static class CustomChart { 603 | 604 | private final String chartId; 605 | 606 | protected CustomChart(String chartId) { 607 | if (chartId == null) { 608 | throw new IllegalArgumentException("chartId must not be null"); 609 | } 610 | this.chartId = chartId; 611 | } 612 | 613 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 614 | BiConsumer errorLogger, boolean logErrors) { 615 | JsonObjectBuilder builder = new JsonObjectBuilder(); 616 | builder.appendField("chartId", chartId); 617 | try { 618 | JsonObjectBuilder.JsonObject data = getChartData(); 619 | if (data == null) { 620 | // If the data is null we don't send the chart. 621 | return null; 622 | } 623 | builder.appendField("data", data); 624 | } catch (Throwable t) { 625 | if (logErrors) { 626 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 627 | } 628 | return null; 629 | } 630 | return builder.build(); 631 | } 632 | 633 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 634 | } 635 | 636 | public static class SingleLineChart extends CustomChart { 637 | 638 | private final Callable callable; 639 | 640 | /** 641 | * Class constructor. 642 | * 643 | * @param chartId The id of the chart. 644 | * @param callable The callable which is used to request the chart data. 645 | */ 646 | public SingleLineChart(String chartId, Callable callable) { 647 | super(chartId); 648 | this.callable = callable; 649 | } 650 | 651 | @Override 652 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 653 | int value = callable.call(); 654 | if (value == 0) { 655 | // Null = skip the chart 656 | return null; 657 | } 658 | return new JsonObjectBuilder().appendField("value", value).build(); 659 | } 660 | } 661 | 662 | /** 663 | * An extremely simple JSON builder. 664 | * 665 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 666 | * for its use-case. 667 | */ 668 | public static class JsonObjectBuilder { 669 | 670 | private StringBuilder builder = new StringBuilder(); 671 | 672 | private boolean hasAtLeastOneField = false; 673 | 674 | public JsonObjectBuilder() { 675 | builder.append("{"); 676 | } 677 | 678 | /** 679 | * Appends a null field to the JSON. 680 | * 681 | * @param key The key of the field. 682 | * @return A reference to this object. 683 | */ 684 | public JsonObjectBuilder appendNull(String key) { 685 | appendFieldUnescaped(key, "null"); 686 | return this; 687 | } 688 | 689 | /** 690 | * Appends a string field to the JSON. 691 | * 692 | * @param key The key of the field. 693 | * @param value The value of the field. 694 | * @return A reference to this object. 695 | */ 696 | public JsonObjectBuilder appendField(String key, String value) { 697 | if (value == null) { 698 | throw new IllegalArgumentException("JSON value must not be null"); 699 | } 700 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 701 | return this; 702 | } 703 | 704 | /** 705 | * Appends an integer field to the JSON. 706 | * 707 | * @param key The key of the field. 708 | * @param value The value of the field. 709 | * @return A reference to this object. 710 | */ 711 | public JsonObjectBuilder appendField(String key, int value) { 712 | appendFieldUnescaped(key, String.valueOf(value)); 713 | return this; 714 | } 715 | 716 | /** 717 | * Appends an object to the JSON. 718 | * 719 | * @param key The key of the field. 720 | * @param object The object. 721 | * @return A reference to this object. 722 | */ 723 | public JsonObjectBuilder appendField(String key, JsonObject object) { 724 | if (object == null) { 725 | throw new IllegalArgumentException("JSON object must not be null"); 726 | } 727 | appendFieldUnescaped(key, object.toString()); 728 | return this; 729 | } 730 | 731 | /** 732 | * Appends a string array to the JSON. 733 | * 734 | * @param key The key of the field. 735 | * @param values The string array. 736 | * @return A reference to this object. 737 | */ 738 | public JsonObjectBuilder appendField(String key, String[] values) { 739 | if (values == null) { 740 | throw new IllegalArgumentException("JSON values must not be null"); 741 | } 742 | String escapedValues = 743 | Arrays.stream(values) 744 | .map(value -> "\"" + escape(value) + "\"") 745 | .collect(Collectors.joining(",")); 746 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 747 | return this; 748 | } 749 | 750 | /** 751 | * Appends an integer array to the JSON. 752 | * 753 | * @param key The key of the field. 754 | * @param values The integer array. 755 | * @return A reference to this object. 756 | */ 757 | public JsonObjectBuilder appendField(String key, int[] values) { 758 | if (values == null) { 759 | throw new IllegalArgumentException("JSON values must not be null"); 760 | } 761 | String escapedValues = 762 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 763 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 764 | return this; 765 | } 766 | 767 | /** 768 | * Appends an object array to the JSON. 769 | * 770 | * @param key The key of the field. 771 | * @param values The integer array. 772 | * @return A reference to this object. 773 | */ 774 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 775 | if (values == null) { 776 | throw new IllegalArgumentException("JSON values must not be null"); 777 | } 778 | String escapedValues = 779 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 780 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 781 | return this; 782 | } 783 | 784 | /** 785 | * Appends a field to the object. 786 | * 787 | * @param key The key of the field. 788 | * @param escapedValue The escaped value of the field. 789 | */ 790 | private void appendFieldUnescaped(String key, String escapedValue) { 791 | if (builder == null) { 792 | throw new IllegalStateException("JSON has already been built"); 793 | } 794 | if (key == null) { 795 | throw new IllegalArgumentException("JSON key must not be null"); 796 | } 797 | if (hasAtLeastOneField) { 798 | builder.append(","); 799 | } 800 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 801 | hasAtLeastOneField = true; 802 | } 803 | 804 | /** 805 | * Builds the JSON string and invalidates this builder. 806 | * 807 | * @return The built JSON string. 808 | */ 809 | public JsonObject build() { 810 | if (builder == null) { 811 | throw new IllegalStateException("JSON has already been built"); 812 | } 813 | JsonObject object = new JsonObject(builder.append("}").toString()); 814 | builder = null; 815 | return object; 816 | } 817 | 818 | /** 819 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 820 | * 821 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 822 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 823 | * 824 | * @param value The value to escape. 825 | * @return The escaped value. 826 | */ 827 | private static String escape(String value) { 828 | final StringBuilder builder = new StringBuilder(); 829 | for (int i = 0; i < value.length(); i++) { 830 | char c = value.charAt(i); 831 | if (c == '"') { 832 | builder.append("\\\""); 833 | } else if (c == '\\') { 834 | builder.append("\\\\"); 835 | } else if (c <= '\u000F') { 836 | builder.append("\\u000").append(Integer.toHexString(c)); 837 | } else if (c <= '\u001F') { 838 | builder.append("\\u00").append(Integer.toHexString(c)); 839 | } else { 840 | builder.append(c); 841 | } 842 | } 843 | return builder.toString(); 844 | } 845 | 846 | /** 847 | * A super simple representation of a JSON object. 848 | * 849 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 850 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 851 | * JsonObject)}. 852 | */ 853 | public static class JsonObject { 854 | 855 | private final String value; 856 | 857 | private JsonObject(String value) { 858 | this.value = value; 859 | } 860 | 861 | @Override 862 | public String toString() { 863 | return value; 864 | } 865 | } 866 | } 867 | } -------------------------------------------------------------------------------- /src/main/java/dev/unnm3d/loadingscreenremover/PlayerListener.java: -------------------------------------------------------------------------------- 1 | package dev.unnm3d.loadingscreenremover; 2 | 3 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 4 | import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent; 5 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 6 | import lombok.AllArgsConstructor; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.event.EventHandler; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.player.PlayerTeleportEvent; 11 | 12 | @AllArgsConstructor 13 | public class PlayerListener extends SimplePacketListenerAbstract implements Listener { 14 | private final LoadingScreenRemover plugin; 15 | 16 | @EventHandler 17 | public void onPlayerTeleportWorld(PlayerTeleportEvent event) { 18 | if (event.getFrom().getWorld() == null || event.getTo().getWorld() == null) return; 19 | 20 | // If the player is changing worlds, and the environments are the same 21 | if (event.getFrom().getWorld() != event.getTo().getWorld() && 22 | event.getFrom().getWorld().getEnvironment() == event.getTo().getWorld().getEnvironment()) { 23 | plugin.getPlayerManager().addChangingWorldPlayer(event.getPlayer()); 24 | }else return; 25 | plugin.getTaskScheduler().runTaskLater(() -> plugin.getPlayerManager().removeChangingWorldPlayer(event.getPlayer()), 2); 26 | } 27 | 28 | @Override 29 | public void onPacketPlaySend(PacketPlaySendEvent event) { 30 | if (event.getPacketType() != PacketType.Play.Server.RESPAWN) { 31 | return; 32 | } 33 | 34 | if (plugin.getPlayerManager().isPlayerChangingWorlds(event.getPlayer())) { 35 | event.setCancelled(true); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/dev/unnm3d/loadingscreenremover/PlayerManager.java: -------------------------------------------------------------------------------- 1 | package dev.unnm3d.loadingscreenremover; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.Set; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | public class PlayerManager { 10 | private final Set changingWorlds = ConcurrentHashMap.newKeySet(); 11 | 12 | public boolean isPlayerChangingWorlds(@NotNull Player player) { 13 | return changingWorlds.contains(player); 14 | } 15 | 16 | public void addChangingWorldPlayer(@NotNull Player player) { 17 | changingWorlds.add(player); 18 | } 19 | 20 | public void removeChangingWorldPlayer(@NotNull Player player) { 21 | changingWorlds.remove(player); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: LoadingScreenRemover 2 | version: '${version}' 3 | author: Unnm3d 4 | main: dev.unnm3d.loadingscreenremover.LoadingScreenRemover 5 | api-version: '1.18' 6 | folia-supported: true 7 | --------------------------------------------------------------------------------