package world.bentobox.greenhouses.greenhouse; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.World.Environment; import org.bukkit.block.Biome; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.data.Bisected; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Waterlogged; import org.bukkit.block.data.type.Cocoa; import org.bukkit.block.data.type.GlowLichen; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Hoglin; import org.bukkit.entity.Piglin; import org.bukkit.util.Vector; import com.google.common.base.Enums; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.util.Util; import world.bentobox.greenhouses.Greenhouses; import world.bentobox.greenhouses.data.Greenhouse; import world.bentobox.greenhouses.managers.EcoSystemManager.GrowthBlock; import world.bentobox.greenhouses.managers.GreenhouseManager.GreenhouseResult; import world.bentobox.greenhouses.world.AsyncWorldCache; public class BiomeRecipe implements Comparable { private static final String CHANCE_FOR = "% chance for "; private Greenhouses addon; private Biome type; private Material icon; // Biome icon for control panel private int priority; private String name; private String friendlyName; private static final List CEILING_PLANTS = new ArrayList<>(); static { CEILING_PLANTS.add(Material.VINE); Enums.getIfPresent(Material.class, "SPORE_BLOSSOM").toJavaUtil().ifPresent(CEILING_PLANTS::add); Enums.getIfPresent(Material.class, "CAVE_VINES_PLANT").toJavaUtil().ifPresent(CEILING_PLANTS::add); Enums.getIfPresent(Material.class, "TWISTING_VINES_PLANT").toJavaUtil().ifPresent(CEILING_PLANTS::add); Enums.getIfPresent(Material.class, "WEEPING_VINES_PLANT").toJavaUtil().ifPresent(CEILING_PLANTS::add); } private static final List ADJ_BLOCKS = Arrays.asList( BlockFace.DOWN, BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.UP, BlockFace.WEST); private static final List SIDE_BLOCKS = Arrays.asList( BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.WEST); private static final List UNDERWATER_PLANTS; static { List m = new ArrayList<>(); Arrays.stream(Material.values()).filter(c -> c.name().contains("CORAL")).forEach(m::add); m.add(Material.SEA_LANTERN); m.add(Material.SEA_PICKLE); m.add(Material.SEAGRASS); m.add(Material.KELP); m.add(Material.GLOW_LICHEN); UNDERWATER_PLANTS = Collections.unmodifiableList(m); } // Content requirements // Material, Type, Qty. There can be more than one type of material required private final Map requiredBlocks = new EnumMap<>(Material.class); /** * Tree map of plants */ private final TreeMap plantTree = new TreeMap<>(); private final TreeMap underwaterPlants = new TreeMap<>(); // Mobs // Entity Type, Material to Spawn on, Probability private final TreeMap mobTree = new TreeMap<>(); // Conversions // Original Material, Original Type, New Material, New Type, Probability private final Multimap conversionBlocks = ArrayListMultimap.create(); private int mobLimit; private int waterCoverage; private int iceCoverage; private int lavaCoverage; private String permission = ""; private final Random random = new Random(); private int maxMob; /** * Create a degenerate recipe with nothing in it */ public BiomeRecipe() {} /** * @param type - biome * @param priority - priority (higher is better) */ public BiomeRecipe(Greenhouses addon, Biome type, int priority) { this.addon = addon; this.type = type; this.priority = priority; mobLimit = 9; // Default } private void startupLog(String message) { if (addon.getSettings().isStartupLog()) addon.log(message); } /** * @param oldMaterial - material that will convert * @param newMaterial - what it will convert to * @param convChance - percentage chance * @param localMaterial - what material must be next to it for conversion to happen */ public void addConvBlocks(Material oldMaterial, Material newMaterial, double convChance, Material localMaterial) { double probability = Math.min(convChance/100 , 1D); conversionBlocks.put(oldMaterial, new GreenhouseBlockConversions(oldMaterial, newMaterial, probability, localMaterial)); startupLog(" " + convChance + CHANCE_FOR + Util.prettifyText(oldMaterial.toString()) + " to convert to " + Util.prettifyText(newMaterial.toString())); } /** * @param mobType - entity type * @param mobProbability - relative probability * @param mobSpawnOn - material to spawn on * @return true if add is successful */ public boolean addMobs(EntityType mobType, double mobProbability, Material mobSpawnOn) { startupLog(" " + mobProbability + CHANCE_FOR + Util.prettifyText(mobType.toString()) + " to spawn on " + Util.prettifyText(mobSpawnOn.toString())+ "."); double probability = mobProbability/100; double lastProb = mobTree.isEmpty() ? 0D : mobTree.lastKey(); // Add up all the probabilities in the list so far if ((1D - lastProb) >= probability) { // Add to probability tree mobTree.put(lastProb + probability, new GreenhouseMob(mobType, mobSpawnOn)); return true; } else { addon.logError("Mob chances add up to > 100% in " + type.toString() + " biome recipe! Skipping " + mobType); return false; } } /** * Creates a list of plants that can grow, the probability and what they must grow on. * Data is drawn from the file biomes.yml * @param plantMaterial - plant type * @param plantProbability - probability of growing * @param plantGrowOn - material on which it must grow * @return true if add is successful */ public boolean addPlants(Material plantMaterial, double plantProbability, Material plantGrowOn) { double probability = plantProbability/100; TreeMap map = UNDERWATER_PLANTS.contains(plantMaterial) ? underwaterPlants : plantTree; // Add up all the probabilities in the list so far double lastProb = map.isEmpty() ? 0D : map.lastKey(); if ((1D - lastProb) >= probability) { // Add to probability tree map.put(lastProb + probability, new GreenhousePlant(plantMaterial, plantGrowOn)); } else { addon.logError("Plant chances add up to > 100% in " + type.toString() + " biome recipe! Skipping " + plantMaterial.toString()); return false; } startupLog(" " + plantProbability + CHANCE_FOR + Util.prettifyText(plantMaterial.toString()) + " to grow on " + Util.prettifyText(plantGrowOn.toString())); return true; } /** * @param blockMaterial - block material * @param blockQty - number of blocks required */ public void addReqBlocks(Material blockMaterial, int blockQty) { requiredBlocks.put(blockMaterial, blockQty); startupLog(" " + blockMaterial + " x " + blockQty); } /** * Checks greenhouse meets recipe requirements. * @return GreenhouseResult - result */ public CompletableFuture> checkRecipe(Greenhouse gh) { CompletableFuture> r = new CompletableFuture<>(); Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> checkRecipeAsync(r, gh)); return r; } /** * Check greenhouse meets recipe requirements. Expected to be run async. * @param r - future to complete when done * @param gh - greenhouse * @return set of results from the check */ private Set checkRecipeAsync(CompletableFuture> r, Greenhouse gh) { AsyncWorldCache cache = new AsyncWorldCache(gh.getWorld()); Set result = new HashSet<>(); long area = gh.getArea(); Map blockCount = new EnumMap<>(Material.class); // Look through the greenhouse and count what is in there for (int y = gh.getFloorHeight(); y< gh.getCeilingHeight();y++) { for (int x = (int) (gh.getBoundingBox().getMinX()+1); x < gh.getBoundingBox().getMaxX(); x++) { for (int z = (int) (gh.getBoundingBox().getMinZ()+1); z < gh.getBoundingBox().getMaxZ(); z++) { Material t = cache.getBlockType(x, y, z); if (!t.equals(Material.AIR)) { blockCount.putIfAbsent(t, 0); blockCount.merge(t, 1, Integer::sum); } } } } // Calculate % water, ice and lava ratios double waterRatio = (double)blockCount.getOrDefault(Material.WATER, 0)/area * 100; double lavaRatio = (double)blockCount.getOrDefault(Material.LAVA, 0)/area * 100; int ice = blockCount.entrySet().stream().filter(en -> en.getKey().equals(Material.ICE) || en.getKey().equals(Material.BLUE_ICE) || en.getKey().equals(Material.PACKED_ICE)) .mapToInt(Map.Entry::getValue).sum(); double iceRatio = (double)ice/(double)area * 100; // Check required ratios - a zero means none of these are allowed, e.g.desert has no water if (waterCoverage == 0 && waterRatio > 0) { result.add(GreenhouseResult.FAIL_NO_WATER); } if (lavaCoverage == 0 && lavaRatio > 0) { result.add(GreenhouseResult.FAIL_NO_LAVA); } if (iceCoverage == 0 && iceRatio > 0) { result.add(GreenhouseResult.FAIL_NO_ICE); } if (waterCoverage > 0 && waterRatio < waterCoverage) { result.add(GreenhouseResult.FAIL_INSUFFICIENT_WATER); } if (lavaCoverage > 0 && lavaRatio < lavaCoverage) { result.add(GreenhouseResult.FAIL_INSUFFICIENT_LAVA); } if (iceCoverage > 0 && iceRatio < iceCoverage) { result.add(GreenhouseResult.FAIL_INSUFFICIENT_ICE); } // Compare to the required blocks Map missingBlocks = requiredBlocks.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue() - blockCount.getOrDefault(e.getKey(), 0))); // Remove any entries that are 0 or less missingBlocks.values().removeIf(v -> v <= 0); if (!missingBlocks.isEmpty()) { result.add(GreenhouseResult.FAIL_INSUFFICIENT_BLOCKS); gh.setMissingBlocks(missingBlocks); } // Return to main thread to complete Bukkit.getScheduler().runTask(addon.getPlugin(), () -> r.complete(result)); return result; } /** * Check if block should be converted * @param b - block to check */ public void convertBlock(Block b) { Material bType = b.getType(); // Check if there is a block conversion for this block, as while the rest of the method wont do anything if .get() returns nothing anyway it still seems to be quite expensive if(conversionBlocks.keySet().contains(bType)) { for(GreenhouseBlockConversions conversion_option : conversionBlocks.get(bType)) { // Roll the dice before bothering with checking the surrounding block as I think it's more common for greenhouses to be filled with convertable blocks and thus this dice roll wont be "wasted" if(ThreadLocalRandom.current().nextDouble() < conversion_option.probability()) { // Check if any of the adjacent blocks matches the required LocalMaterial, if there are any required LocalMaterials if(conversion_option.localMaterial() != null) { for(BlockFace adjacent_block : ADJ_BLOCKS) { if(b.getRelative(adjacent_block).getType() == conversion_option.localMaterial()) { b.setType(conversion_option.newMaterial()); break; } } } else { b.setType(conversion_option.newMaterial()); } } } } } /** * @return the type */ public Biome getBiome() { return type; } /** * @return true if there are blocks to convert for this biome */ public boolean getBlockConvert() { return !conversionBlocks.isEmpty(); } /** * @return the friendly name */ public String getFriendlyName() { return friendlyName; } /** * @return the iceCoverage */ public int getIceCoverage() { return iceCoverage; } /** * @return the icon */ public Material getIcon() { return icon; } /** * @return the lavaCoverage */ public int getLavaCoverage() { return lavaCoverage; } /** * @return the mobLimit */ public int getMobLimit() { return mobLimit; } /** * @return the name */ public String getName() { return name; } /** * @return the permission */ public String getPermission() { return permission; } /** * @return the priority */ public int getPriority() { return priority; } /** * Spawn a mob on block b if it makes sense and random change suggests it * @param b - block * @return true if a mob was spawned */ public boolean spawnMob(Block b) { if (b.getY() == 0) { return false; } // Center spawned mob Location spawnLoc = b.getLocation().clone().add(new Vector(0.5, 0, 0.5)); BentoBox.getInstance().logDebug("Spawning at " + spawnLoc.getBlock().getType() + " " + spawnLoc); getRandomMob().ifPresent(m -> { BentoBox.getInstance().logDebug("Mob is " + m.mobType()); BentoBox.getInstance().logDebug("mobSpawnOn = " + m.mobSpawnOn()); BentoBox.getInstance().logDebug("Block below is " + b.getRelative(BlockFace.DOWN).getType()); }); boolean result = getRandomMob() // Check if the spawn on block matches, if it exists .filter(m -> Optional.of(m.mobSpawnOn()) .map(b.getRelative(BlockFace.DOWN).getType()::equals) .orElse(true)) // If spawn occurs, check if it can fit inside greenhouse .map(m -> { BentoBox.getInstance().logDebug("Mob is " + m); Entity entity = b.getWorld().spawnEntity(spawnLoc, m.mobType()); preventZombie(entity); return addon .getManager() .getMap() .getGreenhouse(b.getLocation()).map(gh -> { BentoBox.getInstance().logDebug("Checking boundary"); if (!gh.getInternalBoundingBox().contains(entity.getBoundingBox())) { entity.remove(); BentoBox.getInstance().logDebug("Removed"); return false; } return true; }).orElse(false); }).orElse(false); BentoBox.getInstance().logDebug("Result = " + result); return result; } /** * Prevent hoglins and piglins from zombifying if they spawn in the overworld * @param entity - spawned entity */ private void preventZombie(Entity entity) { if (!entity .getWorld() .getEnvironment() .equals(Environment.NORMAL) || !Enums.getIfPresent(EntityType.class, "PIGLIN") .isPresent()) { return; } if (entity instanceof Piglin p) { p.setImmuneToZombification(true); return; } if (entity instanceof Hoglin h) { h.setImmuneToZombification(true); } } /** * @return a mob that can spawn in the greenhouse */ private Optional getRandomMob() { // Return a random mob that can spawn in the biome or empty Double key = mobTree.ceilingKey(random.nextDouble()); return key == null ? Optional.empty() : Optional.ofNullable(mobTree.get(key)); } private Optional getRandomPlant(boolean underwater) { // Grow a random plant that can grow double r = random.nextDouble(); Double key = underwater ? underwaterPlants.ceilingKey(r) : plantTree.ceilingKey(r); return key == null ? Optional.empty() : Optional.ofNullable(underwater ? underwaterPlants.get(key) : plantTree.get(key)); } /** * @return a list of blocks that are required for this recipe */ public List getRecipeBlocks() { return requiredBlocks.entrySet().stream().map(en -> Util.prettifyText(en.getKey().toString()) + " x " + en.getValue()).toList(); } /** * @return the waterCoverage */ public int getWaterCoverage() { return waterCoverage; } /** * Plants a plant on block bl if it makes sense. * @param block - block that can have growth * @param underwater - if the block is underwater or not * @return true if successful */ public boolean growPlant(GrowthBlock block, boolean underwater) { Block bl = block.block(); return getRandomPlant(underwater).map(p -> { if (bl.getY() != 0 && canGrowOn(block, p)) { if (plantIt(bl, p)) { bl.getWorld().spawnParticle(Particle.SNOWBALL, bl.getLocation(), 10, 2, 2, 2); return true; } } return false; }).orElse(false); } /** * Plants the plant * @param bl - block to turn into a plant * @param p - the greenhouse plant to be grown * @return true if successful, false if not */ private boolean plantIt(Block bl, GreenhousePlant p) { boolean underwater = bl.getType().equals(Material.WATER); BlockData dataBottom = p.plantMaterial().createBlockData(); // Check if this is a double-height plant if (dataBottom instanceof Bisected bi) { // Double-height plant bi.setHalf(Bisected.Half.BOTTOM); BlockData dataTop = p.plantMaterial().createBlockData(); ((Bisected) dataTop).setHalf(Bisected.Half.TOP); if (bl.getRelative(BlockFace.UP).getType().equals(Material.AIR)) { bl.setBlockData(dataBottom, false); bl.getRelative(BlockFace.UP).setBlockData(dataTop, false); } else { return false; // No room } } else if (p.plantMaterial().equals(Material.GLOW_LICHEN)) { return placeLichen(bl); } else if (p.plantMaterial().equals(Material.COCOA)) { return placeCocoa(bl); } else { if (dataBottom instanceof Waterlogged wl) { wl.setWaterlogged(underwater); bl.setBlockData(wl, false); } else { // Single height plant bl.setBlockData(dataBottom, false); } } return true; } private boolean placeCocoa(Block bl) { // Get the source block below this one Block b = bl.getRelative(BlockFace.DOWN); if (!b.getType().equals(Material.JUNGLE_LOG)) { return false; } // Find a spot for cocoa BlockFace d = null; for (BlockFace adj : SIDE_BLOCKS) { if (b.getRelative(adj).getType().equals(Material.AIR)) { d = adj; break; } } if (d == null) { return false; } Block bb = b.getRelative(d); bb.setType(Material.COCOA); BlockFace opp = d.getOppositeFace(); if(bb.getBlockData() instanceof Cocoa v){ v.setFacing(opp); bb.setBlockData(v); bb.getState().setBlockData(v); bb.getState().update(true); return true; } return false; } /** * Handles the placing of Glow Lichen. This needs to stick to a block rather than grow on it. * If the block is set to Glow Lichen then it appears as an air block with 6 sides of lichen so * they need to be switched off and only the side next to the block should be set. * @param bl - block where plants would usually be placed * @return true if successful, false if not */ private boolean placeLichen(Block bl) { // Get the source block below this one Block b = bl.getRelative(BlockFace.DOWN); // Find a spot for licen BlockFace d = null; boolean waterLogged = false; for (BlockFace adj : ADJ_BLOCKS) { if (b.getRelative(adj).getType().equals(Material.AIR)) { d = adj; break; } // Lichen can grow under water too if (b.getRelative(adj).getType().equals(Material.WATER)) { d = adj; waterLogged = true; break; } } if (d == null) { return false; } Block bb = b.getRelative(d); bb.setType(Material.GLOW_LICHEN); BlockFace opp = d.getOppositeFace(); if(bb.getBlockData() instanceof GlowLichen v){ for (BlockFace f : v.getAllowedFaces()) { v.setFace(f, false); } v.setFace(opp, true); v.setWaterlogged(waterLogged); bb.setBlockData(v); bb.getState().setBlockData(v); bb.getState().update(true); return true; } return false; } /** * Checks if a particular plant can group at the location of a block * @param block - block being checked * @param p - greenhouse plant * @return true if it can be grown otherwise false */ private boolean canGrowOn(GrowthBlock block, GreenhousePlant p) { // Ceiling plants can only grow on ceiling blocks if (CEILING_PLANTS.contains(p.plantMaterial()) && Boolean.TRUE.equals(block.floor())) { return false; } // Underwater plants can only be placed in water and regular plants cannot be placed in water if (block.block().getType().equals(Material.WATER)) { if (!UNDERWATER_PLANTS.contains(p.plantMaterial())) { return false; } } else if (UNDERWATER_PLANTS.contains(p.plantMaterial())) { return false; } return p.plantGrownOn().equals(block.block().getRelative(Boolean.TRUE.equals(block.floor()) ? BlockFace.DOWN : BlockFace.UP).getType()); } /** * @param friendlyName - set the friendly name */ public void setFriendlyName(String friendlyName) { this.friendlyName = friendlyName; } /** * @param iceCoverage the ice coverage to set */ public void setIcecoverage(int iceCoverage) { if (iceCoverage == 0) { startupLog(" No Ice Allowed"); } else if (iceCoverage > 0) { startupLog(" Ice > " + iceCoverage + "%"); } this.iceCoverage = iceCoverage; } /** * @param icon the icon to set */ public void setIcon(Material icon) { this.icon = icon; } /** * @param lavaCoverage the lava coverage to set */ public void setLavacoverage(int lavaCoverage) { if (lavaCoverage == 0) { startupLog(" No Lava Allowed"); } else if (lavaCoverage > 0) { startupLog(" Lava > " + lavaCoverage + "%"); } this.lavaCoverage = lavaCoverage; } /** * @param mobLimit the mobLimit to set */ public void setMobLimit(int mobLimit) { this.mobLimit = mobLimit; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } /** * @param permission the permission to set */ public void setPermission(String permission) { this.permission = permission; } /** * @param priority the priority to set */ public void setPriority(int priority) { this.priority = priority; } /** * @param type the type to set */ public void setType(Biome type) { this.type = type; } /** * @param waterCoverage the water coverage to set */ public void setWatercoverage(int waterCoverage) { if (waterCoverage == 0) { startupLog(" No Water Allowed"); } else if (waterCoverage > 0) { startupLog(" Water > " + waterCoverage + "%"); } this.waterCoverage = waterCoverage; } @Override public int compareTo(BiomeRecipe o) { return Integer.compare(o.getPriority(), this.getPriority()); } /** * @return true if this recipe has no mobs that may spawn */ public boolean noMobs() { return mobTree.isEmpty(); } /** * @return the mob types that may spawn due to this recipe */ public Set getMobTypes() { return mobTree.values().stream().map(GreenhouseMob::mobType).collect(Collectors.toSet()); } /** * Set the maximum number of mobs in a greenhouse * @param maxMob maximum */ public void setMaxMob(int maxMob) { this.maxMob = maxMob; } /** * @return the maxMob */ public int getMaxMob() { return maxMob; } }