start on poisson disk sampling for feature placement
This commit is contained in:
parent
2375a2b7fc
commit
66c2a318cd
@ -29,12 +29,14 @@ import com.terraforged.api.material.WGTags;
|
|||||||
import com.terraforged.feature.FeatureManager;
|
import com.terraforged.feature.FeatureManager;
|
||||||
import com.terraforged.mod.command.TerraCommand;
|
import com.terraforged.mod.command.TerraCommand;
|
||||||
import com.terraforged.mod.data.DataGen;
|
import com.terraforged.mod.data.DataGen;
|
||||||
|
import com.terraforged.mod.feature.decorator.poisson.PoissonAtSurface;
|
||||||
import com.terraforged.mod.feature.feature.DiskFeature;
|
import com.terraforged.mod.feature.feature.DiskFeature;
|
||||||
import com.terraforged.mod.feature.tree.SaplingManager;
|
import com.terraforged.mod.feature.tree.SaplingManager;
|
||||||
import com.terraforged.mod.util.DataPackFinder;
|
import com.terraforged.mod.util.DataPackFinder;
|
||||||
import com.terraforged.mod.util.Environment;
|
import com.terraforged.mod.util.Environment;
|
||||||
import net.minecraft.world.biome.Biomes;
|
import net.minecraft.world.biome.Biomes;
|
||||||
import net.minecraft.world.gen.feature.Feature;
|
import net.minecraft.world.gen.feature.Feature;
|
||||||
|
import net.minecraft.world.gen.placement.Placement;
|
||||||
import net.minecraftforge.common.BiomeDictionary;
|
import net.minecraftforge.common.BiomeDictionary;
|
||||||
import net.minecraftforge.event.RegistryEvent;
|
import net.minecraftforge.event.RegistryEvent;
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
@ -74,10 +76,17 @@ public class TerraForgedMod {
|
|||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void registerFeatures(RegistryEvent.Register<Feature<?>> event) {
|
public static void registerFeatures(RegistryEvent.Register<Feature<?>> event) {
|
||||||
|
Log.info("Registering features");
|
||||||
FeatureManager.registerTemplates(event);
|
FeatureManager.registerTemplates(event);
|
||||||
event.getRegistry().register(DiskFeature.INSTANCE);
|
event.getRegistry().register(DiskFeature.INSTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerDecorators(RegistryEvent.Register<Placement<?>> event) {
|
||||||
|
Log.info("Registering decorators");
|
||||||
|
event.getRegistry().register(new PoissonAtSurface());
|
||||||
|
}
|
||||||
|
|
||||||
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
|
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||||
public static class ForgeEvents {
|
public static class ForgeEvents {
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
|
@ -193,6 +193,7 @@ public class TerraChunkGenerator extends ObfHelperChunkGenerator<GenerationSetti
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void func_225550_a_(BiomeManager biomeManager, IChunk chunk, GenerationStage.Carving carving) {
|
public final void func_225550_a_(BiomeManager biomeManager, IChunk chunk, GenerationStage.Carving carving) {
|
||||||
|
|
||||||
// World carvers have hardcoded 'carvable' blocks which can be problematic with modded blocks
|
// World carvers have hardcoded 'carvable' blocks which can be problematic with modded blocks
|
||||||
// AirCarverFix shims the actual blockstates to an equivalent carvable type
|
// AirCarverFix shims the actual blockstates to an equivalent carvable type
|
||||||
super.func_225550_a_(biomeManager, new ChunkCarverFix(chunk, context.materials), carving);
|
super.func_225550_a_(biomeManager, new ChunkCarverFix(chunk, context.materials), carving);
|
||||||
|
@ -62,6 +62,10 @@ public class RegionDelegate extends WorldGenRegion {
|
|||||||
this.region = region;
|
this.region = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WorldGenRegion getRegion() {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMainChunkX() {
|
public int getMainChunkX() {
|
||||||
return region.getMainChunkX();
|
return region.getMainChunkX();
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import com.terraforged.core.cell.Cell;
|
||||||
|
import com.terraforged.core.region.chunk.ChunkReader;
|
||||||
|
import com.terraforged.mod.chunk.TerraContainer;
|
||||||
|
import com.terraforged.mod.chunk.fix.RegionDelegate;
|
||||||
|
import me.dags.noise.Module;
|
||||||
|
import me.dags.noise.Source;
|
||||||
|
import me.dags.noise.util.NoiseUtil;
|
||||||
|
import net.minecraft.world.IWorld;
|
||||||
|
import net.minecraft.world.biome.BiomeContainer;
|
||||||
|
import net.minecraft.world.chunk.IChunk;
|
||||||
|
import net.minecraft.world.gen.WorldGenRegion;
|
||||||
|
|
||||||
|
public class BiomeVariance implements Module {
|
||||||
|
|
||||||
|
private final ChunkReader chunk;
|
||||||
|
|
||||||
|
public BiomeVariance(ChunkReader chunk) {
|
||||||
|
this.chunk = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getValue(float x, float y) {
|
||||||
|
int dx = ((int) x) & 15;
|
||||||
|
int dz = ((int) y) & 15;
|
||||||
|
Cell<?> cell = chunk.getCell(dx, dz);
|
||||||
|
return NoiseUtil.map(1 - cell.biomeEdge, 0.05F, 0.25F, 0.2F);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Module of(IWorld world) {
|
||||||
|
if (world instanceof RegionDelegate) {
|
||||||
|
WorldGenRegion region = ((RegionDelegate) world).getRegion();
|
||||||
|
IChunk chunk = region.getChunk(region.getMainChunkX(), region.getMainChunkZ());
|
||||||
|
return of(chunk);
|
||||||
|
}
|
||||||
|
return Source.ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Module of(IChunk chunk) {
|
||||||
|
BiomeContainer container = chunk.getBiomes();
|
||||||
|
if (container instanceof TerraContainer) {
|
||||||
|
ChunkReader reader = ((TerraContainer) container).getChunkReader();
|
||||||
|
return new BiomeVariance(reader);
|
||||||
|
}
|
||||||
|
return Source.ONE;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,161 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import com.terraforged.core.util.concurrent.ObjectPool;
|
||||||
|
import me.dags.noise.util.NoiseUtil;
|
||||||
|
import me.dags.noise.util.Vec2f;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public class Poisson {
|
||||||
|
|
||||||
|
private static final int SAMPLES = 50;
|
||||||
|
|
||||||
|
private final int radius;
|
||||||
|
private final int radius2;
|
||||||
|
private final int maxDistance;
|
||||||
|
private final int regionSize;
|
||||||
|
private final int gridSize;
|
||||||
|
private final float cellSize;
|
||||||
|
private final ObjectPool<Vec2f[][]> pool;
|
||||||
|
|
||||||
|
public Poisson(int radius) {
|
||||||
|
int size = 48;
|
||||||
|
this.radius = radius;
|
||||||
|
this.radius2 = radius * radius;
|
||||||
|
int halfRadius = radius / 2;
|
||||||
|
this.maxDistance = radius * 2;
|
||||||
|
this.regionSize = size - halfRadius;
|
||||||
|
this.cellSize = radius / NoiseUtil.SQRT2;
|
||||||
|
this.gridSize = (int) Math.ceil(regionSize / cellSize);
|
||||||
|
this.pool = new ObjectPool<>(3, () -> new Vec2f[gridSize][gridSize]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visit(int chunkX, int chunkZ, PoissonContext context, BiConsumer<Integer, Integer> consumer) {
|
||||||
|
try (ObjectPool.Item<Vec2f[][]> grid = pool.get()) {
|
||||||
|
clear(grid.getValue());
|
||||||
|
context.startX = (chunkX << 4);
|
||||||
|
context.startZ = (chunkZ << 4);
|
||||||
|
context.endX = context.startX + 16;
|
||||||
|
context.endZ = context.startZ + 16;
|
||||||
|
int regionX = (context.startX >> 5);
|
||||||
|
int regionZ = (context.startZ >> 5);
|
||||||
|
context.offsetX = regionX << 5;
|
||||||
|
context.offsetZ = regionZ << 5;
|
||||||
|
context.random.setSeed(NoiseUtil.hash2D(context.seed, regionX, regionZ));
|
||||||
|
int x = context.random.nextInt(regionSize);
|
||||||
|
int z = context.random.nextInt(regionSize);
|
||||||
|
visit(x, z, grid.getValue(), SAMPLES, context, consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visit(float px, float pz, Vec2f[][] grid, int samples, PoissonContext context, BiConsumer<Integer, Integer> consumer) {
|
||||||
|
for (int i = 0; i < samples; i++) {
|
||||||
|
float angle = context.random.nextFloat() * NoiseUtil.PI2;
|
||||||
|
float distance = radius + (context.random.nextFloat() * maxDistance);
|
||||||
|
float x = px + NoiseUtil.sin(angle) * distance;
|
||||||
|
float z = pz + NoiseUtil.cos(angle) * distance;
|
||||||
|
if (valid(x, z, grid, context)) {
|
||||||
|
Vec2f vec = new Vec2f(x, z);
|
||||||
|
visit(vec, context, consumer);
|
||||||
|
int cellX = (int) (x / cellSize);
|
||||||
|
int cellZ = (int) (z / cellSize);
|
||||||
|
grid[cellZ][cellX] = vec;
|
||||||
|
visit(vec.x, vec.y, grid, samples, context, consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visit(Vec2f pos, PoissonContext context, BiConsumer<Integer, Integer> consumer) {
|
||||||
|
int dx = context.offsetX + (int) pos.x;
|
||||||
|
int dz = context.offsetZ + (int) pos.y;
|
||||||
|
if (dx >= context.startX && dx < context.endX && dz >= context.startZ && dz < context.endZ) {
|
||||||
|
consumer.accept(dx, dz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean valid(float x, float z, Vec2f[][] grid, PoissonContext context) {
|
||||||
|
if (x < 0 || x >= regionSize || z < 0 || z >= regionSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellX = (int) (x / cellSize);
|
||||||
|
int cellZ = (int) (z / cellSize);
|
||||||
|
if (grid[cellZ][cellX] != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float noise = context.density.getValue(context.offsetX + x, context.offsetZ + z);
|
||||||
|
float radius2 = noise * this.radius2;
|
||||||
|
|
||||||
|
int searchRadius = 2;
|
||||||
|
int minX = Math.max(0, cellX - searchRadius);
|
||||||
|
int maxX = Math.min(grid[0].length - 1, cellX + searchRadius);
|
||||||
|
int minZ = Math.max(0, cellZ - searchRadius);
|
||||||
|
int maxZ = Math.min(grid.length - 1, cellZ + searchRadius);
|
||||||
|
|
||||||
|
for (int dz = minZ; dz <= maxZ; dz++) {
|
||||||
|
for (int dx = minX; dx <= maxX; dx++) {
|
||||||
|
Vec2f vec = grid[dz][dx];
|
||||||
|
if (vec == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dist2 = vec.dist2(x, z);
|
||||||
|
if (dist2 < radius2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clear(Vec2f[][] grid) {
|
||||||
|
for (Vec2f[] row : grid) {
|
||||||
|
Arrays.fill(row, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int size = 512;
|
||||||
|
int radius = 10;
|
||||||
|
|
||||||
|
int chunkSize = 16;
|
||||||
|
int chunks = size / chunkSize;
|
||||||
|
|
||||||
|
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Poisson poisson = new Poisson(radius);
|
||||||
|
PoissonContext context = new PoissonContext(213, new Random());
|
||||||
|
|
||||||
|
long time = 0L;
|
||||||
|
long count = 0L;
|
||||||
|
for (int cz = 0; cz < chunks; cz++) {
|
||||||
|
for (int cx = 0; cx < chunks; cx++) {
|
||||||
|
long start = System.nanoTime();
|
||||||
|
poisson.visit(cx, cz, context, (x, z) -> {
|
||||||
|
if (x < 0 || x >= image.getWidth() || z < 0 || z >= image.getHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
image.setRGB(x, z, Color.WHITE.getRGB());
|
||||||
|
});
|
||||||
|
time += (System.nanoTime() - start);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double avg = (double) time / count;
|
||||||
|
System.out.println(avg + "ns");
|
||||||
|
|
||||||
|
JFrame frame = new JFrame();
|
||||||
|
frame.add(new JLabel(new ImageIcon(image)));
|
||||||
|
frame.setVisible(true);
|
||||||
|
frame.pack();
|
||||||
|
frame.setResizable(false);
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
|
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IWorld;
|
||||||
|
import net.minecraft.world.gen.Heightmap;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class PoissonAtSurface extends PoissonDecorator {
|
||||||
|
|
||||||
|
public PoissonAtSurface() {
|
||||||
|
setRegistryName("terraforged", "poisson_surface");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getYAt(IWorld world, BlockPos pos, Random random) {
|
||||||
|
return world.getHeight(Heightmap.Type.WORLD_SURFACE_WG, pos.getX(), pos.getZ());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.mojang.datafixers.Dynamic;
|
||||||
|
import com.mojang.datafixers.types.DynamicOps;
|
||||||
|
import net.minecraft.world.gen.placement.IPlacementConfig;
|
||||||
|
|
||||||
|
public class PoissonConfig implements IPlacementConfig {
|
||||||
|
|
||||||
|
public final int radius;
|
||||||
|
public final int variance;
|
||||||
|
|
||||||
|
public PoissonConfig(int radius, int variance) {
|
||||||
|
this.radius = radius;
|
||||||
|
this.variance = variance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Dynamic<T> serialize(DynamicOps<T> ops) {
|
||||||
|
return new Dynamic<>(ops, ops.createMap(
|
||||||
|
ImmutableMap.of(
|
||||||
|
ops.createString("radius"), ops.createInt(radius),
|
||||||
|
ops.createString("variance_scale"), ops.createInt(variance)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PoissonConfig deserialize(Dynamic<?> dynamic) {
|
||||||
|
int radius = dynamic.get("radius").asInt(4);
|
||||||
|
int variance = dynamic.get("variance_scale").asInt(0);
|
||||||
|
return new PoissonConfig(radius, variance);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import me.dags.noise.Module;
|
||||||
|
import me.dags.noise.Source;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class PoissonContext {
|
||||||
|
|
||||||
|
public int offsetX;
|
||||||
|
public int offsetZ;
|
||||||
|
public int startX;
|
||||||
|
public int startZ;
|
||||||
|
public int endX;
|
||||||
|
public int endZ;
|
||||||
|
public Module density = Source.ONE;
|
||||||
|
|
||||||
|
public final int seed;
|
||||||
|
public final Random random;
|
||||||
|
|
||||||
|
public PoissonContext(long seed, Random random) {
|
||||||
|
this.seed = (int) seed;
|
||||||
|
this.random = random;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import me.dags.noise.Module;
|
||||||
|
import me.dags.noise.Source;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IWorld;
|
||||||
|
import net.minecraft.world.gen.ChunkGenerator;
|
||||||
|
import net.minecraft.world.gen.GenerationSettings;
|
||||||
|
import net.minecraft.world.gen.feature.ConfiguredFeature;
|
||||||
|
import net.minecraft.world.gen.feature.Feature;
|
||||||
|
import net.minecraft.world.gen.feature.IFeatureConfig;
|
||||||
|
import net.minecraft.world.gen.placement.Placement;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public abstract class PoissonDecorator extends Placement<PoissonConfig> {
|
||||||
|
|
||||||
|
private Poisson instance = null;
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
public PoissonDecorator() {
|
||||||
|
super(PoissonConfig::deserialize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final <FC extends IFeatureConfig, F extends Feature<FC>> boolean place(IWorld world, ChunkGenerator<?> generator, Random random, BlockPos pos, PoissonConfig config, ConfiguredFeature<FC, F> feature) {
|
||||||
|
int radius = Math.max(2, Math.min(30, config.radius));
|
||||||
|
Poisson poisson = getInstance(radius);
|
||||||
|
PoissonVisitor visitor = new PoissonVisitor(this, feature, world, generator, random, pos);
|
||||||
|
setVariance(world, visitor, config);
|
||||||
|
int chunkX = pos.getX() >> 4;
|
||||||
|
int chunkZ = pos.getZ() >> 4;
|
||||||
|
poisson.visit(chunkX, chunkZ, visitor, visitor);
|
||||||
|
return visitor.hasPlacedOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Stream<BlockPos> getPositions(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generatorIn, Random random, PoissonConfig configIn, BlockPos pos) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract int getYAt(IWorld world, BlockPos pos, Random random);
|
||||||
|
|
||||||
|
private Poisson getInstance(int radius) {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new Poisson(radius);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVariance(IWorld world, PoissonContext context, PoissonConfig config) {
|
||||||
|
Module module = BiomeVariance.of(world);
|
||||||
|
if (module != Source.ONE) {
|
||||||
|
if (config.variance > 0) {
|
||||||
|
Module variance = Source.simplex((int) world.getSeed(), config.variance, 1).scale(1.5);
|
||||||
|
module = module.mult(variance);
|
||||||
|
}
|
||||||
|
context.density = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package com.terraforged.mod.feature.decorator.poisson;
|
||||||
|
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IWorld;
|
||||||
|
import net.minecraft.world.gen.ChunkGenerator;
|
||||||
|
import net.minecraft.world.gen.feature.ConfiguredFeature;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public class PoissonVisitor extends PoissonContext implements BiConsumer<Integer, Integer> {
|
||||||
|
|
||||||
|
private final BlockPos pos;
|
||||||
|
private final IWorld world;
|
||||||
|
private final ConfiguredFeature<?, ?> feature;
|
||||||
|
private final PoissonDecorator decorator;
|
||||||
|
private final ChunkGenerator<?> generator;
|
||||||
|
private final BlockPos.Mutable mutable = new BlockPos.Mutable();
|
||||||
|
|
||||||
|
private boolean placedOne = false;
|
||||||
|
|
||||||
|
public PoissonVisitor(PoissonDecorator decorator, ConfiguredFeature<?, ?> feature, IWorld world, ChunkGenerator<?> generator, Random random, BlockPos pos) {
|
||||||
|
super(world.getSeed(), random);
|
||||||
|
this.pos = pos;
|
||||||
|
this.world = world;
|
||||||
|
this.feature = feature;
|
||||||
|
this.decorator = decorator;
|
||||||
|
this.generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPlacedOne() {
|
||||||
|
return placedOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(Integer x, Integer z) {
|
||||||
|
mutable.setPos(x, pos.getY(), z);
|
||||||
|
mutable.setY(decorator.getYAt(world, mutable, random));
|
||||||
|
if (feature.place(world, generator, random, mutable)) {
|
||||||
|
placedOne = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user