Merge pull request #434 from CocoTheOwner/saplingOverrides

Add sapling / tree overrides WIP
This commit is contained in:
Dan 2021-07-27 14:37:17 -04:00 committed by GitHub
commit 83b4f645db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 441 additions and 4 deletions

View File

@ -78,6 +78,7 @@ public class Iris extends VolmitPlugin implements Listener {
public static BKLink linkBK; public static BKLink linkBK;
public static MultiverseCoreLink linkMultiverseCore; public static MultiverseCoreLink linkMultiverseCore;
public static MythicMobsLink linkMythicMobs; public static MythicMobsLink linkMythicMobs;
public static TreeManager saplingManager;
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>(); private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
public static IrisCompat compat; public static IrisCompat compat;
public static FileWatcher configWatcher; public static FileWatcher configWatcher;
@ -181,6 +182,7 @@ public class Iris extends VolmitPlugin implements Listener {
linkMultiverseCore = new MultiverseCoreLink(); linkMultiverseCore = new MultiverseCoreLink();
linkBK = new BKLink(); linkBK = new BKLink();
linkMythicMobs = new MythicMobsLink(); linkMythicMobs = new MythicMobsLink();
saplingManager = new TreeManager();
edit = new EditManager(); edit = new EditManager();
configWatcher = new FileWatcher(getDataFile("settings.json")); configWatcher = new FileWatcher(getDataFile("settings.json"));
J.a(() -> IO.delete(getTemp())); J.a(() -> IO.delete(getTemp()));

View File

@ -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
* <br>1. Is the sapling growing in an Iris world? No -> exit</br>
* <br>2. Is the Iris world accessible? No -> exit</br>
* <br>3. Is the sapling overwriting setting on in that dimension? No -> exit</br>
* <br>4. Check biome, region and dimension for overrides for that sapling type -> Found -> use</br>
* <br>5. Exit if none are found, cancel event if one or more are.</br>
* @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<BlockState> 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<? extends TileState> 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<IrisObjectPlacement> placements = new KList<>();
KList<IrisObjectPlacement> 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<IrisObjectPlacement> matchObjectPlacements(KList<IrisObjectPlacement> objects, IrisTreeSize size, TreeType type) {
Predicate<IrisTree> 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<BlockData> valid, World world) {
KList<BlockPosition> 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<BlockData> valid, KList<BlockPosition> 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);
}
}
}

View File

@ -49,6 +49,7 @@ public class DustRevealer {
if (a.getObject(block.getX(), block.getY(), block.getZ()) != null) { if (a.getObject(block.getX(), block.getY(), block.getZ()) != null) {
sender.sendMessage("Found object " + a.getObject(block.getX(), block.getY(), block.getZ())); 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(() -> { J.a(() -> {
new DustRevealer(a, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a.getObject(block.getX(), block.getY(), block.getZ()), new KList<>()); new DustRevealer(a, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a.getObject(block.getX(), block.getY(), block.getZ()), new KList<>());
}); });

View File

@ -77,7 +77,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private KList<IrisEntityInitialSpawn> entityInitialSpawns = new KList<>(); private KList<IrisEntityInitialSpawn> entityInitialSpawns = new KList<>();
@ArrayType(min = 1, type = IrisEffect.class) @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<IrisEffect> effects = new KList<>(); private KList<IrisEffect> effects = new KList<>();
@DependsOn({"biomeStyle", "biomeZoom", "biomeScatter"}) @DependsOn({"biomeStyle", "biomeZoom", "biomeScatter"})

View File

@ -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.") @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; 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.") @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; private IrisShapedGeneratorStyle undercarriage = null;

View File

@ -416,6 +416,10 @@ public class IrisObject extends IrisRegistrant {
return place(x, yv, z, placer, config, rng, null, null, rdata); 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<BlockPosition> listener, CarveResult c, IrisDataManager rdata) { public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, Consumer<BlockPosition> listener, CarveResult c, IrisDataManager rdata) {
IObjectPlacer placer = (config.getHeightmap() != null) ? new IObjectPlacer() { IObjectPlacer placer = (config.getHeightmap() != null) ? new IObjectPlacer() {
final long s = rng.nextLong() + yv + z - x; final long s = rng.nextLong() + yv + z - x;

View File

@ -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.") @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; private double snow = 0;
@Required
@MinNumber(0) @MinNumber(0)
@MaxNumber(1) @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.") @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") @Desc("The loot tables to apply to these objects")
private KList<IrisObjectLoot> loot = new KList<>(); private KList<IrisObjectLoot> loot = new KList<>();
@Desc("This object / these objects override the following trees when they grow...")
@ArrayType(min = 1, type = IrisTree.class)
private KList<IrisTree> trees = new KList<>();
public IrisObjectPlacement toPlacement(String... place) { public IrisObjectPlacement toPlacement(String... place) {
IrisObjectPlacement p = new IrisObjectPlacement(); IrisObjectPlacement p = new IrisObjectPlacement();
p.setPlace(new KList<>(place)); p.setPlace(new KList<>(place));

View File

@ -20,8 +20,10 @@ package com.volmit.iris.engine.object;
import com.volmit.iris.engine.object.annotations.Desc; 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 { public enum IrisTerrainMode {
@Desc("Normal terrain, similar to the vanilla overworld")
NORMAL, NORMAL,
@Desc("Island terrain, more similar to the end, but endless possibilities!")
ISLANDS ISLANDS
} }

View File

@ -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<TreeType> 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<IrisTreeSize> sizes = new KList<>();
@Desc("If enabled, overrides trees of any size")
private boolean anySize;
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -20,7 +20,7 @@ package com.volmit.iris.engine.object;
import com.volmit.iris.engine.object.annotations.Desc; 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 { public enum LootMode {
@Desc("Add to the existing parent loot tables") @Desc("Add to the existing parent loot tables")
ADD, ADD,