├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── de │ └── rexlnico │ └── realtimeplugin │ ├── commands │ └── Commands.java │ ├── main │ └── Main.java │ ├── methodes │ ├── Messages.java │ ├── WorldContainer.java │ └── WorldManager.java │ └── util │ ├── Metrics.java │ ├── NMSUtil.java │ └── Utils.java └── resources ├── plugin.yml ├── template.json └── world.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,maven 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,maven 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij+all Patch ### 79 | # Ignores the whole .idea folder and all .iml files 80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 81 | 82 | .idea/ 83 | 84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 85 | 86 | *.iml 87 | modules.xml 88 | .idea/misc.xml 89 | *.ipr 90 | 91 | # Sonarlint plugin 92 | .idea/sonarlint 93 | 94 | ### Maven ### 95 | target/ 96 | pom.xml.tag 97 | pom.xml.releaseBackup 98 | pom.xml.versionsBackup 99 | pom.xml.next 100 | release.properties 101 | dependency-reduced-pom.xml 102 | buildNumber.properties 103 | .mvn/timing.properties 104 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 105 | .mvn/wrapper/maven-wrapper.jar 106 | 107 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all,maven 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RealTimePlugin 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7bb284eb1f4b4f2f956ae596b135d2cd)](https://app.codacy.com/gh/rexlNico/RealTimePlugin?utm_source=github.com&utm_medium=referral&utm_content=rexlNico/RealTimePlugin&utm_campaign=Badge_Grade_Settings) 3 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | RealTimePlugin 8 | RealTimePlugin 9 | 3.8 10 | 11 | 12 | 13 | spigot-repo 14 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-shade-plugin 24 | 3.1.0 25 | 26 | 27 | 28 | org.bstats 29 | me.name.util 30 | 31 | 32 | 33 | 34 | 35 | package 36 | 37 | shade 38 | 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-compiler-plugin 45 | 46 | 8 47 | 8 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | com.googlecode.json-simple 56 | json-simple 57 | 1.1.1 58 | 59 | 60 | 61 | org.spigotmc 62 | spigot-api 63 | 1.8.8-R0.1-SNAPSHOT 64 | provided 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/commands/Commands.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.commands; 2 | 3 | import de.rexlnico.realtimeplugin.main.Main; 4 | import de.rexlnico.realtimeplugin.methodes.Messages; 5 | import de.rexlnico.realtimeplugin.methodes.WorldContainer; 6 | import org.bukkit.command.Command; 7 | import org.bukkit.command.CommandExecutor; 8 | import org.bukkit.command.CommandSender; 9 | import org.bukkit.command.TabCompleter; 10 | 11 | import java.time.ZonedDateTime; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | public class Commands implements CommandExecutor, TabCompleter { 18 | @Override 19 | public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { 20 | if (!sender.hasPermission("realtime.reload")) { 21 | sender.sendMessage(Messages.prefix + Messages.noPermissions); 22 | return true; 23 | } 24 | if (args.length > 0) { 25 | switch (args[0].toLowerCase()) { 26 | case "reload": 27 | if (args.length > 1) { 28 | String file = args[1]; 29 | WorldContainer world = Main.getWorldManager().getWeatherWorld(file); 30 | if (world == null) { 31 | sender.sendMessage(Messages.prefix + Messages.noFile.replace("%file%", file)); 32 | return false; 33 | } 34 | world.update(); 35 | sender.sendMessage(Messages.prefix + Messages.reloadFileMSG.replace("%file%", file)); 36 | } else { 37 | Main.getWorldManager().loadNew(); 38 | Main.getWorldManager().updateAll(); 39 | sender.sendMessage(Messages.prefix + Messages.reloadAllMSG); 40 | } 41 | return true; 42 | 43 | case "time": 44 | if (args.length > 1) { 45 | String file = args[1]; 46 | WorldContainer world = Main.getWorldManager().getWeatherWorld(file); 47 | if (world == null) { 48 | sender.sendMessage(Messages.prefix + Messages.noFile.replace("%file%", file)); 49 | return false; 50 | } 51 | ZonedDateTime worldDateTime = world.getDateTime(); 52 | String time = worldDateTime.getHour() + ":" + worldDateTime.getMinute(); 53 | sender.sendMessage(Messages.prefix + Messages.timeMSG.replace("%file%", file).replace("%time%", time)); 54 | } else { 55 | sender.sendMessage(Messages.prefix + Messages.reloadHelp); 56 | } 57 | return true; 58 | case "version" : 59 | sender.sendMessage(Messages.prefix + Messages.currentVersion.replace("%version%", Main.getPlugin().getCurrentVersion())); 60 | sender.sendMessage(Messages.prefix + Messages.newestVersion.replace("%version%", Main.getPlugin().getNewestVersion())); 61 | break; 62 | } 63 | } 64 | sender.sendMessage(Messages.prefix + Messages.reloadHelp); 65 | return false; 66 | } 67 | 68 | @Override 69 | public List onTabComplete(CommandSender commandSender, Command command, String s, String[] args) { 70 | if (args.length == 2) { 71 | if (args[0].equalsIgnoreCase("reload")) { 72 | return Main.getWorldManager().getTabComplete(); 73 | } else if (args[0].equalsIgnoreCase("time")) { 74 | return Main.getWorldManager().getTabComplete(); 75 | } 76 | } else if (args.length == 1) { 77 | return Arrays.asList("reload", "time", "version"); 78 | } 79 | 80 | return new ArrayList<>(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/main/Main.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.main; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | import de.rexlnico.realtimeplugin.commands.Commands; 6 | import de.rexlnico.realtimeplugin.methodes.Messages; 7 | import de.rexlnico.realtimeplugin.methodes.WorldManager; 8 | import de.rexlnico.realtimeplugin.util.Metrics; 9 | import org.bukkit.plugin.java.JavaPlugin; 10 | import sun.security.util.IOUtils; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.net.MalformedURLException; 17 | import java.net.URL; 18 | import java.net.http.HttpRequest; 19 | import java.nio.charset.Charset; 20 | 21 | public class Main extends JavaPlugin { 22 | 23 | private static Main plugin; 24 | private static Metrics metrics; 25 | private static WorldManager worldManager; 26 | 27 | public static WorldManager getWorldManager() { 28 | return worldManager; 29 | } 30 | 31 | public static Main getPlugin() { 32 | return plugin; 33 | } 34 | 35 | @Override 36 | public void onEnable() { 37 | plugin = this; 38 | Messages.load(); 39 | getCommand("realtime").setTabCompleter(new Commands()); 40 | getCommand("realtime").setExecutor(new Commands()); 41 | metrics = new Metrics(this, 11510); 42 | try { 43 | worldManager = new WorldManager(); 44 | } catch (IOException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | 49 | public String getCurrentVersion() { 50 | return getDescription().getVersion(); 51 | } 52 | 53 | public String getNewestVersion() { 54 | try { 55 | String body = new java.util.Scanner(new java.net.URL("https://api.spiget.org/v2/resources/69545/versions/latest") 56 | .openStream(), "UTF-8").useDelimiter("\\A").next(); 57 | JsonObject asJsonObject = new JsonParser().parse(body).getAsJsonObject(); 58 | return asJsonObject.get("name").toString(); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } 62 | return getCurrentVersion(); 63 | } 64 | 65 | public static Metrics getMetrics() { 66 | return metrics; 67 | } 68 | 69 | @Override 70 | public void onDisable() { 71 | worldManager.disable(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/methodes/Messages.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.methodes; 2 | 3 | import de.rexlnico.realtimeplugin.main.Main; 4 | import org.bukkit.configuration.InvalidConfigurationException; 5 | import org.bukkit.configuration.file.YamlConfiguration; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.logging.Level; 10 | 11 | public class Messages { 12 | 13 | public static final YamlConfiguration cfg = new YamlConfiguration(); 14 | 15 | public static String prefix = ""; 16 | public static String reloadFileMSG = ""; 17 | public static String reloadAllMSG = ""; 18 | public static String timeMSG = ""; 19 | public static String reloadHelp = ""; 20 | public static String timeHelp = ""; 21 | public static String noFile = ""; 22 | public static String noPermissions = ""; 23 | 24 | public static String currentVersion = ""; 25 | public static String newestVersion = ""; 26 | 27 | public static void load() { 28 | File file = new File(Main.getPlugin().getDataFolder(), "messages.cfg"); 29 | if (file.exists()) { 30 | try { 31 | cfg.load(file); 32 | } catch (IOException | InvalidConfigurationException e) { 33 | Main.getPlugin().getLogger().log(Level.SEVERE, "Could not load messages.cfg!", e); 34 | return; 35 | } 36 | } 37 | 38 | cfg.addDefault("Prefix", "&8[&eRealtime&8] "); 39 | cfg.addDefault("reloadFileMSG", "successfully reloaded %file%"); 40 | cfg.addDefault("reloadAllMSG", "successfully reloaded!"); 41 | cfg.addDefault("timeMSG", "current time for %file% is %time%"); 42 | cfg.addDefault("reloadHelp", "&cPlease use /realtime reload (file)"); 43 | cfg.addDefault("timeHelp", "&cPlease use /realtime time (file)"); 44 | cfg.addDefault("noFile", "&cThe file %file% does not exist!"); 45 | cfg.addDefault("noPermsMSG", "You have insufficient permissions to perform this action!"); 46 | cfg.addDefault("currentVersion", "Current version: %version%"); 47 | cfg.addDefault("newestVersion", "Newest version: %version%"); 48 | cfg.options().copyDefaults(true); 49 | try { 50 | cfg.save(file); 51 | } catch (Exception e) { 52 | Main.getPlugin().getLogger().log(Level.WARNING, "Could not save messages.cfg!", e); 53 | } 54 | prefix = cfg.getString("Prefix").replace("&", "§"); 55 | reloadFileMSG = cfg.getString("reloadFileMSG").replace("&", "§"); 56 | reloadAllMSG = cfg.getString("reloadAllMSG").replace("&", "§"); 57 | timeMSG = cfg.getString("timeMSG").replace("&", "§"); 58 | reloadHelp = cfg.getString("reloadHelp").replace("&", "§"); 59 | timeHelp = cfg.getString("timeHelp").replace("&", "§"); 60 | noFile = cfg.getString("noFile").replace("&", "§"); 61 | noPermissions = cfg.getString("noPermsMSG").replace("&", "§"); 62 | currentVersion = cfg.getString("currentVersion").replace("&", "§"); 63 | newestVersion = cfg.getString("newestVersion").replace("&", "§"); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/methodes/WorldContainer.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.methodes; 2 | 3 | import de.rexlnico.realtimeplugin.main.Main; 4 | import de.rexlnico.realtimeplugin.util.Utils; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.World; 7 | import org.bukkit.scheduler.BukkitScheduler; 8 | import org.bukkit.scheduler.BukkitTask; 9 | import org.json.simple.JSONArray; 10 | import org.json.simple.JSONObject; 11 | import org.json.simple.parser.JSONParser; 12 | 13 | import java.io.File; 14 | import java.io.FileReader; 15 | import java.io.Reader; 16 | import java.nio.charset.StandardCharsets; 17 | import java.time.Instant; 18 | import java.time.ZoneId; 19 | import java.time.ZonedDateTime; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | public class WorldContainer { 24 | 25 | private static final double SECONDS_TO_TICKS_FACTOR = 1_000d / Math.pow(60d, 2d); 26 | private static final long FIRST_FULL_MOON_SINCE_EPOCH = 1814400; //22 Jan 1970 27 | private static final double SECONDS_IN_MOON_CYCLE = 2551392; //29.53 days 28 | private static final int MOON_PHASE_GAME_COUNT = 8; 29 | private static final int MOON_PHASE_GAME_ADVANCE = 24000; 30 | private static final int MOON_PHASE_GAME_CYCLE = MOON_PHASE_GAME_ADVANCE * MOON_PHASE_GAME_COUNT; 31 | 32 | private final File file; 33 | 34 | private BukkitTask task; 35 | private String doDaylightCycle; // save original value of doDaylightCycle 36 | 37 | private boolean active; 38 | private World world; 39 | private long updateInterval; 40 | public boolean time; 41 | public boolean weather; 42 | private String weatherKey; 43 | private String timezone; 44 | private ZoneId zone; 45 | private String[] weatherLocation; 46 | 47 | public WorldContainer(File file) { 48 | this.file = file; 49 | if (!this.file.exists()) { 50 | throw new IllegalArgumentException(String.format("World file %s does not exist.", this.file)); 51 | } 52 | 53 | update(); 54 | } 55 | 56 | public void runUpdate() { 57 | BukkitScheduler scheduler = Bukkit.getScheduler(); 58 | Main plugin = Main.getPlugin(); 59 | 60 | cancelTask(); 61 | 62 | if (active && world != null && (weather || time)) { 63 | List stack = new ArrayList<>(); 64 | 65 | if (time) { 66 | this.doDaylightCycle = world.getGameRuleValue("doDaylightCycle"); 67 | world.setGameRuleValue("doDaylightCycle", "false"); 68 | 69 | stack.add(() -> { 70 | // get time asynchronously 71 | long time = getTime(); 72 | // set time synchronously 73 | scheduler.runTask(plugin, () -> world.setFullTime(time)); 74 | }); 75 | } 76 | if (weather) { 77 | stack.add(() -> { 78 | // get weather asynchronously 79 | String weather = fetchWeather(); 80 | // set weather synchronously 81 | scheduler.runTask(plugin, () -> setWeather(weather)); 82 | }); 83 | } 84 | 85 | if (!stack.isEmpty()) { 86 | task = Bukkit.getScheduler().runTaskTimerAsynchronously(Main.getPlugin(), 87 | () -> stack.forEach(Runnable::run), 0, 20 * updateInterval); 88 | } 89 | } 90 | } 91 | 92 | public ZonedDateTime getDateTime(){ 93 | return ZonedDateTime.ofInstant(Instant.now() /* in UTC */, zone); 94 | } 95 | 96 | public long getTime() { 97 | ZonedDateTime dateTime = getDateTime(); 98 | double secondsSinceFullMoon = (dateTime.toEpochSecond() - FIRST_FULL_MOON_SINCE_EPOCH) % SECONDS_IN_MOON_CYCLE; 99 | double moonPhase = secondsSinceFullMoon / SECONDS_IN_MOON_CYCLE; 100 | long epochOffsetAdjustedSeconds = dateTime.toEpochSecond() + dateTime.getOffset().getTotalSeconds(); 101 | long secondsInDay = epochOffsetAdjustedSeconds % 86400; 102 | int secondsInDayOverflowAdjusted = Utils.overflow(18_000 + (int) (secondsInDay * SECONDS_TO_TICKS_FACTOR), 24_000); 103 | long secondsInYear = epochOffsetAdjustedSeconds % 31536000; 104 | long baseFullTime = (long) Math.floor((secondsInYear * SECONDS_TO_TICKS_FACTOR) / (double) MOON_PHASE_GAME_CYCLE) * MOON_PHASE_GAME_CYCLE; 105 | return baseFullTime + secondsInDayOverflowAdjusted + Math.round(moonPhase * MOON_PHASE_GAME_COUNT) * MOON_PHASE_GAME_ADVANCE; 106 | } 107 | 108 | public void disable() { 109 | if (time && doDaylightCycle != null) { 110 | world.setGameRuleValue("doDaylightCycle", doDaylightCycle); 111 | } 112 | cancelTask(); 113 | } 114 | 115 | public void update() { 116 | 117 | // load JSON data 118 | JSONParser parser = new JSONParser(); 119 | if(!file.exists()){ 120 | cancelTask(); 121 | Main.getWorldManager().unloadWorldContainer(this); 122 | return; 123 | } 124 | try (Reader reader = new FileReader(file.getPath())) { 125 | JSONObject object = (JSONObject) parser.parse(reader); 126 | 127 | String worldS = (String) object.get("world"); 128 | World world = Bukkit.getWorld(worldS); 129 | if (world != null) { 130 | this.world = world; 131 | this.active = (boolean) object.get("active"); 132 | this.updateInterval = Math.max(10, (long) object.get("updateInterval")); 133 | JSONObject timeObject = (JSONObject) object.get("time"); 134 | this.time = (boolean) timeObject.get("active"); 135 | this.timezone = (String) timeObject.get("timezone"); 136 | zone = ZoneId.of(timezone); 137 | JSONObject weatherObject = (JSONObject) object.get("weather"); 138 | this.weather = (boolean) weatherObject.get("active"); 139 | this.weatherKey = (String) weatherObject.get("weatherKey"); 140 | this.weatherLocation = new String[]{((String) weatherObject.get("City")).replace(" ", "%20"), ((String) weatherObject.get("Country")).replace(" ", "%20")}; 141 | } 142 | } catch (Exception e) { 143 | throw new RuntimeException(String.format("Count not load world configuration %s", this.file), e); 144 | } 145 | 146 | runUpdate(); 147 | } 148 | 149 | 150 | private void cancelTask() { 151 | if (task != null) { 152 | BukkitScheduler scheduler = Bukkit.getScheduler(); 153 | int tid = task.getTaskId(); 154 | if (scheduler.isCurrentlyRunning(tid) || scheduler.isQueued(tid)) { 155 | task.cancel(); 156 | task = null; 157 | } 158 | } 159 | } 160 | 161 | public String fetchWeather() { 162 | try { 163 | if (weatherKey.isEmpty()) { 164 | return "sun"; 165 | } 166 | final String search = weatherLocation[0] + "," + weatherLocation[1]; 167 | final String url = "http://api.openweathermap.org/data/2.5/weather?q=" + search + "&APPID=" + weatherKey; 168 | 169 | byte[] response = Utils.httpRequest(url); 170 | if (response == null) { 171 | return "sun"; 172 | } 173 | 174 | final JSONObject object = Utils.parseJSON(new String(response, StandardCharsets.UTF_8)); 175 | return ((String) ((JSONObject) ((JSONArray) object.get("weather")).get(0)).get("main")).toLowerCase(); 176 | } catch (Exception e) { 177 | return "sun"; 178 | } 179 | } 180 | 181 | public void setWeather(String weather) { 182 | switch (weather) { 183 | case "rain": 184 | try { 185 | world.setStorm(true); 186 | world.setWeatherDuration(Integer.MAX_VALUE); 187 | } catch (Exception e) { 188 | e.printStackTrace(); 189 | } 190 | break; 191 | case "thunderstorm": 192 | try { 193 | world.setStorm(true); 194 | world.setWeatherDuration(Integer.MAX_VALUE); 195 | world.setThundering(true); 196 | world.setThunderDuration(Integer.MAX_VALUE); 197 | } catch (Exception e) { 198 | e.printStackTrace(); 199 | } 200 | break; 201 | default: 202 | try { 203 | world.setStorm(false); 204 | world.setWeatherDuration(0); 205 | world.setThundering(false); 206 | world.setThunderDuration(0); 207 | } catch (Exception e) { 208 | e.printStackTrace(); 209 | } 210 | break; 211 | } 212 | } 213 | 214 | public World getWorld() { 215 | return world; 216 | } 217 | 218 | public String[] getWeatherLocation() { 219 | return weatherLocation; 220 | } 221 | 222 | public String getWeatherKey() { 223 | return weatherKey; 224 | } 225 | 226 | public String getTimezone() { 227 | return timezone; 228 | } 229 | 230 | public long getUpdateInterval() { 231 | return updateInterval; 232 | } 233 | 234 | public File getFile() { 235 | return file; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/methodes/WorldManager.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.methodes; 2 | 3 | import de.rexlnico.realtimeplugin.main.Main; 4 | import de.rexlnico.realtimeplugin.util.Metrics; 5 | import org.bukkit.configuration.file.YamlConfiguration; 6 | 7 | import java.io.*; 8 | import java.nio.file.Files; 9 | import java.nio.file.StandardCopyOption; 10 | import java.time.ZoneId; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | public class WorldManager { 16 | 17 | private final ArrayList tabComplete; 18 | ArrayList worlds; 19 | 20 | public WorldManager() throws IOException { 21 | worlds = new ArrayList<>(); 22 | tabComplete = new ArrayList<>(); 23 | loadAll(); 24 | setTabComplete(); 25 | createTimeTable(); 26 | setStats(); 27 | } 28 | 29 | private void loadAll() throws IOException { 30 | File worldsDir = new File(Main.getPlugin().getDataFolder(), "worlds"); 31 | File[] fileList = worldsDir.listFiles(); 32 | if (fileList == null) { 33 | createTemplate(worldsDir); 34 | createDefault(worldsDir); 35 | return; 36 | } 37 | for (File file : fileList) { 38 | if (file.getName().contains(".json")) { 39 | worlds.add(new WorldContainer(file)); 40 | } 41 | } 42 | } 43 | 44 | public void loadNew() { 45 | File directoryPath = new File(Main.getPlugin().getDataFolder(), "worlds"); 46 | File[] fileList = directoryPath.listFiles(); 47 | if (fileList == null) return; 48 | for (File file : fileList) { 49 | if (file.getName().contains(".json")) { 50 | if (!containsFile(file.getName())) { 51 | worlds.add(new WorldContainer(file)); 52 | } 53 | } 54 | } 55 | setTabComplete(); 56 | } 57 | 58 | private void createTimeTable() throws IOException { 59 | File file = new File(Main.getPlugin().getDataFolder(), "WeatherTimezones.cfg"); 60 | if (file.exists()) return; 61 | YamlConfiguration.loadConfiguration(file).save(file); 62 | BufferedWriter writer = new BufferedWriter(new FileWriter(file)); 63 | 64 | for (String s : ZoneId.getAvailableZoneIds()) { 65 | writer.write(s + "\n"); 66 | } 67 | writer.flush(); 68 | writer.close(); 69 | } 70 | 71 | public void unloadWorldContainer(WorldContainer worldContainer){ 72 | worlds.remove(worldContainer); 73 | setTabComplete(); 74 | } 75 | 76 | public ArrayList getTabComplete() { 77 | return tabComplete; 78 | } 79 | 80 | private void setTabComplete() { 81 | tabComplete.clear(); 82 | for (WorldContainer world : worlds) { 83 | tabComplete.add(world.getFile().getName()); 84 | } 85 | setStats(); 86 | } 87 | 88 | public void setStats() { 89 | Map timeZones = new HashMap<>(); 90 | Map weatherCities = new HashMap<>(); 91 | for (WorldContainer world : worlds) { 92 | if (world.time) 93 | timeZones.put(world.getTimezone(), timeZones.getOrDefault(world.getTimezone(), 0) + 1); 94 | if (world.weather) 95 | weatherCities.put(world.getWeatherLocation()[0], weatherCities.getOrDefault(world.getWeatherLocation()[0], 0) + 1); 96 | } 97 | Main.getMetrics().addCustomChart(new Metrics.AdvancedPie("time_zones", () -> timeZones)); 98 | Main.getMetrics().addCustomChart(new Metrics.AdvancedPie("weather_cities", () -> weatherCities)); 99 | Main.getMetrics().addCustomChart(new Metrics.SimplePie("diff_worlds", () -> worlds.size() + "")); 100 | } 101 | 102 | public WorldContainer getWeatherWorld(String name) { 103 | return worlds.stream() 104 | .filter(container -> container.getFile().getName().equals(name)) 105 | .findFirst().orElse(null); 106 | } 107 | 108 | private boolean containsFile(String name) { 109 | return worlds.stream() 110 | .anyMatch(container -> container.getFile().getName().equals(name)); 111 | } 112 | 113 | public void updateAll() { 114 | worlds.forEach(WorldContainer::update); 115 | setTabComplete(); 116 | } 117 | 118 | public void disable() { 119 | worlds.forEach(WorldContainer::disable); 120 | } 121 | 122 | private void createTemplate(File dir) throws IOException { 123 | InputStream root = Main.getPlugin().getResource("template.json"); 124 | File filePath = new File(dir, "template.json"); 125 | YamlConfiguration.loadConfiguration(filePath).save(filePath); 126 | Files.copy(root, filePath.toPath(), StandardCopyOption.REPLACE_EXISTING); 127 | } 128 | 129 | private void createDefault(File dir) throws IOException { 130 | InputStream root = Main.getPlugin().getResource("world.json"); 131 | File filePath = new File(dir, "world.json"); 132 | YamlConfiguration.loadConfiguration(filePath).save(filePath); 133 | Files.copy(root, filePath.toPath(), StandardCopyOption.REPLACE_EXISTING); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/util/Metrics.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.util; 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.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 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 | /** 100 | * Adds a custom chart. 101 | * 102 | * @param chart The chart to add. 103 | */ 104 | public void addCustomChart(CustomChart chart) { 105 | metricsBase.addCustomChart(chart); 106 | } 107 | 108 | private void appendPlatformData(JsonObjectBuilder builder) { 109 | builder.appendField("playerAmount", getPlayerAmount()); 110 | builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); 111 | builder.appendField("bukkitVersion", Bukkit.getVersion()); 112 | builder.appendField("bukkitName", Bukkit.getName()); 113 | builder.appendField("javaVersion", System.getProperty("java.version")); 114 | builder.appendField("osName", System.getProperty("os.name")); 115 | builder.appendField("osArch", System.getProperty("os.arch")); 116 | builder.appendField("osVersion", System.getProperty("os.version")); 117 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 118 | } 119 | 120 | private void appendServiceData(JsonObjectBuilder builder) { 121 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 122 | } 123 | 124 | private int getPlayerAmount() { 125 | try { 126 | // Around MC 1.8 the return type was changed from an array to a collection, 127 | // This fixes java.lang.NoSuchMethodError: 128 | // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; 129 | Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); 130 | return onlinePlayersMethod.getReturnType().equals(Collection.class) 131 | ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() 132 | : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; 133 | } catch (Exception e) { 134 | // Just use the new method if the reflection failed 135 | return Bukkit.getOnlinePlayers().size(); 136 | } 137 | } 138 | 139 | public static class MetricsBase { 140 | 141 | /** The version of the Metrics class. */ 142 | public static final String METRICS_VERSION = "2.2.1"; 143 | 144 | private static final ScheduledExecutorService scheduler = 145 | Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); 146 | 147 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 148 | 149 | private final String platform; 150 | 151 | private final String serverUuid; 152 | 153 | private final int serviceId; 154 | 155 | private final Consumer appendPlatformDataConsumer; 156 | 157 | private final Consumer appendServiceDataConsumer; 158 | 159 | private final Consumer submitTaskConsumer; 160 | 161 | private final Supplier checkServiceEnabledSupplier; 162 | 163 | private final BiConsumer errorLogger; 164 | 165 | private final Consumer infoLogger; 166 | 167 | private final boolean logErrors; 168 | 169 | private final boolean logSentData; 170 | 171 | private final boolean logResponseStatusText; 172 | 173 | private final Set customCharts = new HashSet<>(); 174 | 175 | private final boolean enabled; 176 | 177 | /** 178 | * Creates a new MetricsBase class instance. 179 | * 180 | * @param platform The platform of the service. 181 | * @param serviceId The id of the service. 182 | * @param serverUuid The server uuid. 183 | * @param enabled Whether or not data sending is enabled. 184 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 185 | * appends all platform-specific data. 186 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 187 | * appends all service-specific data. 188 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 189 | * used to delegate the data collection to a another thread to prevent errors caused by 190 | * concurrency. Can be {@code null}. 191 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 192 | * @param errorLogger A consumer that accepts log message and an error. 193 | * @param infoLogger A consumer that accepts info log messages. 194 | * @param logErrors Whether or not errors should be logged. 195 | * @param logSentData Whether or not the sent data should be logged. 196 | * @param logResponseStatusText Whether or not the response status text should be logged. 197 | */ 198 | public MetricsBase( 199 | String platform, 200 | String serverUuid, 201 | int serviceId, 202 | boolean enabled, 203 | Consumer appendPlatformDataConsumer, 204 | Consumer appendServiceDataConsumer, 205 | Consumer submitTaskConsumer, 206 | Supplier checkServiceEnabledSupplier, 207 | BiConsumer errorLogger, 208 | Consumer infoLogger, 209 | boolean logErrors, 210 | boolean logSentData, 211 | boolean logResponseStatusText) { 212 | this.platform = platform; 213 | this.serverUuid = serverUuid; 214 | this.serviceId = serviceId; 215 | this.enabled = enabled; 216 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 217 | this.appendServiceDataConsumer = appendServiceDataConsumer; 218 | this.submitTaskConsumer = submitTaskConsumer; 219 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 220 | this.errorLogger = errorLogger; 221 | this.infoLogger = infoLogger; 222 | this.logErrors = logErrors; 223 | this.logSentData = logSentData; 224 | this.logResponseStatusText = logResponseStatusText; 225 | checkRelocation(); 226 | if (enabled) { 227 | startSubmitting(); 228 | } 229 | } 230 | 231 | public void addCustomChart(CustomChart chart) { 232 | this.customCharts.add(chart); 233 | } 234 | 235 | private void startSubmitting() { 236 | final Runnable submitTask = 237 | () -> { 238 | if (!enabled || !checkServiceEnabledSupplier.get()) { 239 | // Submitting data or service is disabled 240 | scheduler.shutdown(); 241 | return; 242 | } 243 | if (submitTaskConsumer != null) { 244 | submitTaskConsumer.accept(this::submitData); 245 | } else { 246 | this.submitData(); 247 | } 248 | }; 249 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution 250 | // of requests on the 251 | // bStats backend. To circumvent this problem, we introduce some randomness into the initial 252 | // and second delay. 253 | // WARNING: You must not modify and part of this Metrics class, including the submit delay or 254 | // frequency! 255 | // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! 256 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 257 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 258 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 259 | scheduler.scheduleAtFixedRate( 260 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 261 | } 262 | 263 | private void submitData() { 264 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 265 | appendPlatformDataConsumer.accept(baseJsonBuilder); 266 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 267 | appendServiceDataConsumer.accept(serviceJsonBuilder); 268 | JsonObjectBuilder.JsonObject[] chartData = 269 | customCharts.stream() 270 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 271 | .filter(Objects::nonNull) 272 | .toArray(JsonObjectBuilder.JsonObject[]::new); 273 | serviceJsonBuilder.appendField("id", serviceId); 274 | serviceJsonBuilder.appendField("customCharts", chartData); 275 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 276 | baseJsonBuilder.appendField("serverUUID", serverUuid); 277 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 278 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 279 | scheduler.execute( 280 | () -> { 281 | try { 282 | // Send the data 283 | sendData(data); 284 | } catch (Exception e) { 285 | // Something went wrong! :( 286 | if (logErrors) { 287 | errorLogger.accept("Could not submit bStats metrics data", e); 288 | } 289 | } 290 | }); 291 | } 292 | 293 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 294 | if (logSentData) { 295 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 296 | } 297 | String url = String.format(REPORT_URL, platform); 298 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 299 | // Compress the data to save bandwidth 300 | byte[] compressedData = compress(data.toString()); 301 | connection.setRequestMethod("POST"); 302 | connection.addRequestProperty("Accept", "application/json"); 303 | connection.addRequestProperty("Connection", "close"); 304 | connection.addRequestProperty("Content-Encoding", "gzip"); 305 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 306 | connection.setRequestProperty("Content-Type", "application/json"); 307 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 308 | connection.setDoOutput(true); 309 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 310 | outputStream.write(compressedData); 311 | } 312 | StringBuilder builder = new StringBuilder(); 313 | try (BufferedReader bufferedReader = 314 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 315 | String line; 316 | while ((line = bufferedReader.readLine()) != null) { 317 | builder.append(line); 318 | } 319 | } 320 | if (logResponseStatusText) { 321 | infoLogger.accept("Sent data to bStats and received response: " + builder); 322 | } 323 | } 324 | 325 | /** Checks that the class was properly relocated. */ 326 | private void checkRelocation() { 327 | // You can use the property to disable the check in your test environment 328 | if (System.getProperty("bstats.relocatecheck") == null 329 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 330 | // Maven's Relocate is clever and changes strings, too. So we have to use this little 331 | // "trick" ... :D 332 | final String defaultPackage = 333 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 334 | final String examplePackage = 335 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 336 | // We want to make sure no one just copy & pastes the example and uses the wrong package 337 | // names 338 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 339 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 340 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 341 | } 342 | } 343 | } 344 | 345 | /** 346 | * Gzips the given string. 347 | * 348 | * @param str The string to gzip. 349 | * @return The gzipped string. 350 | */ 351 | private static byte[] compress(final String str) throws IOException { 352 | if (str == null) { 353 | return null; 354 | } 355 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 356 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 357 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 358 | } 359 | return outputStream.toByteArray(); 360 | } 361 | } 362 | 363 | public static class AdvancedBarChart extends CustomChart { 364 | 365 | private final Callable> callable; 366 | 367 | /** 368 | * Class constructor. 369 | * 370 | * @param chartId The id of the chart. 371 | * @param callable The callable which is used to request the chart data. 372 | */ 373 | public AdvancedBarChart(String chartId, Callable> callable) { 374 | super(chartId); 375 | this.callable = callable; 376 | } 377 | 378 | @Override 379 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 380 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 381 | Map map = callable.call(); 382 | if (map == null || map.isEmpty()) { 383 | // Null = skip the chart 384 | return null; 385 | } 386 | boolean allSkipped = true; 387 | for (Map.Entry entry : map.entrySet()) { 388 | if (entry.getValue().length == 0) { 389 | // Skip this invalid 390 | continue; 391 | } 392 | allSkipped = false; 393 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 394 | } 395 | if (allSkipped) { 396 | // Null = skip the chart 397 | return null; 398 | } 399 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 400 | } 401 | } 402 | 403 | public static class SimpleBarChart extends CustomChart { 404 | 405 | private final Callable> callable; 406 | 407 | /** 408 | * Class constructor. 409 | * 410 | * @param chartId The id of the chart. 411 | * @param callable The callable which is used to request the chart data. 412 | */ 413 | public SimpleBarChart(String chartId, Callable> callable) { 414 | super(chartId); 415 | this.callable = callable; 416 | } 417 | 418 | @Override 419 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 420 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 421 | Map map = callable.call(); 422 | if (map == null || map.isEmpty()) { 423 | // Null = skip the chart 424 | return null; 425 | } 426 | for (Map.Entry entry : map.entrySet()) { 427 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 428 | } 429 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 430 | } 431 | } 432 | 433 | public static class MultiLineChart extends CustomChart { 434 | 435 | private final Callable> callable; 436 | 437 | /** 438 | * Class constructor. 439 | * 440 | * @param chartId The id of the chart. 441 | * @param callable The callable which is used to request the chart data. 442 | */ 443 | public MultiLineChart(String chartId, Callable> callable) { 444 | super(chartId); 445 | this.callable = callable; 446 | } 447 | 448 | @Override 449 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 450 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 451 | Map map = callable.call(); 452 | if (map == null || map.isEmpty()) { 453 | // Null = skip the chart 454 | return null; 455 | } 456 | boolean allSkipped = true; 457 | for (Map.Entry entry : map.entrySet()) { 458 | if (entry.getValue() == 0) { 459 | // Skip this invalid 460 | continue; 461 | } 462 | allSkipped = false; 463 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 464 | } 465 | if (allSkipped) { 466 | // Null = skip the chart 467 | return null; 468 | } 469 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 470 | } 471 | } 472 | 473 | public static class AdvancedPie extends CustomChart { 474 | 475 | private final Callable> callable; 476 | 477 | /** 478 | * Class constructor. 479 | * 480 | * @param chartId The id of the chart. 481 | * @param callable The callable which is used to request the chart data. 482 | */ 483 | public AdvancedPie(String chartId, Callable> callable) { 484 | super(chartId); 485 | this.callable = callable; 486 | } 487 | 488 | @Override 489 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 490 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 491 | Map map = callable.call(); 492 | if (map == null || map.isEmpty()) { 493 | // Null = skip the chart 494 | return null; 495 | } 496 | boolean allSkipped = true; 497 | for (Map.Entry entry : map.entrySet()) { 498 | if (entry.getValue() == 0) { 499 | // Skip this invalid 500 | continue; 501 | } 502 | allSkipped = false; 503 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 504 | } 505 | if (allSkipped) { 506 | // Null = skip the chart 507 | return null; 508 | } 509 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 510 | } 511 | } 512 | 513 | public abstract static class CustomChart { 514 | 515 | private final String chartId; 516 | 517 | protected CustomChart(String chartId) { 518 | if (chartId == null) { 519 | throw new IllegalArgumentException("chartId must not be null"); 520 | } 521 | this.chartId = chartId; 522 | } 523 | 524 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 525 | BiConsumer errorLogger, boolean logErrors) { 526 | JsonObjectBuilder builder = new JsonObjectBuilder(); 527 | builder.appendField("chartId", chartId); 528 | try { 529 | JsonObjectBuilder.JsonObject data = getChartData(); 530 | if (data == null) { 531 | // If the data is null we don't send the chart. 532 | return null; 533 | } 534 | builder.appendField("data", data); 535 | } catch (Throwable t) { 536 | if (logErrors) { 537 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 538 | } 539 | return null; 540 | } 541 | return builder.build(); 542 | } 543 | 544 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 545 | } 546 | 547 | public static class SingleLineChart extends CustomChart { 548 | 549 | private final Callable callable; 550 | 551 | /** 552 | * Class constructor. 553 | * 554 | * @param chartId The id of the chart. 555 | * @param callable The callable which is used to request the chart data. 556 | */ 557 | public SingleLineChart(String chartId, Callable callable) { 558 | super(chartId); 559 | this.callable = callable; 560 | } 561 | 562 | @Override 563 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 564 | int value = callable.call(); 565 | if (value == 0) { 566 | // Null = skip the chart 567 | return null; 568 | } 569 | return new JsonObjectBuilder().appendField("value", value).build(); 570 | } 571 | } 572 | 573 | public static class SimplePie extends CustomChart { 574 | 575 | private final Callable callable; 576 | 577 | /** 578 | * Class constructor. 579 | * 580 | * @param chartId The id of the chart. 581 | * @param callable The callable which is used to request the chart data. 582 | */ 583 | public SimplePie(String chartId, Callable callable) { 584 | super(chartId); 585 | this.callable = callable; 586 | } 587 | 588 | @Override 589 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 590 | String value = callable.call(); 591 | if (value == null || value.isEmpty()) { 592 | // Null = skip the chart 593 | return null; 594 | } 595 | return new JsonObjectBuilder().appendField("value", value).build(); 596 | } 597 | } 598 | 599 | public static class DrilldownPie extends CustomChart { 600 | 601 | private final Callable>> callable; 602 | 603 | /** 604 | * Class constructor. 605 | * 606 | * @param chartId The id of the chart. 607 | * @param callable The callable which is used to request the chart data. 608 | */ 609 | public DrilldownPie(String chartId, Callable>> callable) { 610 | super(chartId); 611 | this.callable = callable; 612 | } 613 | 614 | @Override 615 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 616 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 617 | Map> map = callable.call(); 618 | if (map == null || map.isEmpty()) { 619 | // Null = skip the chart 620 | return null; 621 | } 622 | boolean reallyAllSkipped = true; 623 | for (Map.Entry> entryValues : map.entrySet()) { 624 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 625 | boolean allSkipped = true; 626 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 627 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 628 | allSkipped = false; 629 | } 630 | if (!allSkipped) { 631 | reallyAllSkipped = false; 632 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 633 | } 634 | } 635 | if (reallyAllSkipped) { 636 | // Null = skip the chart 637 | return null; 638 | } 639 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 640 | } 641 | } 642 | 643 | /** 644 | * An extremely simple JSON builder. 645 | * 646 | *

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

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 803 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 804 | * 805 | * @param value The value to escape. 806 | * @return The escaped value. 807 | */ 808 | private static String escape(String value) { 809 | final StringBuilder builder = new StringBuilder(); 810 | for (int i = 0; i < value.length(); i++) { 811 | char c = value.charAt(i); 812 | if (c == '"') { 813 | builder.append("\\\""); 814 | } else if (c == '\\') { 815 | builder.append("\\\\"); 816 | } else if (c <= '\u000F') { 817 | builder.append("\\u000").append(Integer.toHexString(c)); 818 | } else if (c <= '\u001F') { 819 | builder.append("\\u00").append(Integer.toHexString(c)); 820 | } else { 821 | builder.append(c); 822 | } 823 | } 824 | return builder.toString(); 825 | } 826 | 827 | /** 828 | * A super simple representation of a JSON object. 829 | * 830 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 831 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 832 | * JsonObject)}. 833 | */ 834 | public static class JsonObject { 835 | 836 | private final String value; 837 | 838 | private JsonObject(String value) { 839 | this.value = value; 840 | } 841 | 842 | @Override 843 | public String toString() { 844 | return value; 845 | } 846 | } 847 | } 848 | } -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/util/NMSUtil.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.util; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.Location; 5 | import org.bukkit.Sound; 6 | import org.bukkit.entity.Player; 7 | 8 | import java.lang.reflect.Constructor; 9 | 10 | public class NMSUtil { 11 | public static final String VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; 12 | 13 | public static Class getNMSClass(String name) { 14 | try { 15 | return Class.forName(String.format("net.minecraft.server.%s.%s", VERSION, name)); 16 | } catch (ClassNotFoundException ignored) { 17 | return null; 18 | } 19 | } 20 | 21 | public static void sendPacket(Player player, Object packet) { 22 | try { 23 | Object handle = player.getClass().getMethod("getHandle").invoke(player); 24 | Object playerConnection = handle.getClass().getField("playerConnection").get(handle); 25 | playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet")) 26 | .invoke(playerConnection, packet); 27 | } catch (Exception e) { 28 | e.printStackTrace(); 29 | } 30 | } 31 | 32 | public static void sendLightning(Player player, Location l) { 33 | Class light = getNMSClass("EntityLightning"); 34 | try { 35 | Constructor constu = light.getConstructor(getNMSClass("World"), double.class, double.class, double.class, boolean.class, boolean.class); 36 | Object wh = player.getWorld().getClass().getMethod("getHandle").invoke(player.getWorld()); 37 | Object lighobj = constu.newInstance(wh, l.getX(), l.getY(), l.getZ(), false, false); 38 | Object obj = getNMSClass("PacketPlayOutSpawnEntityWeather").getConstructor(getNMSClass("Entity")).newInstance(lighobj); 39 | sendPacket(player, obj); 40 | player.playSound(player.getLocation(), Sound.AMBIENCE_THUNDER, 100, 1); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | public static void sendGameState(Player player, int type, float state) throws InstantiationException, NoSuchFieldException { 47 | try { 48 | final Object entityPlayer = player.getClass().getMethod("getHandle", new Class[0]).invoke(player); 49 | final Object playerConnection = entityPlayer.getClass().getField("playerConnection").get(entityPlayer); 50 | final Object packet = getNMSClass("PacketPlayOutGameStateChange").getConstructor(Integer.TYPE, Float.TYPE).newInstance(type, state); 51 | playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet")).invoke(playerConnection, packet); 52 | } catch (Exception ignored) { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/de/rexlnico/realtimeplugin/util/Utils.java: -------------------------------------------------------------------------------- 1 | package de.rexlnico.realtimeplugin.util; 2 | 3 | import de.rexlnico.realtimeplugin.main.Main; 4 | import org.bukkit.Location; 5 | import org.bukkit.entity.Player; 6 | import org.json.simple.JSONObject; 7 | import org.json.simple.JSONValue; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.InputStream; 11 | import java.net.MalformedURLException; 12 | import java.net.URL; 13 | import java.net.URLConnection; 14 | import java.util.Random; 15 | import java.util.logging.Level; 16 | 17 | public class Utils { 18 | private static final Random RAND = new Random(); 19 | 20 | public static int overflow(int value, int at) { 21 | while (value > at) { 22 | value -= at; 23 | } 24 | return value; 25 | } 26 | 27 | public static byte[] httpRequest(String url) { 28 | try { 29 | return httpRequest(new URL(url)); 30 | } catch (MalformedURLException e) { 31 | //Main.getPlugin().getLogger().log(Level.SEVERE, String.format("HTTP request to %s failed", url), e); 32 | return null; 33 | } 34 | } 35 | 36 | public static byte[] httpRequest(URL url) { 37 | try { 38 | URLConnection conn = url.openConnection(); 39 | InputStream in = conn.getInputStream(); 40 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 41 | 42 | byte[] buff = new byte[1024]; 43 | int len; 44 | while ((len = in.read(buff)) != -1) { 45 | bout.write(buff, 0, len); 46 | } 47 | 48 | bout.close(); 49 | in.close(); 50 | 51 | return bout.toByteArray(); 52 | } catch (Exception e) { 53 | //Main.getPlugin().getLogger().log(Level.SEVERE, String.format("HTTP request to %s failed", url), e); 54 | return null; 55 | } 56 | } 57 | 58 | public static Location getRandomLocation(Player player) { 59 | Location location; 60 | int x; 61 | int z; 62 | x = RAND.nextInt(player.getLocation().getBlockX() + 25) + 12; 63 | z = RAND.nextInt(player.getLocation().getBlockZ() + 25) + 12; 64 | if (RAND.nextBoolean()) { 65 | x *= -1; 66 | } 67 | if (RAND.nextBoolean()) { 68 | z *= -1; 69 | } 70 | location = new Location(player.getWorld(), player.getLocation().add(x, 0, z).getX(), player.getWorld().getHighestBlockYAt(x, z) + 1, player.getLocation().add(x, 0, z).getZ()); 71 | return location; 72 | } 73 | 74 | public static JSONObject parseJSON(String json) { 75 | return (JSONObject) JSONValue.parse(json); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | main: de.rexlnico.realtimeplugin.main.Main 2 | authors: [ rexlNico, fluse1367 ] 3 | version: 3.8 4 | api-version: 1.13 5 | name: RealTimePlugin 6 | website: https://rexlNico.de 7 | 8 | commands: 9 | realtime: -------------------------------------------------------------------------------- /src/main/resources/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "world": "example", 3 | "updateInterval": 20, 4 | "active": false, 5 | "time": { 6 | "active": false, 7 | "timezone": "Europe/Berlin" 8 | }, 9 | "weather": { 10 | "active": false, 11 | "City": "Berlin", 12 | "Country": "Germany", 13 | "weatherKey": "" 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/resources/world.json: -------------------------------------------------------------------------------- 1 | { 2 | "world": "world", 3 | "updateInterval": 20, 4 | "active": true, 5 | "time": { 6 | "active": true, 7 | "timezone": "Europe/Berlin" 8 | }, 9 | "weather": { 10 | "active": false, 11 | "City": "Berlin", 12 | "Country": "Germany", 13 | "weatherKey": "" 14 | } 15 | } --------------------------------------------------------------------------------