This commit is contained in:
Brian Neumann-Fopiano
2026-08-16 23:53:42 -04:00
parent a1cef36f10
commit 03746468ec
86 changed files with 3681 additions and 833 deletions
@@ -262,6 +262,43 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1));
}
@Test
public void terrainMatchingFootprintsRaiseAndLowerOnlyLowestSolidColumns() throws Exception {
StructureTemplate pathTemplate = template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState()),
block(0, 1, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(15, 0, 0, Blocks.AIR.defaultBlockState()),
block(30, 0, 0, Blocks.AIR.defaultBlockState()),
block(30, 1, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(45, 0, 0, Blocks.DANDELION.defaultBlockState())));
InlineSinglePoolElement element = new InlineSinglePoolElement(
pathTemplate, List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING);
PoolElementStructurePiece piece = rigidTemplatePiece(
element, new BoundingBox(0, 68, 0, 45, 74, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(
List.of(piece), TerrainAdjustment.BEARD_THIN);
BoundingBox area = new BoundingBox(0, 52, 0, 45, 76, 0);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 64);
blocks.remove(new BlockPos(0, 63, 0));
blocks.remove(new BlockPos(0, 64, 0));
put(blocks, 0, 59, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 60, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(blocks, 30, 71, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 30, 72, 0, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area, List.of(surfaceTarget(start)),
(x, z) -> x == 0 ? 60 : x == 30 ? 72 : 64);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 68, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 30, 72, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 65, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 45, 64, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 45, 65, 0));
}
@Test
public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception {
StructureTemplate sparseTemplate = template(List.of(
@@ -2171,6 +2208,19 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getHeight")) {
int x = (int) arguments[1];
int z = (int) arguments[2];
int highest = Integer.MIN_VALUE;
for (Map.Entry<BlockPos, BlockState> entry : blocks.entrySet()) {
BlockPos position = entry.getKey();
if (position.getX() == x && position.getZ() == z
&& !entry.getValue().isAir()) {
highest = Math.max(highest, position.getY());
}
}
return highest == Integer.MIN_VALUE ? 0 : highest + 1;
}
if (methodName.equals("getLevel")) {
return null;
}
@@ -93,8 +93,8 @@ import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.function.NastyRunnable;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.hud.HudActionBar;
import art.arcane.volmlib.util.hud.HudBossBarLane;
import art.arcane.volmlib.util.hud.HudSlotService;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.io.InstanceState;
import art.arcane.volmlib.util.math.M;
@@ -549,7 +549,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
SimdSupport.install();
services = new KMap<>();
setupAudience();
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane());
BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
Bindings.setupSentry();
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
// enable/disable and paid a full-jar class sweep at boot. Infrastructure first,
@@ -777,7 +777,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
removeShutdownHook();
}
if (BukkitPlatform.hasHud()) {
BukkitPlatform.hudSlots().shutdown();
BukkitPlatform.hudBar().shutdown();
BukkitPlatform.hudLanes().shutdown();
}
if (configHotloadEngine != null) {
@@ -148,10 +148,15 @@ public class CommandIris implements DirectorExecutor {
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage("Could not find dimension '" + resolvedType + "'.");
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
MessageArgument.untrusted("dimension", resolvedType)
));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType)
+ " and restart the server.");
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return;
}
@@ -227,10 +232,15 @@ public class CommandIris implements DirectorExecutor {
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage("Could not find dimension '" + resolvedType + "'.");
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
MessageArgument.untrusted("dimension", resolvedType)
));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType)
+ " and restart the server.");
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return;
}
@@ -77,18 +77,12 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.volmlib.util.hud.HudPriority;
import art.arcane.volmlib.util.hud.HudSlotClaim;
import art.arcane.volmlib.util.hud.HudSlotRequest;
import art.arcane.volmlib.util.hud.HudSurface;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
@@ -108,7 +102,6 @@ import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import art.arcane.iris.core.localization.IrisLanguage;
@@ -343,8 +336,7 @@ public class CommandStudio implements DirectorExecutor {
// previously leaked the whole sampler ForkJoinPool, the self-rescheduling
// progress task and both HUD claims for the rest of the server's uptime.
MultiBurst multiBurst = null;
HudSlotClaim titleClaim = null;
HudSlotClaim barClaim = null;
boolean progressShown = false;
int c = -1;
try {
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
@@ -354,31 +346,11 @@ public class CommandStudio implements DirectorExecutor {
var loc = player.getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)));
HudSlotClaim finalTitleClaim = titleClaim;
HudSlotClaim finalBarClaim = barClaim;
AtomicLong lastResolveMs = new AtomicLong(0L);
progressShown = true;
c = J.ar(() -> {
long now = System.currentTimeMillis();
if (now - lastResolveMs.get() >= 250L) {
lastResolveMs.set(now);
finalTitleClaim.resolve();
finalBarClaim.resolve();
}
double jobProgress = (double) completedTasks.get() / totalTasks;
HudSurface barSurface = finalBarClaim.granted();
sender.sendProgress(
jobProgress,
IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS),
finalTitleClaim.granted(),
barSurface
);
if (barSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, BarColor.BLUE, BarStyle.SOLID, 4000L);
} else if (barSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(player, "iris:job");
}
sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS));
BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L);
}, 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
@@ -399,11 +371,8 @@ public class CommandStudio implements DirectorExecutor {
if (c != -1) {
J.car(c);
}
if (titleClaim != null) {
titleClaim.release();
}
if (barClaim != null) {
barClaim.release();
if (progressShown) {
sender.sendAction(" ");
BukkitPlatform.hudLanes().hide(player, "iris:job");
}
if (multiBurst != null) {
@@ -7,6 +7,8 @@ import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
@@ -71,4 +73,26 @@ public class CommandIrisCreateOverwriteContractTest {
assertEquals(Long.valueOf(Long.MAX_VALUE), handler.parse(Long.toString(Long.MAX_VALUE), false));
assertThrows(DirectorParsingException.class, () -> handler.parse("9223372036854775808", false));
}
@Test
public void createAndReplaceUseLocalizedStyledMissingDimensionFeedback() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandIris.java"
));
assertEquals(2, occurrences(source, "COMMAND_IRIS_DIMENSION_NOT_FOUND"));
assertEquals(2, occurrences(source, "COMMAND_IRIS_INSTALL_PACK_AND_RESTART"));
assertFalse(source.contains("sendMessage(\"Could not find dimension"));
assertFalse(source.contains("sendMessage(\"Install its pack with"));
}
private static int occurrences(String value, String match) {
int count = 0;
int offset = 0;
while ((offset = value.indexOf(match, offset)) >= 0) {
count++;
offset += match.length();
}
return count;
}
}
@@ -33,7 +33,9 @@ public final class NativeStructureSurfaceFitter {
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12;
private static final int MAX_VACUUM_TEMPLATE_CELLS = 4_194_304;
private static final int MAX_SURFACE_TEMPLATE_CELLS = 4_194_304;
private static final Set<String> WARNED_SOURCE_BUDGET =
ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_VACUUM_BUDGET =
ConcurrentHashMap.newKeySet();
@@ -45,23 +47,24 @@ public final class NativeStructureSurfaceFitter {
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight) {
return prepareSurfaceStructures(
world, area, targets, surfaceHeight, MAX_VACUUM_TEMPLATE_CELLS);
world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS);
}
static VacuumFoundationPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight, int maximumVacuumTemplateCells) {
IntBinaryOperator surfaceHeight, int maximumTemplateCells) {
if (targets == null || targets.isEmpty()) {
return VacuumFoundationPlan.empty();
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSourceSurfaceAnchors(targets);
List<SurfaceAnchor> anchors = collectSourceSurfaceAnchors(
world, area, targets, maximumTemplateCells);
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
VacuumFootprint vacuum = collectVacuumFootprint(
world, area, targets, maximumVacuumTemplateCells);
world, area, targets, maximumTemplateCells);
if (!vacuum.anchors().isEmpty()) {
fitVacuumTerrain(world, area, vacuum, surfaceHeight);
}
@@ -223,7 +226,14 @@ public final class NativeStructureSurfaceFitter {
}
private static List<SurfaceAnchor> collectSourceSurfaceAnchors(
List<NativeStructureTerrainIntegrator.TerrainTarget> targets) {
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
int maximumTemplateCells) {
BoundingBox influenceArea = new BoundingBox(
area.minX() - SURFACE_TERRAIN_RADIUS, area.minY(),
area.minZ() - SURFACE_TERRAIN_RADIUS,
area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(),
area.maxZ() + SURFACE_TERRAIN_RADIUS);
List<SurfaceAnchor> anchors = new ArrayList<>();
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (!requiresSourceSurfaceTerrain(target)) {
@@ -231,13 +241,34 @@ public final class NativeStructureSurfaceFitter {
}
StructureStart start = target.start();
TerrainAdjustment adjustment = start.getStructure().terrainAdaptation();
BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox();
BlockPos firstCenter = firstBounds.getCenter();
BlockPos referencePosition = new BlockPos(
firstCenter.getX(), firstBounds.minY(), firstCenter.getZ());
TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells);
boolean budgetExceeded = false;
for (StructurePiece piece : start.getPieces()) {
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
StructureTemplatePool.Projection projection =
poolPiece.getElement().getProjection();
if (projection == StructureTemplatePool.Projection.RIGID) {
BoundingBox bounds = poolPiece.getBoundingBox();
anchors.add(surfaceAnchor(
bounds, bounds.minY() + poolPiece.getGroundLevelDelta(),
2, adjustment));
} else if (!budgetExceeded
&& projection == StructureTemplatePool.Projection.TERRAIN_MATCHING
&& intersectsHorizontally(poolPiece.getBoundingBox(), influenceArea)) {
try {
List<SurfaceAnchor> pieceAnchors = new ArrayList<>();
addTerrainMatchingSurfaceAnchors(
world, influenceArea, poolPiece, referencePosition,
adjustment, budget, pieceAnchors);
anchors.addAll(pieceAnchors);
} catch (TemplateBudgetExceeded ignored) {
budgetExceeded = true;
warnSourceBudget(target);
}
}
for (JigsawJunction junction : poolPiece.getJunctions()) {
anchors.add(new SurfaceAnchor(
@@ -255,6 +286,42 @@ public final class NativeStructureSurfaceFitter {
return List.copyOf(anchors);
}
private static void addTerrainMatchingSurfaceAnchors(
WorldGenLevel world, BoundingBox influenceArea,
PoolElementStructurePiece piece, BlockPos referencePosition,
TerrainAdjustment adjustment, TemplateCellBudget budget,
List<SurfaceAnchor> anchors) {
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
NativeStructureTemplateOccupancy.resolve(
world, piece, referencePosition, influenceArea,
() -> world.getLevel().getStructureManager(),
influenceArea::isInside, budget::consume);
if (!occupancy.resolved()) {
return;
}
int groundY = piece.getBoundingBox().minY() + piece.getGroundLevelDelta();
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) {
budget.consume(1);
BoundingBox column = new BoundingBox(
cell.x(), groundY, cell.z(), cell.x(), groundY, cell.z());
anchors.add(surfaceAnchor(column, groundY, 2, adjustment));
}
}
private static void warnSourceBudget(
NativeStructureTerrainIntegrator.TerrainTarget target) {
String structureId = target.structureId() == null
? target.start().getStructure().getClass().getName()
: target.structureId();
if (WARNED_SOURCE_BUDGET.add(structureId)) {
IrisLogging.warn("Native structure SOURCE fitting for '"
+ structureId + "' exceeded its bounded template budget; "
+ "skipping remaining terrain-matching footprints for this start");
}
}
private static VacuumFootprint collectVacuumFootprint(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
@@ -286,7 +353,7 @@ public final class NativeStructureSurfaceFitter {
Map<Long, Integer> targetCaps = new HashMap<>();
Set<Long> targetFoundationBases = new HashSet<>();
Set<Long> targetOccupiedCells = new HashSet<>();
VacuumCellBudget budget = new VacuumCellBudget(maximumTemplateCells);
TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells);
try {
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
@@ -330,7 +397,7 @@ public final class NativeStructureSurfaceFitter {
cell.x(), cell.y(), cell.z()));
}
}
} catch (VacuumBudgetExceeded ignored) {
} catch (TemplateBudgetExceeded ignored) {
warnVacuumBudget(target);
continue;
}
@@ -837,31 +904,31 @@ public final class NativeStructureSurfaceFitter {
}
}
private static final class VacuumCellBudget {
private static final class TemplateCellBudget {
private final int maximum;
private int consumed;
private VacuumCellBudget(int maximum) {
private TemplateCellBudget(int maximum) {
if (maximum < 0) {
throw new IllegalArgumentException(
"Native structure vacuum cell budget cannot be negative");
"Native structure surface template budget cannot be negative");
}
this.maximum = maximum;
}
private void consume(int amount) {
if (amount < 0 || consumed > maximum - amount) {
throw VacuumBudgetExceeded.INSTANCE;
throw TemplateBudgetExceeded.INSTANCE;
}
consumed += amount;
}
}
private static final class VacuumBudgetExceeded extends RuntimeException {
private static final VacuumBudgetExceeded INSTANCE =
new VacuumBudgetExceeded();
private static final class TemplateBudgetExceeded extends RuntimeException {
private static final TemplateBudgetExceeded INSTANCE =
new TemplateBudgetExceeded();
private VacuumBudgetExceeded() {
private TemplateBudgetExceeded() {
super(null, null, false, false);
}
}
@@ -89,6 +89,24 @@ final class NativeStructureTemplateOccupancy {
return lowest;
}
static Map<Long, LowestCell> lowestProcessedSolidCells(Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
OccupancyCell cell = entry.getValue();
if (cell.blocker() || !isSolidBase(cell.state())) {
continue;
}
BlockPos position = BlockPos.of(entry.getKey());
long column = columnKey(position.getX(), position.getZ());
LowestCell current = lowest.get(column);
if (current == null || position.getY() < current.y()) {
lowest.put(column, new LowestCell(
position.getX(), position.getY(), position.getZ(), cell));
}
}
return lowest;
}
static boolean isSolidBase(BlockState state) {
return state != null && state.isSolid()
&& !state.is(Blocks.STRUCTURE_VOID)
@@ -113,7 +113,7 @@ public final class ModdedBlockResolution {
"poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
"lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid",
"lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom",
"crimson_roots", "azure_bluet", "weeping_vines", "weeping_vines_plant", "warped_roots",
"crimson_roots", "azure_bluet", "cactus", "weeping_vines", "weeping_vines_plant", "warped_roots",
"nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes",
"carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass",
"acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button",
@@ -510,6 +510,10 @@ public final class ModdedBlockResolution {
}
public static boolean canPlaceOnto(Block mat, Block onto) {
if (mat == Blocks.CACTUS) {
return onto == Blocks.CACTUS || onto == Blocks.SAND || onto == Blocks.RED_SAND;
}
if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM)
&& (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS
|| mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) {
@@ -7,8 +7,10 @@ import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* The SPI splits block resolution into a null-returning lookup and an air-falling-back lookup. Modded used to
@@ -49,4 +51,13 @@ public class ModdedBlockResolutionContractTest {
assertNotNull(state);
assertEquals(Blocks.OAK_LOG, state.handle().getBlock());
}
@Test
public void cactusPlacementRequiresNativeCactusSupport() {
assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.CACTUS));
assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.SAND));
assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.RED_SAND));
assertFalse(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.STONE));
assertTrue(ModdedBlockResolution.isDecorant(Blocks.CACTUS.defaultBlockState()));
}
}