object cleanup

This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-22 11:29:37 -04:00
1 parent 770240e4f7
commit dce7af1955
12 files changed
+1257 -647

No files matched your search

@@ -0,0 +1,378 @@
package art.arcane.iris.core;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.localization.MessageArgument;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SettingsHotloadWatchTest {
private static final String PERMISSION = "iris.all";
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisSettings previousSettings;
private File dataFolder;
private File settingsFile;
private File overrideFolder;
private SettingsHotloadWatch watch;
@Before
public void setUp() throws Exception {
previousSettings = IrisSettings.settings;
dataFolder = temporaryFolder.newFolder("iris-hotload");
settingsFile = new File(dataFolder, "settings.json");
overrideFolder = new File(dataFolder, "languages/overrides");
Files.createDirectories(overrideFolder.toPath());
String settings = settings("en_US");
Files.writeString(settingsFile.toPath(), settings, StandardCharsets.UTF_8);
IrisSettings.settings = IrisSettings.parseHotloadSnapshot(settings);
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
watch = new SettingsHotloadWatch(
settingsFile,
new SettingsHotloadWatch.Timing(100L, 100L, 100L, 100L)
);
}
@After
public void tearDown() {
if (watch != null) {
watch.close();
}
IrisLanguage.reload(dataFolder, "en_US");
IrisSettings.settings = previousSettings;
}
@Test
public void activeLocaleSnapshotAppliesAndInvalidSnapshotRetainsLastGood() {
File override = override("en_US");
String valid = locale("en_US", "Active {permission}");
assertTrue(watch.applySnapshot(present(override, valid)));
assertEquals("Active " + PERMISSION, permissionMessage());
assertFalse(watch.applySnapshot(present(override, "{ invalid")));
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test
public void activeLocaleDeletionFallsBackToCodeOwnedEnglish() {
File override = override("en_US");
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Temporary {permission}"))));
assertEquals("Temporary " + PERMISSION, permissionMessage());
assertTrue(watch.applySnapshot(missing(override)));
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
assertEquals("en_US", IrisLanguage.activeLocale());
}
@Test
public void inactiveLocaleSnapshotDoesNotChangeTheRuntimeCatalog() {
String before = permissionMessage();
assertTrue(watch.applySnapshot(present(
override("de_DE"),
locale("de_DE", "Inaktiv {permission}")
)));
assertEquals(before, permissionMessage());
assertEquals("en_US", IrisLanguage.activeLocale());
}
@Test
public void settingsSnapshotChangesTheActiveLocaleFromItsImmutableOverride() throws Exception {
Files.writeString(
override("de_DE").toPath(),
locale("de_DE", "Berechtigung {permission}"),
StandardCharsets.UTF_8
);
assertTrue(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
assertEquals("de_DE", IrisSettings.get().getGeneral().getLanguage());
assertEquals("de_DE", IrisLanguage.activeLocale());
assertEquals("Berechtigung " + PERMISSION, permissionMessage());
}
@Test
public void invalidLocaleBlocksItsSettingsSwitchAndRetainsThePreviousRuntime() throws Exception {
Files.writeString(override("de_DE").toPath(), "{ invalid", StandardCharsets.UTF_8);
assertFalse(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
assertEquals("en_US", IrisSettings.get().getGeneral().getLanguage());
assertEquals("en_US", IrisLanguage.activeLocale());
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
}
@Test(timeout = 8_000L)
public void sameMetadataLocaleReplacementStillHotloads() throws Exception {
File override = override("en_US");
FileTime fixedTime = FileTime.fromMillis(10_000L);
String first = locale("en_US", "First {permission}");
String second = locale("en_US", "Other {permission}");
assertEquals(first.getBytes(StandardCharsets.UTF_8).length, second.getBytes(StandardCharsets.UTF_8).length);
Files.writeString(override.toPath(), first, StandardCharsets.UTF_8);
Files.setLastModifiedTime(override.toPath(), fixedTime);
awaitPermissionMessage("First " + PERMISSION);
Files.writeString(override.toPath(), second, StandardCharsets.UTF_8);
Files.setLastModifiedTime(override.toPath(), fixedTime);
awaitPermissionMessage("Other " + PERMISSION);
}
@Test(timeout = 8_000L)
public void closeWaitsForInFlightSnapshotAndStopsLaterApplies() throws Exception {
File override = override("en_US");
CountDownLatch applyEntered = new CountDownLatch(1);
CountDownLatch releaseApply = new CountDownLatch(1);
AtomicInteger automaticApplies = new AtomicInteger();
replaceWatch(new SettingsHotloadWatch.Timing(100L, 100L, 100L, 100L), snapshot -> {
if (!override.equals(snapshot.file())) {
return;
}
automaticApplies.incrementAndGet();
applyEntered.countDown();
try {
releaseApply.await();
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while holding the hotload apply boundary", failure);
}
});
Files.writeString(override.toPath(), locale("en_US", "During {permission}"), StandardCharsets.UTF_8);
Thread checker = new Thread(() -> checkUntilEntered(applyEntered), "Iris-Hotload-Lifecycle-Test");
checker.start();
assertTrue(applyEntered.await(5L, TimeUnit.SECONDS));
CountDownLatch closeStarted = new CountDownLatch(1);
AtomicBoolean closeCompleted = new AtomicBoolean();
Thread closer = new Thread(() -> {
closeStarted.countDown();
watch.close();
closeCompleted.set(true);
}, "Iris-Hotload-Close-Test");
closer.start();
assertTrue(closeStarted.await(1L, TimeUnit.SECONDS));
awaitThreadState(closer, Thread.State.BLOCKED);
assertFalse(closeCompleted.get());
releaseApply.countDown();
checker.join(2_000L);
closer.join(2_000L);
assertFalse(checker.isAlive());
assertFalse(closer.isAlive());
assertTrue(closeCompleted.get());
assertEquals("During " + PERMISSION, permissionMessage());
Files.writeString(override.toPath(), locale("en_US", "After {permission}"), StandardCharsets.UTF_8);
watch.checkConfigHotload();
assertEquals(1, automaticApplies.get());
assertEquals("During " + PERMISSION, permissionMessage());
}
@Test(timeout = 8_000L)
public void manualReloadInvalidatesPendingAutomaticLocaleWork() throws Exception {
File override = override("en_US");
AtomicInteger automaticApplies = new AtomicInteger();
replaceWatch(
new SettingsHotloadWatch.Timing(100L, 400L, 100L, 100L),
snapshot -> {
if (override.equals(snapshot.file())) {
automaticApplies.incrementAndGet();
}
}
);
Files.writeString(override.toPath(), locale("en_US", "First {permission}"), StandardCharsets.UTF_8);
awaitPermissionMessage("First " + PERMISSION);
assertEquals(1, automaticApplies.get());
Files.writeString(override.toPath(), locale("en_US", "Manual {permission}"), StandardCharsets.UTF_8);
pollFor(125L);
assertEquals("First " + PERMISSION, permissionMessage());
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
assertEquals("Manual " + PERMISSION, permissionMessage());
pollFor(650L);
assertEquals(1, automaticApplies.get());
assertEquals("Manual " + PERMISSION, permissionMessage());
}
@Test(timeout = 8_000L)
public void oversizedActiveOverrideReportsOnceAndRetainsLastGoodCatalog() throws Exception {
File override = override("en_US");
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
byte[] oversized = new byte[2 * 1024 * 1024 + 1];
Files.write(override.toPath(), oversized);
String diagnostic = captureErrorsWhilePolling("Hotload file exceeds 2097152 bytes", 1_200L);
assertEquals(1, countOccurrences(
diagnostic,
"Failed to read watched Iris file " + override.getAbsolutePath()
));
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test(timeout = 8_000L)
public void malformedUtf8FailureIsDeduplicatedAndRetainsLastGoodCatalog() throws Exception {
File override = override("en_US");
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
Files.write(override.toPath(), new byte[]{(byte) 0xC3, 0x28});
String diagnostic = captureErrorsWhilePolling("Hotload file is not valid UTF-8", 1_200L);
assertEquals(1, countOccurrences(
diagnostic,
"Failed to read watched Iris file " + override.getAbsolutePath()
));
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test
public void bukkitAndModdedUseTheSameCoreCoordinator() throws Exception {
String bukkit = Files.readString(Path.of(
"../adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java"
));
String modded = Files.readString(Path.of(
"../adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java"
));
assertTrue(bukkit.contains("new SettingsHotloadWatch("));
assertTrue(modded.contains("new SettingsHotloadWatch("));
assertFalse(bukkit.contains("new ConfigHotloadEngine("));
assertFalse(modded.contains("new ConfigHotloadEngine("));
assertFalse(bukkit.contains("IrisLanguage.update()"));
assertFalse(modded.contains("IrisLanguage.update()"));
}
private void awaitPermissionMessage(String expected) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(6L);
while (System.nanoTime() < deadline) {
watch.checkConfigHotload();
if (expected.equals(permissionMessage())) {
return;
}
Thread.sleep(25L);
}
assertEquals(expected, permissionMessage());
}
private void replaceWatch(
SettingsHotloadWatch.Timing timing,
Consumer<ConfigHotloadEngine.StableContentSnapshot> beforeSnapshotApply
) {
watch.close();
watch = new SettingsHotloadWatch(settingsFile, timing, beforeSnapshotApply);
}
private void checkUntilEntered(CountDownLatch entered) {
try {
while (entered.getCount() > 0L) {
watch.checkConfigHotload();
Thread.sleep(10L);
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for a hotload snapshot", failure);
}
}
private void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
while (System.nanoTime() < deadline && thread.getState() != expected) {
Thread.sleep(5L);
}
assertEquals(expected, thread.getState());
}
private void pollFor(long durationMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
while (System.nanoTime() < deadline) {
watch.checkConfigHotload();
Thread.sleep(25L);
}
}
private String captureErrorsWhilePolling(String expected, long durationMillis) throws Exception {
PrintStream originalError = System.err;
ByteArrayOutputStream captured = new ByteArrayOutputStream();
try (PrintStream capture = new PrintStream(captured, true, StandardCharsets.UTF_8)) {
System.setErr(capture);
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
boolean found = false;
while (System.nanoTime() < deadline) {
watch.checkConfigHotload();
found |= captured.toString(StandardCharsets.UTF_8).contains(expected);
Thread.sleep(25L);
}
assertTrue(found);
} finally {
System.setErr(originalError);
}
return captured.toString(StandardCharsets.UTF_8);
}
private int countOccurrences(String value, String target) {
int count = 0;
int cursor = 0;
while ((cursor = value.indexOf(target, cursor)) >= 0) {
count++;
cursor += target.length();
}
return count;
}
private ConfigHotloadEngine.StableContentSnapshot present(File file, String content) {
return new ConfigHotloadEngine.StableContentSnapshot(file, "present", content.trim());
}
private ConfigHotloadEngine.StableContentSnapshot missing(File file) {
return new ConfigHotloadEngine.StableContentSnapshot(file, "missing", null);
}
private File override(String locale) {
return new File(overrideFolder, locale + ".json").getAbsoluteFile();
}
private String permissionMessage() {
return IrisLanguage.plain(
IrisMessages.COMMAND_PERMISSION_DENIED,
MessageArgument.untrusted("permission", PERMISSION)
);
}
private String settings(String locale) {
return "{\"general\":{\"language\":\"" + locale + "\"}}";
}
private String locale(String locale, String permissionMessage) {
return "{\"locale\":\"" + locale + "\",\"messages\":{"
+ "\"iris.command.permission_denied\":\"" + permissionMessage + "\"}}";
}
}
@@ -1,159 +0,0 @@
package art.arcane.iris.core.localization;
import org.junit.Test;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class LocaleHotloadGateTest {
@Test
public void anchorsCooldownAtCompletedApplication() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot initial = snapshot("initial");
LocaleHotloadSnapshot first = snapshot("first");
LocaleHotloadSnapshot second = snapshot("second");
gate.reset(initial);
assertNull(gate.observe(first, 0L));
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
assertNotNull(firstAttempt);
gate.complete(firstAttempt, 500L, true);
assertNull(gate.observe(second, 600L));
assertNull(gate.observe(second, 700L));
assertNull(gate.observe(second, 3_499L));
LocaleHotloadGate.Attempt secondAttempt = gate.observe(second, 3_500L);
assertNotNull(secondAttempt);
assertEquals(second, secondAttempt.snapshot());
}
@Test
public void coalescesCooldownBurstToLatestStableSnapshot() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot first = snapshot("first");
LocaleHotloadSnapshot intermediate = snapshot("intermediate");
LocaleHotloadSnapshot latest = snapshot("latest");
gate.reset(snapshot("initial"));
assertNull(gate.observe(first, 0L));
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
assertNotNull(firstAttempt);
gate.complete(firstAttempt, 200L, true);
assertNull(gate.observe(intermediate, 250L));
assertNull(gate.observe(intermediate, 350L));
assertNull(gate.observe(latest, 400L));
assertNull(gate.observe(latest, 500L));
assertNull(gate.observe(latest, 3_199L));
LocaleHotloadGate.Attempt latestAttempt = gate.observe(latest, 3_200L);
assertNotNull(latestAttempt);
assertEquals(latest, latestAttempt.snapshot());
}
@Test
public void requiresDeletionGraceAndCancelsTransientMissingSnapshot() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot initial = snapshot("initial");
LocaleHotloadSnapshot replacement = snapshot("replacement");
LocaleHotloadSnapshot missing = LocaleHotloadSnapshot.missing(initial.file(), "en_US");
gate.reset(initial);
assertNull(gate.observe(missing, 0L));
assertNull(gate.observe(missing, 499L));
assertNull(gate.observe(replacement, 500L));
LocaleHotloadGate.Attempt replacementAttempt = gate.observe(replacement, 600L);
assertNotNull(replacementAttempt);
assertEquals(replacement, replacementAttempt.snapshot());
gate.complete(replacementAttempt, 600L, true);
assertNull(gate.observe(missing, 700L));
assertNull(gate.observe(missing, 1_199L));
assertNull(gate.observe(missing, 1_200L));
LocaleHotloadGate.Attempt deletionAttempt = gate.observe(missing, 3_600L);
assertNotNull(deletionAttempt);
assertEquals(missing, deletionAttempt.snapshot());
}
@Test
public void unavailableReadBlocksStalePendingSnapshot() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot first = snapshot("first");
LocaleHotloadSnapshot pending = snapshot("pending");
gate.reset(snapshot("initial"));
assertNull(gate.observe(first, 0L));
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
assertNotNull(firstAttempt);
gate.complete(firstAttempt, 200L, true);
assertNull(gate.observe(pending, 300L));
assertNull(gate.observe(pending, 400L));
gate.unavailable();
assertNull(gate.observe(pending, 3_200L));
LocaleHotloadGate.Attempt recoveredAttempt = gate.observe(pending, 3_300L);
assertNotNull(recoveredAttempt);
assertEquals(pending, recoveredAttempt.snapshot());
}
@Test
public void failedApplicationRetriesOnlyAfterCooldown() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot changed = snapshot("changed");
gate.reset(snapshot("initial"));
assertNull(gate.observe(changed, 0L));
LocaleHotloadGate.Attempt failedAttempt = gate.observe(changed, 100L);
assertNotNull(failedAttempt);
gate.complete(failedAttempt, 500L, false);
assertNull(gate.observe(changed, 3_499L));
LocaleHotloadGate.Attempt retryAttempt = gate.observe(changed, 3_500L);
assertNotNull(retryAttempt);
assertEquals(changed, retryAttempt.snapshot());
}
@Test
public void manualResetInvalidatesInFlightAutomaticAttempt() {
LocaleHotloadGate gate = gate();
LocaleHotloadSnapshot automatic = snapshot("automatic");
LocaleHotloadSnapshot manual = snapshot("manual");
LocaleHotloadSnapshot changed = snapshot("changed");
gate.reset(snapshot("initial"));
assertNull(gate.observe(automatic, 0L));
LocaleHotloadGate.Attempt staleAttempt = gate.observe(automatic, 100L);
assertNotNull(staleAttempt);
gate.reset(manual);
gate.complete(staleAttempt, 200L, true);
assertNull(gate.observe(manual, 200L));
assertNull(gate.observe(changed, 300L));
LocaleHotloadGate.Attempt currentAttempt = gate.observe(changed, 400L);
assertNotNull(currentAttempt);
assertNotEquals(staleAttempt.generation(), currentAttempt.generation());
assertEquals(changed, currentAttempt.snapshot());
}
private LocaleHotloadGate gate() {
return new LocaleHotloadGate(new LocaleHotloadGate.Timing(100L, 500L, 3_000L));
}
private LocaleHotloadSnapshot snapshot(String content) {
return LocaleHotloadSnapshot.present(
new File("locale.json"),
"en_US",
content,
content
);
}
}
@@ -0,0 +1,326 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectPlacementRunnerRegressionTest {
private static final int ANCHOR_Y = 80;
private IrisData data;
private Engine engine;
private PlatformBlockState solid;
private PlatformBlockState schematicAir;
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
Map<String, PlatformBlockState> registryStates = new HashMap<>();
when(registries.block(anyString())).thenAnswer(invocation -> {
String key = invocation.getArgument(0);
return registryStates.computeIfAbsent(key, value -> state(value.toLowerCase(), !value.toLowerCase().contains("air")));
});
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(256);
data = mock(IrisData.class);
when(data.getEngine()).thenReturn(engine);
solid = state("minecraft:stone", true);
schematicAir = state("minecraft:air", false);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void placementFailurePropagatesToTheCaller() {
RecordingPlacer placer = new RecordingPlacer(null);
placer.failAfterWrites(1);
IrisObject object = lineObject(3);
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> object.place(0, ANCHOR_Y, 0, placer, placement(), new RNG(2L), data));
assertTrue(error.getMessage().contains("write failed"));
assertEquals(1, placer.writes().size());
}
@Test
public void boreUsesRotatedTranslationAndRandomizedAnchor() {
RecordingPlacer placer = new RecordingPlacer(null);
IrisObjectPlacement placement = placement();
placement.setBore(true);
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
placement.setTranslate(new IrisObjectTranslate().setX(4).setYRandom(3));
int resultY = lineObject(3).place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
List<BlockWrite> airWrites = placer.writesOf(IrisObject.States.AIR);
assertFalse(airWrites.isEmpty());
assertTrue(airWrites.stream().allMatch(write -> write.y() >= resultY && write.y() <= resultY));
assertTrue(airWrites.stream().allMatch(write -> write.x() == 0));
assertTrue(airWrites.stream().allMatch(write -> write.z() <= -3 && write.z() >= -5));
}
@Test
public void collisionCheckUsesTheTransformedBounds() {
PlacedObject forbidden = new PlacedObject(null, object("forbidden"), 1, 0, 0);
when(engine.getObjectPlacement(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
return x == 0 && y == ANCHOR_Y && z == -4 ? forbidden : null;
});
RecordingPlacer placer = new RecordingPlacer(engine);
IrisObjectPlacement placement = placement();
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
placement.setTranslate(new IrisObjectTranslate().setX(4));
placement.getForbiddenCollisions().add("forbidden");
int result = lineObject(3).place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
assertEquals(-1, result);
assertTrue(placer.writes().isEmpty());
}
@Test
public void arbitraryRotationSamplesEveryTransformedFootprintColumn() {
RecordingPlacer placer = new RecordingPlacer(null);
IrisObjectPlacement placement = placement();
placement.setMode(ObjectPlaceMode.MAX_HEIGHT);
placement.setRotation(IrisObjectRotation.of(0, 45, 0));
boxObject(5, 1, 3).place(0, -1, 0, placer, placement, new RNG(2L), data);
assertTrue(placer.sampledColumns().contains("-2:-2"));
assertTrue(placer.sampledColumns().contains("2:2"));
assertTrue(placer.sampledColumns().size() >= 25);
}
@Test
public void snowUsesOnlyBlocksThatWereWritten() {
RecordingPlacer placer = new RecordingPlacer(null);
IrisObject object = new IrisObject(1, 3, 1);
object.setUnsigned(0, 0, 0, solid);
object.setUnsigned(0, 2, 0, schematicAir);
IrisObjectPlacement placement = placement();
placement.setSnow(0.1);
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
List<BlockWrite> snowWrites = placer.writesOf(IrisObject.States.SNOW_LAYERS[0]);
assertEquals(1, snowWrites.size());
assertEquals(ANCHOR_Y, snowWrites.get(0).y());
}
@Test
public void debugPlacementDoesNotMutateSmartBoreCache() {
IrisObject object = new IrisObject(1, 1, 1);
object.setUnsigned(0, 0, 0, IrisObject.States.VAIR);
object.setSmartBored(true);
IrisObjectPlacement placement = placement();
placement.setSmartBore(true);
RecordingPlacer debug = new RecordingPlacer(null);
debug.setDebugSmartBore(true);
RecordingPlacer normal = new RecordingPlacer(null);
object.place(0, ANCHOR_Y, 0, debug, placement, new RNG(2L), data);
object.place(0, ANCHOR_Y, 0, normal, placement, new RNG(2L), data);
PlatformBlockState cachedState = object.getBlocks().get(object.getSigned(0, 0, 0));
assertSame(IrisObject.States.VAIR, cachedState);
if (IrisObject.States.VAIR != IrisObject.States.VAIR_DEBUG) {
assertTrue(cachedState != IrisObject.States.VAIR_DEBUG);
}
}
@Test
public void placementDataIsRequiredAtTheBoundary() {
NullPointerException error = assertThrows(NullPointerException.class,
() -> lineObject(1).place(0, ANCHOR_Y, 0, new RecordingPlacer(null), placement(), new RNG(2L), null));
assertEquals("Object placement data is required.", error.getMessage());
}
private IrisObjectPlacement placement() {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRequireSurfaceSupport(false);
return placement;
}
private IrisObject lineObject(int width) {
IrisObject object = new IrisObject(width, 1, 1);
for (int x = 0; x < width; x++) {
object.setUnsigned(x, 0, 0, solid);
}
return object;
}
private IrisObject boxObject(int width, int height, int depth) {
IrisObject object = new IrisObject(width, height, depth);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
for (int z = 0; z < depth; z++) {
object.setUnsigned(x, y, z, solid);
}
}
}
return object;
}
private IrisObject object(String key) {
IrisObject object = lineObject(1);
object.setLoadKey(key);
return object;
}
private static PlatformBlockState state(String key, boolean solid) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(key);
when(state.isSolid()).thenReturn(solid);
when(state.isOccluding()).thenReturn(solid);
return state;
}
private static final class RecordingPlacer implements IObjectPlacer {
private final List<BlockWrite> writes = new ArrayList<>();
private final Map<String, PlatformBlockState> world = new HashMap<>();
private final List<String> sampledColumns = new ArrayList<>();
private final Engine engine;
private int failAfterWrites = Integer.MAX_VALUE;
private boolean debugSmartBore;
private RecordingPlacer(Engine engine) {
this.engine = engine;
}
private void failAfterWrites(int writes) {
failAfterWrites = writes;
}
private void setDebugSmartBore(boolean debugSmartBore) {
this.debugSmartBore = debugSmartBore;
}
private List<BlockWrite> writes() {
return writes;
}
private List<BlockWrite> writesOf(PlatformBlockState state) {
return writes.stream().filter(write -> write.state() == state).toList();
}
private List<String> sampledColumns() {
return sampledColumns;
}
@Override
public int getHighest(int x, int z, IrisData data) {
sampledColumns.add(x + ":" + z);
return ANCHOR_Y;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
sampledColumns.add(x + ":" + z);
return ANCHOR_Y;
}
@Override
public void set(int x, int y, int z, PlatformBlockState state) {
if (writes.size() >= failAfterWrites) {
throw new IllegalStateException("write failed");
}
writes.add(new BlockWrite(x, y, z, state));
world.put(x + ":" + y + ":" + z, state);
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return world.get(x + ":" + y + ":" + z);
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
PlatformBlockState state = get(x, y, z);
return state != null && state.isSolid();
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public int getFluidHeight() {
return 0;
}
@Override
public boolean isDebugSmartBore() {
return debugSmartBore;
}
@Override
public void setTile(int x, int y, int z, TileData tile) {
}
@Override
public <T> void setData(int x, int y, int z, T data) {
}
@Override
public <T> T getData(int x, int y, int z, Class<T> type) {
return null;
}
@Override
public Engine getEngine() {
return engine;
}
}
private record BlockWrite(int x, int y, int z, PlatformBlockState state) {
}
}
@@ -200,7 +200,7 @@ public class IrisObjectSurfaceSupportPlacementTest {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> place(placer, placement, SURFACE_Y));
assertTrue(error.getMessage().contains("require an active Iris engine"));
assertTrue(error.getMessage().contains("requires an active Iris engine"));
assertTrue(placer.written().isEmpty());
verify(data, never()).getEngine();
}