Improve generation and lifecycle safety

- Reduce terrain, noise, hydrology, and decorator sampling overhead.
- Reduce object transaction allocations while preserving rollback behavior.
- Optimize structure placement and cave anchor sampling.
- Parallelize Studio cache warmup and protect world entry and hotload recovery.
- Validate pack fingerprints and publish pack replacements atomically.
- Notify modded operators about outdated packs without synchronizing pack data.
- Harden world creation, loading, evacuation, and teleport failure recovery.
- Coalesce queued region flushes while preserving chunk save ordering.
- Remove obsolete mantle, pack, and cave planning code.
- Add regression tests and reusable gameplay fixtures.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-06 20:51:28 -04:00
1 parent a2bcd7a302
commit 7f5ce70f49
197 files changed
+7560 -4420

No files matched your search

@@ -50,7 +50,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
int xx = rX + dx;
int yy = y + dy;
int zz = rZ + dz;
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()
? null : hunk.get(xx, yy, zz);
}
}
@@ -70,6 +70,7 @@ import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedPackUpdateService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
@@ -477,6 +478,7 @@ public final class ModdedEngineBootstrap {
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
createdServices.register(ModdedPackUpdateService.class, new ModdedPackUpdateService());
ModdedStudioHotloadService studioHotloadService = createdServices.register(
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
@@ -27,7 +27,6 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Owns the Iris generation pool and the single decision of whether this loader already runs chunk
@@ -43,7 +42,7 @@ public final class ModdedGenPool {
private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L;
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem();
private static final AtomicReference<ExecutorService> GEN_POOL = new AtomicReference<>(createGenPool());
private static ExecutorService generationPool = createGenPool();
private ModdedGenPool() {
}
@@ -63,39 +62,32 @@ public final class ModdedGenPool {
return CHUNK_SYSTEM.description();
}
static ExecutorService pool() {
ExecutorService pool = GEN_POOL.get();
if (pool != null && !pool.isShutdown()) {
return pool;
}
start();
ExecutorService restarted = GEN_POOL.get();
if (restarted == null) {
static synchronized ExecutorService pool() {
if (generationPool.isShutdown()) {
throw new RejectedExecutionException("Iris gen pool is shut down");
}
return restarted;
return generationPool;
}
static void start() {
while (true) {
ExecutorService current = GEN_POOL.get();
if (current != null && !current.isShutdown()) {
return;
}
ExecutorService created = createGenPool();
if (GEN_POOL.compareAndSet(current, created)) {
return;
}
created.shutdownNow();
static synchronized void start() {
if (!generationPool.isShutdown()) {
return;
}
if (!generationPool.isTerminated()) {
throw new IllegalStateException("Iris gen pool cannot restart while prior workers are still active");
}
generationPool = createGenPool();
}
static void shutdown() {
ExecutorService pool = GEN_POOL.getAndSet(null);
if (pool == null) {
return;
ExecutorService pool;
synchronized (ModdedGenPool.class) {
pool = generationPool;
if (pool.isTerminated()) {
return;
}
pool.shutdown();
}
pool.shutdown();
try {
if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) {
return;
@@ -20,13 +20,11 @@ package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
import art.arcane.iris.diagnostics.splash.IrisSplashRenderer;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
public final class ModdedIrisSplash {
@@ -34,18 +32,11 @@ public final class ModdedIrisSplash {
}
public static void print(NativeModdedLoader loader) {
printPacks(loader);
if (isLogoEnabled()) {
printLogo(loader);
}
}
private static void printPacks(NativeModdedLoader loader) {
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)
.thenAccept(lines -> lines.forEach(IrisLogging::info));
}
private static void printLogo(NativeModdedLoader loader) {
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
String[] splash = IrisSplashRenderer.renderPlain();
@@ -27,16 +27,15 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public final class ModdedPrimaryWorldRouter {
private static final int TICK_INTERVAL = 20;
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
private static final ConcurrentMap<UUID, CompletableFuture<Boolean>> routes = new ConcurrentHashMap<>();
private static int tickCounter = 0;
private static volatile String loginRefusal;
private static volatile String absentPrimary;
@@ -45,8 +44,12 @@ public final class ModdedPrimaryWorldRouter {
}
public static void clear() {
routed.clear();
inFlight.clear();
routes.forEach((id, route) -> {
if (routes.remove(id, route)) {
route.cancel(false);
}
});
tickCounter = 0;
loginRefusal = null;
absentPrimary = null;
}
@@ -57,8 +60,10 @@ public final class ModdedPrimaryWorldRouter {
*/
public static void forget(UUID player) {
if (player != null) {
routed.remove(player);
inFlight.remove(player);
CompletableFuture<Boolean> route = routes.remove(player);
if (route != null) {
route.cancel(false);
}
}
}
@@ -110,36 +115,52 @@ public final class ModdedPrimaryWorldRouter {
return;
}
server.forEachPlayer(player -> {
UUID id = player.id();
if (routed.contains(id) || !inFlight.add(id)) {
return;
server.forEachPlayer(player -> routePlayer(server, player, overworld, target, primary));
}
private static void routePlayer(NativeModdedServer server, NativeProtocolPlayer player,
NativeWorld overworld, NativeWorld target, String primary) {
UUID id = player.id();
if (routes.containsKey(id)) {
return;
}
CompletableFuture<Boolean> route = new CompletableFuture<>();
if (routes.putIfAbsent(id, route) != null) {
return;
}
route.whenComplete((success, failure) -> {
if (!Boolean.TRUE.equals(success)) {
routes.remove(id, route);
}
if (!player.isInWorld(overworld)) {
inFlight.remove(id);
routed.add(id);
return;
}
try {
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
ModdedTeleportDeadline.fromNow()));
teleport.whenComplete((success, failure) -> {
inFlight.remove(id);
if (Boolean.TRUE.equals(success) && failure == null) {
routed.add(id);
return;
}
if (failure != null) {
});
if (!player.isInWorld(overworld)) {
route.complete(true);
return;
}
try {
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
ModdedTeleportDeadline.fromNow()));
route.whenComplete((success, failure) -> {
if (!Boolean.TRUE.equals(success) && !teleport.isDone()) {
teleport.cancel(false);
}
});
teleport.whenComplete((success, failure) -> {
if (failure != null) {
if (route.completeExceptionally(failure)) {
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
id, primary, failure);
}
});
} catch (Throwable e) {
inFlight.remove(id);
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
} else {
route.complete(Boolean.TRUE.equals(success));
}
});
} catch (Throwable failure) {
if (route.completeExceptionally(failure)) {
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, failure);
}
});
}
}
/**
@@ -31,6 +31,7 @@ import art.arcane.iris.platform.protocol.IrisVisionRequestService;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.modded.service.ModdedPackUpdateService;
import java.util.Objects;
import java.util.UUID;
@@ -128,6 +129,10 @@ public final class ModdedProtocolHandler {
}
String sessionId = sessionId(player);
ModdedStartup.warnStartupFailuresTo(player);
ModdedPackUpdateService packUpdates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
if (packUpdates != null) {
packUpdates.notifyPlayer(player);
}
IrisSessionRegistry current = registry;
ModdedProtocolTransport currentTransport = transport;
if (current == null || currentTransport == null) {
@@ -18,10 +18,13 @@
package art.arcane.iris.modded;
import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackDownloader;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.modded.command.ModdedPackCommands;
@@ -31,15 +34,11 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
public final class ModdedStartup {
private static final int COMPAT_BOOT_KEY_CAP = 3;
@@ -60,36 +59,9 @@ public final class ModdedStartup {
if (!PREPARED.compareAndSet(false, true)) {
return;
}
reportLegacyPacksDirectory();
validateAllPacks();
}
/**
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
* the legacy directory holds packs, and quietly remove it when it is empty.
*/
private static void reportLegacyPacksDirectory() {
try {
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
if (!legacy.isDirectory()) {
return;
}
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
ModdedIrisLog.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
legacy.getAbsolutePath(), real.getAbsolutePath());
return;
}
String[] entries = legacy.list();
if (entries == null || entries.length == 0) {
legacy.delete();
}
} catch (Throwable e) {
ModdedIrisLog.debug("Iris legacy packs directory check failed", e);
}
}
/**
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
@@ -140,8 +112,7 @@ public final class ModdedStartup {
}
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
PackValidationResult result = validatePack(packDir);
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
@@ -184,16 +155,15 @@ public final class ModdedStartup {
throw new BrokenPackException(pack, List.of(
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
}
PackValidationResult cached = PackValidationRegistry.get(pack);
if (cached != null && cached.getValidatedAtMillis() >= newestModificationMillis(packDir.toPath())) {
// prepareForStartup already validated this pack and nothing in it changed since; re-validating per
// persistent dimension at boot costs a full pack parse each time.
return PackValidationRegistry.requireLoadable(pack);
}
Path packRoot = packDir.toPath();
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
return PackValidationRegistry.requireLoadable(pack);
PackValidationResult cached = PackValidationRegistry.getMatching(
packRoot, PackFingerprints.computePackTreeFingerprint(packDir));
if (cached != null) {
return PackValidationRegistry.requireLoadable(packRoot);
}
validatePack(packDir);
return PackValidationRegistry.requireLoadable(packRoot);
} catch (BrokenPackException e) {
throw e;
} catch (Throwable e) {
@@ -212,6 +182,42 @@ public final class ModdedStartup {
}
}
public static PackValidationResult validatePack(File packDir) {
Path packRoot = packDir.toPath();
PackValidationRegistry.ValidationTicket ticket = PackValidationRegistry.tryBeginValidation(packRoot);
if (ticket == null) {
throw new BrokenPackException(packDir.getName(), List.of("Pack content is currently being changed."));
}
String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packDir);
String contextFingerprint = PackValidationCache.contextFingerprint();
PackValidationResult result;
try {
result = PackValidator.validate(packDir);
} catch (Throwable failure) {
ModdedIrisLog.error("Iris pack validation failed for '{}'", packDir.getName(), failure);
String detail = failure.getMessage();
if (detail == null || detail.isBlank()) {
detail = failure.getClass().getSimpleName();
}
result = new PackValidationResult(
packDir.getName(),
List.of("Pack validation failed with " + failure.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis());
}
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packDir))
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
PackValidationRegistry.remove(packRoot);
PackValidationRegistry.remove(packDir.getName());
throw new BrokenPackException(packDir.getName(), List.of("Pack content or validation context changed during validation."));
}
if (!PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
throw new BrokenPackException(packDir.getName(), List.of("Pack content changed during validation."));
}
PackValidationRegistry.publish(result);
return result;
}
static void reinjectPersistentDimensions(NativeModdedServer server) {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
ModdedDimensionRegistryStore.loadForStartup(server);
@@ -266,23 +272,6 @@ public final class ModdedStartup {
+ GenerationRefusalNotice.summary(failure));
}
private static long newestModificationMillis(Path root) {
long newest = 0L;
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : (Iterable<Path>) walk::iterator) {
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
long modified = attributes.lastModifiedTime().toMillis();
if (modified > newest) {
newest = modified;
}
}
} catch (IOException | RuntimeException unreadable) {
ModdedIrisLog.debug("Iris could not stat {} for validation reuse; revalidating", root, unreadable);
return Long.MAX_VALUE;
}
return newest;
}
/**
* True once the first tick with a player list has re-injected the persistent dimensions. Before that a runtime
* dimension that is not loaded yet is still on its way.
@@ -296,7 +285,7 @@ public final class ModdedStartup {
* the console. Tell the operators who can actually act on it when they join.
*/
public static void warnStartupFailuresTo(NativeProtocolPlayer player) {
if (player == null || !player.isGameMaster()) {
if (player == null || (!player.isGameMaster() && !player.isServerOwner())) {
return;
}
for (String failure : DIMENSION_FAILURES) {
@@ -33,6 +33,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.service.ModdedPackUpdateService;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
import art.arcane.iris.modded.ModdedScheduler;
@@ -445,6 +446,12 @@ public final class IrisModdedCommands {
);
String completionMessage = downloadCompletionMessage(result);
if (result != null) {
if (result.changed()) {
ModdedPackUpdateService updates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
if (updates != null) {
dispatchDownloadFeedback(source, updates::refresh);
}
}
if (completionMessage != null) {
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
}
@@ -18,6 +18,8 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.modded.ModdedStartup;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.pack.validation.PackCompatReport;
import art.arcane.iris.spi.IrisPlatforms;
@@ -25,7 +27,6 @@ import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackResourceCleanup;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@@ -138,8 +139,7 @@ public final class ModdedPackCommands {
int broken = 0;
for (File target : targets) {
try {
PackValidationResult result = PackValidator.validate(target);
PackValidationRegistry.publish(result);
PackValidationResult result = ModdedStartup.validatePack(target);
if (!result.isLoadable()) {
broken++;
}
@@ -0,0 +1,137 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner;
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
import art.arcane.iris.pack.BuiltInPackUpdates;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
public final class ModdedPackUpdateService implements ModdedTickableService {
private final Function<File, CompletableFuture<PackCheck>> checker;
private final Set<UUID> notified = new HashSet<>();
private CompletableFuture<PackCheck> pending;
private List<String> notices = List.of();
private File packsFolder;
private boolean enabled;
private boolean started;
public ModdedPackUpdateService() {
this(ModdedPackUpdateService::checkPacks);
}
ModdedPackUpdateService(Function<File, CompletableFuture<PackCheck>> checker) {
this.checker = checker;
}
@Override
public void onEnable() {
enabled = true;
}
@Override
public void onDisable() {
enabled = false;
if (pending != null) {
pending.cancel(true);
pending = null;
}
started = false;
notices = List.of();
notified.clear();
packsFolder = null;
}
@Override
public void onServerTick(NativeModdedServer server) {
if (!enabled || server == null || !server.hasPlayerList()) {
return;
}
if (!started) {
started = true;
packsFolder = IrisPlatforms.get().packsFolderNoCreate().getAbsoluteFile();
pending = checker.apply(packsFolder);
}
if (pending == null || !pending.isDone()) {
return;
}
CompletableFuture<PackCheck> completed = pending;
pending = null;
try {
publish(completed.join());
server.forEachPlayer(this::notifyPlayer);
} catch (CompletionException error) {
IrisLogging.reportError("Iris pack update check failed", error);
}
}
public void refresh() {
if (!enabled) {
return;
}
if (pending != null) {
pending.cancel(true);
pending = null;
}
started = false;
notices = List.of();
notified.clear();
}
public void notifyPlayer(NativeProtocolPlayer player) {
if (!enabled || notices.isEmpty() || player == null || !player.connected()
|| (!player.isGameMaster() && !player.isServerOwner()) || !notified.add(player.id())) {
return;
}
for (String notice : notices) {
player.sendMessage(notice);
}
}
private static CompletableFuture<PackCheck> checkPacks(File folder) {
return CompletableFuture.supplyAsync(() -> IrisSplashPackScanner.collect(folder, IrisLogging::reportError))
.thenCompose(packs -> BuiltInPackUpdates.check(packs.stream().map(SplashPackMetadata::name).toList())
.thenApply(updates -> new PackCheck(packs, updates)));
}
private void publish(PackCheck result) {
List<String> updates = new ArrayList<>();
IrisLogging.info("Iris packs: " + packsFolder);
for (SplashPackMetadata pack : result.packs()) {
BuiltInPackUpdates.Update update = result.updates().get(pack.name());
String version = pack.version().matches("[0-9]+") ? "v" + pack.version() : "version unknown";
IrisLogging.info(" " + pack.name() + " " + version
+ (update == null ? "" : update.suffix(pack.version())));
if (update != null && update.newerThan(pack.version())) {
updates.add("Iris " + pack.name() + " release metadata is outdated: installed v" + pack.version()
+ ", latest built-in release v" + update.version() + ".");
updates.add("Back up and review custom edits in " + new File(packsFolder, pack.name())
+ "; close Studio, then update with /iris download pack=" + pack.name() + " overwrite=true.");
updates.add("Existing worlds retain their recorded production pack; this updates the authoring pack for new worlds.");
}
}
notices = List.copyOf(updates);
for (String notice : notices) {
IrisLogging.warn(notice);
}
}
record PackCheck(List<SplashPackMetadata> packs, Map<String, BuiltInPackUpdates.Update> updates) {
PackCheck {
packs = List.copyOf(packs);
updates = Map.copyOf(updates);
}
}
}
@@ -57,6 +57,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
@@ -64,6 +65,8 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
private static final long POLL_MILLIS = 250L;
private static final long CHECK_LATCH_MILLIS = 1_000L;
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
private volatile ExecutorService executor;
@@ -81,8 +84,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
@Override
public void onEnable() {
if (executor != null) {
return;
ExecutorService current = executor;
if (current != null && !current.isTerminated()) {
if (!current.isShutdown()) {
return;
}
throw new IllegalStateException("Iris Studio hotload cannot restart while prior workers are still active");
}
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
Thread thread = new Thread(task, "Iris Studio Hotload");
@@ -96,10 +103,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
@Override
public void onDisable() {
ExecutorService active = executor;
executor = null;
if (active != null) {
active.shutdownNow();
if (!shutdownAndDrain(active)) {
throw new IllegalStateException("Iris Studio hotload worker remained active during service shutdown");
}
executor = null;
for (Watch watch : watches.values()) {
watch.close();
}
@@ -197,7 +204,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
@Override
public void onServerTick(NativeModdedServer server) {
ExecutorService active = executor;
if (active == null) {
if (active == null || active.isShutdown()) {
return;
}
long now = System.currentTimeMillis();
@@ -284,6 +291,30 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
&& !isPregeneratorActive(engine);
}
private static boolean shutdownAndDrain(ExecutorService active) {
if (active == null) {
return true;
}
active.shutdown();
try {
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
return true;
}
active.shutdownNow();
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
return true;
}
ModdedIrisLog.error("Iris Studio hotload worker did not stop after shutdownNow",
new IllegalStateException("Iris Studio hotload worker is still active"));
return false;
} catch (InterruptedException exception) {
active.shutdownNow();
Thread.currentThread().interrupt();
ModdedIrisLog.error("Interrupted while draining Iris Studio hotload", exception);
return false;
}
}
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
return true;
@@ -39,6 +39,37 @@ public class ModdedClientPackageIsolationTest {
"art/arcane/iris/nativegen");
private static final int MINIMUM_SCANNED = 50;
@Test
public void clientPresentationCannotAccessPackOrWorldPersistence() throws IOException, URISyntaxException {
Path root = classesRoot();
Path directory = root.resolve(CLIENT_IRIS);
List<String> forbidden = List.of(
"art/arcane/iris/pack/",
"art/arcane/iris/world/storage/",
"art/arcane/iris/world/history/",
"art/arcane/iris/generation/runtime/",
"art/arcane/iris/configuration/",
"java/nio/file/",
"java/nio/channels/FileChannel",
"java/io/File",
"java/io/RandomAccessFile");
List<String> violations = new ArrayList<>();
int scanned = 0;
try (Stream<Path> walk = Files.walk(directory)) {
for (Path classFile : walk.filter(ModdedClientPackageIsolationTest::isClassFile).toList()) {
scanned++;
String bytecode = readAsLatin1(classFile);
for (String dependency : forbidden) {
if (bytecode.contains(dependency)) {
violations.add(root.relativize(classFile) + " -> " + dependency);
}
}
}
}
assertTrue("client presentation classes were not scanned", scanned >= 20);
assertEquals("client presentation can access persistent generation data: " + violations, List.of(), violations);
}
@Test
public void serverSidePackagesNeverReferenceClientOnlyTypes() throws IOException, URISyntaxException {
Path root = classesRoot();
@@ -0,0 +1,44 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockFaces;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
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 ModdedDecoratorHooksTest {
@Test
public void faceFixingExcludesTheUpperBoundaryWhileKeepingTheLastRowAccessible() {
NativeBlockState state = mock(NativeBlockState.class);
NativeBlockState lastRow = mock(NativeBlockState.class);
Hunk<NativeBlockState> hunk = mock(Hunk.class);
when(hunk.getHeight()).thenReturn(4);
when(hunk.get(1, 3, 2)).thenReturn(lastRow);
when(hunk.get(1, 4, 2)).thenThrow(new IndexOutOfBoundsException("upper boundary"));
try (MockedStatic<NativeBlockFaces> faces = mockStatic(NativeBlockFaces.class)) {
faces.when(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)))
.thenAnswer(invocation -> {
NativeBlockFaces.Neighbors neighbors = invocation.getArgument(1);
assertNull(neighbors.secondary(0, 1, 0));
assertSame(lastRow, neighbors.secondary(0, 0, 0));
return state;
});
assertSame(state, new ModdedDecoratorHooks().fixFaces(state, hunk, 1, 2, 99, 3, 108, null));
faces.verify(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)));
}
verify(hunk, never()).get(1, 4, 2);
verify(hunk).get(1, 3, 2);
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
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.junit.Assert.fail;
public class ModdedGenPoolTest {
@Test
public void executorLookupDoesNotRestartStoppedPool() throws Exception {
ModdedGenPool.start();
ExecutorService original = ModdedGenPool.pool();
ModdedGenPool.shutdown();
try {
ModdedGenPool.pool();
fail("Stopped generation pool must reject executor lookup");
} catch (RejectedExecutionException expected) {
assertTrue(original.isShutdown());
}
ModdedGenPool.start();
try {
assertNotSame(original, ModdedGenPool.pool());
} finally {
ModdedGenPool.shutdown();
}
}
@Test
public void restartWaitsForPreviousWorkersToTerminate() throws Exception {
ModdedGenPool.start();
ExecutorService original = ModdedGenPool.pool();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
original.execute(() -> {
entered.countDown();
boolean released = false;
while (!released) {
try {
release.await();
released = true;
} catch (InterruptedException ignored) {
Thread.interrupted();
}
}
});
assertTrue(entered.await(5L, TimeUnit.SECONDS));
try {
Thread.currentThread().interrupt();
ModdedGenPool.shutdown();
assertTrue(Thread.interrupted());
assertFalse(original.isTerminated());
try {
ModdedGenPool.start();
fail("Generation pool must retain workers until they terminate");
} catch (IllegalStateException expected) {
assertFalse(original.isTerminated());
}
} finally {
Thread.interrupted();
release.countDown();
assertTrue(original.awaitTermination(5L, TimeUnit.SECONDS));
ModdedGenPool.start();
assertNotSame(original, ModdedGenPool.pool());
ExecutorService restarted = ModdedGenPool.pool();
ModdedGenPool.start();
assertSame(restarted, ModdedGenPool.pool());
ModdedGenPool.shutdown();
}
}
}
@@ -0,0 +1,143 @@
package art.arcane.iris.modded;
import art.arcane.iris.modded.command.ModdedPackCommands;
import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mockStatic;
public class ModdedPackValidationPublicationTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@After
public void clearValidation() {
PackValidationRegistry.clear();
}
@Test
public void validationPublishesTheCanonicalRootAndPackName() throws Exception {
File root = temporaryFolder.newFolder("terrain");
File equivalent = new File(root, ".");
PackValidationResult result = loadable(root);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(equivalent)).thenReturn(result);
assertSame(result, ModdedStartup.validatePack(equivalent));
assertSame(result, PackValidationRegistry.requireLoadable(root.toPath()));
assertSame(result, PackValidationRegistry.requireLoadable(root.getName()));
}
}
@Test
public void rootInvalidationCannotReusePackNameValidation() throws Exception {
File root = temporaryFolder.newFolder("terrain");
PackValidationRegistry.publish(loadable(root));
PackValidationResult replacement = loadable(root);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
MockedStatic<ModdedPackCommands> commands = mockStatic(ModdedPackCommands.class)) {
commands.when(ModdedPackCommands::packsRoot).thenReturn(root.getParentFile());
validator.when(() -> PackValidator.validate(root)).thenReturn(replacement);
assertSame(replacement, ModdedStartup.requirePackForWorldCreation(root.getName()));
validator.verify(() -> PackValidator.validate(root));
assertSame(replacement, PackValidationRegistry.requireLoadable(root.toPath()));
}
}
@Test
public void changedContentWithPreservedTimestampCannotReuseValidation() throws Exception {
File root = temporaryFolder.newFolder("terrain");
File content = new File(root, "dimension.json");
Files.writeString(content.toPath(), "valid");
PackValidationResult original = loadable(root);
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(),
System.currentTimeMillis());
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
MockedStatic<ModdedPackCommands> commands = mockStatic(ModdedPackCommands.class)) {
commands.when(ModdedPackCommands::packsRoot).thenReturn(root.getParentFile());
validator.when(() -> PackValidator.validate(root)).thenReturn(original, broken);
ModdedStartup.validatePack(root);
Files.writeString(content.toPath(), "wrong");
Files.setLastModifiedTime(content.toPath(), FileTime.fromMillis(1L));
Files.setLastModifiedTime(root.toPath(), FileTime.fromMillis(1L));
assertThrows(BrokenPackException.class,
() -> ModdedStartup.requirePackForWorldCreation(root.getName()));
}
}
@Test
public void externalEditDuringValidationCannotPublishMixedSnapshot() throws Exception {
File root = temporaryFolder.newFolder("terrain");
File content = new File(root, "dimension.json");
Files.writeString(content.toPath(), "original");
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
Files.writeString(content.toPath(), "changed");
return loadable(root);
});
assertThrows(BrokenPackException.class, () -> ModdedStartup.validatePack(root));
assertNull(PackValidationRegistry.get(root.toPath()));
}
}
@Test
public void mutationDuringValidationPreventsStalePublication() throws Exception {
File root = temporaryFolder.newFolder("terrain");
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(root.toPath())) {
return loadable(root);
}
});
try {
ModdedStartup.validatePack(root);
fail("Changed pack must reject stale validation");
} catch (BrokenPackException expected) {
assertNull(PackValidationRegistry.get(root.toPath()));
assertNull(PackValidationRegistry.get(root.getName()));
}
}
}
@Test
public void validationFailurePublishesBlockedRoot() throws Exception {
File root = temporaryFolder.newFolder("terrain");
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
validator.when(() -> PackValidator.validate(root)).thenThrow(new IllegalStateException("Invalid terrain"));
PackValidationResult result = ModdedStartup.validatePack(root);
assertFalse(result.isLoadable());
assertSame(result, PackValidationRegistry.get(root.toPath()));
assertSame(result, PackValidationRegistry.get(root.getName()));
}
}
private static PackValidationResult loadable(File root) {
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
}
}
@@ -4,7 +4,10 @@ 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 net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.invocation.InvocationOnMock;
@@ -38,6 +41,12 @@ public class ModdedPrimaryWorldRouterTest {
private static final int TICK_INTERVAL = 20;
private static final String PRIMARY = "iris:primary";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@After
public void clearRouter() {
ModdedPrimaryWorldRouter.clear();
@@ -233,6 +242,41 @@ public class ModdedPrimaryWorldRouterTest {
return new ModdedDimensionRegistryStore.PersistentDimension(PRIMARY, "overworld", "overworld", 1337L);
}
@Test
public void disconnectCancelsOldRoutingWithoutAffectingReconnectedPlayer() {
NativeModdedServer server = mock(NativeModdedServer.class);
NativeWorld overworld = mock(NativeWorld.class);
NativeWorld primary = mock(NativeWorld.class);
when(overworld.nativeHandle()).thenReturn(new Object());
when(primary.nativeHandle()).thenReturn(new Object());
when(server.overworld()).thenReturn(overworld);
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
UUID id = UUID.randomUUID();
when(player.id()).thenReturn(id);
when(player.isInWorld(overworld)).thenReturn(true);
onlinePlayers(server, player);
CompletableFuture<Boolean> oldRoute = new CompletableFuture<>();
CompletableFuture<Boolean> newRoute = new CompletableFuture<>();
ModdedModConfig config = config(true, PRIMARY);
try (MockedStatic<ModdedModConfig> configs = mockStatic(ModdedModConfig.class);
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
MockedStatic<NativeWorldTeleport> teleport = mockStatic(NativeWorldTeleport.class)) {
configs.when(ModdedModConfig::get).thenReturn(config);
manager.when(() -> ModdedDimensionManager.level(server, PRIMARY)).thenReturn(primary);
teleport.when(() -> NativeWorldTeleport.teleport(any(), any())).thenReturn(oldRoute, newRoute);
evaluate(server);
ModdedPrimaryWorldRouter.forget(id);
assertTrue(oldRoute.isCancelled());
evaluate(server);
oldRoute.complete(true);
evaluate(server);
teleport.verify(() -> NativeWorldTeleport.teleport(any(), any()), times(2));
assertFalse(newRoute.isDone());
ModdedPrimaryWorldRouter.clear();
assertTrue(newRoute.isCancelled());
}
}
private static void evaluate(NativeModdedServer server) {
for (int tick = 0; tick < TICK_INTERVAL; tick++) {
ModdedPrimaryWorldRouter.tick(server);
@@ -57,6 +57,11 @@ public class ModdedStartupReinjectFailureTest {
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
ModdedStartup.warnStartupFailuresTo(operator);
ModdedStartup.warnStartupFailuresTo(player);
NativeProtocolPlayer owner = mock(NativeProtocolPlayer.class);
when(owner.isServerOwner()).thenReturn(true);
ModdedStartup.warnStartupFailuresTo(owner);
verify(owner).sendMessage(argThat((String message) -> message.contains("iris:moon")
&& message.contains("generation history is unusable")));
verify(operator).sendMessage(argThat((String message) -> message.contains("iris:moon")
&& message.contains("generation history is unusable")));
@@ -21,6 +21,8 @@ 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 net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -98,6 +100,7 @@ public class IrisModdedCommandsTeleportTest {
when(server.getLevel(dimension)).thenReturn(serverLevel);
ServerPlayer serverPlayer = mock(ServerPlayer.class);
when(serverPlayer.getUUID()).thenReturn(UUID.randomUUID());
when(serverPlayer.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 1.8F));
NativeWorld level = mock(NativeWorld.class);
when(level.nativeHandle()).thenReturn(serverLevel);
@@ -0,0 +1,209 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
import art.arcane.iris.pack.BuiltInPackUpdates;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.LevelBasedPermissionSet;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedPackUpdateServiceTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void pendingCheckNotifiesOnlineAndLaterOperatorsOnceOnServerTick() {
CompletableFuture<ModdedPackUpdateService.PackCheck> pending = new CompletableFuture<>();
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> pending);
NativeProtocolPlayer online = player(true, false);
NativeModdedServer server = server(online);
File root = new File("config/irisworldgen/packs").getAbsoluteFile();
try (MockedStatic<IrisPlatforms> platforms = platform(root)) {
service.onEnable();
service.onServerTick(server);
service.notifyPlayer(online);
verify(online, never()).sendMessage(anyString());
pending.complete(outdated());
verify(online, never()).sendMessage(anyString());
service.onServerTick(server);
service.onServerTick(server);
service.notifyPlayer(online);
verify(online, times(1)).sendMessage(contains("installed v1, latest built-in release v2"));
verify(online, times(1)).sendMessage(contains(new File(root, "overworld").toString()));
verify(online, times(1)).sendMessage(contains("/iris download pack=overworld overwrite=true"));
verify(online, never()).sendMessage(contains("plugins/"));
NativeProtocolPlayer later = player(true, false);
service.notifyPlayer(later);
verify(later).sendMessage(contains("release metadata is outdated"));
service.onDisable();
}
}
@Test
public void integratedOwnerWithoutCheatsReceivesNoticeAndRemoteOrdinaryPlayersDoNot() {
NativeProtocolPlayer owner = player(false, true);
NativeProtocolPlayer remote = player(false, false);
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> CompletableFuture.completedFuture(outdated()));
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
service.onEnable();
service.onServerTick(server(owner));
verify(owner).sendMessage(contains("release metadata is outdated"));
service.notifyPlayer(remote);
verify(remote, never()).sendMessage(anyString());
service.onDisable();
}
}
@Test
public void disableCancelsCheckAndRestartDoesNotPublishOldNotices() {
CompletableFuture<ModdedPackUpdateService.PackCheck> first = new CompletableFuture<>();
CompletableFuture<ModdedPackUpdateService.PackCheck> second = new CompletableFuture<>();
AtomicInteger checks = new AtomicInteger();
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> checks.getAndIncrement() == 0 ? first : second);
NativeProtocolPlayer operator = player(true, false);
NativeModdedServer server = server(operator);
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
service.onEnable();
service.onServerTick(server);
service.onDisable();
assertTrue(first.isCancelled());
assertFalse(first.complete(outdated()));
service.onEnable();
service.onServerTick(server);
second.complete(new ModdedPackUpdateService.PackCheck(List.of(), Map.of()));
service.onServerTick(server);
verify(operator, never()).sendMessage(anyString());
assertEquals(2, checks.get());
service.onDisable();
}
}
@Test
public void currentCustomUnknownAndUnavailableVersionsNeverBecomeOutdated() {
ModdedPackUpdateService.PackCheck result = new ModdedPackUpdateService.PackCheck(List.of(
new SplashPackMetadata("overworld", "unknown"),
new SplashPackMetadata("underworld", "3"),
new SplashPackMetadata("custom", "1")), Map.of(
"overworld", new BuiltInPackUpdates.Update("2"),
"underworld", new BuiltInPackUpdates.Update("2")));
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> CompletableFuture.completedFuture(result));
NativeProtocolPlayer operator = player(true, false);
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
service.onEnable();
service.onServerTick(server(operator));
verify(operator, never()).sendMessage(anyString());
service.onDisable();
}
}
@Test
public void successfulAuthoringUpdateClearsOldNoticesAndRescansInstalledVersion() {
CompletableFuture<ModdedPackUpdateService.PackCheck> current = new CompletableFuture<>();
AtomicInteger checks = new AtomicInteger();
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> checks.getAndIncrement() == 0
? CompletableFuture.completedFuture(outdated()) : current);
NativeProtocolPlayer operator = player(true, false);
NativeModdedServer server = server(operator);
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
service.onEnable();
service.onServerTick(server);
verify(operator).sendMessage(contains("release metadata is outdated"));
service.refresh();
NativeProtocolPlayer later = player(true, false);
service.notifyPlayer(later);
verify(later, never()).sendMessage(anyString());
service.onServerTick(server);
current.complete(new ModdedPackUpdateService.PackCheck(List.of(new SplashPackMetadata("overworld", "2")),
Map.of("overworld", new BuiltInPackUpdates.Update("2"))));
service.onServerTick(server);
service.notifyPlayer(later);
verify(later, never()).sendMessage(anyString());
assertEquals(2, checks.get());
service.onDisable();
}
}
@Test
public void nativeOwnerCheckDoesNotGrantGameMasterPermissions() {
ServerPlayer handle = mock(ServerPlayer.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
when(handle.level()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(handle.permissions()).thenReturn(LevelBasedPermissionSet.ALL);
NativeProtocolPlayer player = NativeProtocolPlayer.fromHandle(handle);
when(server.isSingleplayerOwner(handle.nameAndId())).thenReturn(true);
assertTrue(player.isServerOwner());
assertFalse(player.isGameMaster());
when(server.isSingleplayerOwner(handle.nameAndId())).thenReturn(false);
assertFalse(player.isServerOwner());
assertFalse(player.isGameMaster());
}
private static ModdedPackUpdateService.PackCheck outdated() {
return new ModdedPackUpdateService.PackCheck(List.of(new SplashPackMetadata("overworld", "1")),
Map.of("overworld", new BuiltInPackUpdates.Update("2")));
}
private static MockedStatic<IrisPlatforms> platform(File root) {
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.packsFolderNoCreate()).thenReturn(root);
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
platforms.when(IrisPlatforms::get).thenReturn(platform);
return platforms;
}
private static NativeProtocolPlayer player(boolean operator, boolean owner) {
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
when(player.id()).thenReturn(UUID.randomUUID());
when(player.connected()).thenReturn(true);
when(player.isGameMaster()).thenReturn(operator);
when(player.isServerOwner()).thenReturn(owner);
return player;
}
@SuppressWarnings("unchecked")
private static NativeModdedServer server(NativeProtocolPlayer player) {
NativeModdedServer server = mock(NativeModdedServer.class);
when(server.hasPlayerList()).thenReturn(true);
doAnswer(invocation -> {
invocation.getArgument(0, Consumer.class).accept(player);
return null;
}).when(server).forEachPlayer(any());
return server;
}
}
@@ -4,14 +4,65 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import org.mockito.InOrder;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedStudioHotloadServiceTest {
@Test
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
ExecutorService executor = mock(ExecutorService.class);
setExecutor(service, executor);
when(executor.awaitTermination(30L, TimeUnit.SECONDS)).thenReturn(false);
when(executor.awaitTermination(5L, TimeUnit.SECONDS)).thenReturn(true);
service.onDisable();
InOrder order = inOrder(executor);
order.verify(executor).shutdown();
order.verify(executor).awaitTermination(30L, TimeUnit.SECONDS);
order.verify(executor).shutdownNow();
order.verify(executor).awaitTermination(5L, TimeUnit.SECONDS);
service.onEnable();
service.onDisable();
}
@Test
public void undrainedWorkerPreventsRestart() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
ExecutorService executor = mock(ExecutorService.class);
setExecutor(service, executor);
when(executor.isShutdown()).thenReturn(true);
try {
service.onDisable();
fail("Hotload shutdown must report a worker that did not terminate");
} catch (IllegalStateException expected) {
verify(executor).shutdownNow();
}
try {
service.onEnable();
fail("Hotload must not start another worker before the previous worker terminates");
} catch (IllegalStateException expected) {
assertFalse(executor.isTerminated());
}
when(executor.isTerminated()).thenReturn(true);
service.onEnable();
service.onDisable();
}
@Test
public void detectsDatapackImportsAcrossLoadedDimensions() {
IrisDimension empty = new IrisDimension();
@@ -45,4 +96,10 @@ public class ModdedStudioHotloadServiceTest {
scheduler.shutdown();
}
}
private static void setExecutor(ModdedStudioHotloadService service, ExecutorService executor) throws Exception {
Field field = ModdedStudioHotloadService.class.getDeclaredField("executor");
field.setAccessible(true);
field.set(service, executor);
}
}
@@ -0,0 +1,142 @@
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
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.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NativeTeleportSafetyTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void rejectsTallAndHazardousSupportsAndAcceptsSolidFloor() {
ServerPlayer player = player();
ServerLevel level = level();
for (BlockState support : new BlockState[]{Blocks.OAK_FENCE.defaultBlockState(),
Blocks.MAGMA_BLOCK.defaultBlockState(), Blocks.CACTUS.defaultBlockState()}) {
setSupport(level, support);
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
}
setSupport(level, Blocks.STONE.defaultBlockState());
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
}
@Test
public void fractionalPositionUsesWholePlayerBodyAndRequiresNeighborChunk() {
ServerPlayer player = player();
ServerLevel level = level();
setSupport(level, Blocks.STONE.defaultBlockState());
when(level.noCollision(any(), any(AABB.class))).thenAnswer(invocation -> {
AABB body = invocation.getArgument(1);
return body.maxX <= 16D;
});
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.5D, 4, 8.5D));
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.9D, 4, 8.5D));
when(level.noCollision(any(), any(AABB.class))).thenReturn(true);
when(level.getChunkSource().hasChunk(1, 0)).thenReturn(false);
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.9D, 4, 8.5D));
EntityDimensions dimensions = player.getDimensions(Pose.STANDING);
assertEquals(0, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(15.5D, 4D, 8.5D), new ChunkPos(0, 0)));
assertEquals(1, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(15.9D, 4D, 8.5D), new ChunkPos(0, 0)));
assertEquals(1, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(-16.1D, 4D, -8.5D), new ChunkPos(-2, -1)));
}
@Test
public void scaledStandingBodyStaysInsideActualWorldHeight() {
ServerLevel level = level();
setSupport(level, Blocks.STONE.defaultBlockState());
ServerPlayer player = player();
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 13F));
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 12F));
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 0, 8.5D));
}
@Test
public void reconnectDuringChunkWarmupDoesNotTeleportReplacementSession() {
MinecraftServer server = mock(MinecraftServer.class, RETURNS_DEEP_STUBS);
when(server.isSameThread()).thenReturn(true);
ServerLevel level = level();
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, Identifier.parse("iris:test"));
when(level.getServer()).thenReturn(server);
when(level.dimension()).thenReturn(key);
when(server.getLevel(key)).thenReturn(level);
ServerChunkCache chunks = level.getChunkSource();
when(chunks.hasChunk(anyInt(), anyInt())).thenReturn(false);
CompletableFuture<Void> loaded = new CompletableFuture<>();
doReturn(loaded).when(chunks).addTicketAndLoadWithRadius(any(), any(), anyInt());
ServerPlayer original = player();
UUID id = UUID.randomUUID();
when(original.getUUID()).thenReturn(id);
when(server.getPlayerList().getPlayer(id)).thenReturn(original);
NativeWorld world = mock(NativeWorld.class);
when(world.nativeHandle()).thenReturn(level);
CompletableFuture<Boolean> result = NativeWorldTeleport.teleport(NativeProtocolPlayer.fromHandle(original),
new NativeWorldTeleport.Destination(NativeModdedServer.fromHandle(server), world, 8.5D, 4D, 8.5D, 0L));
assertFalse(result.isDone());
ServerPlayer replacement = player();
when(server.getPlayerList().getPlayer(id)).thenReturn(replacement);
loaded.complete(null);
assertFalse(result.join());
verify(chunks).removeTicketWithRadius(any(), any(), anyInt());
}
private ServerPlayer player() {
ServerPlayer player = mock(ServerPlayer.class);
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 1.8F));
return player;
}
private ServerLevel level() {
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(level.getChunkSource()).thenReturn(chunks);
when(level.getMinY()).thenReturn(0);
when(level.getMaxY()).thenReturn(16);
when(chunks.hasChunk(anyInt(), anyInt())).thenReturn(true);
when(level.noCollision(any(), any(AABB.class))).thenReturn(true);
return level;
}
private void setSupport(ServerLevel level, BlockState support) {
when(level.getBlockState(any(BlockPos.class))).thenAnswer(invocation -> {
BlockPos position = invocation.getArgument(0);
return position.getY() == 3 ? support : Blocks.AIR.defaultBlockState();
});
}
}