From 0152bb05c7c54ca47c48281559a1316774bce6ac Mon Sep 17 00:00:00 2001 From: sky Date: Sat, 7 Mar 2020 20:40:49 +0800 Subject: [PATCH] Update Hologram --- build.gradle | 3 +- .../io/izzel/taboolib/TabooLibLoader.java | 7 +- .../taboolib/cronus/bukkit/ItemStack.java | 13 +- .../{Metrics.java => metrics/BStats.java} | 10 +- .../io/izzel/taboolib/metrics/CStats.java | 713 ++++++++++++++++++ .../module/config/TConfigWatcher.java | 6 +- .../taboolib/module/hologram/Hologram.java | 195 +++++ .../module/hologram/HologramViewer.java | 80 ++ .../taboolib/module/hologram/THologram.java | 74 ++ .../module/hologram/THologramHandler.java | 208 +++++ .../module/hologram/THologramSchedule.java | 31 + .../io/izzel/taboolib/module/nms/NMS.java | 5 + .../io/izzel/taboolib/module/nms/NMSImpl.java | 16 + .../izzel/taboolib/module/packet/Packet.java | 14 + 14 files changed, 1360 insertions(+), 15 deletions(-) rename src/main/scala/io/izzel/taboolib/{Metrics.java => metrics/BStats.java} (98%) create mode 100644 src/main/scala/io/izzel/taboolib/metrics/CStats.java create mode 100644 src/main/scala/io/izzel/taboolib/module/hologram/Hologram.java create mode 100644 src/main/scala/io/izzel/taboolib/module/hologram/HologramViewer.java create mode 100644 src/main/scala/io/izzel/taboolib/module/hologram/THologram.java create mode 100644 src/main/scala/io/izzel/taboolib/module/hologram/THologramHandler.java create mode 100644 src/main/scala/io/izzel/taboolib/module/hologram/THologramSchedule.java diff --git a/build.gradle b/build.gradle index 6787fbc..7dabdec 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'me.skymc' -version = '5.17' +version = '5.18' sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -37,7 +37,6 @@ dependencies { shadow group: 'me.clip', name: 'placeholderapi', version: '2.10.4' shadow group: 'com.google.inject', name: 'guice', version: '4.2.2' shadow fileTree(dir: 'libs', includes: ['*.jar']) - compile "org.jetbrains.kotlin:kotlin-stdlib" } shadowJar { diff --git a/src/main/scala/io/izzel/taboolib/TabooLibLoader.java b/src/main/scala/io/izzel/taboolib/TabooLibLoader.java index cc620f9..406ecaf 100644 --- a/src/main/scala/io/izzel/taboolib/TabooLibLoader.java +++ b/src/main/scala/io/izzel/taboolib/TabooLibLoader.java @@ -4,6 +4,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import io.izzel.taboolib.client.TabooLibClient; import io.izzel.taboolib.client.TabooLibServer; +import io.izzel.taboolib.metrics.BStats; +import io.izzel.taboolib.metrics.CStats; import io.izzel.taboolib.module.dependency.TDependencyInjector; import io.izzel.taboolib.module.inject.TSchedule; import io.izzel.taboolib.util.Files; @@ -33,8 +35,9 @@ public class TabooLibLoader { // 加载依赖 TDependencyInjector.inject(TabooLib.getPlugin(), TabooLib.class); // 插件统计 - Metrics metrics = new Metrics(TabooLib.getPlugin()); - metrics.addCustomChart(new Metrics.SingleLineChart("plugins_using_taboolib", () -> Math.toIntExact(Arrays.stream(Bukkit.getPluginManager().getPlugins()).filter(TabooLibAPI::isDependTabooLib).count()))); + BStats bStats = new BStats(TabooLib.getPlugin()); + CStats cStats = new CStats(TabooLib.getPlugin()); + bStats.addCustomChart(new BStats.SingleLineChart("plugins_using_taboolib", () -> Math.toIntExact(Arrays.stream(Bukkit.getPluginManager().getPlugins()).filter(TabooLibAPI::isDependTabooLib).count()))); // 读取插件类 setupClasses(TabooLib.getPlugin()); // 读取加载器 diff --git a/src/main/scala/io/izzel/taboolib/cronus/bukkit/ItemStack.java b/src/main/scala/io/izzel/taboolib/cronus/bukkit/ItemStack.java index 8d902d0..2809b10 100644 --- a/src/main/scala/io/izzel/taboolib/cronus/bukkit/ItemStack.java +++ b/src/main/scala/io/izzel/taboolib/cronus/bukkit/ItemStack.java @@ -1,22 +1,25 @@ package io.izzel.taboolib.cronus.bukkit; +import com.google.common.collect.Lists; import io.izzel.taboolib.util.item.Items; import org.bukkit.entity.Player; +import java.util.List; + /** * @Author 坏黑 * @Since 2019-05-23 22:45 */ public class ItemStack { - private String type; + private List type; private String name; private String lore; private int damage; private int amount; public ItemStack(String type, String name, String lore, int damage, int amount) { - this.type = type; + this.type = type == null ? null : Lists.newArrayList(type.split("\\|")); this.name = name; this.lore = lore; this.damage = damage; @@ -24,7 +27,7 @@ public class ItemStack { } public boolean isType(org.bukkit.inventory.ItemStack itemStack) { - return type == null || itemStack.getType().name().equalsIgnoreCase(type); + return type == null || type.stream().anyMatch(e -> e.equalsIgnoreCase(itemStack.getType().name())); } public boolean isName(org.bukkit.inventory.ItemStack itemStack) { @@ -59,8 +62,8 @@ public class ItemStack { return Items.takeItem(player.getInventory(), this::isSimilar, amount); } - public String getType() { - return type; + public List getType() { + return Lists.newArrayList(type); } public String getName() { diff --git a/src/main/scala/io/izzel/taboolib/Metrics.java b/src/main/scala/io/izzel/taboolib/metrics/BStats.java similarity index 98% rename from src/main/scala/io/izzel/taboolib/Metrics.java rename to src/main/scala/io/izzel/taboolib/metrics/BStats.java index 89d46af..b3261ca 100644 --- a/src/main/scala/io/izzel/taboolib/Metrics.java +++ b/src/main/scala/io/izzel/taboolib/metrics/BStats.java @@ -1,4 +1,4 @@ -package io.izzel.taboolib; +package io.izzel.taboolib.metrics; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; @@ -29,7 +29,7 @@ import java.util.zip.GZIPOutputStream; * * @author Bastian */ -public class Metrics { +public class BStats { // The version of this bStats class public static final int B_STATS_VERSION = 1; @@ -48,7 +48,7 @@ public class Metrics { new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'b', 'u', 'k', 'k', 'i', 't'}); final String examplePackage = new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); // We want to make sure nobody just copy & pastes the example and use the wrong package names - if (Metrics.class.getPackage().getName().equals(defaultPackage) || Metrics.class.getPackage().getName().equals(examplePackage)) { + if (BStats.class.getPackage().getName().equals(defaultPackage) || BStats.class.getPackage().getName().equals(examplePackage)) { throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); } } @@ -65,7 +65,7 @@ public class Metrics { * * @param plugin The plugin which stats should be submitted. */ - public Metrics(Plugin plugin) { + public BStats(Plugin plugin) { if (plugin == null) { throw new IllegalArgumentException("Plugin cannot be null!"); } @@ -104,7 +104,7 @@ public class Metrics { } } // Register our service - Bukkit.getServicesManager().register(Metrics.class, this, plugin, ServicePriority.Normal); + Bukkit.getServicesManager().register(BStats.class, this, plugin, ServicePriority.Normal); if (!found) { // We are the first! startSubmitting(); diff --git a/src/main/scala/io/izzel/taboolib/metrics/CStats.java b/src/main/scala/io/izzel/taboolib/metrics/CStats.java new file mode 100644 index 0000000..5b78252 --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/metrics/CStats.java @@ -0,0 +1,713 @@ +package io.izzel.taboolib.metrics; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.ServicePriority; + +import javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +/** + * cStats collects some data for plugin authors. + *

+ * Check out https://cstats.iroselle.com/ to learn more about cStats! + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class CStats { + + static { + // You can use the property to disable the check in your test environment + if (System.getProperty("cStats.relocatecheck") == null || !System.getProperty("cStats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D + final String defaultPackage = new String( + new byte[]{'c','o','m','.','i','r','o','s','e','l','l','e','.','c','s','t','a','t','s','.','b','u','k','k','i','t'}); + final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure nobody just copy & pastes the example and use the wrong package names + if (CStats.class.getPackage().getName().equals(defaultPackage) || CStats.class.getPackage().getName().equals(examplePackage)) { + throw new IllegalStateException("cStats cStats class has not been relocated correctly!"); + } + } + } + + // The version of this cStats class + public static final int C_STATS_VERSION = 1; + + // The url to which the data is sent + private static final String URL = "https://cstats.iroselle.com/submitData/bukkit"; + + // Is cStats enabled on this server? + private boolean enabled; + + // Should failed requests be logged? + private static boolean logFailedRequests; + + // Should the sent data be logged? + private static boolean logSentData; + + // Should the response text be logged? + private static boolean logResponseStatusText; + + // The uuid of the server + private static String serverUUID; + + // The plugin + private final Plugin plugin; + + // A list with all custom charts + private final List charts = new ArrayList<>(); + + /** + * Class constructor. + * + * @param plugin The plugin which stats should be submitted. + */ + public CStats(Plugin plugin) { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null!"); + } + this.plugin = plugin; + + // Get the config file + File cStatsFolder = new File(plugin.getDataFolder().getParentFile(), "cStats"); + File configFile = new File(cStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Check if the config file exists + if (!config.isSet("serverUuid")) { + + // Add default values + config.addDefault("enabled", true); + // Every server gets it's unique random id. + config.addDefault("serverUuid", UUID.randomUUID().toString()); + // Should failed request be logged? + config.addDefault("logFailedRequests", false); + // Should the sent data be logged? + config.addDefault("logSentData", false); + // Should the response text be logged? + config.addDefault("logResponseStatusText", false); + + // Inform the server owners about cStats + config.options().header( + "cStats collects some data for plugin authors like how many servers are using their plugins.\n" + + "To honor their work, you should not disable it.\n" + + "This has nearly no effect on the server performance!\n" + + "Check out https://cstats.iroselle.com/ to learn more :)" + ).copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { } + } + + // Load the data + enabled = config.getBoolean("enabled", true); + serverUUID = config.getString("serverUuid"); + logFailedRequests = config.getBoolean("logFailedRequests", false); + logSentData = config.getBoolean("logSentData", false); + logResponseStatusText = config.getBoolean("logResponseStatusText", false); + + if (enabled) { + boolean found = false; + // Search for all other cStats cStats classes to see if we are the first one + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("C_STATS_VERSION"); // Our identifier :) + found = true; // We aren't the first + break; + } catch (NoSuchFieldException ignored) { } + } + // Register our service + Bukkit.getServicesManager().register(CStats.class, this, plugin, ServicePriority.Normal); + if (!found) { + // We are the first! + startSubmitting(); + } + } + } + + /** + * Checks if cStats is enabled. + * + * @return Whether cStats is enabled or not. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + if (chart == null) { + throw new IllegalArgumentException("Chart cannot be null!"); + } + charts.add(chart); + } + + /** + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { + final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!plugin.isEnabled()) { // Plugin was disabled + timer.cancel(); + return; + } + // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler + // Don't be afraid! The connection to the cStats server is still async, only the stats collection is sync ;) + Bukkit.getScheduler().runTask(plugin, () -> submitData()); + } + }, 1000 * 60 * 5, 1000 * 60 * 30); + // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start + // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! + // WARNING: Just don't do it! + } + + /** + * Gets the plugin specific data. + * This method is called using Reflection. + * + * @return The plugin specific data. + */ + public JsonObject getPluginData() { + JsonObject data = new JsonObject(); + + String pluginName = plugin.getDescription().getName(); + String pluginVersion = plugin.getDescription().getVersion(); + + data.addProperty("pluginName", pluginName); // Append the name of the plugin + data.addProperty("pluginVersion", pluginVersion); // Append the version of the plugin + JsonArray customCharts = new JsonArray(); + for (CustomChart customChart : charts) { + // Add the data of the custom charts + JsonObject chart = customChart.getRequestJsonObject(); + if (chart == null) { // If the chart is null, we skip it + continue; + } + customCharts.add(chart); + } + data.add("customCharts", customCharts); + + return data; + } + + /** + * Gets the server specific data. + * + * @return The server specific data. + */ + private JsonObject getServerData() { + // Minecraft specific data + int playerAmount; + try { + // Around MC 1.8 the return type was changed to a collection from an array, + // This fixes java.lang.NoSuchMethodError: org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + playerAmount = onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + playerAmount = Bukkit.getOnlinePlayers().size(); // Just use the new method if the Reflection failed + } + int onlineMode = Bukkit.getOnlineMode() ? 1 : 0; + String bukkitVersion = Bukkit.getVersion(); + String bukkitName = Bukkit.getName(); + + // OS/Java specific data + String javaVersion = System.getProperty("java.version"); + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + JsonObject data = new JsonObject(); + + data.addProperty("serverUUID", serverUUID); + + data.addProperty("playerAmount", playerAmount); + data.addProperty("onlineMode", onlineMode); + data.addProperty("bukkitVersion", bukkitVersion); + data.addProperty("bukkitName", bukkitName); + + data.addProperty("javaVersion", javaVersion); + data.addProperty("osName", osName); + data.addProperty("osArch", osArch); + data.addProperty("osVersion", osVersion); + data.addProperty("coreCount", coreCount); + + return data; + } + + /** + * Collects the data and sends it afterwards. + */ + private void submitData() { + final JsonObject data = getServerData(); + + JsonArray pluginData = new JsonArray(); + // Search for all other cStats cStats classes to get their plugin data + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("C_STATS_VERSION"); // Our identifier :) + + for (RegisteredServiceProvider provider : Bukkit.getServicesManager().getRegistrations(service)) { + try { + Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider()); + if (plugin instanceof JsonObject) { + pluginData.add((JsonObject) plugin); + } else { // old cStats version compatibility + try { + Class jsonObjectJsonSimple = Class.forName("org.json.simple.JSONObject"); + if (plugin.getClass().isAssignableFrom(jsonObjectJsonSimple)) { + Method jsonStringGetter = jsonObjectJsonSimple.getDeclaredMethod("toJSONString"); + jsonStringGetter.setAccessible(true); + String jsonString = (String) jsonStringGetter.invoke(plugin); + JsonObject object = new JsonParser().parse(jsonString).getAsJsonObject(); + pluginData.add(object); + } + } catch (ClassNotFoundException e) { + // minecraft version 1.14+ + if (logFailedRequests) { + this.plugin.getLogger().log(Level.SEVERE, "Encountered unexpected exception", e); + } + } + } + } catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { } + } + } catch (NoSuchFieldException ignored) { } + } + + data.add("plugins", pluginData); + + // Create a new thread for the connection to the cStats server + new Thread(() -> { + try { + // Send the data + sendData(plugin, data); + } catch (Exception e) { + // Something went wrong! :( + if (logFailedRequests) { + plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e); + } + } + }).start(); + } + + /** + * Sends the data to the cStats server. + * + * @param plugin Any plugin. It's just used to get a logger instance. + * @param data The data to send. + * @throws Exception If the request failed. + */ + private static void sendData(Plugin plugin, JsonObject data) throws Exception { + if (data == null) { + throw new IllegalArgumentException("Data cannot be null!"); + } + if (Bukkit.isPrimaryThread()) { + throw new IllegalAccessException("This method must not be called from the main thread!"); + } + if (logSentData) { + plugin.getLogger().info("Sending data to cStats: " + data); + } + HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); + + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + + // Add headers + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format + connection.setRequestProperty("User-Agent", "MC-Server/" + C_STATS_VERSION); + + // Send data + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + } + + StringBuilder builder = new StringBuilder(); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + + if (logResponseStatusText) { + plugin.getLogger().info("Sent data to cStats and received response: " + builder); + } + } + + /** + * Gzips the given String. + * + * @param str The string to gzip. + * @return The gzipped String. + * @throws IOException If the compression failed. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + + /** + * Represents a custom chart. + */ + public static abstract class CustomChart { + + // The id of the chart + final String chartId; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + CustomChart(String chartId) { + if (chartId == null || chartId.isEmpty()) { + throw new IllegalArgumentException("ChartId cannot be null or empty!"); + } + this.chartId = chartId; + } + + private JsonObject getRequestJsonObject() { + JsonObject chart = new JsonObject(); + chart.addProperty("chartId", chartId); + try { + JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + chart.add("data", data); + } catch (Throwable t) { + if (logFailedRequests) { + Bukkit.getLogger().log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return chart; + } + + protected abstract JsonObject getChartData() throws Exception; + + } + + /** + * Represents a custom simple pie. + */ + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + data.addProperty("value", value); + return data; + } + } + + /** + * Represents a custom advanced pie. + */ + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + JsonObject values = new JsonObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.addProperty(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.add("values", values); + return data; + } + } + + /** + * Represents a custom drilldown pie. + */ + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + JsonObject values = new JsonObject(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObject value = new JsonObject(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + value.addProperty(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + values.add(entryValues.getKey(), value); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + data.add("values", values); + return data; + } + } + + /** + * Represents a custom single line chart. + */ + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + data.addProperty("value", value); + return data; + } + + } + + /** + * Represents a custom multi line chart. + */ + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + JsonObject values = new JsonObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.addProperty(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.add("values", values); + return data; + } + + } + + /** + * Represents a custom simple bar chart. + */ + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + JsonObject values = new JsonObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + JsonArray categoryValues = new JsonArray(); + categoryValues.add(new JsonPrimitive(entry.getValue())); + values.add(entry.getKey(), categoryValues); + } + data.add("values", values); + return data; + } + + } + + /** + * Represents a custom advanced bar chart. + */ + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObject getChartData() throws Exception { + JsonObject data = new JsonObject(); + JsonObject values = new JsonObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + continue; // Skip this invalid + } + allSkipped = false; + JsonArray categoryValues = new JsonArray(); + for (int categoryValue : entry.getValue()) { + categoryValues.add(new JsonPrimitive(categoryValue)); + } + values.add(entry.getKey(), categoryValues); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.add("values", values); + return data; + } + } + +} \ No newline at end of file diff --git a/src/main/scala/io/izzel/taboolib/module/config/TConfigWatcher.java b/src/main/scala/io/izzel/taboolib/module/config/TConfigWatcher.java index fd509c3..22b6240 100644 --- a/src/main/scala/io/izzel/taboolib/module/config/TConfigWatcher.java +++ b/src/main/scala/io/izzel/taboolib/module/config/TConfigWatcher.java @@ -46,7 +46,11 @@ public class TConfigWatcher { } public void addSimpleListener(File file, Runnable runnable) { - addListener(file, null, obj -> runnable.run()); + try { + addListener(file, null, obj -> runnable.run()); + } catch (Throwable t) { + t.printStackTrace(); + } } public void addOnListen(File file, Object obj, Consumer consumer) { diff --git a/src/main/scala/io/izzel/taboolib/module/hologram/Hologram.java b/src/main/scala/io/izzel/taboolib/module/hologram/Hologram.java new file mode 100644 index 0000000..9f21706 --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/module/hologram/Hologram.java @@ -0,0 +1,195 @@ +package io.izzel.taboolib.module.hologram; + +import com.google.common.collect.Sets; +import io.izzel.taboolib.module.nms.NMS; +import io.izzel.taboolib.module.packet.TPacketHandler; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.Set; + +/** + * @Author sky + * @Since 2020-03-07 16:28 + */ +public class Hologram { + + private Set viewers = Sets.newConcurrentHashSet(); + private String text; + private Location location; + private boolean viewAll = false; + private boolean deleted = false; + private boolean autoDelete = false; + private int viewDistance = 50; + + Hologram(Location location, String text, Player... viewers) { + THologram.getHolograms().add(this); + this.text = text; + this.location = location.clone(); + for (Player viewer : viewers) { + addViewer(viewer); + } + } + + public Hologram toAll() { + this.viewAll = true; + return this; + } + + public Hologram autoDelete() { + this.autoDelete = true; + return this; + } + + public Hologram refresh() { + if (deleted) { + return this; + } + if (viewAll) { + Bukkit.getOnlinePlayers().forEach(this::addViewer); + } + viewers.forEach(this::refresh); + return this; + } + + public Hologram refresh(HologramViewer viewer) { + if (deleted) { + return this; + } + viewer.setVisible(location.getWorld().equals(viewer.getPlayer().getWorld()) && location.distance(viewer.getPlayer().getLocation()) < viewDistance); + if (viewer.isVisible()) { + THologram.submit(() -> { + if (viewer.isSpawned()) { + NMS.handle().sendPacketEntityTeleport(viewer.getPlayer(), viewer.getId(), location); + } else { + viewer.setSpawned(true); + TPacketHandler.sendPacket(viewer.getPlayer(), THologramHandler.copy(viewer.getId(), location).get()); + TPacketHandler.sendPacket(viewer.getPlayer(), THologramHandler.copy(viewer.getId()).get()); + } + TPacketHandler.sendPacket(viewer.getPlayer(), THologramHandler.copy(viewer.getId(), text).get()); + }); + } else { + destroy(viewer); + } + return this; + } + + public Hologram flash(String text) { + if (deleted) { + return this; + } + this.text = text; + THologram.submit(() -> viewers.forEach(v -> TPacketHandler.sendPacket(v.getPlayer(), THologramHandler.copy(v.getId(), text).get()))); + return this; + } + + public Hologram flash(Location location) { + if (deleted) { + return this; + } + this.location = location.clone(); + THologram.submit(() -> viewers.forEach(v -> NMS.handle().sendPacketEntityTeleport(v.getPlayer(), v.getId(), location))); + return this; + } + + public Hologram delete() { + destroy(); + deleted = true; + return this; + } + + public Hologram destroy() { + if (deleted) { + return this; + } + viewers.forEach(this::destroy); + return this; + } + + public Hologram destroy(HologramViewer viewer) { + if (deleted) { + return this; + } + viewer.setSpawned(false); + THologram.submit(() -> NMS.handle().sendPacketEntityDestroy(viewer.getPlayer(), viewer.getId())); + return this; + } + + public void addViewer(Player player) { + if (deleted) { + return; + } + if (!isViewer(player)) { + HologramViewer viewer = new HologramViewer(player); + viewers.add(viewer); + refresh(viewer); + } + } + + public void removeViewer(Player player) { + if (deleted) { + return; + } + HologramViewer viewer = getViewer(player); + viewers.remove(viewer); + destroy(viewer); + if (viewers.isEmpty() && autoDelete) { + deleted = true; + } + } + + public boolean isViewer(Player player) { + return viewers.stream().anyMatch(i -> i.getPlayer().getName().equals(player.getName())); + } + + public HologramViewer getViewer(Player player) { + return viewers.stream().filter(i -> i.getPlayer().getName().equals(player.getName())).findFirst().orElse(null); + } + + // ********************************* + // + // Getter and Setter + // + // ********************************* + + public Set getViewers() { + return viewers; + } + + public String getText() { + return text; + } + + public Location getLocation() { + return location.clone(); + } + + public boolean isViewAll() { + return viewAll; + } + + public boolean isDeleted() { + return deleted; + } + + public boolean isAutoDelete() { + return autoDelete; + } + + public int getViewDistance() { + return viewDistance; + } + + public void setText(String text) { + this.text = text; + } + + public void setLocation(Location location) { + this.location = location; + } + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; + } +} diff --git a/src/main/scala/io/izzel/taboolib/module/hologram/HologramViewer.java b/src/main/scala/io/izzel/taboolib/module/hologram/HologramViewer.java new file mode 100644 index 0000000..e67a489 --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/module/hologram/HologramViewer.java @@ -0,0 +1,80 @@ +package io.izzel.taboolib.module.hologram; + +import org.bukkit.entity.Player; + +import java.util.Objects; + +/** + * @Author sky + * @Since 2020-03-07 16:56 + */ +public class HologramViewer { + + private int id; + private Player player; + private boolean spawned; + private boolean visible; + + HologramViewer(Player player) { + this.id = THologram.nextIndex(player); + this.player = player; + } + + public void setVisible(boolean visible) { + this.spawned = false; + this.visible = visible; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HologramViewer)) { + return false; + } + HologramViewer that = (HologramViewer) o; + return player.getName().equals(that.getPlayer().getName()); + } + + @Override + public int hashCode() { + return Objects.hash(player.getName()); + } + + @Override + public String toString() { + return "HologramViewer{" + + "id=" + id + + ", player=" + player + + ", spawned=" + spawned + + ", visible=" + visible + + '}'; + } + + // ********************************* + // + // Getter and Setter + // + // ********************************* + + public int getId() { + return id; + } + + public Player getPlayer() { + return player; + } + + public boolean isSpawned() { + return spawned; + } + + public boolean isVisible() { + return visible; + } + + public void setSpawned(boolean spawned) { + this.spawned = spawned; + } +} diff --git a/src/main/scala/io/izzel/taboolib/module/hologram/THologram.java b/src/main/scala/io/izzel/taboolib/module/hologram/THologram.java new file mode 100644 index 0000000..9b70f00 --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/module/hologram/THologram.java @@ -0,0 +1,74 @@ +package io.izzel.taboolib.module.hologram; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.izzel.taboolib.module.inject.PlayerContainer; +import io.izzel.taboolib.module.inject.TSchedule; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @Author sky + * @Since 2020-03-07 14:24 + */ +public class THologram { + + private static ExecutorService executorService = Executors.newSingleThreadExecutor(); + + @PlayerContainer + private static Map index = Maps.newConcurrentMap(); + private static List holograms = Lists.newCopyOnWriteArrayList(); + + public static List getHolograms() { + return holograms; + } + + public static int nextIndex(Player player) { + return index.put(player.getName(), index.computeIfAbsent(player.getName(), e -> 449599702) + 1); + } + + public static Hologram create(Location location, String text) { + return new Hologram(location, text); + } + + public static Hologram create(Location location, String text, Player... viewers) { + return new Hologram(location, text, viewers); + } + + @TSchedule(period = 100, async = true) + public static void release() { + holograms.removeIf(Hologram::isDeleted); + } + + public static void remove(Player player) { + holograms.forEach(hologram -> hologram.removeViewer(player)); + } + + public static void refresh(Player player) { + for (Hologram hologram : holograms) { + HologramViewer viewer = hologram.getViewer(player); + if (viewer != null) { + hologram.refresh(viewer); + } + } + } + + public static void submit(Runnable runnable) { + executorService.submit(() -> { + if (THologramHandler.isLearned()) { + runnable.run(); + } else { + try { + Thread.sleep(100L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + } +} diff --git a/src/main/scala/io/izzel/taboolib/module/hologram/THologramHandler.java b/src/main/scala/io/izzel/taboolib/module/hologram/THologramHandler.java new file mode 100644 index 0000000..beeef87 --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/module/hologram/THologramHandler.java @@ -0,0 +1,208 @@ +package io.izzel.taboolib.module.hologram; + +import com.google.common.collect.Lists; +import com.google.common.collect.Queues; +import io.izzel.taboolib.TabooLib; +import io.izzel.taboolib.Version; +import io.izzel.taboolib.module.inject.TListener; +import io.izzel.taboolib.module.lite.SimpleReflection; +import io.izzel.taboolib.module.packet.Packet; +import io.izzel.taboolib.module.packet.TPacket; +import io.izzel.taboolib.util.Ref; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.*; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.List; +import java.util.Queue; +import java.util.UUID; + +/** + * @Author sky + * @Since 2020-03-07 14:23 + */ +@TListener +class THologramHandler implements Listener { + + private static ArmorStand learnTarget = null; + private static Packet packetInit = null; + private static Packet packetSpawn = null; + private static Packet packetName = null; + private static THologramSchedule currentSchedule = null; + private static Queue queueSchedule = Queues.newArrayDeque(); + + private static boolean learned = false; + + @TPacket(type = TPacket.Type.RECEIVE) + static boolean d(Player player, Packet packet) { + if (packet.is("PacketPlayInPosition") && !learned) { + learned = true; + Bukkit.getScheduler().runTask(TabooLib.getPlugin(), () -> learn(player)); + } + return true; + } + + @TPacket(type = TPacket.Type.SEND) + static boolean e(Player player, Packet packet) { + if (learnTarget == null) { + return true; + } + if (packet.is("PacketPlayOutSpawnEntity") && packet.read("a", Integer.TYPE) == learnTarget.getEntityId()) { + packetSpawn = packet; + return false; + } + if (packet.is("PacketPlayOutEntityMetadata") && packet.read("a", Integer.TYPE) == learnTarget.getEntityId()) { + if (currentSchedule != null) { + currentSchedule.after(packet); + } else { + packetInit = packet; + } + return false; + } + return true; + } + + public static boolean isLearned() { + return packetInit != null && packetSpawn != null && packetName != null; + } + + public static Packet copy(int id, Location location) { + Packet packet = THologramHandler.getPacketSpawn().copy("e", "f", "j", "h", "i", "j", "k", "l"); + packet.write("a", id); + if (Version.isAfter(Version.v1_9)) { + packet.write("b", UUID.randomUUID()); + packet.write("c", location.getX()); + packet.write("d", location.getY()); + packet.write("e", location.getZ()); + } else { + packet.write("b", location.getX()); + packet.write("c", location.getY()); + packet.write("d", location.getZ()); + } + return packet; + } + + public static Packet copy(int id) { + Packet packet = THologramHandler.getPacketInit().copy("b"); + packet.write("a", id); + return packet; + } + + public static Packet copy(int id, String name) { + Packet packet = THologramHandler.getPacketName().copy(); + packet.write("a", id); + List copy = Lists.newArrayList(); + List item = THologramHandler.getPacketName().read("b", List.class); + for (Object element : item) { + SimpleReflection.checkAndSave(element.getClass()); + Object a = SimpleReflection.getFieldValue(element.getClass(), element, "a"); + Object c = SimpleReflection.getFieldValue(element.getClass(), element, "c"); + try { + Object i = Ref.getUnsafe().allocateInstance(element.getClass()); + SimpleReflection.setFieldValue(element.getClass(), i, "a", a); + SimpleReflection.setFieldValue(element.getClass(), i, "b", name); + SimpleReflection.setFieldValue(element.getClass(), i, "c", c); + copy.add(i); + } catch (InstantiationException e) { + e.printStackTrace(); + } + } + packet.write("b", copy); + return packet; + } + + public static void reset() { + queueSchedule.clear(); + queueSchedule.offer(new THologramSchedule(() -> packetInit != null) { + @Override + public void before() { + learnTarget.setCustomName(" "); + } + + @Override + public void after(Packet packet) { + packetName = packet; + } + }); + } + + public static void learn(Player player) { + player.getWorld().spawn(player.getLocation(), ArmorStand.class, c -> { + learnTarget = c; + learnTarget.setSmall(true); + learnTarget.setMarker(true); + learnTarget.setVisible(false); + learnTarget.setCustomName(" "); + learnTarget.setCustomNameVisible(true); + learnTarget.setBasePlate(false); + }); + reset(); + new BukkitRunnable() { + @Override + public void run() { + THologramSchedule schedule = queueSchedule.peek(); + if (schedule == null) { + cancel(); + learnTarget.remove(); + } else if (schedule.check()) { + currentSchedule = queueSchedule.poll(); + currentSchedule.before(); + } + } + }.runTaskTimer(TabooLib.getPlugin(), 1, 1); + } + + @EventHandler + public void e(PlayerJoinEvent e) { + THologram.refresh(e.getPlayer()); + } + + @EventHandler + public void e(PlayerQuitEvent e) { + THologram.remove(e.getPlayer()); + } + + @EventHandler + public void e(PlayerTeleportEvent e) { + THologram.refresh(e.getPlayer()); + } + + @EventHandler + public void e(PlayerChangedWorldEvent e) { + THologram.refresh(e.getPlayer()); + } + + @EventHandler + public void e(PlayerMoveEvent e) { + if (!e.getFrom().getBlock().equals(e.getTo().getBlock())) { + Bukkit.getScheduler().runTaskAsynchronously(TabooLib.getPlugin(), () -> THologram.refresh(e.getPlayer())); + } + } + + // ********************************* + // + // Getter and Setter + // + // ********************************* + + public static ArmorStand getLearnTarget() { + return learnTarget; + } + + public static Packet getPacketSpawn() { + return packetSpawn; + } + + public static Packet getPacketInit() { + return packetInit; + } + + public static Packet getPacketName() { + return packetName; + } +} diff --git a/src/main/scala/io/izzel/taboolib/module/hologram/THologramSchedule.java b/src/main/scala/io/izzel/taboolib/module/hologram/THologramSchedule.java new file mode 100644 index 0000000..116c96e --- /dev/null +++ b/src/main/scala/io/izzel/taboolib/module/hologram/THologramSchedule.java @@ -0,0 +1,31 @@ +package io.izzel.taboolib.module.hologram; + +import io.izzel.taboolib.module.packet.Packet; + +import java.util.concurrent.Callable; + +/** + * @Author sky + * @Since 2020-03-07 14:28 + */ +abstract class THologramSchedule { + + private Callable condition; + + public THologramSchedule(Callable condition) { + this.condition = condition; + } + + public boolean check() { + try { + return (Boolean) condition.call(); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + abstract public void before(); + + abstract public void after(Packet packet); +} diff --git a/src/main/scala/io/izzel/taboolib/module/nms/NMS.java b/src/main/scala/io/izzel/taboolib/module/nms/NMS.java index 5b2398a..95124fe 100644 --- a/src/main/scala/io/izzel/taboolib/module/nms/NMS.java +++ b/src/main/scala/io/izzel/taboolib/module/nms/NMS.java @@ -85,4 +85,9 @@ public abstract class NMS { abstract public boolean inBoundingBox(Entity entity, Vector vector); abstract public Location getLastLocation(ProjectileHitEvent event); + + abstract public void sendPacketEntityDestroy(Player player, int entity); + + abstract public void sendPacketEntityTeleport(Player player, int entity, Location location); + } diff --git a/src/main/scala/io/izzel/taboolib/module/nms/NMSImpl.java b/src/main/scala/io/izzel/taboolib/module/nms/NMSImpl.java index d6b00c7..43a196d 100644 --- a/src/main/scala/io/izzel/taboolib/module/nms/NMSImpl.java +++ b/src/main/scala/io/izzel/taboolib/module/nms/NMSImpl.java @@ -446,4 +446,20 @@ public class NMSImpl extends NMS { } return null; } + + @Override + public void sendPacketEntityDestroy(Player player, int entity) { + ((CraftPlayer) player).getHandle().playerConnection.sendPacket(new net.minecraft.server.v1_13_R2.PacketPlayOutEntityDestroy(entity)); + } + + @Override + public void sendPacketEntityTeleport(Player player, int entity, Location location) { + Object teleport = new net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport(); + SimpleReflection.checkAndSave(net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport.class); + SimpleReflection.setFieldValue(net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport.class, teleport, "a", entity); + SimpleReflection.setFieldValue(net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport.class, teleport, "b", location.getX()); + SimpleReflection.setFieldValue(net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport.class, teleport, "c", location.getY()); + SimpleReflection.setFieldValue(net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport.class, teleport, "d", location.getZ()); + ((CraftPlayer) player).getHandle().playerConnection.sendPacket((net.minecraft.server.v1_13_R2.PacketPlayOutEntityTeleport) teleport); + } } diff --git a/src/main/scala/io/izzel/taboolib/module/packet/Packet.java b/src/main/scala/io/izzel/taboolib/module/packet/Packet.java index 3b1c81e..4fe44e7 100644 --- a/src/main/scala/io/izzel/taboolib/module/packet/Packet.java +++ b/src/main/scala/io/izzel/taboolib/module/packet/Packet.java @@ -52,6 +52,20 @@ public class Packet { SimpleReflection.setFieldValue(this.packetClass, origin, key, value); } + public Packet copy(String... copyField) { + Object packet; + try { + packet = packetClass.newInstance(); + } catch (Throwable t) { + t.printStackTrace(); + return null; + } + for (String field : copyField) { + SimpleReflection.setFieldValue(this.packetClass, packet, field, SimpleReflection.getFieldValue(this.packetClass, origin, field)); + } + return new Packet(packet); + } + public Object get() { return origin; }