diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index cfb6fcf4a..919a9e03a 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -342,11 +342,16 @@ public class IrisCreator { int chunkX = rawLocation.getBlockX() >> 4; int chunkZ = rawLocation.getBlockZ() >> 4; try { - CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, true); + CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, false); if (chunkFuture != null) { - chunkFuture.get(15, TimeUnit.SECONDS); + chunkFuture.get(10, TimeUnit.SECONDS); } } catch (Throwable ignored) { + return rawLocation; + } + + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return rawLocation; } CompletableFuture regionFuture = new CompletableFuture<>(); @@ -376,19 +381,19 @@ public class IrisCreator { int z = source.getBlockZ(); int minY = world.getMinHeight() + 1; int maxY = world.getMaxHeight() - 2; - int topY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES); - int startY = Math.max(minY, Math.min(maxY, topY + 1)); + int sourceY = source.getBlockY(); + int startY = Math.max(minY, Math.min(maxY, sourceY)); float yaw = source.getYaw(); float pitch = source.getPitch(); - int upperBound = Math.min(maxY, startY + 16); + int upperBound = Math.min(maxY, startY + 32); for (int y = startY; y <= upperBound; y++) { if (isSafeStandingLocation(world, x, y, z)) { return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch); } } - int lowerBound = Math.max(minY, startY - 24); + int lowerBound = Math.max(minY, startY - 64); for (int y = startY - 1; y >= lowerBound; y--) { if (isSafeStandingLocation(world, x, y, z)) { return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 942910da4..81bfe076b 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -433,6 +433,10 @@ public class IrisComplex implements DataProvider { for (IrisGenerator gen : generators) { String key = gen.getLoadKey(); + if (key == null || key.isBlank()) { + continue; + } + max += biome.getGenLinkMax(key, engine); min += biome.getGenLinkMin(key, engine); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 8fd2aea65..56e3cba8d 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -107,6 +107,7 @@ public class IrisEngine implements Engine { private double maxBiomeLayerDensity; private double maxBiomeDecoratorDensity; private IrisComplex complex; + private final AtomicBoolean modeFallbackLogged; public IrisEngine(EngineTarget target, boolean studio) { this.studio = studio; @@ -129,6 +130,7 @@ public class IrisEngine implements Engine { context = new IrisContext(this); cleaning = new AtomicBoolean(false); noisemapPrebakeRunning = new AtomicBoolean(false); + modeFallbackLogged = new AtomicBoolean(false); execution = getData().getEnvironment().with(this); if (studio) { getData().dump(); @@ -165,10 +167,29 @@ public class IrisEngine implements Engine { } private void prehotload() { - worldManager.close(); - complex.close(); - effects.close(); - mode.close(); + EngineWorldManager currentWorldManager = worldManager; + worldManager = null; + if (currentWorldManager != null) { + currentWorldManager.close(); + } + + IrisComplex currentComplex = complex; + complex = null; + if (currentComplex != null) { + currentComplex.close(); + } + + EngineEffects currentEffects = effects; + effects = null; + if (currentEffects != null) { + currentEffects.close(); + } + + EngineMode currentMode = mode; + mode = null; + if (currentMode != null) { + currentMode.close(); + } execution = getData().getEnvironment().with(this); J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace()); @@ -178,12 +199,14 @@ public class IrisEngine implements Engine { try { Iris.debug("Setup Engine " + getCacheID()); cacheId = RNG.r.nextInt(); - worldManager = new IrisWorldManager(this); - complex = new IrisComplex(this); + complex = ensureComplex(); effects = new IrisEngineEffects(this); hash32 = new CompletableFuture<>(); mantle.hotload(); setupMode(); + IrisWorldManager manager = new IrisWorldManager(this); + worldManager = manager; + manager.startManager(); getDimension().getEngineScripts().forEach(execution::execute); J.a(this::computeBiomeMaxes); J.a(() -> { @@ -207,11 +230,76 @@ public class IrisEngine implements Engine { } private void setupMode() { - if (mode != null) { - mode.close(); + EngineMode currentMode = mode; + if (currentMode != null) { + currentMode.close(); } - mode = getDimension().getMode().create(this); + mode = null; + mode = ensureMode(); + } + + private EngineMode ensureMode() { + EngineMode currentMode = mode; + if (currentMode != null) { + return currentMode; + } + + synchronized (this) { + currentMode = mode; + if (currentMode != null) { + return currentMode; + } + + try { + IrisComplex readyComplex = ensureComplex(); + if (readyComplex == null) { + throw new IllegalStateException("Iris complex is unavailable"); + } + + IrisDimensionMode configuredMode = getDimension().getMode(); + if (configuredMode == null) { + configuredMode = new IrisDimensionMode(); + getDimension().setMode(configuredMode); + } + + currentMode = configuredMode.create(this); + if (currentMode == null) { + throw new IllegalStateException("Dimension mode factory returned null"); + } + } catch (Throwable e) { + Iris.reportError(e); + if (modeFallbackLogged.compareAndSet(false, true)) { + Iris.warn("Failed to initialize configured dimension mode for " + getDimension().getLoadKey() + ", falling back to OVERWORLD mode."); + } + currentMode = IrisDimensionModeType.OVERWORLD.create(this); + } + + mode = currentMode; + return currentMode; + } + } + + private IrisComplex ensureComplex() { + IrisComplex currentComplex = complex; + if (currentComplex != null) { + return currentComplex; + } + + if (closed) { + return null; + } + + synchronized (this) { + currentComplex = complex; + if (currentComplex != null) { + return currentComplex; + } + + currentComplex = new IrisComplex(this); + complex = currentComplex; + return currentComplex; + } } private void scheduleStartupNoisemapPrebake() { @@ -447,17 +535,39 @@ public class IrisEngine implements Engine { PregeneratorJob.shutdownInstance(); closed = true; J.car(art); - getWorldManager().close(); + EngineWorldManager currentWorldManager = getWorldManager(); + if (currentWorldManager != null) { + currentWorldManager.close(); + } getTarget().close(); saveEngineData(); getMantle().close(); - getComplex().close(); - mode.close(); + IrisComplex currentComplex = complex; + if (currentComplex != null) { + currentComplex.close(); + } + complex = null; + EngineMode currentMode = mode; + if (currentMode != null) { + currentMode.close(); + } + mode = null; + effects = null; + worldManager = null; getData().dump(); getData().clearLists(); Iris.service(PreservationSVC.class).dereference(); Iris.debug("Engine Fully Shutdown!"); - complex = null; + } + + @Override + public IrisComplex getComplex() { + return ensureComplex(); + } + + @Override + public EngineMode getMode() { + return ensureMode(); } @Override @@ -510,7 +620,8 @@ public class IrisEngine implements Engine { } } } else { - mode.generate(x, z, blocks, vbiomes, multicore); + EngineMode activeMode = ensureMode(); + activeMode.generate(x, z, blocks, vbiomes, multicore); } World realWorld = getWorld().realWorld(); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 737876a33..2a334cd29 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -195,7 +195,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager { }; looper.setPriority(Thread.MIN_PRIORITY); looper.setName("Iris World Manager " + getTarget().getWorld().name()); - looper.start(); + } + + public void startManager() { + if (!looper.isAlive()) { + looper.start(); + } } private void discoverChunks() { @@ -528,6 +533,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return; } + IrisComplex complex = getEngine().getComplex(); + if (complex == null) { + return; + } + if (initial) { energy += 1.2; } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java new file mode 100644 index 000000000..124f610a7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -0,0 +1,265 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisCaveFieldModule; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; + +public class IrisCaveCarver3D { + private static final byte LIQUID_AIR = 0; + private static final byte LIQUID_WATER = 1; + private static final byte LIQUID_LAVA = 2; + private static final byte LIQUID_FORCED_AIR = 3; + + private final Engine engine; + private final IrisData data; + private final IrisCaveProfile profile; + private final CNG baseDensity; + private final CNG detailDensity; + private final CNG warpDensity; + private final CNG surfaceBreakDensity; + private final RNG thresholdRng; + private final KList modules; + private final double normalization; + private final MatterCavern carveAir; + private final MatterCavern carveWater; + private final MatterCavern carveLava; + private final MatterCavern carveForcedAir; + + public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { + this.engine = engine; + this.data = engine.getData(); + this.profile = profile; + this.carveAir = new MatterCavern(true, "", LIQUID_AIR); + this.carveWater = new MatterCavern(true, "", LIQUID_WATER); + this.carveLava = new MatterCavern(true, "", LIQUID_LAVA); + this.carveForcedAir = new MatterCavern(true, "", LIQUID_FORCED_AIR); + this.modules = new KList<>(); + + RNG baseRng = new RNG(engine.getSeedManager().getCarve()); + this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data); + this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data); + this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data); + this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data); + this.thresholdRng = baseRng.nextParallelRNG(489_112); + + double weight = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight()); + int index = 0; + for (IrisCaveFieldModule module : profile.getModules()) { + CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); + ModuleState state = new ModuleState(module, moduleDensity); + modules.add(state); + weight += Math.abs(state.weight); + index++; + } + + normalization = weight <= 0 ? 1 : weight; + } + + public int carve(MantleWriter writer, int chunkX, int chunkZ) { + int worldHeight = writer.getMantle().getWorldHeight(); + int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin())); + int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax())); + int sampleStep = Math.max(1, profile.getSampleStep()); + int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); + int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); + double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); + double surfaceBreakThresholdBoost = Math.max(0, profile.getSurfaceBreakThresholdBoost()); + int waterMinDepthBelowSurface = Math.max(0, profile.getWaterMinDepthBelowSurface()); + boolean waterRequiresFloor = profile.isWaterRequiresFloor(); + boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); + if (maxY < minY) { + return 0; + } + + int x0 = chunkX << 4; + int z0 = chunkZ << 4; + int carved = 0; + + for (int x = x0; x < x0 + 16; x++) { + for (int z = z0; z < z0 + 16; z++) { + double threshold = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + int columnSurface = engine.getHeight(x, z); + int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurface - surfaceClearance)); + boolean surfaceBreakColumn = allowSurfaceBreak + && signed(surfaceBreakDensity.noise(x, z)) >= surfaceBreakNoiseThreshold; + int columnMaxY = surfaceBreakColumn + ? Math.min(maxY, Math.max(minY, columnSurface)) + : clearanceTopY; + int surfaceBreakFloorY = Math.max(minY, columnSurface - surfaceBreakDepth); + if (columnMaxY < minY) { + continue; + } + + for (int y = minY; y <= columnMaxY; y += sampleStep) { + double localThreshold = threshold; + if (surfaceBreakColumn && y >= surfaceBreakFloorY) { + localThreshold += surfaceBreakThresholdBoost; + } + + if (sampleDensity(x, y, z) <= localThreshold) { + int carveMaxY = Math.min(columnMaxY, y + sampleStep - 1); + for (int yy = y; yy <= carveMaxY; yy++) { + writer.setData(x, yy, z, resolveMatter(x, yy, z, localThreshold, waterMinDepthBelowSurface, waterRequiresFloor)); + carved++; + } + } + } + } + } + + return carved; + } + + private double sampleDensity(int x, int y, int z) { + double warpedX = x; + double warpedY = y; + double warpedZ = z; + double warpStrength = profile.getWarpStrength(); + + if (warpStrength > 0) { + double offsetX = signed(warpDensity.noise(x, y, z)) * warpStrength; + double offsetY = signed(warpDensity.noise(y, z, x)) * warpStrength; + double offsetZ = signed(warpDensity.noise(z, x, y)) * warpStrength; + warpedX += offsetX; + warpedY += offsetY; + warpedZ += offsetZ; + } + + double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * profile.getBaseWeight(); + density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * profile.getDetailWeight(); + + for (ModuleState module : modules) { + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noise(warpedX, warpedY, warpedZ)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + return density / normalization; + } + + private MatterCavern resolveMatter(int x, int y, int z, double localThreshold, int waterMinDepthBelowSurface, boolean waterRequiresFloor) { + int lavaHeight = engine.getDimension().getCaveLavaHeight(); + int fluidHeight = engine.getDimension().getFluidHeight(); + + if (profile.isAllowLava() && y <= lavaHeight) { + return carveLava; + } + + if (profile.isAllowWater() && y <= fluidHeight) { + int surfaceY = engine.getHeight(x, z); + if (surfaceY - y < waterMinDepthBelowSurface) { + return carveAir; + } + + double depthFactor = Math.max(0, Math.min(1.5, (fluidHeight - y) / 48D)); + double cutoff = 0.35 + (depthFactor * 0.2); + double aquifer = signed(detailDensity.noise(x, y * 0.5D, z)); + if (aquifer <= cutoff) { + return carveAir; + } + + if (waterRequiresFloor && !hasAquiferCupSupport(x, y, z, localThreshold)) { + return carveAir; + } + + return carveWater; + } + + if (!profile.isAllowLava() && y <= lavaHeight) { + return carveForcedAir; + } + + return carveAir; + } + + private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) { + int floorY = Math.max(0, y - 1); + int deepFloorY = Math.max(0, y - 2); + int aboveY = Math.min(engine.getHeight() - 1, y + 1); + if (!isDensitySolid(x, floorY, z, threshold)) { + return false; + } + + if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) { + return false; + } + + int support = 0; + if (isDensitySolid(x + 1, y, z, threshold)) { + support++; + } + if (isDensitySolid(x - 1, y, z, threshold)) { + support++; + } + if (isDensitySolid(x, y, z + 1, threshold)) { + support++; + } + if (isDensitySolid(x, y, z - 1, threshold)) { + support++; + } + if (isDensitySolid(x, aboveY, z, threshold)) { + support++; + } + + return support >= 4; + } + + private boolean isDensitySolid(int x, int y, int z, double threshold) { + return sampleDensity(x, y, z) > threshold; + } + + private double signed(double value) { + return (value * 2D) - 1D; + } + + private static final class ModuleState { + private final CNG density; + private final int minY; + private final int maxY; + private final double weight; + private final double threshold; + private final boolean invert; + + private ModuleState(IrisCaveFieldModule module, CNG density) { + IrisRange range = module.getVerticalRange(); + this.density = density; + this.minY = (int) Math.floor(range.getMin()); + this.maxY = (int) Math.ceil(range.getMax()); + this.weight = module.getWeight(); + this.threshold = module.getThreshold(); + this.invert = module.isInvert(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 10e2def42..3a40abd2e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -25,14 +25,21 @@ import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisCarving; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.RNG; +import java.util.IdentityHashMap; +import java.util.Map; + @ComponentFlag(ReservedFlag.CARVED) public class MantleCarvingComponent extends IrisMantleComponent { + private final Map profileCarvers = new IdentityHashMap<>(); + public MantleCarvingComponent(EngineMantle engineMantle) { super(engineMantle, ReservedFlag.CARVED, 0); } @@ -49,9 +56,18 @@ public class MantleCarvingComponent extends IrisMantleComponent { @ChunkCoordinates private void carve(MantleWriter writer, RNG rng, int cx, int cz, IrisRegion region, IrisBiome biome) { - carve(getDimension().getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - carve(biome.getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - carve(region.getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); + IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); + IrisCaveProfile biomeProfile = biome.getCaveProfile(); + IrisCaveProfile regionProfile = region.getCaveProfile(); + IrisCaveProfile activeProfile = resolveActiveProfile(dimensionProfile, regionProfile, biomeProfile); + if (isProfileEnabled(activeProfile)) { + carveProfile(activeProfile, writer, cx, cz); + return; + } + + carve(getDimension().getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); + carve(biome.getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); + carve(region.getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); } @ChunkCoordinates @@ -59,17 +75,60 @@ public class MantleCarvingComponent extends IrisMantleComponent { carving.doCarving(writer, rng, getEngineMantle().getEngine(), cx << 4, -1, cz << 4, 0); } + private RNG nextCarveRng(RNG rng, int cx, int cz) { + return new RNG((rng.nextLong() * cx) + 490495L + cz); + } + + @ChunkCoordinates + private void carveProfile(IrisCaveProfile profile, MantleWriter writer, int cx, int cz) { + if (!isProfileEnabled(profile)) { + return; + } + + IrisCaveCarver3D carver = getCarver(profile); + carver.carve(writer, cx, cz); + } + + private IrisCaveCarver3D getCarver(IrisCaveProfile profile) { + synchronized (profileCarvers) { + IrisCaveCarver3D carver = profileCarvers.get(profile); + if (carver != null) { + return carver; + } + + IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); + profileCarvers.put(profile, createdCarver); + return createdCarver; + } + } + + private boolean isProfileEnabled(IrisCaveProfile profile) { + return profile != null && profile.isEnabled(); + } + + private IrisCaveProfile resolveActiveProfile(IrisCaveProfile dimensionProfile, IrisCaveProfile regionProfile, IrisCaveProfile biomeProfile) { + if (isProfileEnabled(biomeProfile)) { + return biomeProfile; + } + + if (isProfileEnabled(regionProfile)) { + return regionProfile; + } + + return dimensionProfile; + } + protected int computeRadius() { - var dimension = getDimension(); + IrisDimension dimension = getDimension(); int max = 0; max = Math.max(max, dimension.getCarving().getMaxRange(getData(), 0)); - for (var i : dimension.getAllRegions(this::getData)) { + for (IrisRegion i : dimension.getAllRegions(this::getData)) { max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); } - for (var i : dimension.getAllBiomes(this::getData)) { + for (IrisBiome i : dimension.getAllBiomes(this::getData)) { max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 905ebd2c2..d126ec80b 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -63,21 +63,29 @@ public class MantleObjectComponent extends IrisMantleComponent { int xxx = 8 + (x << 4); int zzz = 8 + (z << 4); IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); + IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(xxx, zzz); + IrisBiome caveBiome = getComplex().getCaveBiomeStream().get(xxx, zzz); if (traceRegen) { Iris.info("Regen object layer start: chunk=" + x + "," + z - + " biome=" + biome.getLoadKey() + + " surfaceBiome=" + surfaceBiome.getLoadKey() + + " caveBiome=" + caveBiome.getLoadKey() + " region=" + region.getLoadKey() - + " biomePlacers=" + biome.getSurfaceObjects().size() - + " regionPlacers=" + region.getSurfaceObjects().size()); + + " biomeSurfacePlacers=" + surfaceBiome.getSurfaceObjects().size() + + " biomeCavePlacers=" + caveBiome.getCarvingObjects().size() + + " regionSurfacePlacers=" + region.getSurfaceObjects().size() + + " regionCavePlacers=" + region.getCarvingObjects().size()); } - ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, biome, region, traceRegen); + ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, traceRegen); if (traceRegen) { Iris.info("Regen object layer done: chunk=" + x + "," + z - + " biomePlacersChecked=" + summary.biomePlacersChecked() - + " biomePlacersTriggered=" + summary.biomePlacersTriggered() - + " regionPlacersChecked=" + summary.regionPlacersChecked() - + " regionPlacersTriggered=" + summary.regionPlacersTriggered() + + " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked() + + " biomeSurfacePlacersTriggered=" + summary.biomeSurfacePlacersTriggered() + + " biomeCavePlacersChecked=" + summary.biomeCavePlacersChecked() + + " biomeCavePlacersTriggered=" + summary.biomeCavePlacersTriggered() + + " regionSurfacePlacersChecked=" + summary.regionSurfacePlacersChecked() + + " regionSurfacePlacersTriggered=" + summary.regionSurfacePlacersTriggered() + + " regionCavePlacersChecked=" + summary.regionCavePlacersChecked() + + " regionCavePlacersTriggered=" + summary.regionCavePlacersTriggered() + " objectAttempts=" + summary.objectAttempts() + " objectPlaced=" + summary.objectPlaced() + " objectRejected=" + summary.objectRejected() @@ -92,32 +100,40 @@ public class MantleObjectComponent extends IrisMantleComponent { } @ChunkCoordinates - private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome biome, IrisRegion region, boolean traceRegen) { - int biomeChecked = 0; - int biomeTriggered = 0; - int regionChecked = 0; - int regionTriggered = 0; + private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, boolean traceRegen) { + int biomeSurfaceChecked = 0; + int biomeSurfaceTriggered = 0; + int biomeCaveChecked = 0; + int biomeCaveTriggered = 0; + int regionSurfaceChecked = 0; + int regionSurfaceTriggered = 0; + int regionCaveChecked = 0; + int regionCaveTriggered = 0; int attempts = 0; int placed = 0; int rejected = 0; int nullObjects = 0; int errors = 0; + IrisCaveProfile biomeCaveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile()); + IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome.getCaveProfile()); + int biomeSurfaceExclusionDepth = resolveSurfaceObjectExclusionDepth(biomeCaveProfile); + int regionSurfaceExclusionDepth = resolveSurfaceObjectExclusionDepth(regionCaveProfile); - for (IrisObjectPlacement i : biome.getSurfaceObjects()) { - biomeChecked++; + for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) { + biomeSurfaceChecked++; boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); if (traceRegen) { Iris.info("Regen object placer chance: chunk=" + x + "," + z - + " scope=biome" + + " scope=biome-surface" + " chanceResult=" + chance + " chanceBase=" + i.getChance() + " densityMid=" + i.getDensity() + " objects=" + i.getPlace().size()); } if (chance) { - biomeTriggered++; + biomeSurfaceTriggered++; try { - ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, traceRegen, x, z, "biome"); + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, traceRegen, x, z, "biome-surface"); attempts += result.attempts(); placed += result.placed(); rejected += result.rejected(); @@ -126,7 +142,41 @@ public class MantleObjectComponent extends IrisMantleComponent { } catch (Throwable e) { errors++; Iris.reportError(e); - Iris.error("Failed to place objects in the following biome: " + biome.getName()); + Iris.error("Failed to place objects in the following biome: " + surfaceBiome.getName()); + Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + Iris.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + + for (IrisObjectPlacement i : caveBiome.getCarvingObjects()) { + if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + continue; + } + biomeCaveChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + Iris.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=biome-cave" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + biomeCaveTriggered++; + try { + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, traceRegen, x, z, "biome-cave"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + Iris.reportError(e); + Iris.error("Failed to place cave objects in the following biome: " + caveBiome.getName()); Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); Iris.error("Are these objects missing?"); e.printStackTrace(); @@ -135,20 +185,20 @@ public class MantleObjectComponent extends IrisMantleComponent { } for (IrisObjectPlacement i : region.getSurfaceObjects()) { - regionChecked++; + regionSurfaceChecked++; boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); if (traceRegen) { Iris.info("Regen object placer chance: chunk=" + x + "," + z - + " scope=region" + + " scope=region-surface" + " chanceResult=" + chance + " chanceBase=" + i.getChance() + " densityMid=" + i.getDensity() + " objects=" + i.getPlace().size()); } if (chance) { - regionTriggered++; + regionSurfaceTriggered++; try { - ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, traceRegen, x, z, "region"); + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, traceRegen, x, z, "region-surface"); attempts += result.attempts(); placed += result.placed(); rejected += result.rejected(); @@ -165,11 +215,49 @@ public class MantleObjectComponent extends IrisMantleComponent { } } + for (IrisObjectPlacement i : region.getCarvingObjects()) { + if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + continue; + } + regionCaveChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + Iris.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=region-cave" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + regionCaveTriggered++; + try { + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, traceRegen, x, z, "region-cave"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + Iris.reportError(e); + Iris.error("Failed to place cave objects in the following region: " + region.getName()); + Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + Iris.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + return new ObjectPlacementSummary( - biomeChecked, - biomeTriggered, - regionChecked, - regionTriggered, + biomeSurfaceChecked, + biomeSurfaceTriggered, + biomeCaveChecked, + biomeCaveTriggered, + regionSurfaceChecked, + regionSurfaceTriggered, + regionCaveChecked, + regionCaveTriggered, attempts, placed, rejected, @@ -185,6 +273,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int x, int z, IrisObjectPlacement objectPlacement, + int surfaceObjectExclusionDepth, boolean traceRegen, int chunkX, int chunkZ, @@ -213,6 +302,11 @@ public class MantleObjectComponent extends IrisMantleComponent { } int xx = rng.i(x, x + 15); int zz = rng.i(z, z + 15); + int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v); + if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) { + rejected++; + continue; + } int id = rng.i(0, Integer.MAX_VALUE); try { int result = v.place(xx, -1, zz, writer, objectPlacement, rng, (b, data) -> { @@ -253,16 +347,270 @@ public class MantleObjectComponent extends IrisMantleComponent { return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); } + @ChunkCoordinates + private ObjectPlacementResult placeCaveObject( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + IrisObjectPlacement objectPlacement, + IrisCaveProfile caveProfile, + boolean traceRegen, + int metricChunkX, + int metricChunkZ, + String scope + ) { + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int density = objectPlacement.getDensity(rng, minX, minZ, getData()); + KMap> anchorCache = new KMap<>(); + IrisCaveAnchorMode anchorMode = resolveAnchorMode(objectPlacement, caveProfile); + int anchorScanStep = resolveAnchorScanStep(caveProfile); + int objectMinDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile); + int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile); + + for (int i = 0; i < density; i++) { + attempts++; + IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); + if (object == null) { + nullObjects++; + if (traceRegen) { + Iris.warn("Regen cave object placement null object: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " densityIndex=" + i + + " density=" + density + + " placementKeys=" + objectPlacement.getPlace().toString(",")); + } + continue; + } + + int x = 0; + int z = 0; + int y = -1; + for (int search = 0; search < anchorSearchAttempts; search++) { + int candidateX = rng.i(minX, minX + 15); + int candidateZ = rng.i(minZ, minZ + 15); + int candidateY = findCaveAnchorY(writer, rng, candidateX, candidateZ, anchorMode, anchorScanStep, objectMinDepthBelowSurface, anchorCache); + if (candidateY < 0) { + continue; + } + + x = candidateX; + z = candidateZ; + y = candidateY; + break; + } + + if (y < 0) { + rejected++; + continue; + } + + int id = rng.i(0, Integer.MAX_VALUE); + + try { + int result = object.place(x, y, z, writer, objectPlacement, rng, (b, data) -> { + writer.setData(b.getX(), b.getY(), b.getZ(), object.getLoadKey() + "@" + id); + if (objectPlacement.isDolphinTarget() && objectPlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + + if (result >= 0) { + placed++; + } else { + rejected++; + } + + if (traceRegen) { + Iris.info("Regen cave object placement result: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " object=" + object.getLoadKey() + + " resultY=" + result + + " anchorY=" + y + + " px=" + x + + " pz=" + z + + " densityIndex=" + i + + " density=" + density); + } + } catch (Throwable e) { + errors++; + Iris.reportError(e); + Iris.error("Regen cave object placement exception: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " object=" + object.getLoadKey() + + " densityIndex=" + i + + " density=" + density + + " error=" + e.getClass().getSimpleName() + ":" + e.getMessage()); + } + } + + return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); + } + + private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, KMap> anchorCache) { + long key = Cache.key(x, z); + KList anchors = anchorCache.computeIfAbsent(key, (k) -> scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z)); + if (anchors.isEmpty()) { + return -1; + } + + if (anchors.size() == 1) { + return anchors.get(0); + } + + return anchors.get(rng.i(0, anchors.size() - 1)); + } + + private KList scanCaveAnchorColumn(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z) { + KList anchors = new KList<>(); + int height = getEngineMantle().getEngine().getHeight(); + int step = Math.max(1, anchorScanStep); + int surfaceY = getEngineMantle().getEngine().getHeight(x, z); + int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface)); + if (maxAnchorY <= 1) { + return anchors; + } + + for (int y = 1; y < maxAnchorY; y += step) { + if (!writer.isCarved(x, y, z)) { + continue; + } + + boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z); + boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z); + if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { + anchors.add(y); + } + } + + return anchors; + } + + private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) { + return switch (anchorMode) { + case PROFILE_DEFAULT, FLOOR -> solidBelow; + case CEILING -> solidAbove; + case CENTER -> !solidBelow && !solidAbove; + case ANY -> true; + }; + } + + private IrisCaveProfile resolveCaveProfile(IrisCaveProfile preferred, IrisCaveProfile secondary) { + IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); + if (preferred != null && preferred.isEnabled()) { + return preferred; + } + + if (secondary != null && secondary.isEnabled()) { + return secondary; + } + + if (dimensionProfile != null) { + return dimensionProfile; + } + + return new IrisCaveProfile(); + } + + private IrisCaveAnchorMode resolveAnchorMode(IrisObjectPlacement objectPlacement, IrisCaveProfile caveProfile) { + IrisCaveAnchorMode placementMode = objectPlacement.getCaveAnchorMode(); + if (placementMode != null && !placementMode.equals(IrisCaveAnchorMode.PROFILE_DEFAULT)) { + return placementMode; + } + + if (caveProfile == null) { + return IrisCaveAnchorMode.FLOOR; + } + + IrisCaveAnchorMode profileMode = caveProfile.getDefaultObjectAnchor(); + if (profileMode == null || profileMode.equals(IrisCaveAnchorMode.PROFILE_DEFAULT)) { + return IrisCaveAnchorMode.FLOOR; + } + + return profileMode; + } + + private int resolveAnchorScanStep(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 1; + } + + return Math.max(1, caveProfile.getAnchorScanStep()); + } + + private int resolveObjectMinDepthBelowSurface(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 6; + } + + return Math.max(0, caveProfile.getObjectMinDepthBelowSurface()); + } + + private int resolveSurfaceObjectExclusionDepth(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 5; + } + + return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth()); + } + + private int resolveSurfaceObjectExclusionRadius(IrisObject object) { + if (object == null) { + return 1; + } + + int maxDimension = Math.max(object.getW(), object.getD()); + return Math.max(1, Math.min(8, Math.floorDiv(Math.max(1, maxDimension), 2))); + } + + private int resolveAnchorSearchAttempts(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 6; + } + + return Math.max(1, caveProfile.getAnchorSearchAttempts()); + } + + private boolean hasSurfaceCarveExposure(MantleWriter writer, int x, int z, int depth, int radius) { + int horizontalRadius = Math.max(0, radius); + for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) { + for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) { + int columnX = x + dx; + int columnZ = z + dz; + int surfaceY = getEngineMantle().getEngine().getHeight(columnX, columnZ, true); + int fromY = Math.max(1, surfaceY - Math.max(0, depth)); + int toY = Math.min(getEngineMantle().getEngine().getHeight() - 1, surfaceY + 1); + for (int y = fromY; y <= toY; y++) { + if (writer.isCarved(columnX, y, columnZ)) { + return true; + } + } + } + } + + return false; + } + private boolean isRegenTraceThread() { return Thread.currentThread().getName().startsWith("Iris-Regen-") && IrisSettings.get().getGeneral().isDebug(); } private record ObjectPlacementSummary( - int biomePlacersChecked, - int biomePlacersTriggered, - int regionPlacersChecked, - int regionPlacersTriggered, + int biomeSurfacePlacersChecked, + int biomeSurfacePlacersTriggered, + int biomeCavePlacersChecked, + int biomeCavePlacersTriggered, + int regionSurfacePlacersChecked, + int regionSurfacePlacersTriggered, + int regionCavePlacersChecked, + int regionCavePlacersTriggered, int objectAttempts, int objectPlaced, int objectRejected, diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 108392363..7fdd0e618 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -108,6 +108,8 @@ public class IrisCarveModifier extends EngineAssignedModifier { output.set(rx, yy, rz, context.getFluid().get(rx, rz)); } else if (c.isLava()) { output.set(rx, yy, rz, LAVA); + } else if (c.getLiquid() == 3) { + output.set(rx, yy, rz, AIR); } else { if (getEngine().getDimension().getCaveLavaHeight() > yy) { output.set(rx, yy, rz, LAVA); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index a53b1408f..28e7ad0d2 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -102,6 +102,8 @@ public class IrisBiome extends IrisRegistrant implements IRare { private int lockLayersMax = 7; @Desc("Carving configuration for the dimension") private IrisCarving carving = new IrisCarving(); + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") private IrisFluidBodies fluidBodies = new IrisFluidBodies(); @MinNumber(1) @@ -194,12 +196,21 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public double getGenLinkMax(String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + Integer v = genCacheMax.aquire(() -> { KMap l = new KMap<>(); for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i.getMax()); + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMax()); } @@ -210,12 +221,21 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public double getGenLinkMin(String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + Integer v = genCacheMin.aquire(() -> { KMap l = new KMap<>(); for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i.getMin()); + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMin()); } return l; @@ -225,12 +245,21 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public IrisBiomeGeneratorLink getGenLink(String loadKey) { + if (loadKey == null || loadKey.isBlank()) { + return null; + } + return genCache.aquire(() -> { KMap l = new KMap<>(); for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i); + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i); } return l; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java index 8a8ad3e4c..2624f143d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java @@ -52,12 +52,21 @@ public class IrisBiomeGeneratorLink { public IrisGenerator getCachedGenerator(DataProvider g) { return gen.aquire(() -> { - IrisGenerator gen = g.getData().getGeneratorLoader().load(getGenerator()); + String generatorKey = getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + generatorKey = "default"; + } + + IrisGenerator gen = g.getData().getGeneratorLoader().load(generatorKey); if (gen == null) { gen = new IrisGenerator(); } + if (gen.getLoadKey() == null || gen.getLoadKey().isBlank()) { + gen.setLoadKey(generatorKey); + } + return gen; }); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java new file mode 100644 index 000000000..e6a82f168 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java @@ -0,0 +1,21 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Defines which carved-space anchor to target for cave object placement.") +public enum IrisCaveAnchorMode { + @Desc("Use the active cave profile default anchor mode.") + PROFILE_DEFAULT, + + @Desc("Target cave floor anchors where carved space has solid support below.") + FLOOR, + + @Desc("Target cave ceiling anchors where carved space has solid support above.") + CEILING, + + @Desc("Target carved positions with no immediate solid support above or below.") + CENTER, + + @Desc("Target any carved-space anchor.") + ANY +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java new file mode 100644 index 000000000..b812071aa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java @@ -0,0 +1,36 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("cave-field-module") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a modular cave-density layer.") +@Data +public class IrisCaveFieldModule { + @Desc("Density style used by this module.") + private IrisGeneratorStyle style = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + + @MinNumber(0) + @Desc("Layer contribution multiplier.") + private double weight = 1; + + @MinNumber(-1) + @MaxNumber(1) + @Desc("Threshold offset applied to this layer before blending.") + private double threshold = 0; + + @Desc("Vertical bounds where this module can contribute.") + private IrisRange verticalRange = new IrisRange(0, 384); + + @Desc("Invert this module before weighting.") + private boolean invert = false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java new file mode 100644 index 000000000..5d7a3fe6c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java @@ -0,0 +1,127 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("cave-profile") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a configurable 3D cave profile.") +@Data +public class IrisCaveProfile { + @Desc("Enable profile-driven cave carving.") + private boolean enabled = false; + + @Desc("Global vertical bounds for profile cave carving.") + private IrisRange verticalRange = new IrisRange(0, 384); + + @Desc("Base density style for cave field generation.") + private IrisGeneratorStyle baseDensityStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + + @Desc("Detail density style blended into base caves.") + private IrisGeneratorStyle detailDensityStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX); + + @Desc("Warp style used to distort cave coordinates.") + private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.FLAT); + + @MinNumber(0) + @Desc("Base cave field multiplier.") + private double baseWeight = 1; + + @MinNumber(0) + @Desc("Detail cave field multiplier.") + private double detailWeight = 0.35; + + @MinNumber(0) + @Desc("Coordinate warp strength for cave fields.") + private double warpStrength = 0; + + @Desc("Threshold range used for carve cutoff decisions.") + private IrisStyledRange densityThreshold = new IrisStyledRange(-0.2, 0.2, NoiseStyle.CELLULAR_IRIS_DOUBLE.style()); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Extra threshold bias subtracted from sampled threshold before carve tests.") + private double thresholdBias = 0.16; + + @MinNumber(1) + @MaxNumber(8) + @Desc("Vertical sample step used while evaluating cave density.") + private int sampleStep = 2; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum solid clearance below terrain surface where carving may occur.") + private int surfaceClearance = 4; + + @Desc("Allow profile-driven cave carving to break through terrain surface in selected columns.") + private boolean allowSurfaceBreak = true; + + @Desc("Noise style used to decide where surface-breaking cave columns are allowed.") + private IrisGeneratorStyle surfaceBreakStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.08); + + @MinNumber(-1) + @MaxNumber(1) + @Desc("Minimum signed surface-break noise value required before near-surface carving is allowed.") + private double surfaceBreakNoiseThreshold = 0.62; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Near-surface depth window used for surface-break carve logic.") + private int surfaceBreakDepth = 18; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Additional threshold boost applied while carving in the surface-break depth window.") + private double surfaceBreakThresholdBoost = 0.2; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum depth below terrain surface required for cave-only object anchor placement.") + private int objectMinDepthBelowSurface = 6; + + @MinNumber(0) + @MaxNumber(32) + @Desc("Skip surface-object placement when carved cells exist this many blocks below terrain surface.") + private int surfaceObjectExclusionDepth = 5; + + @ArrayType(type = IrisCaveFieldModule.class, min = 1) + @Desc("Additional layered cave-density modules.") + private KList modules = new KList<>(); + + @Desc("Default cave anchor mode for cave-only object placement.") + private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR; + + @MinNumber(1) + @MaxNumber(8) + @Desc("Vertical scan step used while searching cave anchors.") + private int anchorScanStep = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("Maximum random column retries while searching a valid cave object anchor in the chunk.") + private int anchorSearchAttempts = 6; + + @Desc("Allow cave water placement below fluid level.") + private boolean allowWater = true; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum depth below terrain surface required before cave water may be placed.") + private int waterMinDepthBelowSurface = 12; + + @Desc("Require solid floor support below cave water to reduce cascading cave waterfalls.") + private boolean waterRequiresFloor = true; + + @Desc("Allow cave lava placement based on lava height.") + private boolean allowLava = true; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 7c13d882d..c2358b046 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -142,10 +142,12 @@ public class IrisDimension extends IrisRegistrant { private boolean postProcessingSlabs = true; @Desc("Add painted walls in post processing") private boolean postProcessingWalls = true; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); - @Desc("Configuration of fluid bodies such as rivers & lakes") - private IrisFluidBodies fluidBodies = new IrisFluidBodies(); + @Desc("Carving configuration for the dimension") + private IrisCarving carving = new IrisCarving(); + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); + @Desc("Configuration of fluid bodies such as rivers & lakes") + private IrisFluidBodies fluidBodies = new IrisFluidBodies(); @Desc("forceConvertTo320Height") private Boolean forceConvertTo320Height = false; @Desc("The world environment") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 4e150ca17..c51fad695 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -689,10 +689,7 @@ public class IrisObject extends IrisRegistrant { // todo Convert this to a dedicated mode. y = (getH() + 1) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || - placer.isCarved(x, y - 1, z) || - placer.isCarved(x, y - 2, z) || - placer.isCarved(x, y - 3, z)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { bail = true; } } @@ -700,7 +697,7 @@ public class IrisObject extends IrisRegistrant { if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT) { y = (c != null ? c.getSurface() : placer.getHighest(x, z, getLoader(), config.isUnderwater())) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { bail = true; } } @@ -717,7 +714,7 @@ public class IrisObject extends IrisRegistrant { for (int ii = minZ; ii <= maxZ; ii++) { int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { bail = true; break; } @@ -743,7 +740,7 @@ public class IrisObject extends IrisRegistrant { for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { bail = true; break; } @@ -767,7 +764,7 @@ public class IrisObject extends IrisRegistrant { for (int ii = minZ; ii <= maxZ; ii++) { int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { bail = true; break; } @@ -795,7 +792,7 @@ public class IrisObject extends IrisRegistrant { for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { bail = true; break; } @@ -808,7 +805,7 @@ public class IrisObject extends IrisRegistrant { } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { y = placer.getHighest(x, z, getLoader(), config.isUnderwater()) + rty; if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { bail = true; } } @@ -816,7 +813,7 @@ public class IrisObject extends IrisRegistrant { } else { y = yv; if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { bail = true; } } @@ -825,7 +822,7 @@ public class IrisObject extends IrisRegistrant { if (yv >= 0 && config.isBottom()) { y += Math.floorDiv(h, 2); if (!config.isForcePlace()) { - bail = placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z); + bail = shouldBailForCarvingAnchor(placer, config, x, y, z); } } @@ -1160,6 +1157,23 @@ public class IrisObject extends IrisRegistrant { return y; } + private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) { + boolean carved = isCarvedAnchor(placer, x, y, z); + CarvingMode carvingMode = placement.getCarvingSupport(); + return switch (carvingMode) { + case SURFACE_ONLY -> carved; + case CARVING_ONLY -> !carved; + case ANYWHERE -> false; + }; + } + + private boolean isCarvedAnchor(IObjectPlacer placer, int x, int y, int z) { + return placer.isCarved(x, y, z) + || placer.isCarved(x, y - 1, z) + || placer.isCarved(x, y - 2, z) + || placer.isCarved(x, y - 3, z); + } + public IrisObject rotateCopy(IrisObjectRotation rt) { IrisObject copy = copy(); copy.rotate(rt, 0, 0, 0); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java index 70b3597af..d2ad83891 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -99,6 +99,8 @@ public class IrisObjectPlacement { private boolean underwater = false; @Desc("If set to true, objects will place in carvings (such as underground) or under an overhang.") private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + @Desc("When carving placement is enabled, select which carved-space anchor this placement targets.") + private IrisCaveAnchorMode caveAnchorMode = IrisCaveAnchorMode.PROFILE_DEFAULT; @Desc("If this is defined, this object wont place on the terrain heightmap, but instead on this virtual heightmap") private IrisNoiseGenerator heightmap; @Desc("If set to true, Iris will try to fill the insides of 'rooms' and 'pockets' where air should fit based off of raytrace checks. This prevents a village house placing in an area where a tree already exists, and instead replaces the parts of the tree where the interior of the structure is. \n\nThis operation does not affect warmed-up generation speed however it does slow down loading objects.") @@ -165,6 +167,7 @@ public class IrisObjectPlacement { p.setOnwater(onwater); p.setSmartBore(smartBore); p.setCarvingSupport(carvingSupport); + p.setCaveAnchorMode(caveAnchorMode); p.setUnderwater(underwater); p.setBoreExtendMaxY(boreExtendMaxY); p.setBoreExtendMinY(boreExtendMinY); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 9bfbcd812..1d942d7ad 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -114,6 +114,8 @@ public class IrisRegion extends IrisRegistrant implements IRare { private double caveBiomeZoom = 1; @Desc("Carving configuration for the dimension") private IrisCarving carving = new IrisCarving(); + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") private IrisFluidBodies fluidBodies = new IrisFluidBodies(); @RegistryListResource(IrisBiome.class) diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java index fd6596832..bd12dc112 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/CustomBiomeSource.java @@ -203,8 +203,15 @@ public class CustomBiomeSource extends BiomeSource { int blockX = x << 2; int blockZ = z << 2; - int blockY = (y - engine.getMinHeight()) << 2; - IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + int blockY = y << 2; + int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); + boolean underground = blockY <= surfaceY - 2; + IrisBiome irisBiome = underground + ? engine.getComplex().getCaveBiomeStream().get(blockX, blockZ) + : engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + if (irisBiome == null && underground) { + irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); + } if (irisBiome == null) { return getFallbackBiome(); } @@ -226,7 +233,9 @@ public class CustomBiomeSource extends BiomeSource { return getFallbackBiome(); } - org.bukkit.block.Biome vanillaBiome = irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ); + org.bukkit.block.Biome vanillaBiome = underground + ? irisBiome.getGroundBiome(noiseRng, blockX, blockY, blockZ) + : irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ); Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); if (holder != null) { return holder;