diff --git a/src/main/java/xanadu/enderdragon/EnderDragon.java b/src/main/java/xanadu/enderdragon/EnderDragon.java new file mode 100644 index 0000000..56b0901 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/EnderDragon.java @@ -0,0 +1,102 @@ +package xanadu.enderdragon; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import xanadu.enderdragon.commands.MainCommand; +import xanadu.enderdragon.commands.TabCompleter; +import xanadu.enderdragon.events.*; +import xanadu.enderdragon.listeners.InventoryClick; +import xanadu.enderdragon.metrics.Metrics; + +import java.io.File; + +public final class EnderDragon extends JavaPlugin { + + public static String prefix = ""; + public static Plugin plugin; + public static Server server; + public static PluginManager pm; + public static File data0; + public static File language0; + public static FileConfiguration data; + public static FileConfiguration language; + + @Override + public void onEnable() { + plugin = this; + server = plugin.getServer(); + pm = Bukkit.getPluginManager(); + if (!new File(getDataFolder(), "config.yml").exists()) { + saveDefaultConfig(); + Bukkit.getConsoleSender().sendMessage("§a[EnderDragon] 未检测到config.yml文件,正在生成新的配置文件"); + } + if (!new File(getDataFolder(), "data.yml").exists()) { + this.saveResource("data.yml",false); + Bukkit.getConsoleSender().sendMessage("§a[EnderDragon] 未检测到data.yml文件,正在生成新的配置文件"); + } + if (!new File(getDataFolder(), "language.yml").exists()) { + this.saveResource("language.yml",false); + Bukkit.getConsoleSender().sendMessage("§a[EnderDragon] 未检测到language.yml文件,正在生成新的配置文件"); + } + data0 = new File(plugin.getDataFolder(),"data.yml"); + language0 = new File(plugin.getDataFolder(),"language.yml"); + data = YamlConfiguration.loadConfiguration(data0); + language = YamlConfiguration.loadConfiguration(language0); + prefix = language.getString("prefix"); + if(!getConfig().getString("version").equals("1.8.2") ){ + Bukkit.getConsoleSender().sendMessage("§c[EnderDragon] config.yml版本与插件不对应,请更新配置文件"); + } + if(!data.getString("version").equals("1.8.1") ){ + Bukkit.getConsoleSender().sendMessage("§c[EnderDragon] data.yml版本与插件不对应,请更新配置文件"); + } + if(!language.getString("version").equals("1.8") ){ + Bukkit.getConsoleSender().sendMessage("§c[EnderDragon] language.yml版本与插件不对应,请更新配置文件"); + } + Bukkit.getConsoleSender().sendMessage("§a[EnderDragon] 插件已加载"); + Bukkit.getConsoleSender().sendMessage("§a[EnderDragon] 作者:Xanadu13"); + pm.registerEvents(new CreatureHurt(),this); + pm.registerEvents(new DragonAttack(),this); + pm.registerEvents(new DragonDeath(),this); + pm.registerEvents(new DragonHeal(),this); + pm.registerEvents(new DragonSpawn(),this); + pm.registerEvents(new InventoryClick(),this); + getCommand("enderdragon").setExecutor(new MainCommand()); + getCommand("enderdragon").setTabCompleter(new TabCompleter()); + Metrics metrics = new Metrics(this,14850); + } + + @Override + public void onDisable() + { + Bukkit.getConsoleSender().sendMessage("§e[EnderDragon] 插件已卸载"); + } + + + public static String itemStackToString(ItemStack itemStack) { + YamlConfiguration yml = new YamlConfiguration(); + yml.set("item", itemStack); + return yml.saveToString(); + } + public static ItemStack StringToItemStack(String str) { + YamlConfiguration yml = new YamlConfiguration(); + ItemStack item; + try { + yml.loadFromString(str); + item = yml.getItemStack("item"); + } catch (InvalidConfigurationException ex) { + item = new ItemStack(Material.AIR, 1); + } + return item; + } + +} + + diff --git a/src/main/java/xanadu/enderdragon/commands/MainCommand.java b/src/main/java/xanadu/enderdragon/commands/MainCommand.java new file mode 100644 index 0000000..81b6f26 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/commands/MainCommand.java @@ -0,0 +1,139 @@ +package xanadu.enderdragon.commands; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static xanadu.enderdragon.EnderDragon.*; + +public class MainCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 1) { + if (args[0].equalsIgnoreCase("reload")) { + if(!sender.hasPermission("ed.reload")){ + sender.sendMessage("§4你没有使用该命令的权限"); + return false; + } + plugin.reloadConfig(); + data = YamlConfiguration.loadConfiguration(data0); + language = YamlConfiguration.loadConfiguration(language0); + prefix = language.getString("prefix"); + sender.sendMessage(prefix + "§a配置文件已重载"); + } + } + else if (args[0].equalsIgnoreCase("drop") ) { + if (!(sender instanceof Player || args[1].equalsIgnoreCase("clear"))) { + sender.sendMessage(prefix + "§c这个指令只能由玩家执行"); + return false; + } + if (args[1].equalsIgnoreCase("add")) { + if(!sender.hasPermission("ed.drop.change")){ + sender.sendMessage("§4你没有使用该命令的权限"); + return false; + } + if(args.length == 3 ) { + Player p = (Player) sender; + if(p.getItemInHand().getType() == Material.AIR){ + p.sendMessage(prefix + "§c添加掉落物失败,你手上没有拿物品..."); + return false; + } + String ChanceStr = args[2]; + double chance = 0; + try { + chance = Double.parseDouble(ChanceStr); + } catch (NumberFormatException ex){ + p.sendMessage(prefix + "§c您应该输入数字而不是 " + ChanceStr); + throw new NumberFormatException("\n\n" + + "\33[31;1m" + "错误的命令: /ed drop add " + ChanceStr + "\n" + + "\33[33;1m" + "正确用法: /ed drop add 数字" + "\n" + + "\33[0m"); + } + List datum = data.getStringList("items"); + ItemStack item = p.getItemInHand(); + datum.add(itemStackToString(item)); + datum.add(String.valueOf(chance)); + data.set("items", datum); + try { + data.save(data0); + } catch (IOException e) { + throw new RuntimeException(e); + } + data = YamlConfiguration.loadConfiguration(data0); + p.sendMessage(prefix + "§a掉落物添加成功,概率为: §c" + chance + "%"); + + } + } + if (args[1].equalsIgnoreCase("clear")) { + if(!sender.hasPermission("ed.drop.change")){ + sender.sendMessage("§4你没有使用该命令的权限"); + return false; + } + if(args.length == 2 ) { + data.set("items",""); + try { + data.save(data0); + } catch (IOException e) { + throw new RuntimeException(e); + } + data = YamlConfiguration.loadConfiguration(data0); + sender.sendMessage(prefix + "§a掉落物配置已清空"); + } + } + if (args[1].equalsIgnoreCase("gui")) { + if(!sender.hasPermission("ed.drop.gui")){ + sender.sendMessage("§4你没有使用该命令的权限"); + return false; + } + if(args.length == 2 ) { + Player p = (Player) sender; + String title = plugin.getConfig().getString("special-dragon.drop-gui-title"); + if(title == null){title = "";} + Inventory inv = Bukkit.createInventory(null,54,title); + List datum = data.getStringList("items"); + int max = datum.size() / 2 ; + for(int i=0 ; i lore = meta.getLore(); + if(lore != null) { + lore.add("§6(掉落概率: " + chance + "%)§r"); + meta.setLore(lore); + } + else{ + meta.setLore(Collections.singletonList("§6(掉落概率: " + chance + "%)§r")); + } + item.setItemMeta(meta); + } + inv.setItem(i-1,item); + if(i == 54){break;} + } + p.openInventory(inv); + + } + } + + } + else { + sender.sendMessage("§e/ed reload 重载配置文件"); + sender.sendMessage("§e/ed drop 特殊龙掉落物设置"); + } + return false; + } + +} diff --git a/src/main/java/xanadu/enderdragon/commands/TabCompleter.java b/src/main/java/xanadu/enderdragon/commands/TabCompleter.java new file mode 100644 index 0000000..b844723 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/commands/TabCompleter.java @@ -0,0 +1,45 @@ +package xanadu.enderdragon.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; + +public class TabCompleter implements org.bukkit.command.TabCompleter { + + List arguments = new ArrayList<>(); + + @Override + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + + List result = new ArrayList<>(); + if (args.length == 1) { + arguments.clear(); + arguments.add("reload"); + arguments.add("drop"); + for (String s : arguments) { + if (s.toLowerCase().startsWith(args[0].toLowerCase())) { + result.add(s); + } + } + return result; + } + else if (args.length == 2){ + arguments.clear(); + arguments.add("add"); + arguments.add("clear"); + arguments.add("gui"); + if(args[0].equalsIgnoreCase("drop")) { + for (String s : arguments) { + if (s.toLowerCase().startsWith(args[1].toLowerCase())) { + result.add(s); + } + } + } + return result; + } + return null; + } + +} diff --git a/src/main/java/xanadu/enderdragon/events/CreatureHurt.java b/src/main/java/xanadu/enderdragon/events/CreatureHurt.java new file mode 100644 index 0000000..050c951 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/events/CreatureHurt.java @@ -0,0 +1,28 @@ +package xanadu.enderdragon.events; + +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +import static xanadu.enderdragon.EnderDragon.*; + +public class CreatureHurt implements Listener { + @EventHandler + public void OnCreatureHurt(EntityDamageByEntityEvent e){ + int DamageVisual = plugin.getConfig().getInt("special-dragon.damage-visible"); + String message = language.getString("damage-display"); + if(DamageVisual == 0){return;} + if(e.getDamager().getType() != EntityType.PLAYER){return;} + if(e.getEntity().getType() != EntityType.ENDER_DRAGON){return;} + Player p = (Player) e.getDamager(); + double damage = e.getDamage(); + message = message.replaceAll("%damage%", String.valueOf(damage)); + if(DamageVisual == 1){p.sendTitle("",message,5,40,5);} + if(DamageVisual == 2){p.sendMessage(message);} + if(DamageVisual == 3){p.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message));} + } +} diff --git a/src/main/java/xanadu/enderdragon/events/DragonAttack.java b/src/main/java/xanadu/enderdragon/events/DragonAttack.java new file mode 100644 index 0000000..e6c1e9c --- /dev/null +++ b/src/main/java/xanadu/enderdragon/events/DragonAttack.java @@ -0,0 +1,63 @@ +package xanadu.enderdragon.events; + +import org.bukkit.entity.EnderDragon; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.List; + +import static xanadu.enderdragon.EnderDragon.*; + +public class DragonAttack implements Listener { + + @EventHandler + public void OnDragonAttack(EntityDamageByEntityEvent e){ + if(e.getDamager().getType() != EntityType.ENDER_DRAGON){return;} + if(!e.getDamager().getScoreboardTags().contains("special")){return;} + double multiple = plugin.getConfig().getDouble("special-dragon.damage-multiply"); + double rate = plugin.getConfig().getDouble("special-dragon.suck-blood.rate") / 100; + double BasicSuck = plugin.getConfig().getDouble("special-dragon.suck-blood.base-suck-blood"); + boolean SuckBlood = plugin.getConfig().getBoolean("special-dragon.suck-blood.enable"); + boolean OnlyPlayer = plugin.getConfig().getBoolean("special-dragon.suck-blood.only-player"); + List effects = plugin.getConfig().getStringList("special-dragon.attack-effect"); + if(e.getEntity() instanceof Player) { + Player p = (Player) e.getEntity(); + if (multiple > 0) { + double damage = e.getDamage(); + e.setDamage(damage * multiple); + } + for (String str : effects) { + if (str.length() > 0) { + String type = str.substring(0, str.indexOf(" ")); + int time = Integer.parseInt(str.substring(str.indexOf(" ") + 1, str.lastIndexOf(" "))); + int level = Integer.parseInt(str.substring(str.lastIndexOf(" ") + 1)); + PotionEffectType effectType = PotionEffectType.getByName(type.toUpperCase()); + if (effectType != null) { + p.addPotionEffect(new PotionEffect(effectType, time * 20, level - 1)); + } + } + } + if(!SuckBlood){return;} + double FinalDamage = e.getFinalDamage(); + EnderDragon dragon = (EnderDragon) e.getDamager(); + double health = dragon.getHealth()+FinalDamage*rate+BasicSuck; + if(health > dragon.getMaxHealth()){health = dragon.getMaxHealth();} + dragon.setHealth(health); + + } + else{ + if((!SuckBlood) || OnlyPlayer){return;} + double FinalDamage = e.getFinalDamage(); + EnderDragon dragon = (EnderDragon) e.getDamager(); + double health = dragon.getHealth()+FinalDamage*rate+BasicSuck; + if(health > dragon.getMaxHealth()){health = dragon.getMaxHealth();} + dragon.setHealth(health); + } + + } +} diff --git a/src/main/java/xanadu/enderdragon/events/DragonDeath.java b/src/main/java/xanadu/enderdragon/events/DragonDeath.java new file mode 100644 index 0000000..4030800 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/events/DragonDeath.java @@ -0,0 +1,121 @@ +package xanadu.enderdragon.events; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.scheduler.BukkitRunnable; +import xanadu.enderdragon.EnderDragon; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static xanadu.enderdragon.EnderDragon.*; + +public class DragonDeath implements Listener { + @EventHandler + public void OnDragonDeath(EntityDeathEvent e) throws IOException { + if(e.getEntity().getType() != EntityType.ENDER_DRAGON){return;} + int times = data.getInt("times"); + data.set("times",times+1); + data.save(data0); + Player p = e.getEntity().getKiller(); + int exp = plugin.getConfig().getInt("special-dragon.exp-drop"); + int x = plugin.getConfig().getInt("special-dragon.dragon-egg-spawn.x"); + int y = plugin.getConfig().getInt("special-dragon.dragon-egg-spawn.y"); + int z = plugin.getConfig().getInt("special-dragon.dragon-egg-spawn.z"); + long delay = plugin.getConfig().getInt("special-dragon.dragon-egg-spawn.delay"); + boolean UseCMD = plugin.getConfig().getBoolean("command.enable"); + boolean EggChance = plugin.getConfig().getInt("special-dragon.dragon-egg-spawn.chance") > ThreadLocalRandom.current().nextInt(0, 100); + boolean AllBroadcast = !plugin.getConfig().getBoolean("only-special-death-remind"); + String skill = language.getString("killer-message"); + String KillMsg = language.getString("dragon-killing-broadcast"); + String nobody = language.getString("nobody-kill"); + String InvIsFull = language.getString("player-inv-full"); + if(KillMsg != null) { + if(p != null) { + if ((e.getEntity().getScoreboardTags().contains("special")) || (AllBroadcast)) { + Bukkit.broadcastMessage(prefix + KillMsg.replaceAll("%times%", String.valueOf(times)).replaceAll("%player%", p.getDisplayName())); + } + } + else { + StringBuilder names = new StringBuilder(); + for (Entity entity : e.getEntity().getNearbyEntities(5,5,5)) { + if (entity instanceof Player) { + names.append(((Player) entity).getDisplayName()).append(","); + } + } + String name = names.toString(); + if(name.equals("")){name = nobody;} + if(name.endsWith(",")){name = name.substring(0,name.length()-1);} + if ((e.getEntity().getScoreboardTags().contains("special")) || (AllBroadcast)) { + Bukkit.broadcastMessage(prefix + KillMsg.replaceAll("%times%", String.valueOf(times)).replaceAll("%player%", name)); + } + } + } + if(UseCMD) { + if (e.getEntity().getScoreboardTags().contains("special")) { + List CMDList = plugin.getConfig().getStringList("command.special-dragon"); + for (String command : CMDList) { + if (!(p == null && command.contains("%player%"))) { + server.dispatchCommand(server.getConsoleSender(), command.replaceAll("%player%", p.getName())); + } + } + } + else { + List CMDList = plugin.getConfig().getStringList("command.normal-dragon"); + for (String command : CMDList) { + if (!(p == null && command.contains("%player%"))) { + server.dispatchCommand(server.getConsoleSender(), command.replaceAll("%player%", p.getName())); + } + } + } + } + if(e.getEntity().getScoreboardTags().contains("special")){ + e.setDroppedExp(exp); + if (EggChance) { + BukkitRunnable runnable = new BukkitRunnable() { + @Override + public void run() { + Block block = e.getEntity().getWorld().getBlockAt(x, y, z); + block.setType(Material.DRAGON_EGG); + } + }; + runnable.runTaskLater(EnderDragon.getPlugin(EnderDragon.class), delay ); + } + if(p != null){p.sendMessage(prefix + skill);} + + List datum = data.getStringList("items"); + int max = datum.size() / 2 ; + boolean warning = false; + for(int i=0 ; i Math.random() * 100; + if(judge){ + String str = datum.get(i*2-2); + ItemStack item = StringToItemStack(str); + if(p == null){ + e.getEntity().getWorld().dropItem(e.getEntity().getLocation(),item); + } + else{ + if (p.getInventory().firstEmpty() == -1) { + warning = true; + } + PlayerInventory inv = p.getInventory(); + inv.addItem(item); + } + } + } + if(warning){p.sendMessage(prefix + InvIsFull);} + } + } +} diff --git a/src/main/java/xanadu/enderdragon/events/DragonHeal.java b/src/main/java/xanadu/enderdragon/events/DragonHeal.java new file mode 100644 index 0000000..efa4679 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/events/DragonHeal.java @@ -0,0 +1,24 @@ +package xanadu.enderdragon.events; + +import org.bukkit.entity.EntityType; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityRegainHealthEvent; + +import static xanadu.enderdragon.EnderDragon.plugin; + +public class DragonHeal implements Listener { + @EventHandler + public void OnDragonHeal(EntityRegainHealthEvent e){ + if(e.getEntity().getType() != EntityType.ENDER_DRAGON){return;} + if(!e.getRegainReason().equals(EntityRegainHealthEvent.RegainReason.ENDER_CRYSTAL)){return;} + double NormalHeal = plugin.getConfig().getDouble("normal-dragon.crystal-heal"); + double SpecialHeal = plugin.getConfig().getDouble("special-dragon.crystal-heal"); + if(e.getEntity().getScoreboardTags().contains("special")){ + e.setAmount(SpecialHeal / 2); + } + else{ + e.setAmount(NormalHeal / 2); + } + } +} diff --git a/src/main/java/xanadu/enderdragon/events/DragonSpawn.java b/src/main/java/xanadu/enderdragon/events/DragonSpawn.java new file mode 100644 index 0000000..3771e58 --- /dev/null +++ b/src/main/java/xanadu/enderdragon/events/DragonSpawn.java @@ -0,0 +1,67 @@ +package xanadu.enderdragon.events; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.entity.EntityType; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.scoreboard.Team; + +import java.util.concurrent.ThreadLocalRandom; + +import static xanadu.enderdragon.EnderDragon.*; + +public class DragonSpawn implements Listener { + + @EventHandler + public void OnDragonSpawn(CreatureSpawnEvent e){ + if(e.getEntity().getType() != EntityType.ENDER_DRAGON){return;} + int times = data.getInt("times"); + double health = plugin.getConfig().getDouble("special-dragon.max-health"); + double SpawnHealth = plugin.getConfig().getDouble("special-dragon.spawn-health"); + int circle = plugin.getConfig().getInt("special-dragon.respawn-circle"); + boolean chance = plugin.getConfig().getInt("special-dragon.chance") > ThreadLocalRandom.current().nextInt(0, 100); + boolean SpecialMsg0 = plugin.getConfig().getBoolean("special-dragon.spawn-remind"); + String SpawnMsg = language.getString("dragon-spawn-broadcast"); + String SpecialMsg = language.getString("special-broadcast"); + String Name = plugin.getConfig().getString("special-dragon.name"); + String NormalName = plugin.getConfig().getString("normal-dragon.name"); + String color = plugin.getConfig().getString("special-dragon.glow-color"); + if(SpawnMsg == null){SpawnMsg = "none";} + if(!SpawnMsg.equals("none")){ + Bukkit.broadcastMessage(prefix + SpawnMsg.replaceAll("%times%", String.valueOf(times))); + } + if(times % circle == 0 && chance) { + e.getEntity().addScoreboardTag("special"); + if(health > 0) { + AttributeInstance MaxHealth = e.getEntity().getAttribute(Attribute.GENERIC_MAX_HEALTH); + assert MaxHealth != null; + MaxHealth.addModifier(new AttributeModifier("最大生命值", health - 200, AttributeModifier.Operation.ADD_NUMBER)); + } + e.getEntity().setHealth(SpawnHealth); + if (color != null) { + if(!color.equalsIgnoreCase("disable")) { + color = color.toUpperCase(); + if(server.getScoreboardManager().getMainScoreboard().getTeam("enderdragon-glow") == null) { + server.getScoreboardManager().getMainScoreboard().registerNewTeam("enderdragon-glow"); + } + Team team = server.getScoreboardManager().getMainScoreboard().getTeam("enderdragon-glow"); + team.setColor(ChatColor.valueOf(color)); + team.addEntry(e.getEntity().getUniqueId().toString()); + e.getEntity().setGlowing(true); + } + } + e.getEntity().setCustomName(Name); + if(SpecialMsg0 && SpecialMsg != null){ + Bukkit.broadcastMessage(prefix + SpecialMsg); + } + } + else{ + e.getEntity().setCustomName(NormalName); + } + } +} diff --git a/src/main/java/xanadu/enderdragon/listeners/InventoryClick.java b/src/main/java/xanadu/enderdragon/listeners/InventoryClick.java new file mode 100644 index 0000000..d1911be --- /dev/null +++ b/src/main/java/xanadu/enderdragon/listeners/InventoryClick.java @@ -0,0 +1,14 @@ +package xanadu.enderdragon.listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; + +public class InventoryClick implements Listener { + @EventHandler(priority = EventPriority.HIGH) + public void OnInventoryClick(InventoryClickEvent e){ + if(!e.getView().getTitle().contains("§b特殊龙掉落物§r")){return;} + e.setCancelled(true); + } +} diff --git a/src/main/java/xanadu/enderdragon/metrics/Metrics.java b/src/main/java/xanadu/enderdragon/metrics/Metrics.java new file mode 100644 index 0000000..d8b197e --- /dev/null +++ b/src/main/java/xanadu/enderdragon/metrics/Metrics.java @@ -0,0 +1,849 @@ +package xanadu.enderdragon.metrics; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public class Metrics { + + private final Plugin plugin; + + private final MetricsBase metricsBase; + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at What is my plugin id? + */ + public Metrics(JavaPlugin plugin, int serviceId) { + this.plugin = plugin; + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous.") + .copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + // Load the data + boolean enabled = config.getBoolean("enabled", true); + String serverUUID = config.getString("serverUuid"); + boolean logErrors = config.getBoolean("logFailedRequests", false); + boolean logSentData = config.getBoolean("logSentData", false); + boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); + metricsBase = + new MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), + plugin::isEnabled, + (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), + (message) -> this.plugin.getLogger().log(Level.INFO, message), + logErrors, + logSentData, + logResponseStatusText); + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + metricsBase.addCustomChart(chart); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("playerAmount", getPlayerAmount()); + builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); + builder.appendField("bukkitVersion", Bukkit.getVersion()); + builder.appendField("bukkitName", Bukkit.getName()); + builder.appendField("javaVersion", System.getProperty("java.version")); + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + private void appendServiceData(JsonObjectBuilder builder) { + builder.appendField("pluginVersion", plugin.getDescription().getVersion()); + } + + private int getPlayerAmount() { + try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + return onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + // Just use the new method if the reflection failed + return Bukkit.getOnlinePlayers().size(); + } + } + + public static class MetricsBase { + + /** The version of the Metrics class. */ + public static final String METRICS_VERSION = "3.0.0"; + + private static final ScheduledExecutorService scheduler = + Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); + + private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; + + private final String platform; + + private final String serverUuid; + + private final int serviceId; + + private final Consumer appendPlatformDataConsumer; + + private final Consumer appendServiceDataConsumer; + + private final Consumer submitTaskConsumer; + + private final Supplier checkServiceEnabledSupplier; + + private final BiConsumer errorLogger; + + private final Consumer infoLogger; + + private final boolean logErrors; + + private final boolean logSentData; + + private final boolean logResponseStatusText; + + private final Set customCharts = new HashSet<>(); + + private final boolean enabled; + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be {@code null}. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + public MetricsBase( + String platform, + String serverUuid, + int serviceId, + boolean enabled, + Consumer appendPlatformDataConsumer, + Consumer appendServiceDataConsumer, + Consumer submitTaskConsumer, + Supplier checkServiceEnabledSupplier, + BiConsumer errorLogger, + Consumer infoLogger, + boolean logErrors, + boolean logSentData, + boolean logResponseStatusText) { + this.platform = platform; + this.serverUuid = serverUuid; + this.serviceId = serviceId; + this.enabled = enabled; + this.appendPlatformDataConsumer = appendPlatformDataConsumer; + this.appendServiceDataConsumer = appendServiceDataConsumer; + this.submitTaskConsumer = submitTaskConsumer; + this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; + this.errorLogger = errorLogger; + this.infoLogger = infoLogger; + this.logErrors = logErrors; + this.logSentData = logSentData; + this.logResponseStatusText = logResponseStatusText; + checkRelocation(); + if (enabled) { + // WARNING: Removing the option to opt-out will get your plugin banned from bStats + startSubmitting(); + } + } + + public void addCustomChart(CustomChart chart) { + this.customCharts.add(chart); + } + + private void startSubmitting() { + final Runnable submitTask = + () -> { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown(); + return; + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(this::submitData); + } else { + this.submitData(); + } + }; + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution + // of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into the initial + // and second delay. + // WARNING: You must not modify and part of this Metrics class, including the submit delay or + // frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! + long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); + long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); + } + + private void submitData() { + final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); + appendPlatformDataConsumer.accept(baseJsonBuilder); + final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); + appendServiceDataConsumer.accept(serviceJsonBuilder); + JsonObjectBuilder.JsonObject[] chartData = + customCharts.stream() + .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) + .filter(Objects::nonNull) + .toArray(JsonObjectBuilder.JsonObject[]::new); + serviceJsonBuilder.appendField("id", serviceId); + serviceJsonBuilder.appendField("customCharts", chartData); + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); + baseJsonBuilder.appendField("serverUUID", serverUuid); + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); + JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); + scheduler.execute( + () -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e); + } + } + }); + } + + private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: " + data.toString()); + } + String url = String.format(REPORT_URL, platform); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", "Metrics-Service/1"); + 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) { + infoLogger.accept("Sent data to bStats and received response: " + builder); + } + } + + /** Checks that the class was properly relocated. */ + private void checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || !System.getProperty("bstats.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[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); + final String examplePackage = + new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure no one just copy & pastes the example and uses the wrong package + // names + if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) + || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + 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(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + 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) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + 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) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public abstract static class CustomChart { + + private final String chartId; + + protected CustomChart(String chartId) { + if (chartId == null) { + throw new IllegalArgumentException("chartId must not be null"); + } + this.chartId = chartId; + } + + public JsonObjectBuilder.JsonObject getRequestJsonObject( + BiConsumer errorLogger, boolean logErrors) { + JsonObjectBuilder builder = new JsonObjectBuilder(); + builder.appendField("chartId", chartId); + try { + JsonObjectBuilder.JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + builder.appendField("data", data); + } catch (Throwable t) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return builder.build(); + } + + protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + 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) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + 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 JsonObjectBuilder.JsonObject getChartData() throws Exception { + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + /** + * An extremely simple JSON builder. + * + *

While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + public static class JsonObjectBuilder { + + private StringBuilder builder = new StringBuilder(); + + private boolean hasAtLeastOneField = false; + + public JsonObjectBuilder() { + builder.append("{"); + } + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendNull(String key) { + appendFieldUnescaped(key, "null"); + return this; + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String value) { + if (value == null) { + throw new IllegalArgumentException("JSON value must not be null"); + } + appendFieldUnescaped(key, "\"" + escape(value) + "\""); + return this; + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int value) { + appendFieldUnescaped(key, String.valueOf(value)); + return this; + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject object) { + if (object == null) { + throw new IllegalArgumentException("JSON object must not be null"); + } + appendFieldUnescaped(key, object.toString()); + return this; + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values) + .map(value -> "\"" + escape(value) + "\"") + .collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private void appendFieldUnescaped(String key, String escapedValue) { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + if (key == null) { + throw new IllegalArgumentException("JSON key must not be null"); + } + if (hasAtLeastOneField) { + builder.append(","); + } + builder.append("\"").append(escape(key)).append("\":").append(escapedValue); + hasAtLeastOneField = true; + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + public JsonObject build() { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + JsonObject object = new JsonObject(builder.append("}").toString()); + builder = null; + return object; + } + + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private static String escape(String value) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + builder.append("\\\""); + } else if (c == '\\') { + builder.append("\\\\"); + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c)); + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * A super simple representation of a JSON object. + * + *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not + * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, + * JsonObject)}. + */ + public static class JsonObject { + + private final String value; + + private JsonObject(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..f2d468c --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,129 @@ +#配置文件版本,请勿修改 +version: 1.8.2 + +#普通末影龙 +normal-dragon: + name: '末影龙' + + #被末影水晶治疗时,末影龙每秒的回血量(原版末影龙每秒回复2点生命) + crystal-heal: 2.0 + +#特殊末影龙 +special-dragon: + name: '特殊末影龙' + + #每几次有概率复活出特殊的末影龙 + #千万不要设置该项为0!!!!!否则会引发报错 + #如果不希望复活出特殊末影龙,请设置下方的概率为0 + respawn-circle: 10 + + #生成特殊末影龙的概率,请填入0-100的整数(单位:%) + chance: 100 + + #生成特殊龙后全服提醒 + spawn-remind: true + + #若设置为false代表普通末影龙死亡也会全服播报 + #若设置为true代表仅特殊末影龙死亡会全服播报 + only-special-death-remind: false + + #特殊末影龙最大血量 + #请填入大于0的数字,否则会按原版末影龙血量生成 + #示例:“100”、“1000.0”、“200.00” + #TIPS:原版末影龙血量为200 + max-health: 400 + + #特殊末影龙生成时血量 + #请填入大于0的数字,否则会按特殊末影龙最大血量生成 + #请勿设置此值大于特殊龙最大血量(max-health) + spawn-health: 200 + + #特殊龙对玩家伤害倍数 + #请输入大于0的数字,否则会按原版末影龙伤害计算 + #示例:“0.8”、“1.25”、“3” + damage-multiply: 1.0 + + #特殊龙攻击玩家对玩家造成的药水效果 + #请严格安装'药水效果 持续时间 药水等级'的格式 + #持续时间请填入大于0的整数(单位:秒) + #药水等级取值1-256的整数,输入1即代表1级,与effect指令不同! + attack-effect: + - 'wither 10 1' + - 'hunger 60 1' + - 'weakness 30 2' + + #特殊末影龙掉落经验值 + #TIPS:原版第一次击杀末影龙掉落12000经验,此后每一次击杀掉落500经验(请填入整数) + exp-drop: 12000 + + #龙蛋生成设置 + dragon-egg-spawn: + + #生成龙蛋概率 + #请填入0-100的整数(单位:%) + #填入0代表不生成龙蛋 + chance: 100 + + #生成龙蛋延迟 + #末影龙死后多少时间生成龙蛋(单位:游戏刻,20游戏刻=1秒) + #填入大于或等于0的整数 + #末影龙死后大约20秒(400游戏刻)后末地传送门生成,因此加一个延迟可以形成龙蛋和传送门几乎一起出现的效果 + #此外,如果在龙蛋生成前玩家在下方放了火把,床之类的方块,龙蛋下落后会直接形成掉落物回主世界,加一个延迟可以减少不必要的麻烦 + delay: 410 + + #龙蛋生成坐标(末地) + #y轴设置70一般没问题,想精确的话请依照自己服务器情况而定 + x: 0 + y: 70 + z: 0 + + #特殊龙发光的颜色 + #若要关闭发光功能,设置该项为“disable” + #所有可用颜色如下(不区分大小写) + #AQUA-青色,BLACK-黑色,BLUE-蓝色,DARK_AQUA-深青,DARK_BLUE-深蓝,DARK_GRAY-深灰,DARK_GREEN-深绿,DARK_PURPLE-深紫,DARK_RED-深红,GOLD-金色,GRAY-灰色,GREEN-绿色,LIGHT_PURPLE-紫色,RED-红色,WHITE-白色,YELLOW-黄色 + glow-color: GOLD + + #玩家攻击末影龙时伤害显示 + #填入0代表不显示 + #填入1代表显示在副标题栏(会有略微视野遮挡) + #输入2代表显示在聊天栏(可能会刷屏) + #输入3代表显示在ActionBar(推荐) + damage-visible: 3 + + #指令/ed drop gui 打开的页面的标题 + drop-gui-title: '§b特殊龙掉落物§r' + + #被末影水晶治疗时,特殊龙每秒的回血量(原版末影龙被水晶治疗时每秒回复2点生命) + crystal-heal: 5.0 + + #特殊龙对其他生物造成伤害后吸血 + #吸血量=伤害*吸血率(自动按百分数计算)+基础吸血量 + suck-blood: + enable: true + + #设置为true时,特殊龙攻击其他生物时不吸血 + only-player: true + + #吸血率(单位:%) + rate: 50 + + #基础吸血量 + base-suck-blood: 10 + +command: + #是否启用在以下生物死亡后执行特定指令 + #以下指令均可用%player%代表击杀末影龙的玩家的名字 + #如果玩家从未造成过有效攻击(比如一直用床炸而从未亲自攻击),涉及%player%的这条指令将被忽略 + #举例:- 'give %player% diamond 1' + enable: false + + #普通末影龙 + normal-dragon: + - '' + - '' + - '' + #特殊龙 + special-dragon: + - '' + - '' + - '' \ No newline at end of file diff --git a/src/main/resources/data.yml b/src/main/resources/data.yml new file mode 100644 index 0000000..3be3172 --- /dev/null +++ b/src/main/resources/data.yml @@ -0,0 +1,10 @@ +#配置文件版本,请勿修改 +version: 1.8.1 + +# 已生成过的末影龙数量 +# 如果你安装本插件时,末影龙已被杀死过,请自行修改这个值(请填入整数) +# 指令召唤和指令杀死的末影龙不会被计入次数 +# 注意:插件开始使用后不需再修改这个值 +times: 1 + +items: [] \ No newline at end of file diff --git a/src/main/resources/language.yml b/src/main/resources/language.yml new file mode 100644 index 0000000..d4e28b8 --- /dev/null +++ b/src/main/resources/language.yml @@ -0,0 +1,31 @@ +#配置文件版本,请勿修改 +version: 1.8 + +#消息提示前缀 +prefix: '§7[§eEnderDragon§7]§r ' + +#末影龙生成时全服提示内容 +#仅在此处有效:可用%times%代替这条末影龙的数字序号,输入“none”代表不提示 +dragon-spawn-broadcast: '§a第 %times% 条末影龙已被复活' + +#生成特殊龙后全服提醒内容 +#只有当上一个值为true时,该提醒内容才有效 +special-broadcast: '§6这条末影龙散发着神秘的气息...' + +#末影龙被击杀时全服提示内容 +#仅在此处有效:可用%times%代替这条末影龙的数字序号,可用%player%代替击杀末影龙的玩家的名字 +#插件会智能检测周围的玩家来替换%player%,如果实在检测不到将用下方条目中的内容替代%player% +dragon-killing-broadcast: '§b第 %times% 条末影龙已被 %player% 击杀' + +#无玩家参与时,%player%将被该项代替 +nobody-kill: '玩家远程' + +#发给击杀特殊龙的玩家的话 +killer-message: '§6恭喜你,这条龙掉落了龙蛋和大量经验!' + +#每次玩家攻击末影龙时,显示给攻击者的提示 +#仅在此处有效:可用%damage%代替伤害数值 +damage-display: '对末影龙的攻击伤害:%damage%' + +#特殊末影龙成功掉落物品,但是击杀者背包已满时,显示给击杀者的提示 +player-inv-full: '§c您的背包没有空余格子,特殊末影龙战利品可能已掉落在您脚下' \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..1641ef2 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,23 @@ +name: EnderDragon +version: 1.8.2 +main: xanadu.enderdragon.EnderDragon +api-version: 1.13 +#若要将插件装在低于1.13版本的服务器,请修改api-version的值为相应版本号 +#举例:如果我要装在1.12.2的服务器,那么设置如下: +#api-version: 1.12 +commands: + enderdragon: + description: '插件主指令' + aliases: [ed] + permission: '' + permission-message: '§4你没有使用该命令的权限' +permissions: + ed.reload: + description: '允许重载插件配置文件' + default: op + ed.drop.change: + description: '允许修改特殊龙掉落物' + default: op + ed.drop.gui: + description: '允许查看特殊龙掉落物' + default: true \ No newline at end of file