diff --git a/.idea/misc.xml b/.idea/misc.xml index 9b2882f..5d55df2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 2777d9f..3267468 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,25 @@ This plugin adds naturally spawning Giants with AI to your Minecraft server. ### Signing Public key goes into `resources/verifies_downloaded_jars.pem` -A test (and default) keystore is provided: -- keystore: `testkeystore` -- storepass: `123456` -- alias: `testkey` - -When using `mvn`, override with `-Djarsigner.` -``` -mvn clean package -Djarsigner.keystore=/home/user/mykeystore.jks -Djarsigner.alias=mykey -``` +A default keystore is not provided. To create a keystore and export public key: ``` keytool -keystore testkeystore2.jks -genkeypair -keyalg RSA -alias testkey -validity 999999 keytool -exportcert -alias testkey -keystore testkeystore2.jks -file cert.cer -rfc openssl x509 -inform pem -in cert.cer -pubkey -noout > public_key.pem -``` \ No newline at end of file +``` + +When using `mvn`, override with `-Djarsigner.` +``` +mvn clean package -Djarsigner.keystore=/home/user/mykeystore.jks -Djarsigner.alias=mykey +``` + +### Color scheme +The following color scheme is used for chat messages: +- Errors: RED +- "Soft errors" (like no permission): GRAY +- Status messages: GRAY +- Notice / call for action: YELLOW (optionally also BOLD) +- Information: GOLD +- Highlight: AQUA (TODO do that) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 15d2523..b8c0218 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,12 @@ 4.0.0 eu.m724 giants - 2.0.8 + 2.0.12-SNAPSHOT - 11 - ${project.basedir}/testkeystore.jks - testkey + 17 + ${project.basedir}/keystore.jks + mykey 123456 UTF-8 UTF-8 @@ -57,7 +57,7 @@ eu.m724 jarupdater - 0.1.5 + 0.1.10 @@ -147,7 +147,7 @@ scm:git:git@git.m724.eu:Minecon724/giants.git - giants-2.0.8 + HEAD diff --git a/src/main/java/eu/m724/giants/Configuration.java b/src/main/java/eu/m724/giants/Configuration.java deleted file mode 100644 index a754743..0000000 --- a/src/main/java/eu/m724/giants/Configuration.java +++ /dev/null @@ -1,139 +0,0 @@ -package eu.m724.giants; - -import org.bukkit.Material; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.inventory.ItemStack; -import org.bukkit.plugin.Plugin; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.Vector; - -import java.io.File; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - -public class Configuration { - private final File file; - private final Logger logger; - - String updater; - - boolean ai; - - double attackDamage; - int attackDelay; - Vector attackReach; - int jumpMode; - int jumpCondition; - int jumpDelay; - double jumpHeight = -1; - - double chance; - List worldBlacklist; - Set effects = new HashSet<>(); - Set drops = new HashSet<>(); - - public Configuration(Plugin plugin, File file) { - this.file = file; - this.logger = Logger.getLogger(plugin.getLogger().getName() + ".Configuration"); - } - - public void load() { - YamlConfiguration config = YamlConfiguration.loadConfiguration(file); - - updater = config.getString("updater", "true"); - if (updater.equalsIgnoreCase("true")) { - updater = "release"; - } else if (updater.equalsIgnoreCase("false")) { - updater = null; - } - - ai = config.getBoolean("ai"); - - chance = config.getDouble("chance"); - attackDamage = config.getDouble("attackDamage"); - attackDelay = config.getInt("attackDelay"); - jumpMode = config.getInt("jumpMode"); - jumpCondition = config.getInt("jumpCondition"); - jumpDelay = config.getInt("jumpDelay"); - - if (jumpMode != 0) { - jumpHeight = config.getDouble("jumpHeight", -1); - if (jumpHeight == -1) { - jumpHeight = defaultJumpHeight(); - } - logger.info("Jumping is experimental."); - logger.info("Jump mode: " + jumpMode); - logger.info("Jump condition: " + jumpCondition); - logger.info("Jump height: " + jumpHeight); - } - - double _attackReach = config.getDouble("attackReach"); - double _attackVerticalReach = config.getDouble("attackVerticalReach"); - attackReach = new Vector(_attackReach, _attackVerticalReach, _attackReach); - - worldBlacklist = config.getStringList("blacklist"); - - for (String line : config.getStringList("effects")) { - String[] parts = line.split(":"); - - try { - PotionEffectType effectType = PotionEffectType.getByName(parts[0]); - if (effectType == null) { - throw new IllegalArgumentException("Invalid PotionEffectType"); - } - int amplifier = Integer.parseInt(parts[1]); - effects.add(new PotionEffect(effectType, Integer.MAX_VALUE, amplifier)); - } catch (IllegalArgumentException e) { - logger.warning("Parsing a potion effect failed:"); - logger.warning(line); - logger.warning(e.getMessage()); - } - } - - for (Map dropMap : config.getMapList("drops")) { - try { - ItemStack itemStack; - if (dropMap.containsKey("itemStack")) { - itemStack = (ItemStack) dropMap.get("itemStack"); - } else { - Material material = Material.getMaterial((String) dropMap.get("material")); - if (material == null) { - throw new IllegalArgumentException("Invalid Material"); - } - itemStack = new ItemStack(material, 1); - } - - int min = (int) dropMap.get("quantityMin"); - int max = (int) dropMap.get("quantityMax"); - double chance; - try { - chance = (double) dropMap.get("chance"); - } catch (ClassCastException e) { // pointlessest error ever - chance = ((Integer) dropMap.get("chance")).doubleValue(); - } - - drops.add(new Drop(itemStack, min, max, chance)); - } catch (IllegalArgumentException e) { - logger.warning("Parsing a drop failed:"); - logger.warning(e.getMessage()); - } - } - } - - public double defaultJumpHeight() { - switch (jumpMode) { - case 1: - case 2: - return 0.42; - case 3: - case 4: - return 1.2; - } - - return -1; - } -} diff --git a/src/main/java/eu/m724/giants/Drop.java b/src/main/java/eu/m724/giants/Drop.java index d5884ca..ad51803 100644 --- a/src/main/java/eu/m724/giants/Drop.java +++ b/src/main/java/eu/m724/giants/Drop.java @@ -1,25 +1,34 @@ package eu.m724.giants; +import org.bukkit.Location; import org.bukkit.inventory.ItemStack; import java.util.concurrent.ThreadLocalRandom; -public class Drop { - public final ItemStack itemStack; - public final int min, max; - public final double chance; - - public Drop(ItemStack itemStack, int min, int max, double chance) { - this.itemStack = itemStack; - this.min = min; - this.max = max; - this.chance = chance; - } - - public ItemStack generateItemStack() { +public record Drop(ItemStack itemStack, int min, int max, double chance) { + /** + * Randomizes quantity and returns {@link ItemStack}.
+ * This should be called every drop. + * + * @return A {@link ItemStack} with randomized quantity + */ + private ItemStack generate() { int amount = ThreadLocalRandom.current().nextInt(min, max + 1); + ItemStack itemStack = this.itemStack.clone(); itemStack.setAmount(amount); return itemStack; } + + /** + * Drops the item at {@code location} taking into account quantity and chance. + * + * @param location The location to drop the drop at + */ + public void dropAt(Location location) { + if (chance > ThreadLocalRandom.current().nextDouble()) { + ItemStack itemStack = generate(); + location.getWorld().dropItemNaturally(location, itemStack); + } + } } diff --git a/src/main/java/eu/m724/giants/GiantProcessor.java b/src/main/java/eu/m724/giants/GiantProcessor.java deleted file mode 100644 index 456c592..0000000 --- a/src/main/java/eu/m724/giants/GiantProcessor.java +++ /dev/null @@ -1,232 +0,0 @@ -package eu.m724.giants; - -import org.bukkit.Location; -import org.bukkit.NamespacedKey; -import org.bukkit.entity.*; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntitySpawnEvent; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.persistence.PersistentDataType; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.Vector; - -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; -import java.util.logging.Logger; - -// TODO move ai stuff to another class -/** - * A processor class that processes giants - */ -public class GiantProcessor implements Listener { - private static final int VERSION = 1; - - private final JavaPlugin plugin; - private final Configuration configuration; - private final Logger logger; - - // private final Set trackedGiants = new HashSet<>(); - private final Set trackedHusks = new HashSet<>(); - private final Map giantLocationMap = new HashMap<>(); - private final Map giantLastJump = new HashMap<>(); - - private final ThreadLocalRandom random = ThreadLocalRandom.current(); - private final NamespacedKey huskKey; - - public GiantProcessor(JavaPlugin plugin, Configuration configuration) { - this.plugin = plugin; - this.configuration = configuration; - this.logger = Logger.getLogger(plugin.getLogger().getName() + ".GiantProcessor"); - this.huskKey = new NamespacedKey(plugin, "husk"); - } - - void start() { - if (configuration.ai) { - new BukkitRunnable() { - @Override - public void run() { - for (Husk husk : Set.copyOf(trackedHusks)) { - if (husk.isValid()) { - Location huskLocation = husk.getLocation(); - Entity giant = husk.getVehicle(); - - if (giant instanceof Giant) { - if (giant.isValid()) { // TODO reconsider - giant.getWorld().getNearbyEntities( - giant.getBoundingBox().expand(configuration.attackReach), - e -> (e instanceof Player && !e.isInvulnerable()) - ).forEach(p -> ((Player) p).damage(configuration.attackDamage, giant)); - giant.setRotation(huskLocation.getYaw(), huskLocation.getPitch()); - - // jumping - if (configuration.jumpMode != 0) { - // tracking location is only required for jumping - Location prevLocation = giantLocationMap.get(giant); - Location location = giant.getLocation(); - if (prevLocation == null) { - prevLocation = location; - } - giantLocationMap.put(giant, location); - - LivingEntity target = husk.getTarget(); - if (target != null) { - processJump(giant, prevLocation, location, target.getLocation()); - } - } - } - } else { - // no vehicle means the giant doesn't exist anymore and the husk should also not exist - husk.setHealth(0); - - trackedHusks.remove(husk); - logger.fine("Husk killed because Giant died at " + husk.getLocation()); - } - } else { - trackedHusks.remove(husk); - logger.fine("Husk unloaded at " + husk.getLocation()); - } - } - } - }.runTaskTimer(plugin, configuration.attackDelay, 0L); - } - - plugin.getServer().getPluginManager().registerEvents(this, plugin); - } - - private void processJump(Entity giant, Location prevLocation, Location location, Location targetLocation) { - long now = System.currentTimeMillis(); - if (now - giantLastJump.getOrDefault(giant, 0L) < configuration.jumpDelay) { - return; - } - - if (giant.isOnGround()) { - giantLastJump.put(giant, now); - if (configuration.jumpCondition == 0) { - if (targetLocation.subtract(location).getY() > 0) { - jump(giant); - } - } else if (configuration.jumpCondition == 1) { - Location delta = prevLocation.subtract(location); - if (targetLocation.subtract(location).getY() > 0 && (delta.getX() == 0 || delta.getZ() == 0)) { - jump(giant); - } - } else if (configuration.jumpCondition == 2) { - Location delta = prevLocation.subtract(location); - if (delta.getX() == 0 || delta.getZ() == 0) { - jump(giant); - } - - } // I could probably simplify that code - } - } - - private void jump(Entity giant) { - if (configuration.jumpMode == 1) { - giant.setVelocity(new Vector(0, configuration.jumpHeight, 0)); - } else if (configuration.jumpMode == 2) { - giant.teleport(giant.getLocation().add(0, configuration.jumpHeight, 0)); - } - } // TODO those should be moved - - public LivingEntity spawnGiant(Location pos) { - LivingEntity entity = (LivingEntity) pos.getWorld().spawnEntity(pos, EntityType.GIANT); - - if (configuration.ai) { - // the husk basically moves the giant - LivingEntity passenger = (LivingEntity) pos.getWorld().spawnEntity(pos, EntityType.HUSK); - new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1).apply(passenger); - passenger.setInvulnerable(true); - passenger.setPersistent(true); - passenger.getPersistentDataContainer().set(huskKey, PersistentDataType.INTEGER, VERSION); - - entity.addPassenger(passenger); - - trackedHusks.add((Husk) passenger); - } - - configuration.effects.forEach(entity::addPotionEffect); - //trackedGiants.add((Giant) entity); - - logger.fine("Spawned a Giant at " + pos); - - return entity; - } - - @EventHandler - public void onChunkLoad(ChunkLoadEvent event) { - Entity[] entities = event.getChunk().getEntities(); - logger.fine("Chunk loaded: " + event.getChunk().getX() + " " + event.getChunk().getZ()); - - Husk[] husks = Arrays.stream(entities) - .filter(entity -> entity instanceof Husk && entity.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER)) - .map(Husk.class::cast) - .toArray(Husk[]::new); - - for (Husk husk : husks) { - logger.fine("Husk found at " + husk.getLocation()); - - Entity giant = husk.getVehicle(); - - if (giant instanceof Giant) { - trackedHusks.add(husk); - //trackedGiants.add((Giant) giant); - - logger.fine("Tracking a loaded Giant at " + giant.getLocation()); - } else { - // kill stray husks, that is those without a giant - husk.setHealth(0); - - logger.fine("Stray Husk killed at " + husk.getLocation()); - } - } - } - - @EventHandler - public void entitySpawn(EntitySpawnEvent e) { - if (configuration.worldBlacklist.contains(e.getLocation().getWorld().getName())) - return; - - if (e.getEntityType() == EntityType.ZOMBIE) { - if (configuration.chance > random.nextDouble()) { - e.setCancelled(true); - spawnGiant(e.getLocation()); - - logger.fine("Spawned a Giant by chance at " + e.getLocation()); - } - } - } - - @EventHandler - public void entityDeath(EntityDeathEvent e) { - LivingEntity entity = e.getEntity(); - - if (entity.getType() == EntityType.GIANT) { - Location location = entity.getLocation(); - logger.fine("A Giant died at " + location); - - for (Drop drop : configuration.drops) { - logger.fine("Rolling a drop"); - - if (drop.chance > random.nextDouble()) { - ItemStack is = drop.generateItemStack(); - entity.getWorld().dropItemNaturally(location, is); - - logger.fine("Dropped " + is); - } - } - - for (Entity passenger : entity.getPassengers()) { - if (passenger.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER)) { - ((LivingEntity) passenger).setHealth(0); - logger.fine("Killed a Husk"); - } - } - } - } -} diff --git a/src/main/java/eu/m724/giants/GiantsCommand.java b/src/main/java/eu/m724/giants/GiantsCommand.java index 14366c0..d7bfa35 100644 --- a/src/main/java/eu/m724/giants/GiantsCommand.java +++ b/src/main/java/eu/m724/giants/GiantsCommand.java @@ -1,6 +1,8 @@ package eu.m724.giants; import eu.m724.giants.updater.UpdateCommand; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -19,126 +21,89 @@ import java.util.Map; public class GiantsCommand implements CommandExecutor { private final GiantsPlugin plugin; - private final Configuration configuration; private final UpdateCommand updateCommand; - public GiantsCommand(GiantsPlugin plugin, Configuration configuration, UpdateCommand updateCommand) { + public GiantsCommand(GiantsPlugin plugin, UpdateCommand updateCommand) { this.plugin = plugin; - this.configuration = configuration; this.updateCommand = updateCommand; } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length == 0) { - sender.sendMessage("Giants " + plugin.getDescription().getVersion()); + sender.sendMessage(ChatColor.GRAY + "Giants " + plugin.getDescription().getVersion()); return true; } String action = args[0]; if (!sender.hasPermission("giants.command." + action)) { - sender.sendMessage("You don't have permission to use this command, or it doesn't exist."); + sender.sendMessage(ChatColor.GRAY + "You don't have permission to use this command, or it doesn't exist."); return true; } - Player player = sender instanceof Player ? (Player) sender : null; - - if (action.equals("spawn")) { - if (player != null) { - plugin.spawnGiant(player.getLocation()); - sender.sendMessage("Spawned a Giant"); - } else { - sender.sendMessage("Only players can use this command."); - } - } else if (action.equals("serialize")) { - if (player != null) { - ItemStack itemStack = player.getInventory().getItemInMainHand(); - - List> list = new ArrayList<>(); - Map map = new LinkedHashMap<>(); - map.put("chance", 1); - map.put("quantityMin", itemStack.getAmount()); - map.put("quantityMax", itemStack.getAmount()); - map.put("itemStack", itemStack); - list.add(map); - - YamlConfiguration yamlConfiguration = new YamlConfiguration(); - - try { - Method method = yamlConfiguration.getClass().getMethod("setInlineComments", String.class, List.class); - - yamlConfiguration.set("v", list); - method.invoke(yamlConfiguration, "v", List.of("Copy the below content to your config.yml")); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - yamlConfiguration.set("v", null); // two latter exceptions happen after setting - yamlConfiguration.set("copy_everything_below_to_config_yml", list); - } - - long now = System.currentTimeMillis(); - String name = "item-" + now + ".yml"; - File file = new File(plugin.getDataFolder(), name); - - try { - yamlConfiguration.save(file); - sender.sendMessage("Saved to plugins/Giants/" + name + ". To add it as a drop, see instructions in the file."); - } catch (IOException e) { - sender.sendMessage("Error saving file. See console for details."); - throw new RuntimeException("Error saving file to " + file.getName(), e); - } - } else { - sender.sendMessage("Only players can use this command."); - } - } else if (action.equals("jm")) { // TODO remove - if (args.length > 1) { - int mode = Integer.parseInt(args[1]); - configuration.jumpMode = mode; - - sender.sendMessage("Jump mode set to " + mode); - sender.sendMessage("This command doesn't check if it's valid, an invalid value turns off jumping."); - - if (configuration.jumpHeight == -1) { - configuration.jumpHeight = configuration.defaultJumpHeight(); - sender.sendMessage("Jump height set to " + configuration.jumpHeight + ". Modify it with /giants jumpheight"); - } - } else { - sender.sendMessage("Jump mode: " + configuration.jumpMode); - } - } else if (action.equals("jh")) { - if (args.length > 1) { - double height = Double.parseDouble(args[1]); - configuration.jumpHeight = height; - - sender.sendMessage("Jump height set to " + height); - } else { - sender.sendMessage("Jump height: " + configuration.jumpHeight); - } - } else if (action.equals("jc")) { - if (args.length > 1) { - int condition = Integer.parseInt(args[1]); - configuration.jumpCondition = condition; - - sender.sendMessage("Jump condition set to " + condition); - } else { - sender.sendMessage("Jump condition: " + configuration.jumpCondition); - } - } else if (action.equals("jd")) { - if (args.length > 1) { - int delay = Integer.parseInt(args[1]); - configuration.jumpDelay = delay; - - sender.sendMessage("Jump delay set to " + delay); - } else { - sender.sendMessage("Jump delay: " + configuration.jumpDelay); - } + if (action.equals("serialize")) { + serializeCommand(sender); } else if (action.equals("update")) { if (updateCommand != null) - updateCommand.onCommand(sender, command, label, args); + updateCommand.updateCommand(sender, args); else - sender.sendMessage("Updater is disabled"); + sender.sendMessage(ChatColor.GRAY + "Updater is disabled"); } return true; } + + private void serializeCommand(CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return; + } + + ItemStack itemStack = player.getInventory().getItemInMainHand(); + + if (itemStack.getType() == Material.AIR) { + sender.sendMessage(ChatColor.RED + "You must hold an item in your main hand."); + return; + } + + try { + String name = serializeDrop(player.getInventory().getItemInMainHand()); + sender.sendMessage("Saved to plugins/Giants/" + name + ". Please follow the instructions in that file."); + } catch (IOException e) { + sender.sendMessage("Error saving file. See console for details."); + throw new RuntimeException("Error saving drop configuration file", e); + } + } + + private String serializeDrop(ItemStack itemStack) throws IOException { + List> list = new ArrayList<>(); + Map map = new LinkedHashMap<>(); + map.put("chance", 1); + map.put("quantityMin", itemStack.getAmount()); + map.put("quantityMax", itemStack.getAmount()); + map.put("itemStack", itemStack); + list.add(map); + + YamlConfiguration yamlConfiguration = new YamlConfiguration(); + + try { + Method method = yamlConfiguration.getClass().getMethod("setInlineComments", String.class, List.class); + + yamlConfiguration.set("v", list); + method.invoke(yamlConfiguration, "v", List.of("Copy the below content to your config.yml")); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + yamlConfiguration.set("v", null); // two latter exceptions happen after setting + yamlConfiguration.set("copy_everything_below_to_config_yml", list); + } + + long now = System.currentTimeMillis(); + String name = "item-" + now + ".yml"; + File file = new File(plugin.getDataFolder(), name); + + yamlConfiguration.save(file); + + return name; + } } diff --git a/src/main/java/eu/m724/giants/GiantsPlugin.java b/src/main/java/eu/m724/giants/GiantsPlugin.java index 1fd19cc..f396ffd 100644 --- a/src/main/java/eu/m724/giants/GiantsPlugin.java +++ b/src/main/java/eu/m724/giants/GiantsPlugin.java @@ -1,13 +1,14 @@ package eu.m724.giants; -import eu.m724.giants.updater.JarVerifier; +import eu.m724.giants.ai.GiantProcessor; +import eu.m724.giants.configuration.Configuration; import eu.m724.giants.updater.PluginUpdater; import eu.m724.giants.updater.UpdateCommand; +import eu.m724.jarupdater.verify.VerificationException; import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; import org.bukkit.Location; import org.bukkit.command.CommandExecutor; -import org.bukkit.entity.Giant; -import org.bukkit.entity.LivingEntity; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; @@ -17,7 +18,7 @@ import java.io.InputStream; public class GiantsPlugin extends JavaPlugin implements CommandExecutor { private final File configFile = new File(getDataFolder(), "config.yml"); - private final Configuration configuration = new Configuration(this, configFile); + private static Configuration configuration; private final GiantProcessor giantProcessor = new GiantProcessor(this, configuration); @Override @@ -26,57 +27,60 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor { saveResource("config.yml", false); } - configuration.load(); - - UpdateCommand updateCommand = null; - if (configuration.updater != null) { - PluginUpdater updater = PluginUpdater.build(this, getFile(), configuration.updater); - updater.initNotifier(); - updateCommand = new UpdateCommand(updater); - } - - getCommand("giants").setExecutor(new GiantsCommand(this, configuration, updateCommand)); + configuration = Configuration.load(this, configFile); giantProcessor.start(); - new Metrics(this, 14131); + PluginUpdater updater = null; + UpdateCommand updateCommand = null; + if (configuration.updaterEnabled()) { + try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) { + updater = PluginUpdater.build(this, getFile(), configuration.updaterChannel(), keyInputStream); + } catch (IOException e) { + getLogger().severe("Failed to load updater"); + e.printStackTrace(); + } + + if (updater != null) { + try { + updater.verifyJar( + getFile().getPath().replace(".paper-remapped/", "") // paper remapping removes data from manifest + ); + } catch (VerificationException e) { + getLogger().warning(e.getMessage()); + getLogger().warning("Plugin JAR is of invalid signature. If this persists, re-download the JAR from SpigotMC."); + getLogger().warning("If on Paper, try removing plugins/.paper-remapped"); + + } + updater.initNotifier(); + updateCommand = new UpdateCommand(updater); + } - try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) { - JarVerifier.verifyWithRsaKey( - getFile().getPath().replace(".paper-remapped/", ""), // paper remapping removes data from manifest - keyInputStream - ); - } catch (IOException e) { - getLogger().warning(e.getMessage()); - getLogger().warning("Failed checking JAR signature. This is not important right now, but it usually forecasts future problems."); - } catch (JarVerifier.VerificationException e) { - getLogger().warning(e.getMessage()); - getLogger().warning("Plugin JAR is of invalid signature. It's possible that the signature has changed, in which case it's normal. But I can't verify that, you must see the version changelog yourself."); } - /* bStats is optional. not anymore - try { - Class clazz = Class.forName("eu.m724.giants.bukkit.Metrics"); - Constructor constructor = clazz.getDeclaredConstructor(JavaPlugin.class, int.class); - constructor.newInstance(this, 14131); - getLogger().info("Enabled bStats"); - } catch (Exception e) { - getLogger().info("Not using bStats (" + e.getClass().getName() + ")"); - }*/ + getCommand("giants").setExecutor(new GiantsCommand(this, updateCommand)); + Metrics metrics = new Metrics(this, 14131); + metrics.addCustomChart(new SimplePie("jump_mode", () -> String.valueOf(configuration.jumpMode()))); + metrics.addCustomChart(new SimplePie("jump_condition", () -> String.valueOf(configuration.jumpCondition()))); + metrics.addCustomChart(new SimplePie("jump_delay", () -> String.valueOf(configuration.jumpDelay()))); + metrics.addCustomChart(new SimplePie("jump_height", () -> String.valueOf(configuration.jumpHeight()))); } - + public static Configuration getConfiguration() { + return configuration; + } // TODO api, untested /** - * Spawns a {@link Giant} at the specified {@link Location} + * Checks if a giant can be spawned at a location
+ * The check is very approximate, but works for most scenarios * * @param location The location - * @return The spawned {@link Giant} (as a {@link LivingEntity}, but you can cast) + * @return Whether a giant can be spawned */ - public LivingEntity spawnGiant(Location location) { - return giantProcessor.spawnGiant(location); + public boolean isSpawnableAt(Location location) { + return giantProcessor.isSpawnableAt(location); } } diff --git a/src/main/java/eu/m724/giants/ai/GiantJumper.java b/src/main/java/eu/m724/giants/ai/GiantJumper.java new file mode 100644 index 0000000..1bf679c --- /dev/null +++ b/src/main/java/eu/m724/giants/ai/GiantJumper.java @@ -0,0 +1,51 @@ +package eu.m724.giants.ai; + +import eu.m724.giants.GiantsPlugin; +import eu.m724.giants.configuration.Configuration; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; + +import java.util.HashMap; +import java.util.Map; + +public class GiantJumper { + private final Configuration configuration = GiantsPlugin.getConfiguration(); + + private final Map giantLastJump = new HashMap<>(); + + void processJump(Entity giant, Location prevLocation, Location location, Location targetLocation) { + long now = System.currentTimeMillis(); + if (now - giantLastJump.getOrDefault(giant, 0L) < configuration.jumpDelay()) { + return; + } + + if (giant.isOnGround()) { + giantLastJump.put(giant, now); + if (configuration.jumpCondition() == 0) { + if (targetLocation.subtract(location).getY() > 0) { + jump(giant); + } + } else if (configuration.jumpCondition() == 1) { + Location delta = prevLocation.subtract(location); + if (targetLocation.subtract(location).getY() > 0 && (delta.getX() == 0 || delta.getZ() == 0)) { + jump(giant); + } + } else if (configuration.jumpCondition() == 2) { + Location delta = prevLocation.subtract(location); + if (delta.getX() == 0 || delta.getZ() == 0) { + jump(giant); + } + + } // I could probably simplify that code + } + } + + private void jump(Entity giant) { + if (configuration.jumpMode() == 1) { + giant.setVelocity(new Vector(0, configuration.jumpHeight(), 0)); + } else if (configuration.jumpMode() == 2) { + giant.teleport(giant.getLocation().add(0, configuration.jumpHeight(), 0)); + } + } +} diff --git a/src/main/java/eu/m724/giants/ai/GiantProcessor.java b/src/main/java/eu/m724/giants/ai/GiantProcessor.java new file mode 100644 index 0000000..400a898 --- /dev/null +++ b/src/main/java/eu/m724/giants/ai/GiantProcessor.java @@ -0,0 +1,163 @@ +package eu.m724.giants.ai; + +import eu.m724.giants.Drop; +import eu.m724.giants.configuration.Configuration; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +// TODO move ai stuff to another class +/** + * A processor class that processes giants + */ +public class GiantProcessor implements Listener { + private static final int VERSION = 1; + + private final JavaPlugin plugin; + final Configuration configuration; + private final Logger logger; + + final Set trackedHusks = new HashSet<>(); + final Map giantLocationMap = new HashMap<>(); + + private final ThreadLocalRandom random = ThreadLocalRandom.current(); + private final NamespacedKey huskKey; + + public GiantProcessor(JavaPlugin plugin, Configuration configuration) { + this.plugin = plugin; + this.configuration = configuration; + this.logger = Logger.getLogger(plugin.getLogger().getName() + ".GiantProcessor"); + logger.setLevel(Level.ALL); + this.huskKey = new NamespacedKey(plugin, "husk"); + } + + public void start() { + if (configuration.aiEnabled()) { + new GiantTicker(this).schedule(plugin); + } + + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * The check is very approximate + * + * @param location the location + * @return whether a giant can be spawned here + */ + public boolean isSpawnableAt(Location location) { + for (int y=0; y<=12; y++) { + if (!location.clone().add(0, y, 0).getBlock().isEmpty()) // isPassable also seems good + return false; + } + + return true; + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + Entity[] entities = event.getChunk().getEntities(); + logger.fine("Chunk loaded: " + event.getChunk().getX() + " " + event.getChunk().getZ()); + + Husk[] husks = Arrays.stream(entities) + .filter(entity -> entity instanceof Husk && entity.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER)) + .map(Husk.class::cast) + .toArray(Husk[]::new); + + for (Husk husk : husks) { + logger.fine("Husk found at " + husk.getLocation()); + + Entity giant = husk.getVehicle(); + + if (giant instanceof Giant) { + trackedHusks.add(husk); + + logger.fine("Tracking a loaded Giant at " + giant.getLocation()); + } else { + // kill stray husks, that is those without a giant + husk.setHealth(0); + + logger.fine("Stray Husk killed at " + husk.getLocation()); + } + } + } + + public void applyGiantsLogic(Giant giant) { + if (configuration.aiEnabled()) { + // The husk moves the giant. That's the magic. + LivingEntity passenger = (LivingEntity) giant.getWorld().spawnEntity(giant.getLocation(), EntityType.HUSK); + new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1).apply(passenger); + passenger.setInvulnerable(true); + passenger.setPersistent(true); + passenger.getPersistentDataContainer().set(huskKey, PersistentDataType.INTEGER, VERSION); + + giant.addPassenger(passenger); + + trackedHusks.add((Husk) passenger); + } + + configuration.potionEffects().forEach(giant::addPotionEffect); + } + + @EventHandler + public void entitySpawn(EntitySpawnEvent e) { + if (e.getEntityType() == EntityType.GIANT) { + logger.fine("Handling spawned Giant at " + e.getLocation()); + + var giant = (Giant) e.getEntity(); + if (giant.hasAI()) // NoAI flag + applyGiantsLogic(giant); + } + + if (configuration.worldBlacklist().contains(e.getLocation().getWorld().getName())) + return; + + if (e.getEntityType() == EntityType.ZOMBIE) { + if (configuration.spawnChance() > random.nextDouble()) { + logger.fine("Trying to spawn a Giant by chance at " + e.getLocation()); + if (isSpawnableAt(e.getLocation())) { + logger.fine("Spawned a Giant by chance at " + e.getLocation()); + e.getLocation().getWorld().spawnEntity(e.getLocation(), EntityType.GIANT); + e.setCancelled(true); + } + } + } + } + + @EventHandler + public void entityDeath(EntityDeathEvent e) { + LivingEntity entity = e.getEntity(); + + if (entity.getType() == EntityType.GIANT) { + Location location = entity.getLocation(); + logger.fine("A Giant died at " + location); + + for (Drop drop : configuration.drops()) { + logger.fine("Rolling a drop: " + drop.itemStack().toString()); + + drop.dropAt(location); + } + + for (Entity passenger : entity.getPassengers()) { + if (passenger.getPersistentDataContainer().has(huskKey, PersistentDataType.INTEGER)) { + ((LivingEntity) passenger).setHealth(0); + logger.fine("Killed a Husk"); + } + } + } + } +} diff --git a/src/main/java/eu/m724/giants/ai/GiantTicker.java b/src/main/java/eu/m724/giants/ai/GiantTicker.java new file mode 100644 index 0000000..adff019 --- /dev/null +++ b/src/main/java/eu/m724/giants/ai/GiantTicker.java @@ -0,0 +1,75 @@ +package eu.m724.giants.ai; + +import eu.m724.giants.GiantsPlugin; +import eu.m724.giants.configuration.Configuration; +import org.bukkit.Location; +import org.bukkit.entity.*; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.Set; + +/** + * Ticks giants + */ +public class GiantTicker extends BukkitRunnable { + private final Configuration configuration = GiantsPlugin.getConfiguration(); + private final GiantJumper jumper = new GiantJumper(); + + private final GiantProcessor giantProcessor; + + public GiantTicker(GiantProcessor giantProcessor) { + this.giantProcessor = giantProcessor; + } + + void schedule(Plugin plugin) { + this.runTaskTimer(plugin, 0, configuration.attackDelay()); + } + + @Override + public void run() { + for (Husk husk : Set.copyOf(giantProcessor.trackedHusks)) { + if (husk.isValid()) { + Location huskLocation = husk.getLocation(); + Entity giant = husk.getVehicle(); + + if (giant instanceof Giant) { + if (giant.isValid()) { // TODO reconsider + giant.getWorld().getNearbyEntities( + giant.getBoundingBox().expand(configuration.attackReach()), + e -> (e instanceof Player && !e.isInvulnerable()) + ).forEach(p -> ((Player) p).damage(configuration.attackDamage(), giant)); + giant.setRotation(huskLocation.getYaw(), huskLocation.getPitch()); + + // TODO move whole into that class? + if (configuration.jumpMode() != 0) { + // tracking location is only required for jumping + Location prevLocation = giantProcessor.giantLocationMap.get(giant); + Location location = giant.getLocation(); + if (prevLocation == null) { + prevLocation = location; + } + giantProcessor.giantLocationMap.put(giant, location); + + LivingEntity target = husk.getTarget(); + if (target != null) { + jumper.processJump(giant, prevLocation, location, target.getLocation()); + } + } + } + } else { + // no vehicle means the giant doesn't exist anymore and the husk should also not exist + husk.setHealth(0); + + giantProcessor.trackedHusks.remove(husk); + //logger.fine("Husk killed because Giant died at " + husk.getLocation()); + } + } else { + giantProcessor.trackedHusks.remove(husk); + //logger.fine("Husk unloaded at " + husk.getLocation()); + } + } + } + + +} diff --git a/src/main/java/eu/m724/giants/configuration/Configuration.java b/src/main/java/eu/m724/giants/configuration/Configuration.java new file mode 100644 index 0000000..bf2b216 --- /dev/null +++ b/src/main/java/eu/m724/giants/configuration/Configuration.java @@ -0,0 +1,128 @@ +package eu.m724.giants.configuration; + +import eu.m724.giants.Drop; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.util.Vector; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; + +public record Configuration( + boolean updaterEnabled, + String updaterChannel, + + boolean aiEnabled, + + double attackDamage, + int attackDelay, + Vector attackReach, + + int jumpMode, + int jumpCondition, + int jumpDelay, + double jumpHeight, + + double spawnChance, + List worldBlacklist, + List potionEffects, + List drops + +) { + // TODO not use logger here + private static Logger LOGGER; + + public static Configuration load(Plugin plugin, File file) { + LOGGER = Logger.getLogger(plugin.getLogger().getName() + ".Configuration"); + + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + + boolean updaterEnabled = true; + String updaterChannel = config.getString("updater", "release"); + + if (updaterChannel.equalsIgnoreCase("false")) { + updaterEnabled = false; + } + + + boolean aiEnabled = config.getBoolean("ai"); + + double attackDamage = config.getDouble("attackDamage"); + int attackDelay = config.getInt("attackDelay"); + Vector attackReach = new Vector( + config.getDouble("attackReach"), + config.getDouble("attackVerticalReach"), + config.getDouble("attackReach") + ); + + int jumpMode = config.getInt("jumpMode"); + int jumpCondition = config.getInt("jumpCondition"); + int jumpDelay = config.getInt("jumpDelay"); + double jumpHeight = config.getDouble("jumpHeight", defaultJumpHeight(jumpMode)); + + double spawnChance = config.getDouble("chance"); + + List worldBlacklist = config.getStringList("blacklist"); + + List potionEffects = config.getStringList("effects").stream() + .map(Configuration::makePotionEffect) + .filter(Objects::nonNull) + .toList(); + + List drops = config.getMapList("drops").stream() + .map(Configuration::makeDrop) + .filter(Objects::nonNull) + .toList(); + + return new Configuration( + updaterEnabled, updaterChannel, aiEnabled, attackDamage, attackDelay, attackReach, jumpMode, jumpCondition, jumpDelay, jumpHeight, spawnChance, worldBlacklist, potionEffects, drops + ); + } + + private static double defaultJumpHeight(int jumpMode) { + return switch (jumpMode) { + case 1, 2 -> 0.42; + case 3, 4 -> 1.2; + default -> -1; + }; + } + + private static Drop makeDrop(Map dropMap) { + try { + return ListParsers.makeDrop(dropMap); + } catch (ParseException e) { + LOGGER.warning("Failed to parse drop:"); + LOGGER.warning(" At: " + dropMap); + LOGGER.warning(" " + e.getMessage()); + } + + return null; + } + + private static PotionEffect makePotionEffect(String line) { + try { + return ListParsers.makePotionEffect(line); + } catch (ParseException e) { + LOGGER.warning("Failed to parse potion effect:"); + LOGGER.warning(" At line: " + line); + LOGGER.warning(" " + e.getMessage()); + } + + return null; + } + + + static void assertParse(boolean assertion, String message) throws ParseException { + if (!assertion) throw new ParseException(message); + } + + public static class ParseException extends Exception { + public ParseException(String message) { + super(message); + } + } +} diff --git a/src/main/java/eu/m724/giants/configuration/ListParsers.java b/src/main/java/eu/m724/giants/configuration/ListParsers.java new file mode 100644 index 0000000..2a12be1 --- /dev/null +++ b/src/main/java/eu/m724/giants/configuration/ListParsers.java @@ -0,0 +1,69 @@ +package eu.m724.giants.configuration; + +import eu.m724.giants.Drop; +import eu.m724.giants.configuration.Configuration.ParseException; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.Map; + +import static eu.m724.giants.configuration.Configuration.assertParse; + +public class ListParsers { + public static PotionEffect makePotionEffect(String line) throws ParseException { + if (line == null || line.trim().isEmpty()) { + throw new ParseException("Cannot parse empty or null line into a PotionEffect."); + } + + String[] parts = line.split(":", 2); + assertParse(parts.length == 2, "Invalid PotionEffect format (expected 'TYPE:AMPLIFIER')"); + + PotionEffectType effectType = PotionEffectType.getByName(parts[0].trim()); + assertParse(effectType != null, "Unknown PotionEffectType: " + parts[0].trim()); + + int amplifier; + try { + amplifier = Integer.parseInt(parts[1].trim()); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid amplifier format (expected integer): " + parts[1].trim()); + } + + assertParse(amplifier > 0, "Amplifier must be bigger than 0, is: " + amplifier); + assertParse(amplifier < 256, "Amplifier must be at most 255, is: " + amplifier); + + return new PotionEffect(effectType, Integer.MAX_VALUE, amplifier); + } + + // TODO refactor this + public static Drop makeDrop(Map dropMap) throws ParseException { + ItemStack itemStack; + if (dropMap.containsKey("itemStack")) { + itemStack = (ItemStack) dropMap.get("itemStack"); + } else { + Material material = Material.getMaterial((String) dropMap.get("material")); + assertParse(material != null, "Invalid Material: " + dropMap.get("material")); + + itemStack = new ItemStack(material, 1); + } + + int min = (int) dropMap.get("quantityMin"); + int max = (int) dropMap.get("quantityMax"); + assertParse(min > 0, "Minimum quantity must be more than 0, is: " + min); + assertParse(max > 0, "Maximum quantity must be more than 0, is: " + max); + assertParse(min < 100, "Minimum quantity must be less than 100, is: " + min); + assertParse(max < 100, "Maximum quantity must be less than 100, is: " + max); + + double chance = ((Number) dropMap.get("chance")).doubleValue(); + + if (chance > 1 && chance <= 100) { + chance /= 100; // user might have misunderstood + } + + assertParse(chance > 0, "Chance must be more than 0, is: " + chance); + assertParse(chance <= 1, "Chance must be at most 1, is: " + chance); + + return new Drop(itemStack, min, max, chance); + } +} diff --git a/src/main/java/eu/m724/giants/updater/JarVerifier.java b/src/main/java/eu/m724/giants/updater/JarVerifier.java deleted file mode 100644 index e4a0133..0000000 --- a/src/main/java/eu/m724/giants/updater/JarVerifier.java +++ /dev/null @@ -1,134 +0,0 @@ -package eu.m724.giants.updater; - -import java.io.IOException; -import java.io.InputStream; -import java.security.CodeSigner; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Enumeration; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -public class JarVerifier { - /** - * Loads an RSA public key from a PEM file - * - * @param keyInputStream inputStream of the public key file - * @return {@link RSAPublicKey} instance of the public key - * @throws IOException if reading the key input stream failed - */ - private static RSAPublicKey loadPublicKey(InputStream keyInputStream) throws IOException { - // Read the key file - String keyContent = new String(keyInputStream.readAllBytes()); - - // Remove PEM headers and newlines - keyContent = keyContent.replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replaceAll("\\s+", ""); - - // Decode the key - byte[] keyBytes = Base64.getDecoder().decode(keyContent); - - // Create public key specification - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - - // Generate public key - try { - KeyFactory kf = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) kf.generatePublic(spec); - } catch (GeneralSecurityException e) { - // because this shouldn't happen - throw new RuntimeException(e); - } - } - - /** - * Verifies if a JAR file's signature matches an RSA public key - * - * @param jarPath the path of the JAR file - * @param keyInputStream inputStream of the public key file - * @throws VerificationException if verification failed - */ - public static void verifyWithRsaKey(String jarPath, InputStream keyInputStream) throws VerificationException { - try { - // Load the RSA public key - RSAPublicKey publicKey = loadPublicKey(keyInputStream); - - // Open the JAR file - try (JarFile jarFile = new JarFile(jarPath, true)) { - byte[] buffer = new byte[8192]; - Enumeration entries = jarFile.entries(); - - // Get manifest to check signature files - Manifest manifest = jarFile.getManifest(); - if (manifest == null) { - throw new VerificationException("JAR has no manifest"); - } - - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - - if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) { - continue; - } - - int bytesRead = 0; - // Read entry to trigger signature verification - try (InputStream is = jarFile.getInputStream(entry)) { - while ((bytesRead += is.read(buffer)) != -1) { - if (bytesRead > 1024 * 1024 * 100) { // unusual for a file to have >100 MiB - throw new IOException("File too large: " + entry.getName()); - } - } - } - - // Get signers for this entry - CodeSigner[] signers = entry.getCodeSigners(); - if (signers == null || signers.length == 0) { - throw new VerificationException("Unsigned entry: " + entry.getName()); - } - - // Check if any signer's public key matches our RSA key - boolean keyMatch = false; - for (CodeSigner signer : signers) { - for (Certificate cert : signer.getSignerCertPath().getCertificates()) { - PublicKey certPublicKey = cert.getPublicKey(); - if (certPublicKey instanceof RSAPublicKey) { - RSAPublicKey rsaKey = (RSAPublicKey) certPublicKey; - if (rsaKey.getModulus().equals(publicKey.getModulus()) && - rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) { - keyMatch = true; - break; - } - } - } - if (keyMatch) break; - } - - if (!keyMatch) { - throw new VerificationException("Entry not signed with matching RSA key: " + entry.getName()); - } - } - } - - } catch (IOException e) { - throw new VerificationException("Verification error: " + e.getMessage(), e); - } - } - - public static class VerificationException extends Exception { - public VerificationException(String message) { - super(message); - } - - public VerificationException(String message, Exception exception) { - super(message, exception); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/m724/giants/updater/PluginUpdater.java b/src/main/java/eu/m724/giants/updater/PluginUpdater.java index 9af7f19..1891503 100644 --- a/src/main/java/eu/m724/giants/updater/PluginUpdater.java +++ b/src/main/java/eu/m724/giants/updater/PluginUpdater.java @@ -7,55 +7,41 @@ import eu.m724.jarupdater.live.GiteaMetadataDAO; import eu.m724.jarupdater.live.MetadataDAO; import eu.m724.jarupdater.live.MetadataFacade; import eu.m724.jarupdater.updater.Updater; +import eu.m724.jarupdater.verify.SignatureVerifier; +import eu.m724.jarupdater.verify.VerificationException; +import eu.m724.jarupdater.verify.Verifier; import org.bukkit.plugin.Plugin; import java.io.File; -import java.nio.file.NoSuchFileException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; +import java.io.IOException; +import java.io.InputStream; public class PluginUpdater extends Updater { private final Plugin plugin; boolean updatePending = false; - private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Plugin plugin) { - super(environment, metadataProvider, downloader); + private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier, Plugin plugin) { + super(environment, metadataProvider, downloader, verifier); this.plugin = plugin; } - public static PluginUpdater build(Plugin plugin, File file, String channel) { + public static PluginUpdater build(Plugin plugin, File file, String channel, InputStream keyInputStream) throws IOException { Environment environment = new PluginEnvironment(plugin, channel, file.toPath()); MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/giants-metadata", "master"); MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO); Downloader downloader = new SimpleDownloader("giants"); + SignatureVerifier verifier = new SignatureVerifier(); + verifier.loadPublicKey(keyInputStream); - return new PluginUpdater(environment, metadataFacade, downloader, plugin); + return new PluginUpdater(environment, metadataFacade, downloader, verifier, plugin); + } + + public void verifyJar(String jarPath) throws VerificationException { + verifier.verify(jarPath); } public void initNotifier() { UpdateNotifier updateNotifier = new UpdateNotifier(plugin, this, (version) -> {}); updateNotifier.register(); } - - @Override - public CompletableFuture installLatestVersion() throws NoSuchFileException { - return installLatestVersion(true); - } - - public CompletableFuture installLatestVersion(boolean verify) throws NoSuchFileException { - if (this.downloaded == null) { - throw new NoSuchFileException("Download it first"); - } else { - return this.downloaded.thenCompose((file) -> { - if (verify) { - try { - JarVerifier.verifyWithRsaKey(file.getPath(), plugin.getResource("verifies_downloaded_jars.pem")); - } catch (JarVerifier.VerificationException e) { - throw new CompletionException(e); - } - } - return this.downloader.install(file, this.environment.getRunningJarFilePath().toFile()); - }); - } - } } diff --git a/src/main/java/eu/m724/giants/updater/UpdateCommand.java b/src/main/java/eu/m724/giants/updater/UpdateCommand.java index d0d55e0..215dfe6 100644 --- a/src/main/java/eu/m724/giants/updater/UpdateCommand.java +++ b/src/main/java/eu/m724/giants/updater/UpdateCommand.java @@ -1,7 +1,11 @@ package eu.m724.giants.updater; -import org.bukkit.command.Command; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import java.nio.file.NoSuchFileException; import java.time.LocalDate; @@ -17,72 +21,86 @@ public class UpdateCommand { this.updater = updater; } - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - sender.sendMessage("Please wait..."); - sender.sendMessage("Channel: " + updater.getEnvironment().getChannel()); + private void sendChangelogMessage(CommandSender sender, String changelogUrl) { + if (changelogUrl != null) { + if (sender instanceof Player) { + TextComponent textComponent = new TextComponent("Click here to open changelog"); + textComponent.setUnderlined(true); + textComponent.setColor(ChatColor.AQUA); + textComponent.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText(changelogUrl))); + textComponent.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, changelogUrl)); + sender.spigot().sendMessage(textComponent); + } else { + sender.sendMessage("Changelog: " + changelogUrl); + } + } + } + + public void updateCommand(CommandSender sender, String[] args) { + sender.sendMessage(ChatColor.GRAY + "Please wait..."); + sender.sendMessage(ChatColor.GRAY + "Channel: " + updater.getEnvironment().getChannel()); if (updater.updatePending) { - sender.sendMessage("Server restart required"); + sender.sendMessage(ChatColor.YELLOW + "" + ChatColor.BOLD + "(!) Server restart required"); } - if (args.length == 1) { // remember this function is proxied + String action = args.length > 1 ? args[1] : null; // remember this function is proxied + + if (action == null) { updater.getLatestVersion().thenAccept(metadata -> { + updater.getCurrentVersion().thenAccept(metadata2 -> { + sender.sendMessage(ChatColor.GOLD + "You're on Giants " + metadata2.getLabel() + " released " + formatDate(metadata2.getTimestamp())); + sendChangelogMessage(sender, metadata2.getChangelogUrl()); + }).exceptionally(e -> { + sender.sendMessage(ChatColor.RED + "Error retrieving information about current version, see console for details. " + e.getMessage()); + e.printStackTrace(); + return null; + }); + if (metadata != null) { - sender.sendMessage("An update is available!"); - sender.sendMessage("Giants " + metadata.getLabel() + " released " + formatDate(metadata.getTimestamp())); - sender.sendMessage("To download: /giants update download"); + sender.sendMessage(ChatColor.YELLOW + "" + ChatColor.BOLD + "An update is available!"); + sender.sendMessage(ChatColor.GOLD + "Giants " + metadata.getLabel() + " released " + formatDate(metadata.getTimestamp())); + sendChangelogMessage(sender, metadata.getChangelogUrl()); + sender.sendMessage(ChatColor.GOLD + "To download: /giants update download"); } else { - sender.sendMessage("No new updates"); - updater.getCurrentVersion().thenAccept(metadata2 -> { - sender.sendMessage("You're on Giants " + metadata2.getLabel() + " released " + formatDate(metadata2.getTimestamp())); - }).exceptionally(e -> { - sender.sendMessage("Error retrieving information about current version, see console for details. " + e.getMessage()); - e.printStackTrace(); - return null; - }); + sender.sendMessage(ChatColor.GRAY + "No new updates"); } }).exceptionally(e -> { - sender.sendMessage("Error checking for update. See console for details."); + sender.sendMessage(ChatColor.RED + "Error checking for update. See console for details."); e.printStackTrace(); return null; }); } else { - String action = args[1]; // remember this function is proxied - if (!sender.hasPermission("giants.update." + action)) { - sender.sendMessage("You don't have permission to use this command, or it doesn't exist."); - return true; + sender.sendMessage(ChatColor.GRAY + "You don't have permission to use this command, or it doesn't exist."); + return; } if (action.equals("download")) { - sender.sendMessage("Started download"); + sender.sendMessage(ChatColor.GRAY + "Started download"); updater.downloadLatestVersion().thenAccept(file -> { - sender.sendMessage("Download finished, install with /giants update install"); + sender.sendMessage(ChatColor.GREEN + "Download finished, install with /giants update install"); // TODO make this clickable }).exceptionally(e -> { - sender.sendMessage("Download failed. See console for details."); + sender.sendMessage(ChatColor.RED + "Download failed. See console for details."); e.printStackTrace(); return null; }); } else if (action.equals("install")) { try { updater.installLatestVersion().thenAccept(v -> { - sender.sendMessage("Installation completed, restart server to apply."); + sender.sendMessage(ChatColor.GREEN + "Installation completed, restart server to apply."); updater.updatePending = true; }).exceptionally(e -> { - sender.sendMessage("Install failed, see console for details. " + e.getMessage()); + sender.sendMessage(ChatColor.RED + "Install failed, see console for details. " + e.getMessage()); e.printStackTrace(); return null; }); } catch (NoSuchFileException e) { - sender.sendMessage("First, download the update: /giants update download"); + sender.sendMessage(ChatColor.YELLOW + "Download the update first: /giants update download"); } - } else { - return false; } } - - return true; } private String formatDate(long timestamp) { diff --git a/src/main/java/eu/m724/giants/updater/UpdateNotifier.java b/src/main/java/eu/m724/giants/updater/UpdateNotifier.java index eca4bfe..e563a71 100644 --- a/src/main/java/eu/m724/giants/updater/UpdateNotifier.java +++ b/src/main/java/eu/m724/giants/updater/UpdateNotifier.java @@ -9,7 +9,6 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; -import java.io.IOException; import java.util.concurrent.CompletionException; import java.util.function.Consumer; @@ -36,11 +35,7 @@ public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO try { latestVersion = updater.getLatestVersion().join(); } catch (CompletionException e) { - Throwable ex = e.getCause(); - - if (ex instanceof IOException) - plugin.getLogger().info("error trying to contact update server: " + ex.getMessage()); - else e.printStackTrace(); + plugin.getLogger().info("Error trying to contact update server: " + e.getCause().getMessage()); } if (latestVersion == null) return; @@ -53,7 +48,6 @@ public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO } updateConsumer.accept(latestVersion); - } @EventHandler diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index baa67bd..47ae44a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,5 +1,5 @@ # Notes: -# To have no values in a list, remove every value below it and change to [] like `effects: []` +# To clear a list, remove every value below it and change to [] like `effects: []` # Enable updater # It still requires an admin to confirm update @@ -10,6 +10,20 @@ updater: true # If disabled, the giant will not move or attack ai: true +# In hearts, 0.5 is half a heart +attackDamage: 1.0 + +# Attack delay / speed, in ticks +# 20 is 1 second +attackDelay: 20 + +# Self-explanatory, 0 will attack only colliding (touching) entities +# There's no wall check yet, so it will hit through walls +attackReach: 2 + +# Vertical reach +attackVerticalReach: 1 + # Makes giants jump. Very experimental. # I prefer velocity mode # 0 - disabled @@ -27,20 +41,6 @@ jumpDelay: 200 # -1: auto jumpHeight: -1 -# In hearts, 0.5 is half a heart -attackDamage: 1.0 - -# Attack delay / speed, in ticks -# 20 is 1 second -attackDelay: 20 - -# Self-explanatory, 0 will attack only colliding (touching) entities -# There's no wall check yet, so it will hit through walls -attackReach: 2 - -# Vertical reach -attackVerticalReach: 1 - ### # Chance of a zombie becoming a giant. This is per each zombie spawn, natural or not. diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 29bda0e..251b16b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -9,10 +9,8 @@ load: STARTUP commands: giants: description: Utility command for Giants + permissions: - giants.command.spawn: - description: Permits /giants spawn - default: op giants.command.serialize: description: Permits /giants serialize default: op diff --git a/testkeystore.jks b/testkeystore.jks deleted file mode 100644 index 6c10b16..0000000 Binary files a/testkeystore.jks and /dev/null differ