Terrain is important

- Fixed Ores generating all over the surface.
- fixed UI bug
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 15:20:51 -04:00
1 parent 0510c108be
commit a368d42558
15 files changed
+408 -88

No files matched your search

@@ -42,13 +42,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class WorldRuntimeControlService {
private static final int STUDIO_ENTRY_VIEW_DISTANCE = 2;
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
private static final int SAFE_ENTRY_UPWARD_ALLOWANCE = 32;
private static final double BLOCK_CENTER = 0.5D;
private static final double COLLISION_EPSILON = 0.000001D;
private static final Method PLAYER_GET_VIEW_DISTANCE_METHOD = findPlayerMethod("getViewDistance");
private static final Method PLAYER_SET_VIEW_DISTANCE_METHOD = findPlayerMethod("setViewDistance", int.class);
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
Material.CACTUS,
Material.CAMPFIRE,
@@ -370,7 +367,6 @@ public final class WorldRuntimeControlService {
CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player);
PlayerViewDistanceRestore viewDistanceRestore = new PlayerViewDistanceRestore(player);
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
future.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success)) {
@@ -380,7 +376,6 @@ public final class WorldRuntimeControlService {
if (teleport != null && !teleport.isDone()) {
teleport.cancel(false);
}
viewDistanceRestore.restore();
modeRestore.restore();
});
boolean scheduled = J.runEntity(player, () -> {
@@ -390,9 +385,7 @@ public final class WorldRuntimeControlService {
}
if (gameMode != null) {
modeRestore.apply(gameMode);
viewDistanceRestore.apply();
if (future.isDone()) {
viewDistanceRestore.restore();
modeRestore.restore();
return;
}
@@ -417,7 +410,6 @@ public final class WorldRuntimeControlService {
if (Boolean.TRUE.equals(success)) {
if (future.complete(true)) {
viewDistanceRestore.restore();
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
}
return;
@@ -804,14 +796,6 @@ public final class WorldRuntimeControlService {
return method.invoke(instance);
}
private static Method findPlayerMethod(String methodName, Class<?>... parameterTypes) {
try {
return Player.class.getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ignored) {
return null;
}
}
@FunctionalInterface
interface TeleportExecutor {
CompletableFuture<Boolean> teleport(Player player, Location location);
@@ -857,56 +841,4 @@ public final class WorldRuntimeControlService {
}
}
}
private static final class PlayerViewDistanceRestore {
private final Player player;
private final AtomicBoolean changed;
private final AtomicBoolean restored;
private int previousViewDistance;
private PlayerViewDistanceRestore(Player player) {
this.player = player;
changed = new AtomicBoolean(false);
restored = new AtomicBoolean(false);
}
private void apply() {
if (PLAYER_GET_VIEW_DISTANCE_METHOD == null || PLAYER_SET_VIEW_DISTANCE_METHOD == null) {
return;
}
try {
Object currentValue = PLAYER_GET_VIEW_DISTANCE_METHOD.invoke(player);
if (!(currentValue instanceof Number)) {
return;
}
int currentViewDistance = ((Number) currentValue).intValue();
if (currentViewDistance <= STUDIO_ENTRY_VIEW_DISTANCE) {
return;
}
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, STUDIO_ENTRY_VIEW_DISTANCE);
previousViewDistance = currentViewDistance;
changed.set(true);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to apply a temporary player view distance for a studio teleport.", failure);
}
}
private void restore() {
if (!changed.get() || !restored.compareAndSet(false, true)) {
return;
}
Runnable restoration = () -> {
try {
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, previousViewDistance);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to restore a player's view distance after a studio teleport.", failure);
}
};
if (!J.runEntity(player, restoration)) {
IrisLogging.reportError(
"Failed to restore a player's view distance after a studio teleport.",
new IllegalStateException("The player entity scheduler rejected the view-distance restoration."));
}
}
}
}
@@ -518,7 +518,7 @@ public class BoardSVC implements IrisService, BoardProvider {
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getComplex().getSlopeStream().get(x, z), 2))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BLOCK_UPDATES, MessageArgument.trusted("updates", Form.f(engine.getBlockUpdatesPerSecond()))));
lines.add("&7&m ");
this.lines = lines;
@@ -137,6 +137,7 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<Double> overlayStream;
private ProceduralStream<Double> heightFluidStream;
private ProceduralStream<Double> slopeStream;
private ProceduralStream<Double> naturalSlopeStream;
private ProceduralStream<Double> riverDistanceStream;
private ProceduralStream<Double> riverFlowStream;
private ProceduralStream<Double> riverCarveWeightStream;
@@ -316,7 +317,8 @@ public class IrisComplex implements DataProvider {
(x, z) -> naturalHeightStream.getDouble(x, z),
(x, z) -> describeNaturalHeight(engine, x, z),
this::sampleNaturalOcean,
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint)
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
() -> engine.getPlatformHooks().isMainThread()
));
}
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
@@ -325,6 +327,7 @@ public class IrisComplex implements DataProvider {
.round();
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
@@ -704,6 +707,11 @@ public class IrisComplex implements DataProvider {
return (int) Math.round(naturalHeightStream.getDouble(x, z));
}
/** The terrain slope of a column from the natural height stream only: never touches hydrology or its caches. */
public double naturalSlope(int x, int z) {
return naturalSlopeStream.getDouble(x, z);
}
/** The surface biome a column would have without any river content, from natural streams only. */
public IrisBiome naturalSurfaceBiome(int x, int z) {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
@@ -348,6 +348,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getPlatformHooks().isMainThread() && !getComplex().isHydrologyPlanned(x, z);
}
/** The terrain slope at a column, answered from natural terrain when the server thread may not wait. */
@BlockCoordinates
default double getSlope(int x, int z) {
if (answersFromNaturalTerrain(x, z)) {
return getComplex().naturalSlope(x, z);
}
return getComplex().getSlopeStream().getDouble(x, z);
}
@BlockCoordinates
default int getHeight(int x, int z) {
return getHeight(x, z, true);
@@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
public final class HydrologyTileCache implements AutoCloseable {
private static final int DEFAULT_MAXIMUM_ENTRIES = 64;
@@ -31,6 +32,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
private final Executor prefetchExecutor;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<HydrologyTile>> planning;
private final BooleanSupplier waitingForbidden;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES);
@@ -40,13 +42,26 @@ public final class HydrologyTileCache implements AutoCloseable {
this(planner, maximumEntries, null);
}
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries, Executor prefetchExecutor) {
this(planner, maximumEntries, prefetchExecutor, null);
}
/**
* @param prefetchExecutor runs neighbour tile planning ahead of generation, or null to plan
* tiles only when a chunk needs them
* @param waitingForbidden true on a thread that must never wait for a cold plan, so a sample
* from it is answered as "no hydrology here" while the tiles it needs
* are handed to the prefetch executor; null lets every caller wait
*/
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries, Executor prefetchExecutor) {
public HydrologyTileCache(
HydrologyPlanner planner,
int maximumEntries,
Executor prefetchExecutor,
BooleanSupplier waitingForbidden
) {
this.planner = Objects.requireNonNull(planner, "planner");
this.prefetchExecutor = prefetchExecutor;
this.waitingForbidden = waitingForbidden;
this.planning = new ConcurrentHashMap<>();
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive.");
@@ -149,17 +164,31 @@ public final class HydrologyTileCache implements AutoCloseable {
if (composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
return true;
}
boolean planned = true;
return !requestUnplanned(chunkX, chunkZ);
}
/**
* Hands every tile the chunk needs and does not have to the prefetch executor and says whether
* any was missing. Nothing is planned on the caller, so this is safe from a thread that must
* never wait for a cold plan.
*/
private boolean requestUnplanned(int chunkX, int chunkZ) {
boolean missing = false;
for (HydrologyTileKey key : relevantKeys(chunkX, chunkZ)) {
if (tiles.getIfPresent(key) != null) {
continue;
}
planned = false;
missing = true;
if (prefetchExecutor != null) {
prefetch(key);
}
}
return planned;
return missing;
}
/** Whether the calling thread must never wait for a cold hydrology plan. */
private boolean waitsForbidden() {
return waitingForbidden != null && waitingForbidden.getAsBoolean();
}
public HydrologyRenderSample renderAt(int blockX, int blockZ) {
@@ -206,6 +235,14 @@ public final class HydrologyTileCache implements AutoCloseable {
return local.columns();
}
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
if (composedChunks.getIfPresent(packedChunk) == null
&& waitsForbidden()
&& requestUnplanned(chunkX, chunkZ)) {
// A thread that must never wait (the server thread answering a query) gets a riverless
// answer while the tiles it would have waited for are planned on the prefetch executor.
// The empty result is never cached, so the next query composes the real columns.
return ChunkColumns.empty(chunkX, chunkZ);
}
ChunkColumns columns = composedChunks.get(
packedChunk,
ignored -> composeChunkColumns(chunkX, chunkZ)
@@ -467,6 +504,11 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
/** A chunk with no hydrology at all, the answer for a caller that may not wait for its plan. */
private static ChunkColumns empty(int chunkX, int chunkZ) {
return new ChunkColumns(chunkX, chunkZ, new HydrologyColumnSample[CHUNK_COLUMN_COUNT]);
}
private Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
int localX = Math.floorMod(blockX, CHUNK_SIZE);
int localZ = Math.floorMod(blockZ, CHUNK_SIZE);
@@ -125,7 +125,12 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
0,
context.caveViewFactory()
);
this.cache = new HydrologyTileCache(planner, MAXIMUM_CACHE_TILES, IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
this.cache = new HydrologyTileCache(
planner,
MAXIMUM_CACHE_TILES,
IrisPlatforms.isBound() ? MultiBurst.hydrology : null,
context.waitingForbidden()
);
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
}
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.HydrologyCaveVoxelViewFactory;
import art.arcane.iris.engine.object.IrisDimension;
import java.util.Objects;
import java.util.function.BooleanSupplier;
public record IrisHydrologyRuntimeContext(
long seed,
@@ -15,8 +16,14 @@ public record IrisHydrologyRuntimeContext(
IrisHydrologyNaturalHeightProvider naturalHeightProvider,
IrisHydrologyNaturalHeightDescriber naturalHeightDescriber,
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
HydrologyCaveVoxelViewFactory caveViewFactory
HydrologyCaveVoxelViewFactory caveViewFactory,
BooleanSupplier waitingForbidden
) {
/**
* {@code waitingForbidden} is true on a thread that must never wait for a cold hydrology plan,
* the server thread above all: a sample taken there is answered as "no hydrology here" while the
* tiles it needs are planned on the prefetch pool.
*/
public IrisHydrologyRuntimeContext {
if (worldHeight < 3) {
throw new IllegalArgumentException("worldHeight must be at least three");
@@ -28,5 +35,6 @@ public record IrisHydrologyRuntimeContext(
Objects.requireNonNull(naturalHeightDescriber);
Objects.requireNonNull(naturalOceanClassifier);
Objects.requireNonNull(caveViewFactory);
Objects.requireNonNull(waitingForbidden);
}
}
@@ -99,6 +99,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (k.getSpawnChance() < rng.d())
return;
boolean oreDeposit = k.isOre(getData());
for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) {
if (k.getPerClumpSpawnChance() < rng.d())
continue;
@@ -137,7 +139,6 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
continue;
int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
boolean oreDeposit = k.isOre(getData());
IrisBiome surfaceBiome = context.getBiome().get(x, z);
IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter()
? getEngine().getCaveBiome(
@@ -189,14 +190,23 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (!canReplaceDepositTarget(current)) {
continue;
}
if (!k.canReplace(current)) {
continue;
}
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
continue;
}
if (shouldDiscardExposed(
k.getDiscardChanceOnAirExposure(), rng.d(), isAdjacentToAir(data, nx, ny, nz))) {
IrisBiome candidateSurfaceBiome = null;
boolean exteriorSurface = false;
if (oreDeposit) {
candidateSurfaceBiome = context.getBiome().get(nx, nz);
exteriorSurface = k.hasSurfaceReplaceableBlocks(candidateSurfaceBiome)
&& isTerrainSurface(data, nx, ny, nz, columnSurface);
}
if (!canReplaceDepositHost(k, current, candidateSurfaceBiome, exteriorSurface)) {
continue;
}
boolean adjacentToAir = k.getDiscardChanceOnAirExposure() > 0D
&& isAdjacentToAir(data, nx, ny, nz);
if (shouldDiscardExposed(k.getDiscardChanceOnAirExposure(), rng.d(), adjacentToAir)) {
continue;
}
@@ -247,6 +257,15 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return state != null && !state.isAir() && !state.isFluid();
}
static boolean canReplaceDepositHost(
IrisDepositGenerator generator, PlatformBlockState state,
IrisBiome surfaceBiome, boolean terrainSurface) {
if (terrainSurface && generator.hasSurfaceReplaceableBlocks(surfaceBiome)) {
return generator.canReplaceSurface(state, surfaceBiome);
}
return generator.canReplace(state);
}
static int sampleHeight(
IrisDepositHeightDistribution distribution, RNG rng,
int configuredMinimum, int configuredMaximum, int clippedMaximum) {
@@ -276,6 +295,17 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return adjacentToAir && chance > 0D && sample < Math.min(1D, chance);
}
static boolean isTerrainSurface(
Hunk<PlatformBlockState> data, int x, int y, int z, int columnSurface) {
return y == columnSurface
|| isExteriorAirAt(data, x - 1, y, z)
|| isExteriorAirAt(data, x + 1, y, z)
|| isExteriorAirAt(data, x, y - 1, z)
|| isExteriorAirAt(data, x, y + 1, z)
|| isExteriorAirAt(data, x, y, z - 1)
|| isExteriorAirAt(data, x, y, z + 1);
}
static boolean isAdjacentToAir(Hunk<PlatformBlockState> data, int x, int y, int z) {
return isAirAt(data, x - 1, y, z)
|| isAirAt(data, x + 1, y, z)
@@ -295,6 +325,18 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return state == null || state.isAir();
}
private static boolean isExteriorAirAt(Hunk<PlatformBlockState> data, int x, int y, int z) {
if (x < 0 || x >= data.getWidth()
|| y < 0 || y >= data.getHeight()
|| z < 0 || z >= data.getDepth()) {
return false;
}
PlatformBlockState state = data.getRaw(x, y, z);
return state == null
|| (state.isAir()
&& !"minecraft:cave_air".equals(IrisProceduralBlocks.materialKey(state)));
}
static boolean passesOreFrequency(double multiplier, double sample) {
return multiplier >= 1D || sample < Math.max(0D, multiplier);
}
@@ -96,6 +96,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> surfaceOreReplaceableBlockData = new AtomicCache<>();
private final transient AtomicCache<EnumMap<IrisDecorationPart, IrisDecorator[]>> decoratorBuckets = new AtomicCache<>();
private final transient AtomicCache<Biome> derivativeResolved = new AtomicCache<>();
private final transient AtomicCache<Biome> vanillaDerivativeResolved = new AtomicCache<>();
@@ -207,6 +208,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
private KList<IrisDepositGenerator> deposits = new KList<>();
@ArrayType(type = String.class)
@Desc("Optional exact host block ids that replace each ore deposit's surfaceReplaceableBlocks on exterior terrain inside this biome. Omit to inherit the deposit setting; an empty list forbids surface ore.")
private KList<String> surfaceOreReplaceableBlocks = null;
@MinNumber(0)
@MaxNumber(1)
@Desc("Multiplier applied to the frequency of every ore deposit whose center is inside this biome. A value of 0.4 keeps 40% of ore veins while leaving non-ore deposits unchanged.")
@@ -226,6 +230,14 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(type = IrisOreGenerator.class, min = 1)
private KList<IrisOreGenerator> ores = new KList<>();
public boolean hasSurfaceOreReplaceableBlocks() {
return IrisBiomeOres.hasSurfaceOreReplaceableBlocks(this);
}
public boolean canReplaceSurfaceOre(PlatformBlockState state) {
return IrisBiomeOres.canReplaceSurfaceOre(this, state);
}
public PlatformBlockState generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) {
return IrisBiomeOres.generateOres(this, x, y, z, rng, data, surface);
}
@@ -299,6 +311,12 @@ public class IrisBiome extends IrisRegistrant implements IRare {
undergroundOreBoundsCache.reset();
}
public IrisBiome setSurfaceOreReplaceableBlocks(KList<String> surfaceOreReplaceableBlocks) {
this.surfaceOreReplaceableBlocks = surfaceOreReplaceableBlocks;
surfaceOreReplaceableBlockData.reset();
return this;
}
public KList<IrisOreGenerator> getSurfaceOreGenerators() {
return IrisBiomeOres.getSurfaceOreGenerators(this);
}
@@ -21,7 +21,9 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.RNG;
/**
@@ -54,6 +56,19 @@ final class IrisBiomeOres {
return !getUndergroundOres(biome).isEmpty();
}
static boolean hasSurfaceOreReplaceableBlocks(IrisBiome biome) {
return biome.getSurfaceOreReplaceableBlocks() != null;
}
static boolean canReplaceSurfaceOre(IrisBiome biome, PlatformBlockState state) {
if (!hasSurfaceOreReplaceableBlocks(biome)) {
return true;
}
return biome.getSurfaceOreReplaceableBlockData()
.aquire(() -> resolveSurfaceOreReplaceableBlocks(biome))
.contains(IrisProceduralBlocks.materialKey(state));
}
static KList<IrisOreGenerator> getSurfaceOreGenerators(IrisBiome biome) {
return getOres(biome, true);
}
@@ -126,4 +141,15 @@ final class IrisBiomeOres {
}
return null;
}
private static KSet<String> resolveSurfaceOreReplaceableBlocks(IrisBiome biome) {
KSet<String> resolved = new KSet<>();
for (String key : biome.getSurfaceOreReplaceableBlocks()) {
PlatformBlockState state = B.getStateOrNull(key, false);
if (state != null) {
resolved.add(IrisProceduralBlocks.materialKey(state));
}
}
return resolved;
}
}
@@ -56,6 +56,7 @@ public class IrisDepositGenerator {
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<Boolean> ore = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> replaceableBlockData = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> surfaceReplaceableBlockData = new AtomicCache<>();
private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> scaledObjects = new ConcurrentHashMap<>();
@Required
@MinNumber(-8192)
@@ -120,6 +121,9 @@ public class IrisDepositGenerator {
@ArrayType(min = 1, type = String.class)
@Desc("Optional block ids this deposit may replace. An empty list retains Iris's any-solid behavior.")
private KList<String> replaceableBlocks = new KList<>();
@ArrayType(min = 1, type = String.class)
@Desc("Optional exact host block ids required when an ore candidate occupies an exterior terrain surface. Cave-air walls are unaffected. An empty list retains the normal replaceableBlocks behavior.")
private KList<String> surfaceReplaceableBlocks = new KList<>();
@Desc("Chooses whether includedBiomes and excludedBiomes inspect the surface biome or the cave biome at the deposit origin.")
private IrisDepositBiomeScope biomeScope = IrisDepositBiomeScope.CAVE;
@ArrayType(min = 1, type = String.class)
@@ -476,6 +480,36 @@ public class IrisDepositGenerator {
.contains(IrisProceduralBlocks.materialKey(state));
}
public IrisDepositGenerator setSurfaceReplaceableBlocks(KList<String> surfaceReplaceableBlocks) {
this.surfaceReplaceableBlocks = surfaceReplaceableBlocks == null
? new KList<>()
: surfaceReplaceableBlocks;
surfaceReplaceableBlockData.reset();
return this;
}
public boolean canReplaceSurface(PlatformBlockState state) {
if (surfaceReplaceableBlocks == null || surfaceReplaceableBlocks.isEmpty()) {
return true;
}
return surfaceReplaceableBlockData.aquire(this::resolveSurfaceReplaceableBlocks)
.contains(IrisProceduralBlocks.materialKey(state));
}
public boolean hasSurfaceReplaceableBlocks(IrisBiome surfaceBiome) {
if (surfaceBiome != null && surfaceBiome.hasSurfaceOreReplaceableBlocks()) {
return true;
}
return surfaceReplaceableBlocks != null && !surfaceReplaceableBlocks.isEmpty();
}
public boolean canReplaceSurface(PlatformBlockState state, IrisBiome surfaceBiome) {
if (surfaceBiome != null && surfaceBiome.hasSurfaceOreReplaceableBlocks()) {
return surfaceBiome.canReplaceSurfaceOre(state);
}
return canReplaceSurface(state);
}
public boolean matchesBiome(IrisBiome surfaceBiome, IrisBiome caveBiome) {
IrisBiome selected = biomeScope == IrisDepositBiomeScope.SURFACE ? surfaceBiome : caveBiome;
if (includedBiomes != null && !includedBiomes.isEmpty() && !matchesAnyBiome(selected, includedBiomes)) {
@@ -491,8 +525,16 @@ public class IrisDepositGenerator {
}
private KSet<String> resolveReplaceableBlocks() {
return resolveBlockKeys(replaceableBlocks);
}
private KSet<String> resolveSurfaceReplaceableBlocks() {
return resolveBlockKeys(surfaceReplaceableBlocks);
}
private KSet<String> resolveBlockKeys(KList<String> keys) {
KSet<String> resolved = new KSet<>();
for (String key : replaceableBlocks) {
for (String key : keys) {
PlatformBlockState state = B.getStateOrNull(key, false);
if (state != null) {
resolved.add(IrisProceduralBlocks.materialKey(state));
@@ -16,9 +16,11 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -91,12 +93,11 @@ public class WorldRuntimeControlServiceTeleportTest {
}
@Test
public void successfulModeTeleportTemporarilyReducesAndRestoresViewDistance() {
public void teleportNeverTouchesThePlayersViewDistance() {
Player player = mock(Player.class);
Location destination = mock(Location.class);
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
when(player.getViewDistance()).thenReturn(12);
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
@@ -107,10 +108,9 @@ public class WorldRuntimeControlServiceTeleportTest {
nativeTeleport.complete(true);
assertTrue(result.join());
InOrder viewDistances = inOrder(player);
viewDistances.verify(player).setViewDistance(2);
viewDistances.verify(player).setViewDistance(12);
verify(player).setGameMode(GameMode.SPECTATOR);
verify(player, never()).setViewDistance(anyInt());
verify(player, never()).getViewDistance();
}
}
@@ -13,6 +13,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
@@ -661,4 +662,44 @@ public class HydrologyTileCacheTest {
cache.columnAt(40, 40);
assertTrue(cache.isPlanned(40, 40));
}
@Test
public void aThreadThatMayNotWaitSamplesRiverlesslyAndAsksThePrefetchExecutorInstead() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
List<Runnable> queued = new ArrayList<>();
AtomicBoolean forbidden = new AtomicBoolean(true);
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, forbidden::get);
assertTrue(cache.columnAt(40, 40).isEmpty());
verify(planner, org.mockito.Mockito.never()).plan(any(HydrologyTileKey.class));
assertFalse(queued.isEmpty());
for (Runnable task : List.copyOf(queued)) {
task.run();
}
// Once the plan lands the same thread composes the real columns instead of the empty answer.
assertTrue(cache.isPlanned(40, 40));
assertTrue(cache.columnAt(40, 40).isEmpty());
verify(tile, org.mockito.Mockito.atLeastOnce()).columnAt(anyInt(), anyInt());
}
@Test
public void aThreadThatMayWaitStillPlansTheTilesItNeeds() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
AtomicBoolean forbidden = new AtomicBoolean(false);
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, null, forbidden::get);
assertTrue(cache.columnAt(40, 40).isEmpty());
verify(planner, org.mockito.Mockito.atLeastOnce()).plan(any(HydrologyTileKey.class));
assertTrue(cache.isPlanned(40, 40));
}
}
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
import art.arcane.iris.engine.object.IrisDepositPlacementScope;
import art.arcane.iris.spi.PlatformBlockState;
@@ -75,6 +77,24 @@ public class IrisDepositModifierContainmentTest {
assertFalse(IrisDepositModifier.canReplaceDepositTarget(null));
}
@Test
public void surfacePolicyReplacesBuriedHostPolicyWithoutAffectingCaves() {
IrisDepositGenerator generator = mock(IrisDepositGenerator.class);
IrisBiome biome = mock(IrisBiome.class);
PlatformBlockState sand = mock(PlatformBlockState.class);
PlatformBlockState basalt = mock(PlatformBlockState.class);
when(generator.hasSurfaceReplaceableBlocks(biome)).thenReturn(true);
when(generator.canReplaceSurface(sand, biome)).thenReturn(true);
when(generator.canReplace(sand)).thenReturn(false);
when(generator.canReplaceSurface(basalt, biome)).thenReturn(false);
when(generator.canReplace(basalt)).thenReturn(true);
assertTrue(IrisDepositModifier.canReplaceDepositHost(generator, sand, biome, true));
assertFalse(IrisDepositModifier.canReplaceDepositHost(generator, sand, biome, false));
assertFalse(IrisDepositModifier.canReplaceDepositHost(generator, basalt, biome, true));
assertTrue(IrisDepositModifier.canReplaceDepositHost(generator, basalt, biome, false));
}
@Test
public void oreFrequencyMultiplierKeepsConfiguredShareOfVeins() {
assertTrue(IrisDepositModifier.passesOreFrequency(0.4D, 0.399D));
@@ -153,6 +173,33 @@ public class IrisDepositModifierContainmentTest {
assertFalse(IrisDepositModifier.shouldDiscardExposed(0D, 0D, true));
}
@Test
public void terrainSurfaceIncludesExteriorAirButExcludesCaveAir() {
Hunk<PlatformBlockState> data = Hunk.newHunk(3, 3, 3);
PlatformBlockState solid = mock(PlatformBlockState.class);
PlatformBlockState exteriorAir = mock(PlatformBlockState.class);
PlatformBlockState caveAir = mock(PlatformBlockState.class);
when(exteriorAir.isAir()).thenReturn(true);
when(exteriorAir.key()).thenReturn("minecraft:air");
when(caveAir.isAir()).thenReturn(true);
when(caveAir.key()).thenReturn("minecraft:cave_air");
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
for (int z = 0; z < 3; z++) {
data.setRaw(x, y, z, solid);
}
}
}
assertTrue(IrisDepositModifier.isTerrainSurface(data, 1, 1, 1, 1));
assertFalse(IrisDepositModifier.isTerrainSurface(data, 1, 1, 1, 2));
data.setRaw(2, 1, 1, exteriorAir);
assertTrue(IrisDepositModifier.isTerrainSurface(data, 1, 1, 1, 2));
data.setRaw(2, 1, 1, caveAir);
assertFalse(IrisDepositModifier.isTerrainSurface(data, 1, 1, 1, 2));
assertTrue(IrisDepositModifier.isAdjacentToAir(data, 1, 1, 1));
}
@Test
public void exposureProbeChecksOnlyInBoundsOrthogonalNeighbors() {
Hunk<PlatformBlockState> data = Hunk.newHunk(3, 3, 3);
@@ -3,8 +3,12 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
@@ -12,8 +16,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisDepositTuningTest {
@@ -53,6 +59,100 @@ public class IrisDepositTuningTest {
assertEquals(1D, biome.getOreDepositFrequencyMultiplier(), 0D);
assertEquals(1D, biome.getOreDepositSizeMultiplier(), 0D);
assertNull(biome.getSurfaceOreReplaceableBlocks());
}
@Test
public void surfaceHostFilterDefaultsToUnrestricted() {
IrisDepositGenerator generator = new IrisDepositGenerator();
assertTrue(generator.getSurfaceReplaceableBlocks().isEmpty());
assertTrue(generator.canReplaceSurface(mock(PlatformBlockState.class)));
}
@Test
public void surfaceHostFilterAllowsOnlyConfiguredMaterials() {
IrisDepositGenerator generator = new IrisDepositGenerator();
generator.setSurfaceReplaceableBlocks(new KList<String>().qadd("minecraft:stone"));
PlatformBlockState stone = mock(PlatformBlockState.class);
PlatformBlockState dirt = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
when(dirt.key()).thenReturn("minecraft:dirt");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(stone);
blocks.when(() -> B.getStateOrNull("minecraft:dirt", false)).thenReturn(dirt);
assertTrue(generator.canReplaceSurface(stone));
assertFalse(generator.canReplaceSurface(dirt));
generator.setSurfaceReplaceableBlocks(new KList<String>().qadd("minecraft:dirt"));
assertFalse(generator.canReplaceSurface(stone));
assertTrue(generator.canReplaceSurface(dirt));
}
}
@Test
public void biomeSurfaceHostPolicyOverridesDepositPolicy() {
IrisDepositGenerator generator = new IrisDepositGenerator();
generator.setSurfaceReplaceableBlocks(new KList<String>().qadd("minecraft:stone"));
IrisBiome inherited = new IrisBiome();
IrisBiome desert = new IrisBiome();
desert.setSurfaceOreReplaceableBlocks(
new KList<String>().qadd("minecraft:stone").qadd("minecraft:sand"));
IrisBiome disabled = new IrisBiome();
disabled.setSurfaceOreReplaceableBlocks(new KList<>());
PlatformBlockState stone = mock(PlatformBlockState.class);
PlatformBlockState sand = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
when(sand.key()).thenReturn("minecraft:sand");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(stone);
blocks.when(() -> B.getStateOrNull("minecraft:sand", false)).thenReturn(sand);
assertTrue(generator.canReplaceSurface(stone, inherited));
assertFalse(generator.canReplaceSurface(sand, inherited));
assertTrue(generator.canReplaceSurface(sand, desert));
assertTrue(generator.hasSurfaceReplaceableBlocks(disabled));
assertFalse(generator.canReplaceSurface(stone, disabled));
}
}
@Test
public void biomeSurfaceHostPolicyDistinguishesOmittedAndEmptyJson() {
Gson gson = new Gson();
IrisBiome inherited = gson.fromJson("{\"name\":\"Inherited\"}", IrisBiome.class);
IrisBiome disabled = gson.fromJson(
"{\"name\":\"Disabled\",\"surfaceOreReplaceableBlocks\":[]}", IrisBiome.class);
assertNull(inherited.getSurfaceOreReplaceableBlocks());
assertNotNull(disabled.getSurfaceOreReplaceableBlocks());
assertTrue(disabled.getSurfaceOreReplaceableBlocks().isEmpty());
assertFalse(inherited.hasSurfaceOreReplaceableBlocks());
assertTrue(disabled.hasSurfaceOreReplaceableBlocks());
}
@Test
public void biomeSurfaceHostPolicySetterInvalidatesResolvedHosts() {
IrisBiome biome = new IrisBiome();
PlatformBlockState stone = mock(PlatformBlockState.class);
PlatformBlockState sand = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
when(sand.key()).thenReturn("minecraft:sand");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(stone);
blocks.when(() -> B.getStateOrNull("minecraft:sand", false)).thenReturn(sand);
biome.setSurfaceOreReplaceableBlocks(new KList<String>().qadd("minecraft:stone"));
assertTrue(biome.canReplaceSurfaceOre(stone));
assertFalse(biome.canReplaceSurfaceOre(sand));
biome.setSurfaceOreReplaceableBlocks(new KList<String>().qadd("minecraft:sand"));
assertFalse(biome.canReplaceSurfaceOre(stone));
assertTrue(biome.canReplaceSurfaceOre(sand));
}
}
@Test