diff --git a/.gitattributes b/.gitattributes index bdb0cab..412eeda 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,11 @@ # Custom for Visual Studio *.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union # Standard to msysgit *.doc diff=astextplain diff --git a/.gitignore b/.gitignore index 96374c4..fd2b366 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,41 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + .DS_Store .AppleDouble .LSOverride +# Icon must end with two \r +Icon + # Thumbnails -._* +._* + +# Personal stuff +*.project +*.classpath +/bin +/.settings # Files that might appear on external disk .Spotlight-V100 diff --git a/src/com/esophose/playerparticles/ConfigManager.java b/src/com/esophose/playerparticles/ConfigManager.java new file mode 100644 index 0000000..4c1eeb6 --- /dev/null +++ b/src/com/esophose/playerparticles/ConfigManager.java @@ -0,0 +1,186 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +import java.io.File; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import com.esophose.playerparticles.libraries.particles.ParticleEffect.ParticleType; + +public class ConfigManager { + + private static ConfigManager instance = new ConfigManager("effectData"); + private static ConfigManager styleInstance = new ConfigManager("styleData"); + private File file; + private FileConfiguration config; + + public static ConfigManager getInstance() { + return instance; + } + + public static ConfigManager getStyleInstance() { + return styleInstance; + } + + private ConfigManager(String fileName) { + if (!PlayerParticles.getPlugin().getDataFolder().exists()) PlayerParticles.getPlugin().getDataFolder().mkdir(); + + file = new File(PlayerParticles.getPlugin().getDataFolder(), fileName + ".yml"); + + if (!file.exists()) { + try { file.createNewFile(); } + catch (Exception e) { e.printStackTrace(); } + } + + config = YamlConfiguration.loadConfiguration(file); + } + + public void flushData() { + for(String key : config.getKeys(false)) { + config.set(key, null); + } + try {config.save(file);} + catch (IOException e) {e.printStackTrace();} + } + + public static void flushDatabase() { + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("TRUNCATE playerparticles;"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + public void setParticle(ParticleType type, Player player){ + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("UPDATE playerparticles SET particle = '" + type.toString().toLowerCase().replace("_", "") + "' WHERE player_name = '" + player.getName() + "';"); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + config.set(player.getName(), type.toString().toLowerCase().replace("_", "")); + try {config.save(file);} + catch (IOException e) {e.printStackTrace();} + } + } + + public void resetParticle(Player player){ + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("UPDATE playerparticles SET particle = NULL WHERE player_name = '" + player.getName() + "';"); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + config.set(player.getName(), ParticleStyle.NONE.toString().toLowerCase().replace("_", "")); + try {config.save(file);} + catch (IOException e) {e.printStackTrace();} + } + } + + public ParticleType getParticle(Player player){ + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + ResultSet res = statement.executeQuery("SELECT * FROM playerparticles WHERE player_name = '" + player.getName() + "';"); + res.next(); + return ParticleCreator.particleFromString(res.getString("particle")); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + String effectToLowerCase = (String) config.getString(player.getName()); + return ParticleCreator.particleFromString(effectToLowerCase); + } + return null; + } + + public void setStyle(ParticleStyle style, Player player) { + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("UPDATE playerparticles SET style = '" + style.toString().toLowerCase().replace("_", "") + "' WHERE player_name = '" + player.getName() + "';"); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + config.set(player.getName(), style.toString().toLowerCase().replace("_", "")); + try {config.save(file);} + catch (IOException e) {e.printStackTrace();} + } + } + + public void resetStyle(Player player) { + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("UPDATE playerparticles SET style = '" + ParticleStyle.NONE.toString().toLowerCase().replace("_", "") + "' WHERE player_name = '" + player.getName() + "';"); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + config.set(player.getName(), ParticleStyle.NONE.toString().toLowerCase().replace("_", "")); + try {config.save(file);} + catch (IOException e) {e.printStackTrace();} + } + } + + public ParticleStyle getStyle(Player player) { + if(PlayerParticles.useMySQL) { + Statement statement; + try { + statement = PlayerParticles.c.createStatement(); + ResultSet res = statement.executeQuery("SELECT * FROM playerparticles WHERE player_name = '" + player.getName() + "';"); + res.next(); + return ParticleStyle.styleFromString(res.getString("style")); + } catch (SQLException e) { + e.printStackTrace(); + } + }else{ + String styleToLowerCase = (String) config.getString(player.getName()); + return ParticleStyle.styleFromString(styleToLowerCase); + } + return ParticleStyle.NONE; + } + + @SuppressWarnings("unchecked") + public boolean isWorldDisabled(String world) { + if(PlayerParticles.getPlugin().getConfig().get("disabled-worlds") != null && ((ArrayList) PlayerParticles.getPlugin().getConfig().get("disabled-worlds")).contains(world)) { + return true; + }else return false; + } + + @SuppressWarnings("unchecked") + public ArrayList getDisabledWorlds() { + if(PlayerParticles.getPlugin().getConfig().get("disabled-worlds") != null) { + return ((ArrayList) PlayerParticles.getPlugin().getConfig().get("disabled-worlds")); + }else return null; + } + +} diff --git a/src/com/esophose/playerparticles/MessageManager.java b/src/com/esophose/playerparticles/MessageManager.java new file mode 100644 index 0000000..0d93fca --- /dev/null +++ b/src/com/esophose/playerparticles/MessageManager.java @@ -0,0 +1,54 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +public class MessageManager { + + private static MessageManager instance = new MessageManager(); + private boolean messagesEnabled, prefix, checkForUpdates; + private String messagePrefix; + + private MessageManager() { + messagesEnabled = PlayerParticles.getPlugin().getConfig().getBoolean("messages-enabled"); + prefix = PlayerParticles.getPlugin().getConfig().getBoolean("use-message-prefix"); + messagePrefix = PlayerParticles.getPlugin().getConfig().getString("message-prefix"); + messagePrefix = messagePrefix.replace("&", "§"); + checkForUpdates = PlayerParticles.getPlugin().getConfig().getBoolean("check-updates"); + } + + public void reload() { + messagesEnabled = PlayerParticles.getPlugin().getConfig().getBoolean("messages-enabled"); + prefix = PlayerParticles.getPlugin().getConfig().getBoolean("use-message-prefix"); + messagePrefix = PlayerParticles.getPlugin().getConfig().getString("message-prefix"); + messagePrefix = messagePrefix.replace("&", "§"); + checkForUpdates = PlayerParticles.getPlugin().getConfig().getBoolean("check-updates"); + } + + public static MessageManager getInstance() { + return instance; + } + + public boolean shouldCheckUpdates() { + return checkForUpdates; + } + + public void sendMessage(Player player, String message, ChatColor color) { + if(!messagesEnabled) return; + if(this.prefix){ + message = messagePrefix + color + " " + message; + }else{ + message = color + message; + } + player.sendMessage(message); + } + +} diff --git a/src/com/esophose/playerparticles/ParticleCommandCompleter.java b/src/com/esophose/playerparticles/ParticleCommandCompleter.java new file mode 100644 index 0000000..d75cd7a --- /dev/null +++ b/src/com/esophose/playerparticles/ParticleCommandCompleter.java @@ -0,0 +1,50 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +import java.util.List; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +public class ParticleCommandCompleter implements TabCompleter { + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) { + + if(cmd.getName().equalsIgnoreCase("pp")) { + + if(args.length == 1) { + + List list = PermissionHandler.getParticlesUserHasPermissionFor((Player)sender); + if(PermissionHandler.canReload((Player)sender)) list.add("reload"); + list.add("list"); + list.add("styles"); + list.add("style"); + list.add("version"); + list.add("worlds"); + list.add("help"); + return list; + + } + + if(args.length == 2) { + + return PermissionHandler.getStylesUserHasPermissionFor((Player)sender); + + } + + } + + return null; + } + +} diff --git a/src/com/esophose/playerparticles/ParticleCreator.java b/src/com/esophose/playerparticles/ParticleCreator.java new file mode 100644 index 0000000..e495882 --- /dev/null +++ b/src/com/esophose/playerparticles/ParticleCreator.java @@ -0,0 +1,446 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; + +import com.esophose.playerparticles.libraries.particles.ParticleEffect; +import com.esophose.playerparticles.libraries.particles.ParticleEffect.ParticleType; + +public class ParticleCreator extends BukkitRunnable implements Listener { + + private static HashMap map = new HashMap(); + private static HashMap styleMap = new HashMap(); + private double step = 0; + + private double helixStep = 0; + private double helixYStep = 0; + private boolean reverse = false; + + private double mysqltimer = 0; + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent e){ + if(PlayerParticles.useMySQL) { + Statement s = null; + Statement statement = null; + ResultSet res = null; + try { + s = PlayerParticles.c.createStatement(); + res = s.executeQuery("SELECT * FROM playerparticles WHERE player_name = '" + e.getPlayer().getName() + "';"); + if(!res.next()) { + statement = PlayerParticles.c.createStatement(); + statement.executeUpdate("INSERT INTO playerparticles SET player_name = '" + e.getPlayer().getName() + "', particle = NULL, style = 'none';"); + System.out.println("New player added to PlayerParticles database: " + e.getPlayer().getName()); + } + } catch (SQLException e2) { + e2.printStackTrace(); + } finally { + if(s != null) try { s.close(); } catch (SQLException e1) { e1.printStackTrace(); } + if(statement != null) try { statement.close(); } catch (SQLException e1) { e1.printStackTrace(); } + if(res != null) try { res.close(); } catch (SQLException e1) { e1.printStackTrace(); } + } + } + if(ConfigManager.getInstance().getParticle(e.getPlayer()) == null) return; + map.put(e.getPlayer().getName(), ConfigManager.getInstance().getParticle(e.getPlayer())); + styleMap.put(e.getPlayer().getName(), ConfigManager.getStyleInstance().getStyle(e.getPlayer())); + updateMap(); + updateStyleMap(); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e){ + if(map.containsKey(e.getPlayer().getName())){ + map.remove(e.getPlayer().getName()); + } + if(styleMap.containsKey(e.getPlayer().getName())) { + styleMap.remove(e.getPlayer().getName()); + } + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent e) { + if(map.containsKey(e.getPlayer().getName()) && styleMap.get(e.getPlayer().getName()) == ParticleStyle.MOVE) { + Location loc = e.getPlayer().getLocation(); + loc.setY(loc.getY() + 1); + handleStyleNone(map.get(e.getPlayer().getName()), loc); + } + } + + public static void addMap(Player player, ParticleType effect){ + map.remove(player.getName()); + map.put(player.getName(), effect); + } + + public static void removeMap(Player player){ + map.remove(player.getName()); + } + + public static void updateMap(){ + map.clear(); + for(Player player : Bukkit.getOnlinePlayers()){ + if(ConfigManager.getInstance().getParticle(player) == null) continue; + map.put(player.getName(), ConfigManager.getInstance().getParticle(player)); + } + } + + public static void addStyleMap(Player player, ParticleStyle style) { + styleMap.remove(player.getName()); + styleMap.put(player.getName(), style); + } + + public static void removeStyleMap(Player player){ + styleMap.remove(player.getName()); + } + + public static void updateStyleMap(){ + styleMap.clear(); + for(Player player : Bukkit.getOnlinePlayers()){ + styleMap.put(player.getName(), ConfigManager.getStyleInstance().getStyle(player)); + } + } + + public static ParticleType particleFromString(String particle){ + for(ParticleType effect : ParticleType.values()){ + if(effect.toString().toLowerCase().replace("_", "").equals(particle)) return effect; + } + return null; + } + + @Override + public void run() { + step++; + if(step > 30) { + step = 0; + } + helixStep++; + if(helixStep > 90) { + helixStep = 0; + } + if(reverse) { + helixYStep++; + if(helixYStep > 60) reverse = false; + }else{ + helixYStep--; + if(helixYStep < -60) reverse = true; + } + if(PlayerParticles.useMySQL) { + mysqltimer++; + if(mysqltimer > 600) { + try { + if(PlayerParticles.c != null && PlayerParticles.c.isClosed()) { + PlayerParticles.c = PlayerParticles.mySQL.openConnection(); + if(PlayerParticles.c.isClosed()) { + System.out.println("[PlayerParticles] Cannot connect to database! Is the database available and is your connection information correct?"); + } + } + } catch (SQLException | ClassNotFoundException e) { + e.printStackTrace(); + } + mysqltimer = 0; + } + } + for(Player player : Bukkit.getOnlinePlayers()){ + if(!map.containsKey(player.getName()) || ConfigManager.getInstance().isWorldDisabled(player.getWorld().getName())) continue; + ParticleType effect = map.get(player.getName()); + if(PermissionHandler.hasPermission(player, effect)){ + Location loc = player.getLocation(); + loc.setY(loc.getY() + 1); + displayParticle(effect, styleMap.get(player.getName()), loc); + } + } + } + + private void displayParticle(ParticleType effect, ParticleStyle style, Location location){ + if(style == null || style == ParticleStyle.NONE) { + handleStyleNone(effect, location); + }else if(style == ParticleStyle.SPIRAL) { + ParticleEffect particle = null; + if(effect == ParticleType.RAINBOW || effect == ParticleType.NOTE) particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 1.0F, 1); + else particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 0.0F, 1); + int points = 16; + double radius = 1; + double slice = 2 * Math.PI / points; + for(int i = 0; i < points; i++) { + double angle = slice * i; + double newX = location.getX() + radius * Math.cos(angle); + double newY = location.getY() + (step / 10) - 1; + double newZ = location.getZ() + radius * Math.sin(angle); + Location newLocation = new Location(location.getWorld(), newX, newY, newZ); + particle.display(newLocation); + } + return; + }else if(style == ParticleStyle.HALO) { + if(step % 2 == 0) return; + ParticleEffect particle = null; + if(effect == ParticleType.RAINBOW || effect == ParticleType.NOTE) particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 1.0F, 1); + else particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 0.0F, 1); + int points = 16; + double radius = .65; + double slice = 2 * Math.PI / points; + for(int i = 0; i < points; i++) { + double angle = slice * i; + double newX = location.getX() + radius * Math.cos(angle); + double newY = location.getY() + 1.5; + double newZ = location.getZ() + radius * Math.sin(angle); + Location newLocation = new Location(location.getWorld(), newX, newY, newZ); + particle.display(newLocation); + } + return; + }else if(style == ParticleStyle.POINT) { + ParticleEffect particle = null; + if(effect == ParticleType.RAINBOW || effect == ParticleType.NOTE) particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 1.0F, 1); + else particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 0.0F, 1); + particle.display(location.add(0.0, 1.5, 0.0)); + return; + }else if(style == ParticleStyle.SPIN) { + ParticleEffect particle = null; + if(effect == ParticleType.RAINBOW || effect == ParticleType.NOTE) particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 1.0F, 1); + else particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 0.0F, 1); + int points = 15; + double radius = .5; + double slice = 2 * Math.PI / points; + double angle = slice * (step % 15); + double newX = location.getX() + radius * Math.cos(angle); + double newY = location.getY() + 1.5; + double newZ = location.getZ() + radius * Math.sin(angle); + Location newLocation = new Location(location.getWorld(), newX, newY, newZ); + particle.display(newLocation); + }else if(style == ParticleStyle.QUADHELIX) { + ParticleEffect particle = null; + if(effect == ParticleType.RAINBOW || effect == ParticleType.NOTE) particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 1.0F, 1); + else particle = new ParticleEffect(effect, 0.0F, 0.0F, 0.0F, 0.0F, 1); + for(int i = 0; i < 4; i++) { + double dx = -(Math.cos((helixStep / 90) * (Math.PI * 2) + ((Math.PI / 2) * i))) * ((60 - Math.abs(helixYStep)) / 60); + double dy = ((helixYStep) / 60) * 1.5; + double dz = -(Math.sin((helixStep / 90) * (Math.PI * 2) + ((Math.PI / 2) * i))) * ((60 - Math.abs(helixYStep)) / 60); + particle.display(new Location(location.getWorld(), location.getX() + dx, location.getY() + dy, location.getZ() + dz)); + } + return; + } + } + + public void handleStyleNone(ParticleType effect, Location location) { + if(effect == null || location == null) return; + if(effect.equals(ParticleType.ANGRY_VILLAGER)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.BUBBLE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.CLOUD)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.CRIT)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DEPTH_SUSPEND)){ + ParticleEffect particle = new ParticleEffect(effect, 0.5F, 0.5F, 0.5F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DRIP_LAVA)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DRIP_WATER)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.ENCHANTMENT_TABLE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 0.05F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.EXPLODE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.FIREWORKS_SPARK)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.FLAME)){ + ParticleEffect particle = new ParticleEffect(effect, 0.1F, 0.1F, 0.1F, 0.05F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.FOOTSTEP)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.0F, 0.4F, 0.0F, 1); + particle.display(location.subtract(0, 0.98, 0)); + return; + }else + if(effect.equals(ParticleType.HAPPY_VILLAGER)){ + ParticleEffect particle = new ParticleEffect(effect, 0.5F, 0.5F, 0.5F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.HEART)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.HUGE_EXPLOSION)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.INSTANT_SPELL)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.LARGE_EXPLODE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.LARGE_SMOKE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.LAVA)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.MAGIC_CRIT)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.MOB_SPELL)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.MOB_SPELL_AMBIENT)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.NOTE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.6F, 0.6F, 0.6F, 1.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.PORTAL)){ + ParticleEffect particle = new ParticleEffect(effect, 0.5F, 0.5F, 0.5F, 0.05F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.RAINBOW)){ + ParticleEffect particle = new ParticleEffect(effect, 0.5F, 0.5F, 0.5F, 1.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.RED_DUST)){ + ParticleEffect particle = new ParticleEffect(effect, 0.5F, 0.5F, 0.5F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SLIME)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SMOKE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SNOW_SHOVEL)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SNOWBALL_POOF)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SPELL)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SUSPENDED)){ + ParticleEffect particle = new ParticleEffect(effect, 0.8F, 0.8F, 0.8F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.WAKE)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 3); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.WITCH_MAGIC)){ + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.BARRIER)){ + ParticleEffect particle = new ParticleEffect(effect, 1.2F, 1.2F, 1.2F, 0.0F, 1); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DROPLET)){ + ParticleEffect particle = new ParticleEffect(effect, 0.8F, 0.8F, 0.8F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DRAGON_BREATH)) { + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.END_ROD)) { + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.DAMAGE_INDICATOR)) { + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 5); + particle.display(location); + return; + }else + if(effect.equals(ParticleType.SWEEP_ATTACK)) { + ParticleEffect particle = new ParticleEffect(effect, 0.4F, 0.4F, 0.4F, 0.0F, 5); + particle.display(location); + return; + } + } + +} diff --git a/src/com/esophose/playerparticles/ParticleStyle.java b/src/com/esophose/playerparticles/ParticleStyle.java new file mode 100644 index 0000000..77b077d --- /dev/null +++ b/src/com/esophose/playerparticles/ParticleStyle.java @@ -0,0 +1,28 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +public enum ParticleStyle { + + NONE, + SPIRAL, + HALO, + POINT, + MOVE, + SPIN, + QUADHELIX; + + public static ParticleStyle styleFromString(String particle){ + for(ParticleStyle style : ParticleStyle.values()){ + if(style.toString().toLowerCase().replace("_", "").equals(particle)) return style; + } + return ParticleStyle.NONE; + } + +} diff --git a/src/com/esophose/playerparticles/PermissionHandler.java b/src/com/esophose/playerparticles/PermissionHandler.java new file mode 100644 index 0000000..a5f4104 --- /dev/null +++ b/src/com/esophose/playerparticles/PermissionHandler.java @@ -0,0 +1,77 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +package com.esophose.playerparticles; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.entity.Player; + +import com.esophose.playerparticles.libraries.particles.ParticleEffect.ParticleType; + +public class PermissionHandler { + + public static boolean hasPermission(Player player, ParticleType effect) { + if(player.hasPermission("playerparticles.*") || player.hasPermission("playerparticles.particles.*")) return true; + if(effect.equals(ParticleType.RED_DUST) && player.hasPermission("playerparticles.reddust")) { + return true; + }else{ + if(effect.equals(ParticleType.RED_DUST)) return false; + } + if(effect.equals(ParticleType.RAINBOW) && player.hasPermission("playerparticles.rainbow")) { + return true; + }else{ + if(effect.equals(ParticleType.RAINBOW)) return false; + } + if(player.hasPermission("playerparticles." + effect.getName().toLowerCase().replace("_", ""))) return true; + return false; + } + + public static boolean hasStylePermission(Player player, ParticleStyle style) { + if(player.hasPermission("playerparticles.*") || player.hasPermission("playerparticles.styles.*") || style == ParticleStyle.NONE) return true; + if(player.hasPermission("playerparticles.style." + style.toString().toLowerCase().replace("_", ""))) return true; + return false; + } + + public static List getParticlesUserHasPermissionFor(Player p) { + List list = new ArrayList(); + if(p.hasPermission("playerparticles.*") || p.hasPermission("playerparticles.particles.*")) { + for(ParticleType pt : ParticleType.values()) { + list.add(pt.toString().toLowerCase().replace("_", "")); + } + }else{ + for(ParticleType pt : ParticleType.values()) { + if(p.hasPermission("playerparticles." + pt.toString().toLowerCase().replace("_", ""))) list.add(pt.toString().toLowerCase().replace("_", "")); + } + } + return list; + } + + public static List getStylesUserHasPermissionFor(Player p) { + List list = new ArrayList(); + if(p.hasPermission("playerparticles.*") || p.hasPermission("playerparticles.styles.*")) { + for(ParticleStyle ps : ParticleStyle.values()) { + list.add(ps.toString().toLowerCase()); + } + }else{ + for(ParticleStyle pt : ParticleStyle.values()) { + if(p.hasPermission("playerparticles.style." + pt.toString().toLowerCase())) list.add(pt.toString().toLowerCase()); + } + } + return list; + } + + public static boolean canReload(Player p) { + if(p.hasPermission("playerparticles.reload") || p.hasPermission("playerparticles.*")) { + return true; + } + return false; + } + +} diff --git a/src/com/esophose/playerparticles/PlayerParticles.java b/src/com/esophose/playerparticles/PlayerParticles.java new file mode 100644 index 0000000..6c1dd6c --- /dev/null +++ b/src/com/esophose/playerparticles/PlayerParticles.java @@ -0,0 +1,218 @@ +/** + * Copyright Esophose 2016 + * While using any of the code provided by this plugin + * you must not claim it as your own. This plugin may + * be modified and installed on a server, but may not + * be distributed to any person by any means. + */ + +// Fixed worlds missing from /pp help +// Add style "feet" + +package com.esophose.playerparticles; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import com.esophose.playerparticles.libraries.databases.MySQL; +import com.esophose.playerparticles.libraries.particles.ParticleEffect.ParticleType; +import com.esophose.playerparticles.updater.PluginUpdateListener; +import com.esophose.playerparticles.updater.Updater; + +public class PlayerParticles extends JavaPlugin { + + public static String updateVersion = null; + + public static MySQL mySQL = null; + public static Connection c = null; + + public static boolean useMySQL = false; + + public void onEnable(){ + saveDefaultConfig(); + getCommand("pp").setTabCompleter(new ParticleCommandCompleter()); + Bukkit.getPluginManager().registerEvents(new ParticleCreator(), this); + Bukkit.getPluginManager().registerEvents(new PluginUpdateListener(), this); + if(getConfig().getDouble("version") < Double.parseDouble(getDescription().getVersion())){ + File configFile = new File(getDataFolder(), "config.yml"); + configFile.delete(); + saveDefaultConfig(); + reloadConfig(); + getLogger().warning("config.yml has been updated!"); + } + checkDatabase(); + ParticleCreator.updateMap(); + ParticleCreator.updateStyleMap(); + startTasks(); + + // Check for an update + if(MessageManager.getInstance().shouldCheckUpdates()) { + Updater updater = new Updater(this, 82823, this.getFile(), Updater.UpdateType.NO_DOWNLOAD, false); + if(Double.parseDouble(updater.getLatestName().replaceAll("PlayerParticles v", "")) > Double.parseDouble(getPlugin().getDescription().getVersion())) { + updateVersion = updater.getLatestName().replaceAll("PlayerParticles v", ""); + System.out.println("[PlayerParticles] An update (v" + updateVersion + ") is available! You are running v" + getPlugin().getDescription().getVersion()); + } + } + } + + + public static Plugin getPlugin(){ + return Bukkit.getPluginManager().getPlugin("PlayerParticles"); + } + + private void checkDatabase() { + if(getConfig().getBoolean("database-enable")) { + String hostname = getConfig().getString("database-hostname"); + String port = getConfig().getString("database-port"); + String database = getConfig().getString("database-name"); + String user = getConfig().getString("database-user-name"); + String pass = getConfig().getString("database-user-password"); + mySQL = new MySQL(hostname, port, database, user, pass); + try { + c = mySQL.openConnection(); + Statement statement = c.createStatement(); + statement.executeUpdate("CREATE TABLE IF NOT EXISTS playerparticles (player_name VARCHAR(32), particle VARCHAR(32), style VARCHAR(32));"); + useMySQL = true; + } catch (ClassNotFoundException | SQLException e) { + e.printStackTrace(); + System.out.println("Failed to connect to MySQL Database! Check to see if your config is correct!"); + useMySQL = false; + } + }else{ + useMySQL = false; + } + System.out.println("[PlayerParticles] Using mySQL for data storage: " + useMySQL); + } + + private void startTasks() { + double ticks = getConfig().getDouble("ticks-per-particle"); + if(ticks == 0.5){ + new ParticleCreator().runTaskTimer(this, 20, 1); + new ParticleCreator().runTaskTimer(this, 20, 1); + }else + new ParticleCreator().runTaskTimer(this, 20, (long) ticks); + } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + Player p = (Player) sender; + if(args.length == 1 && args[0].equalsIgnoreCase("worlds")) { + String worlds = ""; + if(ConfigManager.getInstance().getDisabledWorlds() == null) { + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-disabled-worlds-none")).replace("&", "§"), ChatColor.GREEN); + } + for(String s : ConfigManager.getInstance().getDisabledWorlds()) { + worlds += s + ", "; + } + if(worlds.length() > 2) worlds = worlds.substring(0, worlds.length() - 2); + if(worlds.equals("")) { + worlds = ((String)getConfig().get("message-disabled-worlds-none")).replace("&", "§"); + }else{ + worlds = ((String)getConfig().get("message-disabled-worlds")).replace("&", "§") + " " + ChatColor.AQUA + worlds; + } + MessageManager.getInstance().sendMessage(p, worlds, ChatColor.GREEN); + return true; + } + if(args.length > 1 && args[0].equalsIgnoreCase("style")) { + String argument = args[1].replace("_", ""); + if(ParticleStyle.styleFromString(argument) != null){ + ParticleStyle style = ParticleStyle.styleFromString(argument); + if(!PermissionHandler.hasStylePermission(p, style)) { + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-no-permission-style")).replace("{STYLE}", ChatColor.AQUA + style.toString().toLowerCase() + ChatColor.RED).replace("&", "§"), ChatColor.RED); + return true; + } + ConfigManager.getStyleInstance().setStyle(style, p); + ParticleCreator.addStyleMap(p, style); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-now-using-style")).replace("{STYLE}", ChatColor.AQUA + style.toString().toLowerCase() + ChatColor.GREEN).replace("&", "§"), ChatColor.GREEN); + return true; + } + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-invalid-type-style")).replace("&", "§") + ChatColor.GREEN + " /pp styles", ChatColor.RED); + return true; + } + if(args.length != 1){ + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-invalid-arguments")).replace("&", "§") + ChatColor.GREEN + " /pp list", ChatColor.RED); + return true; + } + String argument = args[0].replace("_", ""); + if(ParticleCreator.particleFromString(argument) != null){ + ParticleType effect = ParticleCreator.particleFromString(argument); + if(!PermissionHandler.hasPermission(p, effect)){ + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-no-permission")).replace("{PARTICLE}", ChatColor.AQUA + (effect.equals(ParticleType.RAINBOW) ? "rainbow" : effect.getName().toLowerCase() + ChatColor.RED)).replace("&", "§"), ChatColor.RED); + return true; + } + ConfigManager.getInstance().setParticle(effect, p); + ParticleCreator.addMap(p, effect); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-now-using")).replace("{PARTICLE}", ChatColor.AQUA + (effect.equals(ParticleType.RAINBOW) ? "rainbow" : effect.getName().toLowerCase() + ChatColor.GREEN)).replace("&", "§"), ChatColor.GREEN); + return true; + } + if(argument.equalsIgnoreCase("clear")) { + ConfigManager.getInstance().resetParticle(p); + ParticleCreator.removeMap(p); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-cleared-particles")).replace("&", "§"), ChatColor.GREEN); + return true; + } + if(argument.equalsIgnoreCase("version")) { + MessageManager.getInstance().sendMessage(p, "Running PlayerParticles v" + getDescription().getVersion(), ChatColor.GOLD); + MessageManager.getInstance().sendMessage(p, "Plugin created by: Esophose", ChatColor.GOLD); + return true; + } + if(argument.equalsIgnoreCase("help")) { + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-available-commands")).replace("&", "§"), ChatColor.GREEN); + MessageManager.getInstance().sendMessage(p, "list, styles, style, worlds, version, help", ChatColor.AQUA); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-usage")).replace("&", "§") + ChatColor.AQUA + " /pp ", ChatColor.YELLOW); + return true; + } + if(argument.equalsIgnoreCase("list")) { + String toSend = ((String)getConfig().get("message-use")).replace("&", "§") + " "; + for(ParticleType effect : ParticleType.values()){ + if(PermissionHandler.hasPermission(p, effect)){ + toSend = toSend + (effect.equals(ParticleType.RAINBOW) ? "rainbow" : effect.getName().toLowerCase()) + ", "; + continue; + } + } + if(toSend.equals(getConfig().get("message-use") + " ")){ + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-no-particles")).replace("&", "§"), ChatColor.RED); + return true; + } + toSend = toSend + "clear"; + MessageManager.getInstance().sendMessage(p, toSend, ChatColor.GREEN); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-usage")).replace("&", "§") + ChatColor.AQUA + " /pp ", ChatColor.YELLOW); + return true; + } + if(argument.equalsIgnoreCase("style")) { + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-invalid-type-style")).replace("&", "§") + ChatColor.GREEN + " /pp styles", ChatColor.RED); + return true; + } + if(argument.equalsIgnoreCase("styles")) { + String toSend = ((String)getConfig().get("message-use-style")).replace("&", "§") + " "; + for(ParticleStyle style : ParticleStyle.values()){ + if(PermissionHandler.hasStylePermission(p, style)){ + toSend = toSend + (style.toString().toLowerCase()) + ", "; + continue; + } + } + if(toSend.equals(((String)getConfig().get("message-use-style")).replace("&", "§") + " ")) { + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-no-styles")).replace("&", "§"), ChatColor.RED); + return true; + } + MessageManager.getInstance().sendMessage(p, toSend, ChatColor.GREEN); + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-usage")).replace("&", "§") + ChatColor.AQUA + " /pp style ", ChatColor.YELLOW); + return true; + } + + MessageManager.getInstance().sendMessage(p, ((String)getConfig().get("message-invalid-type")).replace("&", "§") + ChatColor.GREEN + " /pp list", ChatColor.RED); + + return true; + } + +} diff --git a/src/com/esophose/playerparticles/libraries/databases/Database.java b/src/com/esophose/playerparticles/libraries/databases/Database.java new file mode 100644 index 0000000..b22bc5d --- /dev/null +++ b/src/com/esophose/playerparticles/libraries/databases/Database.java @@ -0,0 +1,126 @@ +package com.esophose.playerparticles.libraries.databases; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Abstract Database class, serves as a base for any connection method (MySQL, + * SQLite, etc.) + * + * @author -_Husky_- + * @author tips48 + */ +public abstract class Database { + + protected Connection connection; + + /** + * Creates a new Database + * + */ + protected Database() { + this.connection = null; + } + + /** + * Opens a connection with the database + * + * @return Opened connection + * @throws SQLException + * if the connection can not be opened + * @throws ClassNotFoundException + * if the driver cannot be found + */ + public abstract Connection openConnection() throws SQLException, + ClassNotFoundException; + + /** + * Checks if a connection is open with the database + * + * @return true if the connection is open + * @throws SQLException + * if the connection cannot be checked + */ + public boolean checkConnection() throws SQLException { + return connection != null && !connection.isClosed(); + } + + /** + * Gets the connection with the database + * + * @return Connection with the database, null if none + */ + public Connection getConnection() { + return connection; + } + + /** + * Closes the connection with the database + * + * @return true if successful + * @throws SQLException + * if the connection cannot be closed + */ + public boolean closeConnection() throws SQLException { + if (connection == null) { + return false; + } + connection.close(); + return true; + } + + + /** + * Executes a SQL Query
+ * + * If the connection is closed, it will be opened + * + * @param query + * Query to be run + * @return the results of the query + * @throws SQLException + * If the query cannot be executed + * @throws ClassNotFoundException + * If the driver cannot be found; see {@link #openConnection()} + */ + public ResultSet querySQL(String query) throws SQLException, + ClassNotFoundException { + if (!checkConnection()) { + openConnection(); + } + + Statement statement = connection.createStatement(); + + ResultSet result = statement.executeQuery(query); + + return result; + } + + /** + * Executes an Update SQL Query
+ * See {@link java.sql.Statement#executeUpdate(String)}
+ * If the connection is closed, it will be opened + * + * @param query + * Query to be run + * @return Result Code, see {@link java.sql.Statement#executeUpdate(String)} + * @throws SQLException + * If the query cannot be executed + * @throws ClassNotFoundException + * If the driver cannot be found; see {@link #openConnection()} + */ + public int updateSQL(String query) throws SQLException, + ClassNotFoundException { + if (!checkConnection()) { + openConnection(); + } + + Statement statement = connection.createStatement(); + + int result = statement.executeUpdate(query); + + return result; + } +} \ No newline at end of file diff --git a/src/com/esophose/playerparticles/libraries/databases/MySQL.java b/src/com/esophose/playerparticles/libraries/databases/MySQL.java new file mode 100644 index 0000000..732ceeb --- /dev/null +++ b/src/com/esophose/playerparticles/libraries/databases/MySQL.java @@ -0,0 +1,78 @@ +package com.esophose.playerparticles.libraries.databases; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * Connects to and uses a MySQL database + * + * @author -_Husky_- + * @author tips48 + */ +public class MySQL extends Database { + private final String user; + private final String database; + private final String password; + private final String port; + private final String hostname; + + /** + * Creates a new MySQL instance + * + * @param hostname + * Name of the host + * @param port + * Port number + * @param username + * Username + * @param password + * Password + */ + public MySQL(String hostname, String port, String username, + String password) { + this(hostname, port, null, username, password); + } + + /** + * Creates a new MySQL instance for a specific database + * + * @param hostname + * Name of the host + * @param port + * Port number + * @param database + * Database name + * @param username + * Username + * @param password + * Password + */ + public MySQL(String hostname, String port, String database, + String username, String password) { + this.hostname = hostname; + this.port = port; + this.database = database; + this.user = username; + this.password = password; + } + + @Override + public Connection openConnection() throws SQLException, + ClassNotFoundException { + if (checkConnection()) { + return connection; + } + + String connectionURL = "jdbc:mysql://" + + this.hostname + ":" + this.port; + if (database != null) { + connectionURL = connectionURL + "/" + this.database; + } + + Class.forName("com.mysql.jdbc.Driver"); + connection = DriverManager.getConnection(connectionURL, + this.user, this.password); + return connection; + } +} diff --git a/src/com/esophose/playerparticles/libraries/particles/ParticleEffect.java b/src/com/esophose/playerparticles/libraries/particles/ParticleEffect.java new file mode 100644 index 0000000..fdb001b --- /dev/null +++ b/src/com/esophose/playerparticles/libraries/particles/ParticleEffect.java @@ -0,0 +1,439 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2014 Maxim Roncace + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.esophose.playerparticles.libraries.particles; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +/** + * Particle effects utility library + * @author Maxim Roncace + * @version 0.1.0 + */ + +/** + * Slightly modified to suit the needs + * of the plugin + * @author Esophose + */ + +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class ParticleEffect { + + private static Class packetClass = null; + private static Constructor packetConstructor = null; + private static Field[] fields = null; + private static boolean netty = true; + private static Field player_connection = null; + private static Method player_sendPacket = null; + private static HashMap, Method> handles = new HashMap, Method>(); + + private static boolean newParticlePacketConstructor = false; + private static Class enumParticle = null; + + private ParticleType type; + private double speed; + private int count; + private double offsetX, offsetY, offsetZ; + + private static boolean compatible = true; + + static { + String vString = getVersion().replace("v", ""); + double v = 0; + if (!vString.isEmpty()){ + String[] array = vString.split("_"); + v = Double.parseDouble(array[0] + "." + array[1]); + } + try { + if (v < 1.7) { + netty = false; + packetClass = getNmsClass("Packet63WorldParticles"); + packetConstructor = packetClass.getConstructor(); + fields = packetClass.getDeclaredFields(); + } + else { + packetClass = getNmsClass("PacketPlayOutWorldParticles"); + if (v < 1.8){ + Bukkit.getLogger().info("[PlayerParticles] Server is < 1.8 - Falling back to old version"); + packetConstructor = packetClass.getConstructor(String.class, float.class, float.class, float.class, + float.class, float.class, float.class, float.class, int.class); + } + else { // use the new constructor for 1.8 + Bukkit.getLogger().info("[PlayerParticles] Server is >= 1.8 - Using new version"); + newParticlePacketConstructor = true; + enumParticle = (Class)getNmsClass("EnumParticle"); + packetConstructor = packetClass.getDeclaredConstructor(enumParticle, boolean.class, float.class, + float.class, float.class, float.class, float.class, float.class, float.class, int.class, + int[].class); + } + } + } + catch (Exception ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to initialize NMS components!"); + compatible = false; + } + } + + /** + * Constructs a new particle effect for use. + *

+ * Note: different values for speed and radius may hav;e different effects + * depending on the particle's type. + *

+ * @param type the particle type + * @param speed the speed of the particles + * @param count the number of particles to spawn + * @param radius the radius of the particles + */ + public ParticleEffect(ParticleType type, double offsetX, double offsetY, double offsetZ, double speed, int count){ + this.type = type; + this.speed = speed; + this.count = count; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + /** + * Gets the name of this effect + * @return The name of this effect + */ + public String getName() { + return type.name; + } + + /** + * Gets the speed of the particles in this effect + * @return The speed of the particles in this effect + */ + public double getSpeed(){ + return speed; + } + + /** + * Retrieves the number of particles spawned by the effect + * @return The number of particles spawned by the effect + */ + public int getCount(){ + return count; + } + + /** + * Gets the offsetX of the particle effect + * @return The offsetX of the particle effect + */ + public double getOffsetX(){ + return offsetX; + } + + /** + * Gets the offsetY of the particle effect + * @return The offsetY of the particle effect + */ + public double getOffsetY(){ + return offsetY; + } + + /** + * Gets the offsetZ of the particle effect + * @return The offsetZ of the particle effect + */ + public double getOffsetZ(){ + return offsetZ; + } + + /** + * Send a particle effect to all players + * @param location The location to send the effect to + */ + public void display(Location location){ + try { + Object packet = createPacket(location); + for (Player player : Bukkit.getOnlinePlayers()){ + if(player.getLocation().getWorld().getName().equals(location.getWorld().getName())) // Patch a bug where particles will be shown in all worlds + sendPacket(player, packet); + } + } + catch (Exception e){ + e.printStackTrace(); + } + } + + /** + * Constructs a new particle packet. + * @param location the location to spawn the particle effect at + * @return the constructed packet + */ + private Object createPacket(Location location){ + try { + if (this.count <= 0){ + this.count = 1; + } + Object packet; + if (netty){ + if (newParticlePacketConstructor){ + Object particleType = enumParticle.getEnumConstants()[type.getId()]; + packet = packetConstructor.newInstance(particleType, + true, (float)location.getX(), (float)location.getY(), (float)location.getZ(), + (float)this.offsetX, (float)this.offsetY, (float)this.offsetZ, + (float)this.speed, this.count, new int[0]); + } + else { + packet = packetConstructor.newInstance(type.getName(), + (float)location.getX(), (float)location.getY(), (float)location.getZ(), + (float)this.offsetX, (float)this.offsetY, (float)this.offsetZ, + (float)this.speed, this.count); + } + } + else { + packet = packetConstructor.newInstance(); + for (Field f : fields){ + f.setAccessible(true); + if (f.getName().equals("a")) + f.set(packet, type.getName()); + else if (f.getName().equals("b")) + f.set(packet, (float)location.getX()); + else if (f.getName().equals("c")) + f.set(packet, (float)location.getY()); + else if (f.getName().equals("d")) + f.set(packet, (float)location.getZ()); + else if (f.getName().equals("e")) + f.set(packet, this.offsetX); + else if (f.getName().equals("f")) + f.set(packet, this.offsetY); + else if (f.getName().equals("g")) + f.set(packet, this.offsetZ); + else if (f.getName().equals("h")) + f.set(packet, this.speed); + else if (f.getName().equals("i")) + f.set(packet, this.count); + } + } + return packet; + } + catch (IllegalAccessException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to construct particle effect packet!"); + } + catch (InstantiationException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to construct particle effect packet!"); + } + catch (InvocationTargetException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to construct particle effect packet!"); + } + return null; + } + + /** + * Sends a packet to a player. + *

+ * Note: this method is not typesafe! + *

+ * @param p the player to send a packet to + * @param packet the packet to send + * @throws IllegalArgumentException if packet is not of a proper type + */ + private static void sendPacket(Player p, Object packet) throws IllegalArgumentException { + try { + if (player_connection == null){ + player_connection = getHandle(p).getClass().getField("playerConnection"); + for (Method m : player_connection.get(getHandle(p)).getClass().getMethods()){ + if (m.getName().equalsIgnoreCase("sendPacket")){ + player_sendPacket = m; + } + } + } + player_sendPacket.invoke(player_connection.get(getHandle(p)), packet); + } + catch (IllegalAccessException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to send packet!"); + } + catch (InvocationTargetException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to send packet!"); + } + catch (NoSuchFieldException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to send packet!"); + } + } + + /** + * Gets the NMS handle of the given {@link Entity}. + * @param entity the entity get the handle of + * @return the entity's NMS handle + */ + private static Object getHandle(Entity entity){ + try { + if (handles.get(entity.getClass()) != null) + return handles.get(entity.getClass()).invoke(entity); + else { + Method entity_getHandle = entity.getClass().getMethod("getHandle"); + handles.put(entity.getClass(), entity_getHandle); + return entity_getHandle.invoke(entity); + } + } + catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * Gets the NMS class by the given name. + * @param name the name of the NMS class to get + * @return the NMS class of the given name + */ + private static Class getNmsClass(String name){ + String version = getVersion(); + String className = "net.minecraft.server." + version + name; + Class clazz = null; + try { + clazz = Class.forName(className); + } + catch (ClassNotFoundException ex){ + ex.printStackTrace(); + Bukkit.getLogger().severe("[ParticleLib] Failed to load NMS class " + name + "!"); + } + return clazz; + } + + /** + * Determines the version string used by Craftbukkit's safeguard (e.g. 1_7_R4). + * @return the version string used by Craftbukkit's safeguard + */ + private static String getVersion(){ + String[] array = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(","); + if (array.length == 4) + return array[3] + "."; + return ""; + } + + /** + * Gets whether ParticleLib is compatible with the server software. + * @return whether ParticleLib is compatible with the server software. + */ + public static boolean isCompatible(){ + return compatible; + } + + /** + * Enum representing valid particle types in Minecraft 1.8 + */ + public enum ParticleType { + + EXPLODE("explode", 0, 17), + LARGE_EXPLODE("largeexplode", 1, 1), + HUGE_EXPLOSION("hugeexplosion", 2, 0), + FIREWORKS_SPARK("fireworksSpark", 3, 2), + BUBBLE("bubble", 4, 3), + WAKE("wake", 6, -1), + SUSPENDED("suspended", 7, 4), + DEPTH_SUSPEND("depthsuspend", 8, 5), + CRIT("crit", 9, 7), + MAGIC_CRIT("magicCrit", 10, 8), + SMOKE("smoke", 11, -1), + LARGE_SMOKE("largesmoke", 12, 22), + SPELL("spell", 13, 11), + INSTANT_SPELL("instantSpell", 14, 12), + MOB_SPELL("mobSpell", 15, 9), + MOB_SPELL_AMBIENT("mobSpellAmbient", 16, 10), + WITCH_MAGIC("witchMagic", 17, 13), + DRIP_WATER("dripWater", 18, 27), + DRIP_LAVA("dripLava", 19, 28), + ANGRY_VILLAGER("angryVillager", 20, 31), + HAPPY_VILLAGER("happyVillager", 21, 32), + NOTE("note", 23, 24), + PORTAL("portal", 24, 15), + ENCHANTMENT_TABLE("enchantmenttable", 25, 16), + FLAME("flame", 26, 18), + LAVA("lava", 27, 19), + FOOTSTEP("footstep", 28, 20), + CLOUD("cloud", 29, 23), + RED_DUST("reddust", 30, 24), + SNOWBALL_POOF("snowballpoof", 31, 25), + SNOW_SHOVEL("snowshovel", 32, 28), + SLIME("slime", 33, 29), + HEART("heart", 34, 30), + BARRIER("barrier", 35, -1), + DROPLET("droplet", 39, -1), + DRAGON_BREATH("dragonbreath", 42, -1), + END_ROD("endRod", 43, -1), + DAMAGE_INDICATOR("damageIndicator", 44, -1), + SWEEP_ATTACK("sweepAttack", 45, -1), + RAINBOW("rainbow", 30, -1); + + private String name; + private int id; + private int legacyId; + + ParticleType(String name, int id, int legacyId){ + this.name = name; + this.id = id; + this.legacyId = legacyId; + } + + /** + * Gets the name of the particle effect + * + * @return The name of the particle effect + */ + public String getName(){ + return name; + } + + /** + * Gets the ID of the particle effect + * + * @return The ID of the particle effect + */ + int getId(){ + return id; + } + + /** + * Gets the legacy ID (pre-1.8) of the particle effect + * + * @return the legacy ID of the particle effect (or -1 if introduced after 1.7) + */ + int getLegacyId(){ + return legacyId; + } + } + +} \ No newline at end of file diff --git a/src/com/esophose/playerparticles/updater/PluginUpdateListener.java b/src/com/esophose/playerparticles/updater/PluginUpdateListener.java new file mode 100644 index 0000000..a9136b9 --- /dev/null +++ b/src/com/esophose/playerparticles/updater/PluginUpdateListener.java @@ -0,0 +1,23 @@ +package com.esophose.playerparticles.updater; + +import org.bukkit.ChatColor; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import com.esophose.playerparticles.MessageManager; +import com.esophose.playerparticles.PlayerParticles; + +public class PluginUpdateListener implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent e) { + if(e.getPlayer().isOp()) { + if(PlayerParticles.updateVersion != null) { + MessageManager.getInstance().sendMessage(e.getPlayer(), "An update (" + ChatColor.AQUA + "v" + PlayerParticles.updateVersion + ChatColor.YELLOW + ") is available! You are running " + ChatColor.AQUA + "v" + PlayerParticles.getPlugin().getDescription().getVersion(), ChatColor.YELLOW); + MessageManager.getInstance().sendMessage(e.getPlayer(), "Download from: http://dev.bukkit.org/bukkit-plugins/playerparticles/", ChatColor.YELLOW); + } + } + } + +} diff --git a/src/com/esophose/playerparticles/updater/Updater.java b/src/com/esophose/playerparticles/updater/Updater.java new file mode 100644 index 0000000..f87b8ae --- /dev/null +++ b/src/com/esophose/playerparticles/updater/Updater.java @@ -0,0 +1,751 @@ +package com.esophose.playerparticles.updater; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +/** + * Check for updates on BukkitDev for a given plugin, and download the updates if needed. + *

+ * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. + *
+ * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. + *
+ * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. + *

+ * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. + *
+ * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l + * + * @author Gravity + * @version 2.3 + */ + +public class Updater { + + /* Constants */ + + // Remote file's title + private static final String TITLE_VALUE = "name"; + // Remote file's download link + private static final String LINK_VALUE = "downloadUrl"; + // Remote file's release type + private static final String TYPE_VALUE = "releaseType"; + // Remote file's build version + private static final String VERSION_VALUE = "gameVersion"; + // Path to GET + private static final String QUERY = "/servermods/files?projectIds="; + // Slugs will be appended to this to get to the project's RSS feed + private static final String HOST = "https://api.curseforge.com"; + // User-agent when querying Curse + private static final String USER_AGENT = "Updater (by Gravity)"; + // Used for locating version numbers in file names + private static final String DELIMETER = "^v|[\\s_-]v"; + // If the version number contains one of these, don't update. + private static final String[] NO_UPDATE_TAG = { "-DEV", "-PRE", "-SNAPSHOT" }; + // Used for downloading files + private static final int BYTE_SIZE = 1024; + // Config key for api key + private static final String API_KEY_CONFIG_KEY = "api-key"; + // Config key for disabling Updater + private static final String DISABLE_CONFIG_KEY = "disable"; + // Default api key value in config + private static final String API_KEY_DEFAULT = "PUT_API_KEY_HERE"; + // Default disable value in config + private static final boolean DISABLE_DEFAULT = false; + + /* User-provided variables */ + + // Plugin running Updater + private final Plugin plugin; + // Type of update check to run + private final UpdateType type; + // Whether to announce file downloads + private final boolean announce; + // The plugin file (jar) + private final File file; + // The folder that downloads will be placed in + private final File updateFolder; + // The provided callback (if any) + private final UpdateCallback callback; + // Project's Curse ID + private int id = -1; + // BukkitDev ServerMods API key + private String apiKey = null; + + /* Collected from Curse API */ + + private String versionName; + private String versionLink; + private String versionType; + private String versionGameVersion; + + /* Update process variables */ + + // Connection to RSS + private URL url; + // Updater thread + private Thread thread; + // Used for determining the outcome of the update process + private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; + + /** + * Gives the developer the result of the update process. Can be obtained by called {@link #getResult()} + */ + public enum UpdateResult { + /** + * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. + */ + SUCCESS, + /** + * The updater did not find an update, and nothing was downloaded. + */ + NO_UPDATE, + /** + * The server administrator has disabled the updating system. + */ + DISABLED, + /** + * The updater found an update, but was unable to download it. + */ + FAIL_DOWNLOAD, + /** + * For some reason, the updater was unable to contact dev.bukkit.org to download the file. + */ + FAIL_DBO, + /** + * When running the version check, the file on DBO did not contain a recognizable version. + */ + FAIL_NOVERSION, + /** + * The id provided by the plugin running the updater was invalid and doesn't exist on DBO. + */ + FAIL_BADID, + /** + * The server administrator has improperly configured their API key in the configuration. + */ + FAIL_APIKEY, + /** + * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. + */ + UPDATE_AVAILABLE + } + + /** + * Allows the developer to specify the type of update that will be run. + */ + public enum UpdateType { + /** + * Run a version check, and then if the file is out of date, download the newest version. + */ + DEFAULT, + /** + * Don't run a version check, just find the latest update and download it. + */ + NO_VERSION_CHECK, + /** + * Get information about the version and the download size, but don't actually download anything. + */ + NO_DOWNLOAD + } + + /** + * Represents the various release types of a file on BukkitDev. + */ + public enum ReleaseType { + /** + * An "alpha" file. + */ + ALPHA, + /** + * A "beta" file. + */ + BETA, + /** + * A "release" file. + */ + RELEASE + } + + /** + * Initialize the updater. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param announce True if the program should announce the progress of new updates in console. + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, boolean announce) { + this(plugin, id, file, type, null, announce); + } + + /** + * Initialize the updater with the provided callback. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param callback The callback instance to notify when the Updater has finished + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback) { + this(plugin, id, file, type, callback, false); + } + + /** + * Initialize the updater with the provided callback. + * + * @param plugin The plugin that is checking for an update. + * @param id The dev.bukkit.org id of the project. + * @param file The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type Specify the type of update this will be. See {@link UpdateType} + * @param callback The callback instance to notify when the Updater has finished + * @param announce True if the program should announce the progress of new updates in console. + */ + public Updater(Plugin plugin, int id, File file, UpdateType type, UpdateCallback callback, boolean announce) { + this.plugin = plugin; + this.type = type; + this.announce = announce; + this.file = file; + this.id = id; + this.updateFolder = this.plugin.getServer().getUpdateFolderFile(); + this.callback = callback; + + final File pluginFile = this.plugin.getDataFolder().getParentFile(); + final File updaterFile = new File(pluginFile, "Updater"); + final File updaterConfigFile = new File(updaterFile, "config.yml"); + + YamlConfiguration config = new YamlConfiguration(); + config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n' + + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n' + + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration."); + config.addDefault(API_KEY_CONFIG_KEY, API_KEY_DEFAULT); + config.addDefault(DISABLE_CONFIG_KEY, DISABLE_DEFAULT); + + if (!updaterFile.exists()) { + this.fileIOOrError(updaterFile, updaterFile.mkdir(), true); + } + + boolean createFile = !updaterConfigFile.exists(); + try { + if (createFile) { + this.fileIOOrError(updaterConfigFile, updaterConfigFile.createNewFile(), true); + config.options().copyDefaults(true); + config.save(updaterConfigFile); + } else { + config.load(updaterConfigFile); + } + } catch (final Exception e) { + final String message; + if (createFile) { + message = "The updater could not create configuration at " + updaterFile.getAbsolutePath(); + } else { + message = "The updater could not load configuration at " + updaterFile.getAbsolutePath(); + } + this.plugin.getLogger().log(Level.SEVERE, message, e); + } + + if (config.getBoolean(DISABLE_CONFIG_KEY)) { + this.result = UpdateResult.DISABLED; + return; + } + + String key = config.getString(API_KEY_CONFIG_KEY); + if (API_KEY_DEFAULT.equalsIgnoreCase(key) || "".equals(key)) { + key = null; + } + + this.apiKey = key; + + try { + this.url = new URL(Updater.HOST + Updater.QUERY + this.id); + } catch (final MalformedURLException e) { + this.plugin.getLogger().log(Level.SEVERE, "The project ID provided for updating, " + this.id + " is invalid.", e); + this.result = UpdateResult.FAIL_BADID; + } + + if (this.result != UpdateResult.FAIL_BADID) { + this.thread = new Thread(new UpdateRunnable()); + this.thread.start(); + } else { + runUpdater(); + } + } + + /** + * Get the result of the update process. + * + * @return result of the update process. + * @see UpdateResult + */ + public Updater.UpdateResult getResult() { + this.waitForThread(); + return this.result; + } + + /** + * Get the latest version's release type. + * + * @return latest version's release type. + * @see ReleaseType + */ + public ReleaseType getLatestType() { + this.waitForThread(); + if (this.versionType != null) { + for (ReleaseType type : ReleaseType.values()) { + if (this.versionType.equalsIgnoreCase(type.name())) { + return type; + } + } + } + return null; + } + + /** + * Get the latest version's game version (such as "CB 1.2.5-R1.0"). + * + * @return latest version's game version. + */ + public String getLatestGameVersion() { + this.waitForThread(); + return this.versionGameVersion; + } + + /** + * Get the latest version's name (such as "Project v1.0"). + * + * @return latest version's name. + */ + public String getLatestName() { + this.waitForThread(); + return this.versionName; + } + + /** + * Get the latest version's direct file link. + * + * @return latest version's file link. + */ + public String getLatestFileLink() { + this.waitForThread(); + return this.versionLink; + } + + /** + * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish + * before allowing anyone to check the result. + */ + private void waitForThread() { + if ((this.thread != null) && this.thread.isAlive()) { + try { + this.thread.join(); + } catch (final InterruptedException e) { + this.plugin.getLogger().log(Level.SEVERE, null, e); + } + } + } + + /** + * Save an update from dev.bukkit.org into the server's update folder. + * + * @param file the name of the file to save it as. + */ + private void saveFile(String file) { + final File folder = this.updateFolder; + + deleteOldFiles(); + if (!folder.exists()) { + this.fileIOOrError(folder, folder.mkdir(), true); + } + downloadFile(); + + // Check to see if it's a zip file, if it is, unzip it. + final File dFile = new File(folder.getAbsolutePath(), file); + if (dFile.getName().endsWith(".zip")) { + // Unzip + this.unzip(dFile.getAbsolutePath()); + } + if (this.announce) { + this.plugin.getLogger().info("Finished updating."); + } + } + + /** + * Download a file and save it to the specified folder. + */ + private void downloadFile() { + BufferedInputStream in = null; + FileOutputStream fout = null; + try { + URL fileUrl = new URL(this.versionLink); + final int fileLength = fileUrl.openConnection().getContentLength(); + in = new BufferedInputStream(fileUrl.openStream()); + fout = new FileOutputStream(new File(this.updateFolder, file.getName())); + + final byte[] data = new byte[Updater.BYTE_SIZE]; + int count; + if (this.announce) { + this.plugin.getLogger().info("About to download a new update: " + this.versionName); + } + long downloaded = 0; + while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) { + downloaded += count; + fout.write(data, 0, count); + final int percent = (int) ((downloaded * 100) / fileLength); + if (this.announce && ((percent % 10) == 0)) { + this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes."); + } + } + } catch (Exception ex) { + this.plugin.getLogger().log(Level.WARNING, "The auto-updater tried to download a new update, but was unsuccessful.", ex); + this.result = Updater.UpdateResult.FAIL_DOWNLOAD; + } finally { + try { + if (in != null) { + in.close(); + } + } catch (final IOException ex) { + this.plugin.getLogger().log(Level.SEVERE, null, ex); + } + try { + if (fout != null) { + fout.close(); + } + } catch (final IOException ex) { + this.plugin.getLogger().log(Level.SEVERE, null, ex); + } + } + } + + /** + * Remove possibly leftover files from the update folder. + */ + private void deleteOldFiles() { + //Just a quick check to make sure we didn't leave any files from last time... + File[] list = listFilesOrError(this.updateFolder); + for (final File xFile : list) { + if (xFile.getName().endsWith(".zip")) { + this.fileIOOrError(xFile, xFile.mkdir(), true); + } + } + } + + /** + * Part of Zip-File-Extractor, modified by Gravity for use with Updater. + * + * @param file the location of the file to extract. + */ + private void unzip(String file) { + final File fSourceZip = new File(file); + try { + final String zipPath = file.substring(0, file.length() - 4); + ZipFile zipFile = new ZipFile(fSourceZip); + Enumeration e = zipFile.entries(); + while (e.hasMoreElements()) { + ZipEntry entry = e.nextElement(); + File destinationFilePath = new File(zipPath, entry.getName()); + this.fileIOOrError(destinationFilePath.getParentFile(), destinationFilePath.getParentFile().mkdirs(), true); + if (!entry.isDirectory()) { + final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); + int b; + final byte[] buffer = new byte[Updater.BYTE_SIZE]; + final FileOutputStream fos = new FileOutputStream(destinationFilePath); + final BufferedOutputStream bos = new BufferedOutputStream(fos, Updater.BYTE_SIZE); + while ((b = bis.read(buffer, 0, Updater.BYTE_SIZE)) != -1) { + bos.write(buffer, 0, b); + } + bos.flush(); + bos.close(); + bis.close(); + final String name = destinationFilePath.getName(); + if (name.endsWith(".jar") && this.pluginExists(name)) { + File output = new File(this.updateFolder, name); + this.fileIOOrError(output, destinationFilePath.renameTo(output), true); + } + } + } + zipFile.close(); + + // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. + moveNewZipFiles(zipPath); + + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "The auto-updater tried to unzip a new update file, but was unsuccessful.", e); + this.result = Updater.UpdateResult.FAIL_DOWNLOAD; + } finally { + this.fileIOOrError(fSourceZip, fSourceZip.delete(), false); + } + } + + /** + * Find any new files extracted from an update into the plugin's data directory. + * @param zipPath path of extracted files. + */ + private void moveNewZipFiles(String zipPath) { + File[] list = listFilesOrError(new File(zipPath)); + for (final File dFile : list) { + if (dFile.isDirectory() && this.pluginExists(dFile.getName())) { + // Current dir + final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); + // List of existing files in the new dir + final File[] dList = listFilesOrError(dFile); + // List of existing files in the current dir + final File[] oList = listFilesOrError(oFile); + for (File cFile : dList) { + // Loop through all the files in the new dir + boolean found = false; + for (final File xFile : oList) { + // Loop through all the contents in the current dir to see if it exists + if (xFile.getName().equals(cFile.getName())) { + found = true; + break; + } + } + if (!found) { + // Move the new file into the current dir + File output = new File(oFile, cFile.getName()); + this.fileIOOrError(output, cFile.renameTo(output), true); + } else { + // This file already exists, so we don't need it anymore. + this.fileIOOrError(cFile, cFile.delete(), false); + } + } + } + this.fileIOOrError(dFile, dFile.delete(), false); + } + File zip = new File(zipPath); + this.fileIOOrError(zip, zip.delete(), false); + } + + /** + * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. + * + * @param name a name to check for inside the plugins folder. + * @return true if a file inside the plugins folder is named this. + */ + private boolean pluginExists(String name) { + File[] plugins = listFilesOrError(new File("plugins")); + for (final File file : plugins) { + if (file.getName().equals(name)) { + return true; + } + } + return false; + } + + /** + * Check to see if the program should continue by evaluating whether the plugin is already updated, or shouldn't be updated. + * + * @return true if the version was located and is not the same as the remote's newest. + */ + private boolean versionCheck() { + final String title = this.versionName; + if (this.type != UpdateType.NO_VERSION_CHECK) { + final String localVersion = this.plugin.getDescription().getVersion(); + if (title.split(DELIMETER).length >= 2) { + // Get the newest file's version number + final String remoteVersion = title.split(DELIMETER)[title.split(DELIMETER).length - 1].split(" ")[0]; + + if (this.hasTag(localVersion) || !this.shouldUpdate(localVersion, remoteVersion)) { + // We already have the latest version, or this build is tagged for no-update + this.result = Updater.UpdateResult.NO_UPDATE; + return false; + } + } else { + // The file's name did not contain the string 'vVersion' + final String authorInfo = this.plugin.getDescription().getAuthors().isEmpty() ? "" : " (" + this.plugin.getDescription().getAuthors().get(0) + ")"; + this.plugin.getLogger().warning("The author of this plugin" + authorInfo + " has misconfigured their Auto Update system"); + this.plugin.getLogger().warning("File versions should follow the format 'PluginName vVERSION'"); + this.plugin.getLogger().warning("Please notify the author of this error."); + this.result = Updater.UpdateResult.FAIL_NOVERSION; + return false; + } + } + return true; + } + + /** + * If you wish to run mathematical versioning checks, edit this method. + *

+ * With default behavior, Updater will NOT verify that a remote version available on BukkitDev + * which is not this version is indeed an "update". + * If a version is present on BukkitDev that is not the version that is currently running, + * Updater will assume that it is a newer version. + * This is because there is no standard versioning scheme, and creating a calculation that can + * determine whether a new update is actually an update is sometimes extremely complicated. + *

+ *

+ * Updater will call this method from {@link #versionCheck()} before deciding whether + * the remote version is actually an update. + * If you have a specific versioning scheme with which a mathematical determination can + * be reliably made to decide whether one version is higher than another, you may + * revise this method, using the local and remote version parameters, to execute the + * appropriate check. + *

+ *

+ * Returning a value of false will tell the update process that this is NOT a new version. + * Without revision, this method will always consider a remote version at all different from + * that of the local version a new update. + *

+ * @param localVersion the current version + * @param remoteVersion the remote version + * @return true if Updater should consider the remote version an update, false if not. + */ + public boolean shouldUpdate(String localVersion, String remoteVersion) { + return !localVersion.equalsIgnoreCase(remoteVersion); + } + + /** + * Evaluate whether the version number is marked showing that it should not be updated by this program. + * + * @param version a version number to check for tags in. + * @return true if updating should be disabled. + */ + private boolean hasTag(String version) { + for (final String string : Updater.NO_UPDATE_TAG) { + if (version.contains(string)) { + return true; + } + } + return false; + } + + /** + * Make a connection to the BukkitDev API and request the newest file's details. + * + * @return true if successful. + */ + private boolean read() { + try { + final URLConnection conn = this.url.openConnection(); + conn.setConnectTimeout(5000); + + if (this.apiKey != null) { + conn.addRequestProperty("X-API-Key", this.apiKey); + } + conn.addRequestProperty("User-Agent", Updater.USER_AGENT); + + conn.setDoOutput(true); + + final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + final String response = reader.readLine(); + + final JSONArray array = (JSONArray) JSONValue.parse(response); + + if (array.isEmpty()) { + this.plugin.getLogger().warning("The updater could not find any files for the project id " + this.id); + this.result = UpdateResult.FAIL_BADID; + return false; + } + + JSONObject latestUpdate = (JSONObject) array.get(array.size() - 1); + this.versionName = (String) latestUpdate.get(Updater.TITLE_VALUE); + this.versionLink = (String) latestUpdate.get(Updater.LINK_VALUE); + this.versionType = (String) latestUpdate.get(Updater.TYPE_VALUE); + this.versionGameVersion = (String) latestUpdate.get(Updater.VERSION_VALUE); + + return true; + } catch (final IOException e) { + if (e.getMessage().contains("HTTP response code: 403")) { + this.plugin.getLogger().severe("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml"); + this.plugin.getLogger().severe("Please double-check your configuration to ensure it is correct."); + this.result = UpdateResult.FAIL_APIKEY; + } else { + this.plugin.getLogger().severe("The updater could not contact dev.bukkit.org for updating."); + this.plugin.getLogger().severe("If you have not recently modified your configuration and this is the first time you are seeing this message, the site may be experiencing temporary downtime."); + this.result = UpdateResult.FAIL_DBO; + } + this.plugin.getLogger().log(Level.SEVERE, null, e); + return false; + } + } + + /** + * Perform a file operation and log any errors if it fails. + * @param file file operation is performed on. + * @param result result of file operation. + * @param create true if a file is being created, false if deleted. + */ + private void fileIOOrError(File file, boolean result, boolean create) { + if (!result) { + this.plugin.getLogger().severe("The updater could not " + (create ? "create" : "delete") + " file at: " + file.getAbsolutePath()); + } + } + + private File[] listFilesOrError(File folder) { + File[] contents = folder.listFiles(); + if (contents == null) { + this.plugin.getLogger().severe("The updater could not access files at: " + this.updateFolder.getAbsolutePath()); + return new File[0]; + } else { + return contents; + } + } + + /** + * Called on main thread when the Updater has finished working, regardless + * of result. + */ + public interface UpdateCallback { + /** + * Called when the updater has finished working. + * @param updater The updater instance + */ + void onFinish(Updater updater); + } + + private class UpdateRunnable implements Runnable { + @Override + public void run() { + runUpdater(); + } + } + + private void runUpdater() { + if (this.url != null && (this.read() && this.versionCheck())) { + // Obtain the results of the project's file feed + if ((this.versionLink != null) && (this.type != UpdateType.NO_DOWNLOAD)) { + String name = this.file.getName(); + // If it's a zip file, it shouldn't be downloaded as the plugin's name + if (this.versionLink.endsWith(".zip")) { + name = this.versionLink.substring(this.versionLink.lastIndexOf("/") + 1); + } + this.saveFile(name); + } else { + this.result = UpdateResult.UPDATE_AVAILABLE; + } + } + + if (this.callback != null) { + new BukkitRunnable() { + @Override + public void run() { + runCallback(); + } + }.runTask(this.plugin); + } + } + + private void runCallback() { + this.callback.onFinish(this); + } +} \ No newline at end of file diff --git a/src/config.yml b/src/config.yml new file mode 100644 index 0000000..0a76d9c --- /dev/null +++ b/src/config.yml @@ -0,0 +1,178 @@ +# __________.__ __________ __ .__ .__ ________ +# \______ \ | _____ ___.__. __________\______ \_____ ________/ |_|__| ____ | | ____ ______ ___ _\_____ \ +# | ___/ | \__ \< | |/ __ \_ __ \ ___/\__ \\_ __ \ __\ |/ ___\| | _/ __ \ / ___/ \ \/ / _(__ < +# | | | |__/ __ \\___ \ ___/| | \/ | / __ \| | \/| | | \ \___| |_\ ___/ \___ \ \ / / \ +# |____| |____(____ / ____|\___ >__| |____| (____ /__| |__| |__|\___ >____/\___ >____ > \_/ /______ / +# \/\/ \/ \/ \/ \/ \/ \/ + +# ====================================================# +# PlayerParticles Config # +# DO NOT DELETE OR EDIT THE FIELD "version"! # +# ====================================================# + +# DO NOT CHANGE THIS UNDER ANY CIRCUMSTANCE (Will reset your config) +version: 3.7 + +# There are 20 minecraft ticks per second +# The default value of 20 means a particle will be displayed every tick +# That means 20 particles will be displayed per second +# If ticks-per-particle was set to 5, then 4 particles would be displayed per second since 20/5 is 4 +# If your server is experiencing lag after installing this plugin, raising this value should help +# Setting this value to 0.5 will cause 40 particles to appear per second. Be warned this could cause strain on the server. +# Changing this 'may' cause style timing to get messed up +# Only use integer values, excluding 0.5 +# Default: 1 +ticks-per-particle: 1 + +# Check for new versions of the plugin +# Default: true +check-updates: true + +# The worlds which this plugin is disabled in +# Default: [] +disabled-worlds: [] +# - your_world_name_here +# - add_more_under_these + +# If you're using other plugins to execute commands you may wish to turn off messages +# Default: true +messages-enabled: true + +# Whether or not to use the message-prefix field when displaying messages +# Default: true +use-message-prefix: true + +# The prefix to use for all PlayerParticle messages +# Use & to set color / format +# This is useless if use-message-prefix is set to false +# Default: '&c[&ePlayerParticles&c]' +message-prefix: '&7[&3PlayerParticles&7]' + +# ================================================================ # +# MESSAGE CONFIGURATION # +# Important Notes: # +# * {PARTICLE} displays the particle argument used in the command # +# * {STYLE} displays the style argument used in the command # +# * You can not use the apostrophe character! ( ' ) # +# ================================================================ # + +################# +# Particles # +################# + +# No Particle Permission +# Default: 'You do not have permission to use type {PARTICLE} particles!' +message-no-permission: 'You do not have permission to use type {PARTICLE} particles!' + +# /pp list No Particles +# Default: 'You do not have permission to use any particles!' +message-no-particles: 'You do not have permission to use any particles!' + +# Now Using Particles +# Default: 'Now using type {PARTICLE} particles!' +message-now-using: 'Now using type {PARTICLE} particles!' + +# Cleared Particles +# Default: 'Cleared your particles!' +message-cleared-particles: 'Cleared your particles!' + +# You Can Use Particles +# Default: 'You can use:' +message-use: 'You can use:' + +# Invalid Particle Type +# Default: 'Invalid particle type!' +message-invalid-type: 'Invalid particle type!' + +################## +# Styles # +################## + +# No Style Permission +# Default: 'You do not have permission to use the style type {STYLE}!' +message-no-permission-style: 'You do not have permission to use the style type {STYLE}!' + +# /pp styles No Styles +# Default: 'You do not have permission to use any particles!' +message-no-styles: 'You do not have permission to use any styles!' + +# Now Using Style +# Default: 'Now using style type {STYLE}!' +message-now-using-style: 'Now using style type {STYLE}!' + +# Cleared Style +# Default: 'Cleared your particles!' +message-cleared-style: 'Cleared your style!' + +# You Can Use Styles +# Default: 'You can use:' +message-use-style: 'You can use:' + +# Invalid Style Type +# Default: 'Invalid style type!' +message-invalid-type-style: 'Invalid style type!' + +################# +# Other # +################# + +# Usage +# Default: 'Usage:' +message-usage: 'Usage:' + +# Invalid Arguments +# Default: 'Invalid arguments!' +message-invalid-arguments: 'Invalid Arguments!' + +# Reload +# Default: 'Reloaded Config!' +message-reload: 'Reloaded Config!' + +# Available Commands +# Default: 'Available Commands:' +message-available-commands: 'Available Commands:' + +# Disabled Worlds None +# Default: 'Particles are not disabled in any worlds!' +message-disabled-worlds-none: 'Particles are not disabled in any worlds!' + +# Disabled Worlds +# Default: 'Particles are disabled in these worlds:' +message-disabled-worlds: 'Particles are disabled in these worlds:' + +# ================================================================ # +# DATABASE CONFIGURATION # +# Information: # +# * This is meant for people who have multiple servers connected # +# together through BungeeCord. Unless you have multiple servers, # +# it is recommended to keep the database storage disabled for # +# the best performance! # +# ================================================================ # + +# Enable Database +# Default: false +database-enable: false + +# =================================================================== # +# The following are only required if database-enable is set to 'true' # +# =================================================================== # + +# Database Hostname +# Default: '' +database-hostname: '' + +# Database Port +# Default: 3306 +database-port: 3306 + +# Database Name +# Default: '' +database-name: '' + +# Database User Name +# Default: '' +database-user-name: '' + +# Database User Password +# Default: '' +database-user-password: '' \ No newline at end of file diff --git a/src/plugin.yml b/src/plugin.yml new file mode 100644 index 0000000..d9d2d1f --- /dev/null +++ b/src/plugin.yml @@ -0,0 +1,7 @@ +name: PlayerParticles +main: com.esophose.playerparticles.PlayerParticles +version: 3.7 +description: Make Particles Around Players +commands: + pp: + description: Particles Command. \ No newline at end of file