Shared teleport deadline

- Give primary world routing and Iris Vision teleports the 120 second first-chunk deadline /iris tp uses
This commit is contained in:
Brian Neumann-Fopiano
2026-09-29 07:13:27 -04:00
parent 96a400b585
commit f55fde17d4
6 changed files with 145 additions and 9 deletions
@@ -29,7 +29,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -124,7 +123,7 @@ public final class ModdedPrimaryWorldRouter {
try {
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
System.nanoTime() + TimeUnit.SECONDS.toNanos(10)));
ModdedTeleportDeadline.fromNow()));
teleport.whenComplete((success, failure) -> {
inFlight.remove(id);
if (Boolean.TRUE.equals(success) && failure == null) {
@@ -0,0 +1,37 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import java.util.concurrent.TimeUnit;
/**
* Deadline for a teleport that waits on its destination chunk. 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).
*/
public final class ModdedTeleportDeadline {
private static final long DEADLINE_SECONDS = 120L;
private ModdedTeleportDeadline() {
}
public static long fromNow() {
return System.nanoTime() + TimeUnit.SECONDS.toNanos(DEADLINE_SECONDS);
}
}
@@ -36,6 +36,7 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedTeleportDeadline;
import art.arcane.iris.modded.ModdedWorldgenIds;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KMap;
@@ -67,9 +68,6 @@ 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;
@@ -137,7 +135,7 @@ public final class IrisModdedCommands {
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,
System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_DEADLINE_SECONDS)));
ModdedTeleportDeadline.fromNow()));
teleport.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success) && failure == null) {
return;
@@ -25,6 +25,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.studio.render.RenderType;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedTeleportDeadline;
import java.awt.Desktop;
import java.io.File;
@@ -32,7 +33,6 @@ import java.util.List;
import java.util.Optional;
import art.arcane.volmlib.nativelib.view.WorldView;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public final class ModdedVisionOverlay implements GuiOverlay {
@@ -63,8 +63,7 @@ public final class ModdedVisionOverlay implements GuiOverlay {
world.execute(() -> {
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
Optional<WorldView.TeleportOperation> operation = world.teleport(opener,
new WorldView.Destination(blockX + 0.5D, surfaceY, blockZ + 0.5D,
System.nanoTime() + TimeUnit.SECONDS.toNanos(10L)));
new WorldView.Destination(blockX + 0.5D, surfaceY, blockZ + 0.5D, ModdedTeleportDeadline.fromNow()));
if (operation.isEmpty()) {
return;
}
@@ -2,14 +2,21 @@ package art.arcane.iris.modded;
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.NativeWorldTeleport;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import org.junit.After;
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.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -182,6 +189,46 @@ public class ModdedPrimaryWorldRouterTest {
}
}
/**
* Routing waits on the primary world's first chunk, which on a cold world also loads the pack and binds the
* engine, so it gets the same bounded deadline as /iris tp instead of timing out and retrying every second.
*/
@Test
public void routingWaitsForAColdPrimaryWorldWithinTheTeleportDeadline() {
NativeModdedServer server = mock(NativeModdedServer.class);
NativeWorld overworld = mock(NativeWorld.class);
when(overworld.nativeHandle()).thenReturn(new Object());
NativeWorld primary = mock(NativeWorld.class);
when(primary.nativeHandle()).thenReturn(new Object());
when(server.overworld()).thenReturn(overworld);
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
when(player.id()).thenReturn(UUID.randomUUID());
when(player.isInWorld(overworld)).thenReturn(true);
onlinePlayers(server, player);
ModdedModConfig routed = config(true, PRIMARY);
AtomicReference<NativeWorldTeleport.Destination> requested = new AtomicReference<>();
long before = System.nanoTime();
try (MockedStatic<ModdedModConfig> configs = mockStatic(ModdedModConfig.class);
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
MockedStatic<NativeWorldTeleport> teleport = mockStatic(NativeWorldTeleport.class)) {
configs.when(ModdedModConfig::get).thenReturn(routed);
manager.when(() -> ModdedDimensionManager.level(server, PRIMARY)).thenReturn(primary);
teleport.when(() -> NativeWorldTeleport.teleport(any(), any())).thenAnswer((InvocationOnMock invocation) -> {
requested.set(invocation.getArgument(1));
return new CompletableFuture<Boolean>();
});
evaluate(server);
}
long after = System.nanoTime();
assertSame(primary, requested.get().world());
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));
}
private static ModdedDimensionRegistryStore.PersistentDimension persisted() {
return new ModdedDimensionRegistryStore.PersistentDimension(PRIMARY, "overworld", "overworld", 1337L);
}
@@ -0,0 +1,56 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.view.WorldView;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ModdedVisionOverlayTest {
/**
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
* bounded deadline as /iris tp.
*/
@Test
public void teleportWaitsForAColdDestinationWithinTheTeleportDeadline() {
WorldView world = mock(WorldView.class);
doAnswer((InvocationOnMock invocation) -> {
invocation.getArgument(0, Runnable.class).run();
return null;
}).when(world).execute(any());
UUID opener = UUID.randomUUID();
AtomicReference<WorldView.Destination> requested = new AtomicReference<>();
when(world.teleport(eq(opener), any())).thenAnswer((InvocationOnMock invocation) -> {
requested.set(invocation.getArgument(1));
return Optional.empty();
});
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt(), eq(false))).thenReturn(130);
long before = System.nanoTime();
new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world, engine, opener)).teleport(-3.5D, 17.25D);
long after = System.nanoTime();
WorldView.Destination destination = requested.get();
assertEquals(-3.5D, destination.x(), 0D);
assertEquals(68D, destination.y(), 0D);
assertEquals(17.5D, destination.z(), 0D);
assertTrue("deadline must outlast a cold first chunk",
destination.deadlineNanos() - before >= TimeUnit.SECONDS.toNanos(60L));
assertTrue("deadline must be bounded", destination.deadlineNanos() - after <= TimeUnit.SECONDS.toNanos(120L));
}
}