This commit is contained in:
Brian Neumann-Fopiano
2026-08-13 12:38:08 -04:00
parent 14b6280668
commit 1586b4bd1a
68 changed files with 733 additions and 211 deletions
@@ -236,6 +236,19 @@ public final class ModdedPlatform implements IrisPlatform {
*/
@Override
public void reportError(Throwable error) {
reportThrottled("Iris reported error", error);
}
/**
* Contextual reports run the same throttle and emit exactly one trace through the modded
* log; the SPI default's raw stderr copy would bypass the per-signature suppression.
*/
@Override
public void reportError(String context, Throwable error) {
reportThrottled(context == null || context.isBlank() ? "Iris reported error" : context, error);
}
private void reportThrottled(String message, Throwable error) {
if (error == null) {
return;
}
@@ -247,7 +260,7 @@ public final class ModdedPlatform implements IrisPlatform {
sink.accept(error);
return;
}
ModdedIrisLog.error("Iris reported error", error);
ModdedIrisLog.error(message, error);
Consumer<Throwable> capture = CAPTURE_SINK;
if (capture != null) {
try {
@@ -800,6 +800,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
if (!mantleWarmupExecutorStopped) {
try {
// Drain, don't interrupt: an interrupt inside a FileChannel plate read closes
// the channel and the read-failure fallback installs an empty plate that a
// later flush persists over real data. Queued tasks no-op on the closed flag,
// so the await only covers a single in-flight load; escalate on timeout only.
mantleWarmupExecutor.shutdown();
if (!mantleWarmupExecutor.awaitTermination(5L, java.util.concurrent.TimeUnit.SECONDS)) {
IrisLogging.warn("Iris mantle warm-up did not stop before world manager close; forcing interrupt");
mantleWarmupExecutor.shutdownNow();
}
mantleWarmupExecutorStopped = true;
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
mantleWarmupExecutor.shutdownNow();
mantleWarmupExecutorStopped = true;
} catch (Throwable e) {
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.api;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.command.ModdedPregenJob;
import net.minecraft.server.level.ServerLevel;
@@ -130,14 +131,17 @@ public final class IrisModdedAPI {
* never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's
* height range reads as null rather than throwing.
*
* @throws IllegalStateException if the engine's mantle has already been closed
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
*/
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
return null;
}
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
// Lease-gated so the engine shutdown drain covers this public accessor; a mantle
// mid-close refuses the lease instead of racing the region flush.
return EngineLifecycleTasks.call(engine, "modded_api_mantle_get",
() -> engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type), null);
}
/**
@@ -151,28 +155,30 @@ public final class IrisModdedAPI {
* Values written under a custom type are discarded when Iris trims a mantle region unless the type is
* declared with {@link #retainMantleDataForSlice(Class)}.
*
* @throws IllegalStateException if the engine's mantle has already been closed
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
*/
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
Engine engine = getEngine(level);
if (engine == null || data == null) {
return;
}
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
EngineLifecycleTasks.run(engine, "modded_api_mantle_set",
() -> engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data));
}
/**
* Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range
* {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent.
*
* @throws IllegalStateException if the engine's mantle has already been closed
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
*/
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
return;
}
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
EngineLifecycleTasks.run(engine, "modded_api_mantle_delete",
() -> engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type));
}
/**
@@ -69,6 +69,9 @@ final class ModdedCommandSuggestions {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int TAB_FAILURE_KEYS_MAX = 256;
private static final Set<String> REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet();
private static final long PACK_NAME_CACHE_TTL_MS = 3_000L;
private static volatile Set<String> cachedPackNames;
private static volatile long cachedPackNamesAt;
private ModdedCommandSuggestions() {
}
@@ -228,6 +231,13 @@ final class ModdedCommandSuggestions {
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
// Suggestion packets arrive per keystroke; a short-lived snapshot keeps the directory
// walk off the hot path without ever serving stale names for more than a few seconds.
long now = System.currentTimeMillis();
Set<String> cached = cachedPackNames;
if (cached != null && now - cachedPackNamesAt < PACK_NAME_CACHE_TTL_MS) {
return SharedSuggestionProvider.suggest(cached, builder);
}
Set<String> names = new TreeSet<>();
names.add("overworld");
try {
@@ -249,6 +259,8 @@ final class ModdedCommandSuggestions {
} catch (Throwable e) {
warnTabFailure("pack names", context.getSource(), e);
}
cachedPackNames = names;
cachedPackNamesAt = now;
return SharedSuggestionProvider.suggest(names, builder);
}
@@ -70,9 +70,10 @@ final class ModdedCommandTree {
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
// ModdedWhatCommands.tree() gates itself at LEVEL_GAMEMASTERS; the /iris what overlays are read-only,
// so the root builder relaxes the whole subtree here instead of forking that file.
root.then(ModdedWhatCommands.tree().requires(READ_ONLY));
// ModdedWhatCommands.tree() gates itself at LEVEL_GAMEMASTERS and keeps that gate:
// "what markers" drives lease-gated 9x9 mantle scans, and the Bukkit twin puts the
// whole tree behind an op-default permission. requires() would overwrite, not AND.
root.then(ModdedWhatCommands.tree());
root.then(teleportTree("teleport"));
root.then(teleportTree("tp"));
@@ -285,7 +285,20 @@ public final class ModdedObjectCommands {
return 0;
}
File file = new File(engine.getData().getDataFolder(), "objects" + File.separator + name.replace('/', File.separatorChar) + ".iob");
if (file.exists() && !overwrite) {
// Atomic path claim ON the server thread: the async write below turned a plain
// exists() check into a TOCTOU where two rapid saves interleaved into one file.
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
boolean claimed;
try {
claimed = file.createNewFile();
} catch (IOException e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
return 0;
}
if (!claimed && !overwrite) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FILE_ALREADY_EXISTS_USE_IRIS_OBJECT_SAVE_OVERWRITE, MessageArgument.untrusted("name", name)));
return 0;
}
@@ -295,15 +308,16 @@ public final class ModdedObjectCommands {
// async-safe), but the disk write of a local, unshared object is not tick work.
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
MinecraftServer server = source.getServer();
boolean finalClaimed = claimed;
J.a(() -> {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try {
object.write(file);
} catch (IOException e) {
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
if (finalClaimed) {
// Never leave a 0-byte claim file permanently blocking non-overwrite saves.
file.delete();
}
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))));
return;
}
@@ -85,13 +85,16 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
ExecutorService active = warmupExecutor;
warmupExecutor = null;
if (active != null) {
// Drop queued warm-ups (pure prefetch) and AWAIT: the very next shutdown stage
// closes every Mantle, and an in-flight mantle.getChunk would fault a plate back
// in after the close-time flush.
active.shutdownNow();
// NEVER interrupt first: an interrupt inside a FileChannel plate read closes the
// channel (ClosedByInterruptException) and the read-failure fallback installs an
// EMPTY plate that the very next shutdown stage flushes over real data. Queued
// warm-ups self-cancel (warmupExecutor is already null), so shutdown() + await
// only waits on the single in-flight load; escalate only on timeout.
active.shutdown();
try {
if (!active.awaitTermination(5L, TimeUnit.SECONDS)) {
IrisLogging.warn("Iris mantle warm-up did not stop before engine close");
IrisLogging.warn("Iris mantle warm-up did not stop before engine close; forcing interrupt");
active.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -279,6 +282,12 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
}
try {
active.execute(() -> {
// Self-cancel on shutdown: warmupExecutor is nulled first in onDisable, so
// queued prefetches no-op instantly and only an in-flight load is awaited.
if (warmupExecutor == null || mantle.isClosed()) {
warmupQueue.remove(key);
return;
}
try {
mantle.getChunk(chunkX, chunkZ);
} catch (Throwable e) {