mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 16:44:06 +00:00
Terrain is important
- Fixed Ores generating all over the surface. - fixed UI bug
This commit is contained in:
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);
|
||||
|
||||
+6
-1
@@ -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());
|
||||
}
|
||||
|
||||
+9
-1
@@ -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));
|
||||
|
||||
+5
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
+47
@@ -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
|
||||
|
||||
Reference in new issue
Block a user