Bounded teleport deadline

- /iris tp waits up to 120 seconds for the destination chunk
- A refusing generator or removed dimension still fails the command
- Tests cover the deadline and the reported timeout limit
This commit is contained in:
Brian Neumann-Fopiano
2026-09-29 06:50:24 -04:00
parent 782a85c26a
commit 2d5af3b61e
2 changed files with 102 additions and 1 deletions
@@ -67,6 +67,9 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public final class IrisModdedCommands {
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
// The first chunk of a cold dimension also loads the pack and binds the engine; the deadline only bounds a
// destination whose chunk never completes (a refusing generator or a removed dimension).
private static final long TELEPORT_DEADLINE_SECONDS = 120L;
private static final Object DOWNLOAD_MONITOR = new Object();
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = ModdedCommandSuggestions.PACK_NAMES;
@@ -133,7 +136,8 @@ public final class IrisModdedCommands {
String dimensionId = level.name();
NativeModdedServer server = source.server();
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player, new NativeWorldTeleport.Destination(
server, ModdedDimensionManager.level(server, dimensionId), 8.5D, Double.MIN_VALUE, 8.5D, 0L));
server, ModdedDimensionManager.level(server, dimensionId), 8.5D, Double.MIN_VALUE, 8.5D,
System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_DEADLINE_SECONDS)));
teleport.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success) && failure == null) {
return;
@@ -1,18 +1,115 @@
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",