Merge branch 'client-fixes' into review

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-29 06:50:57 -04:00
commit 0cb3417755
55 files changed
+1421 -135

No files matched your search

@@ -0,0 +1,111 @@
package art.arcane.iris.modded;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeServerSpawn;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedEngineBootstrapShutdownTest {
private IrisSettings previousSettings;
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Before
public void openRuntime() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
reopenBurstPools();
}
@After
public void restoreRuntime() {
reopenBurstPools();
IrisModdedCommands.openDownloadAdmission();
IrisSettings.settings = previousSettings;
}
@Test
public void serverStoppingKeepsEnginesAndWorkersOpenUntilServerStopped() throws Exception {
NativeWorld level = level();
Engine engine = mock(Engine.class);
ModdedWorldEngines.installReplacement(level, engine);
Callable<Thread> worker = Thread::currentThread;
ModdedEngineBootstrap.stop();
verify(engine, never()).close();
assertTrue(ModdedWorldEngines.activeEngines().contains(engine));
assertNotSame(Thread.currentThread(), MultiBurst.burst.submit(worker).get(10L, TimeUnit.SECONDS));
ModdedEngineBootstrap.stopped();
verify(engine).close();
assertFalse(ModdedWorldEngines.activeEngines().contains(engine));
assertSame(Thread.currentThread(), MultiBurst.burst.submit(worker).get(10L, TimeUnit.SECONDS));
}
@Test
public void nextServerStartClosesTheEnginesOfAServerThatNeverReachedStopped() {
NativeWorld level = level();
Engine engine = mock(Engine.class);
ModdedWorldEngines.installReplacement(level, engine);
ModdedEngineBootstrap.stop();
try (MockedStatic<ModdedStartup> startup = mockStatic(ModdedStartup.class);
MockedConstruction<NativeServerSpawn> spawn = mockConstruction(NativeServerSpawn.class)) {
ModdedEngineBootstrap.serverAboutToStart(mock(NativeModdedServer.class));
}
verify(engine).close();
assertFalse(ModdedWorldEngines.activeEngines().contains(engine));
}
private static NativeWorld level() {
ServerLevel serverLevel = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(serverLevel.getChunkSource()).thenReturn(chunks);
when(chunks.getGenerator()).thenReturn(mock(ChunkGenerator.class));
NativeWorld level = mock(NativeWorld.class);
when(level.nativeHandle()).thenReturn(serverLevel);
when(level.name()).thenReturn("example:shutdown");
return level;
}
private static void reopenBurstPools() {
MultiBurst.burst.reopen();
MultiBurst.hydrology.reopen();
MultiBurst.ioBurst.reopen();
}
}
@@ -0,0 +1,194 @@
package art.arcane.iris.modded;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Assume;
import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class ModdedMixinAuditTest {
private static final String MIXIN_ANNOTATION = "Lorg/spongepowered/asm/mixin/Mixin;";
private static final Set<String> INJECTOR_ANNOTATIONS = Set.of(
"Lorg/spongepowered/asm/mixin/injection/Inject;",
"Lorg/spongepowered/asm/mixin/injection/Redirect;",
"Lorg/spongepowered/asm/mixin/injection/ModifyArg;",
"Lorg/spongepowered/asm/mixin/injection/ModifyArgs;",
"Lorg/spongepowered/asm/mixin/injection/ModifyVariable;",
"Lorg/spongepowered/asm/mixin/injection/ModifyConstant;");
private static final Pattern NEOFORGE_MIXIN_CONFIG = Pattern.compile(
"\\[\\[mixins]]\\s*config\\s*=\\s*\"([^\"]+)\"");
// One applied mixin per config proves the config was registered, which is all the audit detects.
private static final Set<String> UNAUDITED = Set.of(
"volmlib.entity.mixins.json/WorldGenerationWriteGuardMixin",
"volmlib.entity.mixins.json/ChunkMapTerrainReceiptAccess",
"volmlib.entity.mixins.json/ChunkTerrainReceiptMixin",
"volmlib.entity.mixins.json/LevelChunkTerrainReceiptMixin",
"volmlib.entity.mixins.json/SerializableTerrainReceiptMixin",
"volmlib.fabric.mixins.json/BlockItemMixin",
"volmlib.fabric.mixins.json/BlockMixin",
"volmlib.fabric.mixins.json/PackRepositoryMixin");
@Test
public void everyExpectedHandlerIsAnInjectorDeclaredByItsMixin() throws IOException {
for (ModdedMixinAudit.ExpectedMixin expected : ModdedMixinAudit.EXPECTED) {
MixinConfig config = MixinConfig.read(expected.config());
String section = config.sections().get(expected.mixinName());
assertNotNull(expected.config() + " does not list " + expected.mixinName(), section);
assertEquals(expected.mixinName() + " client-only flag disagrees with its config section",
"client".equals(section), expected.clientOnly());
MixinClass mixin = MixinClass.read(config.packageName() + '.' + expected.mixinName());
assertTrue(expected.mixinName() + " does not target " + expected.targetClass().className()
+ " (targets " + mixin.targets() + ")",
mixin.targets().contains(expected.targetClass().className()));
assertTrue(expected.mixinName() + " declares no injector handler named " + expected.handlerMethod()
+ " (injectors " + mixin.injectors() + ")",
mixin.injectors().contains(expected.handlerMethod()));
}
}
@Test
public void everyRegisteredMixinIsAuditedOrDeliberatelyUnaudited() throws IOException {
Set<String> registered = registeredConfigs();
Assume.assumeFalse("loader metadata on this classpath declares no mixin configs", registered.isEmpty());
Set<String> audited = new LinkedHashSet<>();
for (ModdedMixinAudit.ExpectedMixin expected : ModdedMixinAudit.EXPECTED) {
audited.add(expected.config() + '/' + expected.mixinName());
}
Set<String> listed = new TreeSet<>();
for (String configName : registered) {
for (String mixinName : MixinConfig.read(configName).sections().keySet()) {
listed.add(configName + '/' + mixinName);
}
}
Set<String> uncovered = new TreeSet<>(listed);
uncovered.removeAll(audited);
uncovered.removeAll(UNAUDITED);
assertEquals("mixins neither audited nor deliberately unaudited", Set.of(), uncovered);
Set<String> unregisteredAudits = new TreeSet<>(audited);
unregisteredAudits.removeAll(listed);
assertEquals("audited mixins missing from the registered configs", Set.of(), unregisteredAudits);
}
private static Set<String> registeredConfigs() throws IOException {
Set<String> configs = new LinkedHashSet<>();
String fabric = readResource("fabric.mod.json");
if (fabric != null) {
for (JsonElement entry : JsonParser.parseString(fabric).getAsJsonObject().getAsJsonArray("mixins")) {
configs.add(entry.isJsonObject()
? entry.getAsJsonObject().get("config").getAsString()
: entry.getAsString());
}
}
String neoforge = readResource("META-INF/neoforge.mods.toml");
if (neoforge != null) {
Matcher matcher = NEOFORGE_MIXIN_CONFIG.matcher(neoforge);
while (matcher.find()) {
configs.add(matcher.group(1));
}
}
return configs;
}
private static String readResource(String name) throws IOException {
try (InputStream input = ModdedMixinAuditTest.class.getClassLoader().getResourceAsStream(name)) {
return input == null ? null : new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
}
private record MixinConfig(String packageName, Map<String, String> sections) {
static MixinConfig read(String name) throws IOException {
String json = readResource(name);
assertNotNull("Missing mixin config resource " + name, json);
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
Map<String, String> sections = new LinkedHashMap<>();
for (String section : new String[]{"mixins", "client", "server"}) {
JsonArray entries = root.getAsJsonArray(section);
if (entries == null) {
continue;
}
for (JsonElement entry : entries) {
sections.put(entry.getAsString(), section);
}
}
return new MixinConfig(root.get("package").getAsString(), sections);
}
}
private record MixinClass(Set<String> targets, Set<String> injectors) {
static MixinClass read(String className) throws IOException {
String resource = className.replace('.', '/') + ".class";
byte[] bytes;
try (InputStream input = ModdedMixinAuditTest.class.getClassLoader().getResourceAsStream(resource)) {
assertNotNull("Missing mixin class " + className, input);
bytes = input.readAllBytes();
}
Set<String> targets = new TreeSet<>();
Set<String> injectors = new TreeSet<>();
new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (!MIXIN_ANNOTATION.equals(descriptor)) {
return null;
}
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public AnnotationVisitor visitArray(String name) {
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public void visit(String ignored, Object value) {
if (value instanceof Type type) {
targets.add(type.getClassName());
} else if (value instanceof String target) {
targets.add(target.replace('/', '.'));
}
}
};
}
};
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
return new MethodVisitor(Opcodes.ASM9) {
@Override
public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
if (INJECTOR_ANNOTATIONS.contains(annotation)) {
injectors.add(name);
}
return null;
}
};
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return new MixinClass(targets, injectors);
}
}
}
@@ -0,0 +1,145 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.localization.ModdedCommandMessages;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.localization.MessageArgument;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.invocation.InvocationOnMock;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
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.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisModdedCommandsTeleportTest {
private static final String DIMENSION = "irisworldgen:1";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void teleportWaitsForAColdDestinationWithinABoundedDeadline() {
NativeModdedServer server = mock(NativeModdedServer.class);
NativeCommandSource source = mock(NativeCommandSource.class);
when(source.server()).thenReturn(server);
NativeWorld level = mock(NativeWorld.class);
when(level.name()).thenReturn(DIMENSION);
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
when(player.name()).thenReturn("Tester");
AtomicReference<NativeWorldTeleport.Destination> requested = new AtomicReference<>();
long before = System.nanoTime();
try (MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
MockedStatic<ModdedDimensionManager> dimensions = mockStatic(ModdedDimensionManager.class);
MockedStatic<NativeWorldTeleport> teleport = mockStatic(NativeWorldTeleport.class)) {
generators.when(() -> NativeWorldGenerators.find(level, IrisModdedChunkGenerator.class))
.thenReturn(mock(IrisModdedChunkGenerator.class));
dimensions.when(() -> ModdedDimensionManager.level(server, DIMENSION)).thenReturn(level);
teleport.when(() -> NativeWorldTeleport.teleport(any(), any())).thenAnswer((InvocationOnMock invocation) -> {
requested.set(invocation.getArgument(1));
return new CompletableFuture<Boolean>();
});
assertEquals(1, IrisModdedCommands.tp(source, level, player));
}
long after = System.nanoTime();
long deadline = requested.get().deadlineNanos();
assertTrue("deadline must outlast a cold first chunk", deadline - before >= TimeUnit.SECONDS.toNanos(60L));
assertTrue("deadline must be bounded", deadline - after <= TimeUnit.SECONDS.toNanos(120L));
}
@Test(timeout = 15_000L)
public void expiredTeleportReportsTheCallersDeadline() throws Exception {
MinecraftServer server = mock(MinecraftServer.class);
when(server.isSameThread()).thenReturn(true);
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION, Identifier.parse(DIMENSION));
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(chunks.addTicketAndLoadWithRadius(any(), any(), anyInt())).thenReturn(new CompletableFuture<>());
ServerLevel serverLevel = mock(ServerLevel.class);
when(serverLevel.getServer()).thenReturn(server);
when(serverLevel.dimension()).thenReturn(dimension);
when(serverLevel.getChunkSource()).thenReturn(chunks);
when(server.getLevel(dimension)).thenReturn(serverLevel);
ServerPlayer serverPlayer = mock(ServerPlayer.class);
when(serverPlayer.getUUID()).thenReturn(UUID.randomUUID());
NativeWorld level = mock(NativeWorld.class);
when(level.nativeHandle()).thenReturn(serverLevel);
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(NativeProtocolPlayer.fromHandle(serverPlayer),
new NativeWorldTeleport.Destination(NativeModdedServer.fromHandle(server), level, 8.5D,
Double.MIN_VALUE, 8.5D, System.nanoTime() + TimeUnit.SECONDS.toNanos(2L)));
ExecutionException failure = assertThrows(ExecutionException.class, () -> teleport.get(10L, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof TimeoutException);
assertTrue(failure.getCause().getMessage(), failure.getCause().getMessage().contains("within 2 seconds"));
}
@Test
public void failureReportsItsRootCauseInsteadOfAnUnloadedDimension() {
String message = IrisModdedCommands.teleportFailure(DIMENSION, "Tester",
new CompletionException(new IllegalStateException("Exception generating new chunk")), true, true);
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
MessageArgument.untrusted("dimensionId", DIMENSION),
MessageArgument.untrusted("reason", "Exception generating new chunk")), message);
}
@Test
public void failureWithoutMessageNamesTheFailureType() {
String message = IrisModdedCommands.teleportFailure(DIMENSION, "Tester", new TimeoutException(), true, true);
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
MessageArgument.untrusted("dimensionId", DIMENSION),
MessageArgument.untrusted("reason", "TimeoutException")), message);
}
@Test
public void unloadedDimensionIsReportedOnlyWhenItIsGone() {
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
MessageArgument.untrusted("dimensionId", DIMENSION)),
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, false, true));
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_CANCELLED_PLAYER_OFFLINE,
MessageArgument.untrusted("dimensionId", DIMENSION),
MessageArgument.untrusted("value", "Tester")),
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, true, false));
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_REFUSED,
MessageArgument.untrusted("dimensionId", DIMENSION)),
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, true, true));
}
}
@@ -2,15 +2,30 @@ package art.arcane.iris.modded.service;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedPlatformWorld;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedLootApplier;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldMaintenance;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
import net.minecraft.SharedConstants;
@@ -24,19 +39,33 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FlowingFluid;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.invocation.InvocationOnMock;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.LongConsumer;
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.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedChunkUpdateServiceTest {
@@ -70,6 +99,148 @@ public class ModdedChunkUpdateServiceTest {
assertTrue(chunk.deleted());
}
@Test
public void loadingSavedBiomeDefersThePassAndForgetsOnlyCompletedLoot() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(world);
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
ServerLevel level = mock(ServerLevel.class);
when(level.getMinY()).thenReturn(-64);
when(level.getMaxY()).thenReturn(319);
when(level.getHeight()).thenReturn(384);
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.CHEST.defaultBlockState());
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException(
"Saved biome information is loading at chunk 0,0. Try again shortly.", true);
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class)) {
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
NativeBlockPoint position = invocation.getArgument(2);
if (position.x() == 2) {
throw loading;
}
return null;
});
SavedBiomeUnavailableException thrown = assertThrows(SavedBiomeUnavailableException.class,
() -> new ModdedChunkUpdateService().runUpdatePass(
engine, new ModdedPlatformWorld(level), 0, 0, chunk));
assertSame(loading, thrown);
}
assertFalse(chunk.deleted());
verify(chunk.slice).set(1, 5, 1, null);
verify(chunk.slice, never()).set(2, 5, 2, null);
}
@Test
public void loadingSavedBiomeLeavesTheChunkUnetchedWithoutAnErrorAndRetriesOnTheNextPass() {
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
Engine engine = updateEngine(chunk);
NativeWorld level = chestLevel();
AtomicInteger loadingAttempts = new AtomicInteger();
ModdedChunkUpdateService service = new ModdedChunkUpdateService();
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
NativeBlockPoint position = invocation.getArgument(2);
if (position.x() == 2 && loadingAttempts.getAndIncrement() == 0) {
throw new SavedBiomeUnavailableException("Saved biome information is loading at chunk 0,0.", true);
}
return null;
});
service.updateRegeneratedChunk(engine, level, 0, 0);
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
assertFalse(chunk.deleted());
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
service.updateRegeneratedChunk(engine, level, 0, 0);
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
assertTrue(chunk.deleted());
}
}
@Test
public void permanentSavedBiomeUnavailabilityCompletesThePassWithAWarning() {
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
Engine engine = updateEngine(chunk);
NativeWorld level = chestLevel();
String reason = "This chunk has no exact saved Iris biome assignment for generation 3.";
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
NativeBlockPoint position = invocation.getArgument(2);
if (position.x() == 2) {
throw new SavedBiomeUnavailableException(reason, false);
}
return null;
});
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0);
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
assertTrue(chunk.deleted());
logging.verify(() -> IrisLogging.warnOnce(anyString(), contains(reason)));
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
}
}
@Test
public void failedChunkDoesNotStopTheLevelPassForItsOtherChunks() {
IrisSettings previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
FailingMantleChunk failing = new FailingMantleChunk();
ChestUpdateMantleChunk healthy = new ChestUpdateMantleChunk();
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class);
when(mantle.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> failing.use());
when(mantle.useChunk(4, 0)).thenAnswer((InvocationOnMock invocation) -> healthy.use());
Engine engine = engineWith(mantle);
NativeWorld level = chestLevel();
NativeModdedServer server = mock(NativeModdedServer.class);
when(server.worlds()).thenReturn(List.of(level));
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
when(generator.engineIfBound()).thenReturn(engine);
try (MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedConstruction<NativeWorldMaintenance> maintenance = mockConstruction(NativeWorldMaintenance.class,
(NativeWorldMaintenance mocked, MockedConstruction.Context context) -> {
when(mocked.hasForcedChunks()).thenReturn(true);
doAnswer((InvocationOnMock invocation) -> {
LongConsumer action = invocation.getArgument(0);
action.accept(0L);
action.accept(4L);
return null;
}).when(mocked).forEachForcedChunk(any(LongConsumer.class));
})) {
generators.when(() -> NativeWorldGenerators.find(level, IrisModdedChunkGenerator.class)).thenReturn(generator);
new ModdedChunkUpdateService().onServerTick(server);
assertFalse(failing.isFlagged(MantleFlag.ETCHED));
assertTrue(healthy.isFlagged(MantleFlag.ETCHED));
assertTrue(healthy.deleted());
logging.verify(() -> IrisLogging.errorOnce(anyString(), anyString()));
} finally {
IrisSettings.settings = previousSettings;
}
}
@Test
public void scansWhenPlayersArePresent() {
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
@@ -166,6 +337,94 @@ public class ModdedChunkUpdateServiceTest {
}
}
private static final class ChestUpdateMantleChunk extends MantleChunk<Matter> {
@SuppressWarnings("unchecked")
private final MatterSlice<MatterUpdate> slice = mock(MatterSlice.class);
private final Matter section = mock(Matter.class);
private boolean deleted;
private ChestUpdateMantleChunk() {
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
when(section.hasSlice(MatterUpdate.class)).thenReturn(true);
when(section.getSlice(MatterUpdate.class)).thenReturn(slice);
}
@Override
public Matter get(int index) {
return section;
}
@Override
public <T> void iterate(
Class<T> type,
Consumer4<Integer, Integer, Integer, T> iterator
) {
if (type == MatterUpdate.class) {
iterator.accept(1, 5, 1, type.cast(UpdateMatter.ON));
iterator.accept(2, 5, 2, type.cast(UpdateMatter.ON));
}
}
@Override
public void deleteSlices(Class<?> type) {
if (type == MatterUpdate.class) {
deleted = true;
}
}
private boolean deleted() {
return deleted;
}
}
private static Engine updateEngine(ChestUpdateMantleChunk chunk) {
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class);
when(mantle.isChunkLoaded(0, 0)).thenReturn(true);
when(mantle.hasFlag(0, 0, MantleFlag.ETCHED)).thenAnswer((InvocationOnMock invocation) -> chunk.isFlagged(MantleFlag.ETCHED));
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> chunk.use());
return engineWith(mantle);
}
private static Engine engineWith(Mantle<Matter> mantle) {
IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getMantle()).thenReturn(mantle);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(world);
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
when(engine.getMantle()).thenReturn(engineMantle);
return engine;
}
private static NativeWorld chestLevel() {
NativeBlockState chest = mock(NativeBlockState.class);
when(chest.isStorage()).thenReturn(true);
when(chest.isStorageChest()).thenReturn(true);
NativeWorld level = mock(NativeWorld.class);
when(level.name()).thenReturn("example:updates");
when(level.minHeight()).thenReturn(-64);
when(level.maxHeight()).thenReturn(320);
when(level.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
when(level.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(chest);
return level;
}
private static final class FailingMantleChunk extends MantleChunk<Matter> {
private FailingMantleChunk() {
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
}
@Override
public <T> void iterate(
Class<T> type,
Consumer4<Integer, Integer, Integer, T> iterator
) {
throw new IllegalStateException("corrupt " + type.getSimpleName() + " slice");
}
}
@SuppressWarnings("unchecked")
private static MantleDataAdapter<Matter> emptyAdapter() {
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(