diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e39fa030..06c8429a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * Added Bone Blocks recipe to the Electric Press * Added thai translations * Dried Kelp Blocks can now be used in the Coal Generator +* Added Industrial Miner #### Changes * Fixed a few memory leaks diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/multiblocks/MultiBlock.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/multiblocks/MultiBlock.java index d7541c9a3..5a360cd17 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/multiblocks/MultiBlock.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/multiblocks/MultiBlock.java @@ -122,6 +122,16 @@ public class MultiBlock { } } + // This ensures that the Industrial Miner is still recognized while operating + if (a == Material.PISTON) { + return a == b || b == Material.MOVING_PISTON; + } + + // This ensures that the Industrial Miner is still recognized while operating + if (b == Material.PISTON) { + return a == b || a == Material.MOVING_PISTON; + } + if (b != a) { return false; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMiner.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMiner.java new file mode 100644 index 000000000..0da967977 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMiner.java @@ -0,0 +1,198 @@ +package io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.apache.commons.lang.Validate; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import io.github.thebusybiscuit.cscorelib2.chat.ChatColors; +import io.github.thebusybiscuit.cscorelib2.item.CustomItem; +import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; +import me.mrCookieSlime.Slimefun.SlimefunPlugin; +import me.mrCookieSlime.Slimefun.Objects.Category; +import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.abstractItems.MachineFuel; +import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.multiblocks.MultiBlockMachine; +import me.mrCookieSlime.Slimefun.api.SlimefunItemStack; + +/** + * The {@link IndustrialMiner} is a {@link MultiBlockMachine} that can mine any + * ores it finds in a given range underneath where it was placed. + * + * And for those of you who are wondering... yes this is the replacement for the + * long-time deprecated Digital Miner. + * + * @author TheBusyBiscuit + * + */ +public class IndustrialMiner extends MultiBlockMachine { + + protected final Map activeMiners = new HashMap<>(); + protected final List fuelTypes = new ArrayList<>(); + + private final int range; + private final boolean silkTouch; + + public IndustrialMiner(Category category, SlimefunItemStack item, Material baseMaterial, boolean silkTouch, int range) { + super(category, item, new ItemStack[] { null, null, null, new CustomItem(Material.PISTON, "Piston (facing up)"), new ItemStack(Material.CHEST), new CustomItem(Material.PISTON, "Piston (facing up)"), new ItemStack(baseMaterial), new ItemStack(SlimefunPlugin.getMinecraftVersion().isAtLeast(MinecraftVersion.MINECRAFT_1_14) ? Material.BLAST_FURNACE : Material.FURNACE), new ItemStack(baseMaterial) }, new ItemStack[0], BlockFace.UP); + + this.range = range; + this.silkTouch = silkTouch; + + registerDefaultFuelTypes(); + } + + /** + * This returns whether this {@link IndustrialMiner} will output ores as they are. + * Similar to the Silk Touch {@link Enchantment}. + * + * @return Whether to treat ores with Silk Touch + */ + public boolean hasSilkTouch() { + return silkTouch; + } + + /** + * This method returns the range of the {@link IndustrialMiner}. + * The total area will be determined by the range multiplied by 2 plus the actual center + * of the machine. + * + * So a range of 3 will make the {@link IndustrialMiner} affect an area of 7x7 blocks. + * 3 on all axis, plus the center of the machine itself. + * + * @return The range of this {@link IndustrialMiner} + */ + public int getRange() { + return range; + } + + /** + * This registers the various types of fuel that can be used in the + * {@link IndustrialMiner}. + */ + protected void registerDefaultFuelTypes() { + // Coal & Charcoal + fuelTypes.add(new MachineFuel(4, new ItemStack(Material.COAL))); + fuelTypes.add(new MachineFuel(4, new ItemStack(Material.CHARCOAL))); + + fuelTypes.add(new MachineFuel(40, new ItemStack(Material.COAL_BLOCK))); + fuelTypes.add(new MachineFuel(10, new ItemStack(Material.DRIED_KELP_BLOCK))); + fuelTypes.add(new MachineFuel(4, new ItemStack(Material.BLAZE_ROD))); + + // Logs + for (Material mat : Tag.LOGS.getValues()) { + fuelTypes.add(new MachineFuel(1, new ItemStack(mat))); + } + } + + /** + * This method returns the outcome that mining certain ores yields. + * + * @param ore + * The {@link Material} of the ore that was mined + * + * @return The outcome when mining this ore + */ + public ItemStack getOutcome(Material ore) { + if (hasSilkTouch()) { + return new ItemStack(ore); + } + + Random random = ThreadLocalRandom.current(); + + switch (ore) { + case COAL_ORE: + return new ItemStack(Material.COAL); + case DIAMOND_ORE: + return new ItemStack(Material.DIAMOND); + case EMERALD_ORE: + return new ItemStack(Material.EMERALD); + case NETHER_QUARTZ_ORE: + return new ItemStack(Material.QUARTZ); + case REDSTONE_ORE: + return new ItemStack(Material.REDSTONE, 4 + random.nextInt(2)); + case LAPIS_ORE: + return new ItemStack(Material.LAPIS_LAZULI, 4 + random.nextInt(4)); + default: + // This includes Iron and Gold ore + return new ItemStack(ore); + } + } + + /** + * This registers a new fuel type for this {@link IndustrialMiner}. + * + * @param ores + * The amount of ores this allows you to mine + * @param item + * The item that shall be consumed + */ + public void addFuelType(int ores, ItemStack item) { + Validate.isTrue(ores > 1 && ores % 2 == 0, "The amount of ores must be at least 2 and a multiple of 2."); + fuelTypes.add(new MachineFuel(ores / 2, item)); + } + + @Override + public String getLabelLocalPath() { + return "guide.tooltips.recipes.generator"; + } + + @Override + public List getDisplayRecipes() { + List list = new ArrayList<>(); + + for (MachineFuel fuel : fuelTypes) { + ItemStack item = fuel.getInput().clone(); + ItemMeta im = item.getItemMeta(); + List lore = new ArrayList<>(); + lore.add(ChatColors.color("&8\u21E8 &7Lasts for max. " + fuel.getTicks() + " Ores")); + im.setLore(lore); + item.setItemMeta(im); + list.add(item); + } + + return list; + } + + @Override + public void onInteract(Player p, Block b) { + if (activeMiners.containsKey(b.getLocation())) { + SlimefunPlugin.getLocal().sendMessage(p, "machines.INDUSTRIAL_MINER.already-running"); + return; + } + + Block chest = b.getRelative(BlockFace.UP); + Block[] pistons = findPistons(chest); + + int mod = getRange(); + Location start = b.getLocation().clone().add(-mod, -1, -mod); + Location end = b.getLocation().clone().add(mod, -1, mod); + + IndustrialMinerInstance instance = new IndustrialMinerInstance(this, p.getUniqueId(), chest, pistons, start, end); + instance.start(b); + } + + private Block[] findPistons(Block chest) { + Block northern = chest.getRelative(BlockFace.NORTH); + + if (northern.getType() == Material.PISTON) { + return new Block[] { northern, chest.getRelative(BlockFace.SOUTH) }; + } + else { + return new Block[] { chest.getRelative(BlockFace.WEST), chest.getRelative(BlockFace.EAST) }; + } + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMinerInstance.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMinerInstance.java new file mode 100644 index 000000000..6b5a3f556 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/IndustrialMinerInstance.java @@ -0,0 +1,333 @@ +package io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks; + +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.Effect; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Chest; +import org.bukkit.block.data.type.Piston; +import org.bukkit.block.data.type.PistonHead; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import io.github.thebusybiscuit.cscorelib2.inventory.InvUtils; +import io.github.thebusybiscuit.cscorelib2.inventory.ItemUtils; +import io.github.thebusybiscuit.cscorelib2.protection.ProtectableAction; +import io.github.thebusybiscuit.cscorelib2.scheduling.TaskQueue; +import me.mrCookieSlime.Slimefun.SlimefunPlugin; +import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.abstractItems.MachineFuel; +import me.mrCookieSlime.Slimefun.api.Slimefun; + +class IndustrialMinerInstance implements Runnable { + + private final IndustrialMiner miner; + private final UUID owner; + + private int fuel = 0; + private int ores = 0; + private boolean running = false; + + private final Block chest; + private final Block[] pistons; + + private final Location start; + private final Location end; + private final int height; + + private int x; + private int z; + + public IndustrialMinerInstance(IndustrialMiner miner, UUID owner, Block chest, Block[] pistons, Location start, Location end) { + this.miner = miner; + this.owner = owner; + + this.chest = chest; + this.pistons = pistons; + + this.start = start; + this.end = end; + + this.height = start.getBlockY(); + this.x = start.getBlockX(); + this.z = start.getBlockZ(); + } + + /** + * This starts the {@link IndustrialMiner} at the given {@link Block}. + * + * @param b + * The {@link Block} which marks the center of this {@link IndustrialMiner} + */ + public void start(Block b) { + miner.activeMiners.put(b.getLocation(), this); + running = true; + + warmUp(); + } + + /** + * This method stops the {@link IndustrialMiner}. + */ + public void stop() { + running = false; + miner.activeMiners.remove(chest.getRelative(BlockFace.DOWN).getLocation()); + } + + /** + * This method stops the {@link IndustrialMiner} with an error message. + * The error message is a path to the location in Slimefun's localization files. + * + * @param error + * The error message to send + */ + public void stop(String error) { + Player p = Bukkit.getPlayer(owner); + + if (p != null) { + SlimefunPlugin.getLocal().sendMessage(p, error); + } + + stop(); + } + + /** + * This method starts the warm-up animation for the {@link IndustrialMiner}. + */ + private void warmUp() { + fuel = consumeFuel(); + + if (fuel <= 0) { + // This Miner has not enough fuel. + stop("machines.INDUSTRIAL_MINER.no-fuel"); + return; + } + + TaskQueue queue = new TaskQueue(); + + queue.thenRun(4, () -> setPistonState(pistons[0], true)); + queue.thenRun(10, () -> setPistonState(pistons[0], false)); + + queue.thenRun(8, () -> setPistonState(pistons[1], true)); + queue.thenRun(10, () -> setPistonState(pistons[1], false)); + + queue.thenRun(6, () -> setPistonState(pistons[0], true)); + queue.thenRun(9, () -> setPistonState(pistons[0], false)); + + queue.thenRun(4, () -> setPistonState(pistons[1], true)); + queue.thenRun(7, () -> setPistonState(pistons[1], false)); + + queue.thenRun(3, () -> setPistonState(pistons[0], true)); + queue.thenRun(4, () -> setPistonState(pistons[0], false)); + + queue.thenRun(2, () -> setPistonState(pistons[1], true)); + queue.thenRun(3, () -> setPistonState(pistons[1], false)); + + queue.thenRun(1, () -> setPistonState(pistons[0], true)); + queue.thenRun(3, () -> setPistonState(pistons[0], false)); + + queue.thenRun(1, () -> setPistonState(pistons[1], true)); + queue.thenRun(3, () -> setPistonState(pistons[1], false)); + + queue.thenRun(1, this); + queue.execute(SlimefunPlugin.instance); + } + + @Override + public void run() { + if (!running) { + return; + } + + try { + TaskQueue queue = new TaskQueue(); + + queue.thenRun(1, () -> setPistonState(pistons[0], true)); + queue.thenRun(3, () -> setPistonState(pistons[0], false)); + + queue.thenRun(1, () -> setPistonState(pistons[1], true)); + queue.thenRun(3, () -> setPistonState(pistons[1], false)); + + queue.thenRun(() -> { + Block furnace = chest.getRelative(BlockFace.DOWN); + furnace.getWorld().playEffect(furnace.getLocation(), Effect.STEP_SOUND, Material.STONE); + + for (int y = height; y > 0; y--) { + Block b = start.getWorld().getBlockAt(x, y, z); + + if (!SlimefunPlugin.getProtectionManager().hasPermission(Bukkit.getOfflinePlayer(owner), b, ProtectableAction.BREAK_BLOCK)) { + stop("machines.INDUSTRIAL_MINER.no-permission"); + return; + } + + if (b.getType().name().endsWith("_ORE") && push(miner.getOutcome(b.getType()))) { + furnace.getWorld().playEffect(furnace.getLocation(), Effect.STEP_SOUND, b.getType()); + furnace.getWorld().playSound(furnace.getLocation(), Sound.ENTITY_ARROW_HIT_PLAYER, 0.2F, 1F); + + b.setType(Material.AIR); + fuel--; + ores++; + + // Repeat the same column when we hit an ore. + Slimefun.runSync(this, 5); + return; + } + } + + nextColumn(); + }); + + queue.execute(SlimefunPlugin.instance); + } + catch (Exception e) { + Slimefun.getLogger().log(Level.SEVERE, "An Error occured while running an Industrial Miner", e); + stop(); + } + } + + /** + * This advanced the {@link IndustrialMiner} to the next column + */ + private void nextColumn() { + if (x < end.getBlockX()) { + x++; + } + else if (z < end.getBlockZ()) { + x = start.getBlockX(); + z++; + } + else { + stop(); + + Player p = Bukkit.getPlayer(owner); + + if (p != null) { + p.playSound(p.getLocation(), Sound.ENTITY_ARROW_HIT_PLAYER, 0.4F, 1F); + SlimefunPlugin.getLocal().sendMessage(p, "machines.INDUSTRIAL_MINER.finished", msg -> msg.replace("%ores%", String.valueOf(ores))); + } + + return; + } + + Slimefun.runSync(this, 5); + } + + private boolean push(ItemStack outcome) { + if (fuel < 1) { + fuel = consumeFuel(); + } + + if (fuel > 0) { + if (chest.getType() == Material.CHEST) { + Inventory inv = ((Chest) chest.getState()).getBlockInventory(); + + if (InvUtils.fits(inv, outcome)) { + inv.addItem(outcome); + return true; + } + else { + stop("machines.INDUSTRIAL_MINER.chest-full"); + } + } + else { + // The chest has been destroyed + stop("machines.INDUSTRIAL_MINER.destroyed"); + } + } + else { + stop("machines.INDUSTRIAL_MINER.no-fuel"); + } + + return false; + } + + /** + * This consumes fuel from the given {@link Chest}. + * + * @return The gained fuel value + */ + private int consumeFuel() { + if (chest.getType() == Material.CHEST) { + Inventory inv = ((Chest) chest.getState()).getBlockInventory(); + + for (int i = 0; i < inv.getSize(); i++) { + for (MachineFuel fuelType : miner.fuelTypes) { + ItemStack item = inv.getContents()[i]; + + if (fuelType.test(item)) { + ItemUtils.consumeItem(item, false); + return fuelType.getTicks(); + } + } + } + } + + return 0; + } + + private void setPistonState(Block block, boolean extended) { + if (!running) { + return; + } + + try { + // Smoke Particles around the Chest for dramatic effect + Location particleLoc = chest.getLocation().clone().add(0, -1, 0); + block.getWorld().spawnParticle(Particle.SMOKE_NORMAL, particleLoc, 16, 1, 1, 1, 0); + + if (block.getType() == Material.MOVING_PISTON) { + // Yeah it isn't really cool when this happens + block.getRelative(BlockFace.UP).setType(Material.AIR); + } + else if (block.getType() == Material.PISTON) { + Block above = block.getRelative(BlockFace.UP); + + if (above.isEmpty() || above.getType() == Material.PISTON_HEAD) { + Piston piston = (Piston) block.getBlockData(); + + if (piston.getFacing() == BlockFace.UP) { + piston.setExtended(extended); + block.setBlockData(piston, false); + + // Updating the Piston Head + if (extended) { + PistonHead head = (PistonHead) Material.PISTON_HEAD.createBlockData(); + head.setFacing(BlockFace.UP); + + block.getRelative(BlockFace.UP).setBlockData(head, false); + } + else { + block.getRelative(BlockFace.UP).setType(Material.AIR); + } + + block.getWorld().playSound(block.getLocation(), extended ? Sound.BLOCK_PISTON_EXTEND : Sound.BLOCK_PISTON_CONTRACT, 0.2F, 1F); + } + else { + // The pistons must be facing upwards + stop("machines.INDUSTRIAL_MINER.piston-facing"); + } + } + else { + // The pistons must be facing upwards + stop("machines.INDUSTRIAL_MINER.piston-space"); + } + } + else { + // The piston has been destroyed + stop("machines.INDUSTRIAL_MINER.destroyed"); + } + } + catch (Exception e) { + Slimefun.getLogger().log(Level.SEVERE, "An Error occured while moving a Piston for an Industrial Miner", e); + stop(); + } + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java index cd010a72f..ec70c458a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java @@ -135,6 +135,7 @@ import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.Autom import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.Compressor; import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.EnhancedCraftingTable; import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.GrindStone; +import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.IndustrialMiner; import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.Juicer; import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.MagicWorkbench; import io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks.MakeshiftSmeltery; @@ -1021,6 +1022,8 @@ public final class SlimefunItemSetup { .register(plugin); new AutomatedPanningMachine(categories.basicMachines).register(plugin); + + new IndustrialMiner(categories.basicMachines, SlimefunItems.INDUSTRIAL_MINER, Material.IRON_BLOCK, false, 3).register(plugin); new SlimefunItem(categories.magicalArmor, SlimefunItems.BOOTS_OF_THE_STOMPER, RecipeType.ARMOR_FORGE, new ItemStack[] {null, null, null, new ItemStack(Material.YELLOW_WOOL), null, new ItemStack(Material.YELLOW_WOOL), new ItemStack(Material.PISTON), null, new ItemStack(Material.PISTON)}) diff --git a/src/main/resources/languages/messages_en.yml b/src/main/resources/languages/messages_en.yml index d13fdf0ee..397bb69bb 100644 --- a/src/main/resources/languages/messages_en.yml +++ b/src/main/resources/languages/messages_en.yml @@ -16,11 +16,11 @@ commands: reset-target: '&cYour Knowledge has been reset' backpack: - description: Retrieve an existing backpack - invalid-id: '&4The backpack id must be a non-negative number!' - player-never-joined: '&4No player with that name has ever joined the server!' - backpack-does-not-exist: '&4That backpack does not exist!' - restored-backpack-given: '&bBackpack restored successfully! Added to your inventory!' + description: Retrieve a copy of an existing backpack + invalid-id: '&4The id must be a non-negative number!' + player-never-joined: '&4No player with that name could be found!' + backpack-does-not-exist: '&4The specified backpack does not exist!' + restored-backpack-given: '&aYour backpack has been restored and was added to your inventory!' guide: locked: 'LOCKED' @@ -202,7 +202,17 @@ machines: CARGO_NODES: must-be-placed: '&4Must be placed onto a chest or machine!' - + + INDUSTRIAL_MINER: + no-fuel: '&cYour Industrial Miner ran out of fuel! Put your fuel into the chest above.' + piston-facing: '&cYour Industrial Miner requires pistons to face upwards!' + piston-space: '&cThe two pistons need to have an empty block above them!' + destroyed: '&cYour Industrial Miner seems to have been destroyed.' + already-running: '&cThis Industrial Miner is already running!' + full-chest: '&cThe Chest of your Industrial Miner is full!' + no-permission: '&4You do not seem to have permission to operate an Industrial Miner here!' + finished: '&eYour Industrial Miner has finished! It obtained a total of %ores% ore(s)!' + anvil: not-working: '&4You cannot use Slimefun Items in an anvil!' @@ -210,9 +220,6 @@ backpack: already-open: '&cSorry, this Backpack is open somewhere else!' no-stack: '&cYou cannot stack Backpacks' -miner: - no-ores: '&eSorry, I could not find any Ores nearby!' - workbench: not-enhanced: '&4You cannot use Slimefun Items in a normal workbench' diff --git a/src/main/resources/languages/researches_en.yml b/src/main/resources/languages/researches_en.yml index 47c20ddcf..3160e811f 100644 --- a/src/main/resources/languages/researches_en.yml +++ b/src/main/resources/languages/researches_en.yml @@ -231,3 +231,5 @@ slimefun: kelp_cookie: Tasty Kelp makeshift_smeltery: Improvised Smeltery tree_growth_accelerator: Faster Trees + industrial_miner: Industrial Mining + advanced_industrial_miner: Better Mining