diff --git a/src/main/java/com/volmit/iris/Iris.java b/src/main/java/com/volmit/iris/Iris.java index 05f892d8b..15d64ee5a 100644 --- a/src/main/java/com/volmit/iris/Iris.java +++ b/src/main/java/com/volmit/iris/Iris.java @@ -78,6 +78,7 @@ public class Iris extends VolmitPlugin implements Listener { public static BKLink linkBK; public static MultiverseCoreLink linkMultiverseCore; public static MythicMobsLink linkMythicMobs; + public static TreeManager saplingManager; private static final Queue syncJobs = new ShurikenQueue<>(); public static IrisCompat compat; public static FileWatcher configWatcher; @@ -181,6 +182,7 @@ public class Iris extends VolmitPlugin implements Listener { linkMultiverseCore = new MultiverseCoreLink(); linkBK = new BKLink(); linkMythicMobs = new MythicMobsLink(); + saplingManager = new TreeManager(); edit = new EditManager(); configWatcher = new FileWatcher(getDataFile("settings.json")); J.a(() -> IO.delete(getTemp())); diff --git a/src/main/java/com/volmit/iris/core/TreeManager.java b/src/main/java/com/volmit/iris/core/TreeManager.java new file mode 100644 index 000000000..f50581dc3 --- /dev/null +++ b/src/main/java/com/volmit/iris/core/TreeManager.java @@ -0,0 +1,318 @@ +package com.volmit.iris.core; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.tools.IrisToolbelt; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.framework.IrisAccess; +import com.volmit.iris.engine.object.*; +import com.volmit.iris.engine.object.common.IObjectPlacer; +import com.volmit.iris.engine.object.tile.TileData; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.data.Cuboid; +import com.volmit.iris.util.math.BlockPosition; +import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.math.Vector2d; +import com.volmit.iris.util.scheduling.J; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.TileState; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.StructureGrowEvent; + +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +public class TreeManager implements Listener { + + public TreeManager() { + Iris.instance.registerListener(this); + } + + /**This function does the following + *
1. Is the sapling growing in an Iris world? No -> exit
+ *
2. Is the Iris world accessible? No -> exit
+ *
3. Is the sapling overwriting setting on in that dimension? No -> exit
+ *
4. Check biome, region and dimension for overrides for that sapling type -> Found -> use
+ *
5. Exit if none are found, cancel event if one or more are.
+ * @param event Checks the given event for sapling overrides + */ + @EventHandler + public void onStructureGrowEvent(StructureGrowEvent event) { + + Iris.debug(this.getClass().getName() + " received a structure grow event"); + + // Must be iris world + if (!IrisToolbelt.isIrisWorld(event.getWorld())) { + Iris.debug(this.getClass().getName() + " passed it off to vanilla since not an Iris world"); + return; + } + + // Get world access + IrisAccess worldAccess = IrisToolbelt.access(event.getWorld()); + if (worldAccess == null) { + Iris.debug(this.getClass().getName() + " passed it off to vanilla because could not get IrisAccess for this world"); + Iris.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); + return; + } + + // Return null if not enabled + if (!worldAccess.getCompound().getRootDimension().getTreeSettings().isEnabled()) { + Iris.debug(this.getClass().getName() + " cancelled because tree overrides are disabled"); + return; + } + + Iris.debug("Sapling grew @ " + event.getLocation() + " for " + event.getSpecies().name() + " usedBoneMeal is " + event.isFromBonemeal()); + + // Calculate size, type & placement + // TODO: Patch algorithm to retrieve saplings, as it's not working as intended (only ever returns 1x1) + Cuboid saplingPlane = getSaplings(event.getLocation(), blockData -> event.getLocation().getBlock().getBlockData().equals(blockData), event.getWorld()); + Iris.debug("Sapling plane is: " + saplingPlane.getSizeX() + " by " + saplingPlane.getSizeZ()); + + // Find best object placement based on sizes + IrisObjectPlacement placement = getObjectPlacement(worldAccess, event.getLocation(), event.getSpecies(), new IrisTreeSize(1, 1)); + + // If none were found, just exit + if (placement == null) { + Iris.debug(this.getClass().getName() + " had options but did not manage to find objectPlacements for them"); + return; + } + + // Delete existing saplings + saplingPlane.forEach(block -> block.setType(Material.AIR)); + + // Get object from placer + IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r)); + + // List of new blocks + List blockStateList = new KList<>(); + saplingPlane.forEach(b -> blockStateList.add(b.getState())); + + // Create object placer + IObjectPlacer placer = new IObjectPlacer() { + + @Override + public int getHighest(int x, int z) { + return event.getWorld().getHighestBlockYAt(x, z); + } + + @Override + public int getHighest(int x, int z, boolean ignoreFluid) { + return event.getWorld().getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.WORLD_SURFACE); + } + + @Override + public void set(int x, int y, int z, BlockData d) { + Block b = event.getWorld().getBlockAt(x, y, z); + BlockState state = b.getState(); + state.setBlockData(d); + blockStateList.add(b.getState()); + } + + @Override + public BlockData get(int x, int y, int z) { + return event.getWorld().getBlockAt(x, y, z).getBlockData(); + } + + @Override + public boolean isPreventingDecay() { + return true; + } + + @Override + public boolean isSolid(int x, int y, int z) { + return get(x,y,z).getMaterial().isSolid(); + } + + @Override + public boolean isUnderwater(int x, int z) { + return false; + } + + @Override + public int getFluidHeight() { + Engine engine; + if (worldAccess.getCompound().getSize() > 1) { + engine = worldAccess.getCompound().getEngine(0); + } else { + engine = (Engine) worldAccess.getCompound().getRootDimension(); + } + return engine.getDimension().getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return false; + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + + } + }; + + // TODO: Prevent placing on claimed blocks (however that's defined, idk) + + // TODO: Prevent placing object when overriding blocks + + // Place the object with the placer + object.place( + saplingPlane.getCenter(), + placer, + placement, + RNG.r, + Objects.requireNonNull(worldAccess).getData() + ); + + // Cancel the vanilla placement + event.setCancelled(true); + + // Queue sync task + J.s(() -> { + + // Send out a new event + StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList); + Bukkit.getServer().getPluginManager().callEvent(iGrow); + + // Check if blocks need to be updated + if(!iGrow.isCancelled()){ + for (BlockState block : iGrow.getBlocks()) { + block.update(true, false); + } + } + }); + } + + /** + * Finds a single object placement (which may contain more than one object) for the requirements species, location & size + * @param worldAccess The world to access (check for biome, region, dimension, etc) + * @param location The location of the growth event (For biome/region finding) + * @param type The bukkit TreeType to match + * @param size The size of the sapling area + * @return An object placement which contains the matched tree, or null if none were found / it's disabled. + */ + private IrisObjectPlacement getObjectPlacement(IrisAccess worldAccess, Location location, TreeType type, IrisTreeSize size) { + + KList placements = new KList<>(); + KList allObjects = new KList<>(); + boolean isUseAll = ((Engine)worldAccess.getEngineAccess(location.getBlockY())).getDimension().getTreeSettings().getMode().equals(IrisTreeModes.ALL); + + // Retrieve objectPlacements of type `species` from biome + IrisBiome biome = worldAccess.getBiome(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + placements.addAll(matchObjectPlacements(biome.getObjects(), size, type)); + allObjects.addAll(biome.getObjects()); + + // Add more or find any in the region + if (isUseAll || placements.isEmpty()){ + IrisRegion region = worldAccess.getCompound().getDefaultEngine().getRegion(location.getBlockX(), location.getBlockZ()); + placements.addAll(matchObjectPlacements(region.getObjects(), size, type)); + allObjects.addAll(region.getObjects()); + } + + // TODO: Add more or find any in the dimension + // Add object placer to dimension + // if (isUseAll || placements.isEmpty()){ + // placements.addAll(matchObjectPlacements(worldAccess.getCompound().getRootDimension().getObjects(), size, type)); + // } + + // Check if no matches were found, return a random one if they are + return placements.isNotEmpty() ? placements.getRandom(RNG.r) : null; + } + + /** + * Filters out mismatches and returns matches + * @param objects The object placements to check + * @param size The size of the sapling area to filter with + * @param type The type of the tree to filter with + * @return A list of objectPlacements that matched. May be empty. + */ + private KList matchObjectPlacements(KList objects, IrisTreeSize size, TreeType type) { + + Predicate isValid = irisTree -> ( + irisTree.isAnySize() || irisTree.getSizes().stream().anyMatch(treeSize -> treeSize.doesMatch(size))) && ( + irisTree.isAnyTree() || irisTree.getTreeTypes().stream().anyMatch(treeType -> treeType.equals(type))); + + objects.removeIf(objectPlacement -> objectPlacement.getTrees().stream().noneMatch(isValid)); + + return objects; + } + + /** + * Get the Cuboid of sapling sizes at a location & blockData predicate + * @param at this location + * @param valid with this blockData predicate + * @param world the world to check in + * @return A cuboid containing only saplings + */ + public Cuboid getSaplings(Location at, Predicate valid, World world) { + KList blockPositions = new KList<>(); + grow(at.getWorld(), new BlockPosition(at.getBlockX(), at.getBlockY(), at.getBlockZ()), valid, blockPositions); + BlockPosition a = new BlockPosition(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + BlockPosition b = new BlockPosition(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + // Maximise the block position in x and z to get max cuboid bounds + for(BlockPosition blockPosition : blockPositions) + { + a.max(blockPosition); + b.min(blockPosition); + } + + // Create a cuboid with the size calculated before + Cuboid cuboid = new Cuboid(a.toBlock(world).getLocation(), b.toBlock(world).getLocation()); + boolean cuboidIsValid = true; + + // Loop while the cuboid is larger than 2 + while(Math.min(cuboid.getSizeX(), cuboid.getSizeZ()) > 0) + { + checking: + for(int i = cuboid.getLowerX(); i < cuboid.getUpperX(); i++) + { + for(int j = cuboid.getLowerY(); j < cuboid.getUpperY(); j++) + { + for(int k = cuboid.getLowerZ(); k < cuboid.getUpperZ(); k++) + { + if(!blockPositions.contains(new BlockPosition(i,j,k))) + { + cuboidIsValid = false; + break checking; + } + } + } + } + + // Return this cuboid if it's valid + if(cuboidIsValid) + { + return cuboid; + } + + // Inset the cuboid and try again (revalidate) + cuboid = cuboid.inset(Cuboid.CuboidDirection.Horizontal, 1); + cuboidIsValid = true; + } + + return new Cuboid(at, at); + } + + /** + * Grows the blockPosition list by means of checking neighbours in + * @param world the world to check in + * @param center the location of this position + * @param valid validation on blockData to check block with + * @param l list of block positions to add new neighbors too + */ + private void grow(World world, BlockPosition center, Predicate valid, KList l) { + // Make sure size is less than 50, the block to check isn't already in, and make sure the blockData still matches + if(l.size() <= 50 && !l.contains(center) && valid.test(center.toBlock(world).getBlockData())) + { + l.add(center); + grow(world, center.add(1, 0, 0), valid, l); + grow(world, center.add(-1, 0, 0), valid, l); + grow(world, center.add(0, 0, 1), valid, l); + grow(world, center.add(0, 0, -1), valid, l); + } + } +} diff --git a/src/main/java/com/volmit/iris/core/edit/DustRevealer.java b/src/main/java/com/volmit/iris/core/edit/DustRevealer.java index 19346e41c..654ac9d72 100644 --- a/src/main/java/com/volmit/iris/core/edit/DustRevealer.java +++ b/src/main/java/com/volmit/iris/core/edit/DustRevealer.java @@ -49,6 +49,7 @@ public class DustRevealer { if (a.getObject(block.getX(), block.getY(), block.getZ()) != null) { sender.sendMessage("Found object " + a.getObject(block.getX(), block.getY(), block.getZ())); + Iris.info(sender.getName() + " found object " + a.getObject(block.getX(), block.getY(), block.getZ())); J.a(() -> { new DustRevealer(a, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a.getObject(block.getX(), block.getY(), block.getZ()), new KList<>()); }); diff --git a/src/main/java/com/volmit/iris/engine/object/IrisBiome.java b/src/main/java/com/volmit/iris/engine/object/IrisBiome.java index 9438d6154..42f0f3934 100644 --- a/src/main/java/com/volmit/iris/engine/object/IrisBiome.java +++ b/src/main/java/com/volmit/iris/engine/object/IrisBiome.java @@ -77,7 +77,7 @@ public class IrisBiome extends IrisRegistrant implements IRare { private KList entityInitialSpawns = new KList<>(); @ArrayType(min = 1, type = IrisEffect.class) - @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play arround the player even if where the effect was played is no longer in the biome the player is in.") + @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play around the player even if where the effect was played is no longer in the biome the player is in.") private KList effects = new KList<>(); @DependsOn({"biomeStyle", "biomeZoom", "biomeScatter"}) diff --git a/src/main/java/com/volmit/iris/engine/object/IrisDimension.java b/src/main/java/com/volmit/iris/engine/object/IrisDimension.java index 97c9e84c1..cfff66894 100644 --- a/src/main/java/com/volmit/iris/engine/object/IrisDimension.java +++ b/src/main/java/com/volmit/iris/engine/object/IrisDimension.java @@ -70,6 +70,10 @@ public class IrisDimension extends IrisRegistrant { @Desc("Improves the biome grid variation by shuffling the cell grid more depending on the seed. This makes biomes across multiple seeds look far different than before.") private boolean aggressiveBiomeReshuffle = false; + @Desc("Tree growth override settings") + private IrisTreeSettings treeSettings = new IrisTreeSettings(); + + @Desc("Instead of a flat bottom, applies a clamp (using this noise style) to the bottom instead of a flat bottom. Useful for carving out center-dimensions in a dimension composite world.") private IrisShapedGeneratorStyle undercarriage = null; diff --git a/src/main/java/com/volmit/iris/engine/object/IrisObject.java b/src/main/java/com/volmit/iris/engine/object/IrisObject.java index 717747793..99f5a4cae 100644 --- a/src/main/java/com/volmit/iris/engine/object/IrisObject.java +++ b/src/main/java/com/volmit/iris/engine/object/IrisObject.java @@ -416,6 +416,10 @@ public class IrisObject extends IrisRegistrant { return place(x, yv, z, placer, config, rng, null, null, rdata); } + public int place(Location loc, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisDataManager rdata) { + return place(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), placer, config, rng, rdata); + } + public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, Consumer listener, CarveResult c, IrisDataManager rdata) { IObjectPlacer placer = (config.getHeightmap() != null) ? new IObjectPlacer() { final long s = rng.nextLong() + yv + z - x; diff --git a/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java b/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java index 8cd82807d..ae224305e 100644 --- a/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java +++ b/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java @@ -62,7 +62,6 @@ public class IrisObjectPlacement { @Desc("The maximum layer level of a snow filter overtop of this placement. Set to 0 to disable. Max of 1.") private double snow = 0; - @Required @MinNumber(0) @MaxNumber(1) @Desc("The chance for this to place in a chunk. If you need multiple per chunk, set this to 1 and use density.") @@ -147,6 +146,10 @@ public class IrisObjectPlacement { @Desc("The loot tables to apply to these objects") private KList loot = new KList<>(); + @Desc("This object / these objects override the following trees when they grow...") + @ArrayType(min = 1, type = IrisTree.class) + private KList trees = new KList<>(); + public IrisObjectPlacement toPlacement(String... place) { IrisObjectPlacement p = new IrisObjectPlacement(); p.setPlace(new KList<>(place)); diff --git a/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java b/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java index 88fbb00f6..7eadf9f18 100644 --- a/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java +++ b/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java @@ -20,8 +20,10 @@ package com.volmit.iris.engine.object; import com.volmit.iris.engine.object.annotations.Desc; -@Desc("The terrain mode used to tell iris how to generate land") +@Desc("Terrain modes are used to decide the generator type currently used") public enum IrisTerrainMode { + @Desc("Normal terrain, similar to the vanilla overworld") NORMAL, + @Desc("Island terrain, more similar to the end, but endless possibilities!") ISLANDS } diff --git a/src/main/java/com/volmit/iris/engine/object/IrisTree.java b/src/main/java/com/volmit/iris/engine/object/IrisTree.java new file mode 100644 index 000000000..0614a8480 --- /dev/null +++ b/src/main/java/com/volmit/iris/engine/object/IrisTree.java @@ -0,0 +1,33 @@ +package com.volmit.iris.engine.object; + +import com.volmit.iris.engine.object.annotations.*; +import com.volmit.iris.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.TreeType; + +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Desc("Tree replace options for this object placer") +@Data +public class IrisTree { + + @Required + @Desc("The types of trees overwritten by this object") + @ArrayType(min = 1, type = TreeType.class) + private KList treeTypes; + + @Desc("If enabled, overrides any TreeType") + private boolean anyTree = false; + + @Required + @Desc("The size of the square of saplings this applies to (2 means a 2 * 2 sapling area)") + @ArrayType(min = 1, type = IrisTreeSize.class) + private KList sizes = new KList<>(); + + @Desc("If enabled, overrides trees of any size") + private boolean anySize; +} \ No newline at end of file diff --git a/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java b/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java new file mode 100644 index 000000000..d3474b036 --- /dev/null +++ b/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java @@ -0,0 +1,12 @@ +package com.volmit.iris.engine.object; + +import com.volmit.iris.engine.object.annotations.Desc; + +@Desc("Sapling override object picking options") +public enum IrisTreeModes { + @Desc("Check biome, then region, then dimension, pick the first one that has options") + FIRST, + + @Desc("Check biome, regions, and dimensions, and pick any option from the total list") + ALL +} \ No newline at end of file diff --git a/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java b/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java new file mode 100644 index 000000000..2182f2322 --- /dev/null +++ b/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java @@ -0,0 +1,25 @@ +package com.volmit.iris.engine.object; + +import com.volmit.iris.engine.object.annotations.Desc; +import com.volmit.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Desc("Tree growth override settings") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisTreeSettings { + + @Required + @Desc("Turn replacing on and off") + boolean enabled = false; + + @Desc("Object picking modes") + IrisTreeModes mode = IrisTreeModes.FIRST; +} diff --git a/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java b/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java new file mode 100644 index 000000000..d089f5158 --- /dev/null +++ b/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java @@ -0,0 +1,33 @@ +package com.volmit.iris.engine.object; + +import com.volmit.iris.engine.object.annotations.Desc; +import com.volmit.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Sapling override object picking options") +@Data +public class IrisTreeSize { + + @Required + @Desc("The width of the sapling area") + int width = 1; + + @Required + @Desc("The depth of the sapling area") + int depth = 1; + + /** + * Does the size match + * @param size the size to check match + * @return true if it matches (fits within width and depth) + */ + public boolean doesMatch(IrisTreeSize size){ + return (width == size.getWidth() && depth == size.getDepth()) || (depth == size.getWidth() && width == size.getDepth()); + } +} diff --git a/src/main/java/com/volmit/iris/engine/object/LootMode.java b/src/main/java/com/volmit/iris/engine/object/LootMode.java index 5f3de8fd9..3643ea1b2 100644 --- a/src/main/java/com/volmit/iris/engine/object/LootMode.java +++ b/src/main/java/com/volmit/iris/engine/object/LootMode.java @@ -20,7 +20,7 @@ package com.volmit.iris.engine.object; import com.volmit.iris.engine.object.annotations.Desc; -@Desc("A loot mode is used to descrive what to do with the existing loot layers before adding this loot. Using ADD will simply add this table to the building list of tables (i.e. add dimension tables, region tables then biome tables). By using clear or replace, you remove the parent tables before and add just your tables.") +@Desc("A loot mode is used to describe what to do with the existing loot layers before adding this loot. Using ADD will simply add this table to the building list of tables (i.e. add dimension tables, region tables then biome tables). By using clear or replace, you remove the parent tables before and add just your tables.") public enum LootMode { @Desc("Add to the existing parent loot tables") ADD,