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

@@ -79,6 +79,8 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.generation.concurrent.MultiBurst;
import java.util.ArrayDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedEngineBootstrap {
private static final String[] CORE_SELF_TEST_CLASSES = {
@@ -87,12 +89,17 @@ public final class ModdedEngineBootstrap {
"art.arcane.iris.pack.loading.IrisData"
};
private static final Object LOCK = new Object();
private static final Object STOP_LOCK = new Object();
private static final AtomicBoolean EXIT_FLUSH_REGISTERED = new AtomicBoolean();
private static final long EXIT_FLUSH_SERVER_WAIT_SECONDS = 30L;
private static final ModdedServiceManager UNBOUND_SERVICE_MANAGER = new ModdedServiceManager();
private static volatile NativeModdedLoader loader;
private static volatile BoundRuntime runtime;
private static volatile NativeModdedServer currentServer;
private static volatile NativeModdedServer spawnCaptureServer;
private static volatile boolean initialSpawnWasDefault;
private static volatile boolean stoppedPending;
private static volatile Thread stoppingThread;
private ModdedEngineBootstrap() {
}
@@ -119,10 +126,12 @@ public final class ModdedEngineBootstrap {
}
public static void start(NativeModdedServer server) {
finishPreviousServer("server start");
registerExitFlush();
captureInitialSpawn(server);
currentServer = server;
bind();
// Pair of the stop() burst-pools stage. Load-bearing on integrated servers: once
// Pair of the stopped() burst-pools stage. Load-bearing on integrated servers: once
// closed, MultiBurst falls back to a same-thread executor, so a second world load
// without reopen() would silently run every burst inline.
MultiBurst.burst.reopen();
@@ -150,6 +159,7 @@ public final class ModdedEngineBootstrap {
}
public static void serverAboutToStart(NativeModdedServer server) {
finishPreviousServer("server start");
captureInitialSpawn(server);
ModdedStartup.prepareForStartup();
}
@@ -180,20 +190,16 @@ public final class ModdedEngineBootstrap {
try {
generator.unbindEngine(world);
} catch (Throwable exception) {
// Never propagate into the loader's unload loop: a throw there skips closing every later level. A
// runtime dimension removal retries the eviction itself and fails with the retained mapping.
ModdedIrisLog.error("Iris engine unload failed for {}", world.name(), exception);
if (exception instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (exception instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris engine unload failed for "
+ world.name(), exception);
}
}
public static void stop() {
NativeModdedServer stoppingServer = currentServer;
stoppingThread = Thread.currentThread();
stoppedPending = true;
Throwable failure = null;
failure = runStopStage(failure, "pack downloads", IrisModdedCommands::shutdownDownloads);
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
@@ -205,6 +211,62 @@ public final class ModdedEngineBootstrap {
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
failure = runStopStage(failure, "services", () -> services().disableAll());
reportStopFailure("stopping", failure);
}
/**
* The loaders still drain queued chunk generation after server stopping and only close levels afterwards, so
* engines, their mantle and generation history, and the generation pools stay open until the server stopped.
* Engines of unloaded levels are already closed by {@link #levelUnloaded}; this closes whatever remains.
* Fabric fires it at the tail of stopServer, so a throw there skips it: the next server start or the JVM
* exit runs it instead.
*/
public static void stopped() {
synchronized (STOP_LOCK) {
stoppedPending = false;
stoppingThread = null;
closeStoppedServer();
}
}
private static void finishPreviousServer(String trigger) {
synchronized (STOP_LOCK) {
if (!stoppedPending) {
return;
}
ModdedIrisLog.warn("Iris closing the engines the previous server left open ({})", trigger);
stopped();
}
}
private static void registerExitFlush() {
if (EXIT_FLUSH_REGISTERED.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(new Thread(ModdedEngineBootstrap::flushAtExit, "Iris Exit Flush"));
}
}
private static void flushAtExit() {
if (!stoppedPending) {
return;
}
Thread serverThread = stoppingThread;
if (serverThread != null && serverThread != Thread.currentThread()) {
try {
serverThread.join(TimeUnit.SECONDS.toMillis(EXIT_FLUSH_SERVER_WAIT_SECONDS));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
if (serverThread.isAlive()) {
ModdedIrisLog.warn("Iris skipped its exit flush: the server thread is still stopping");
return;
}
}
finishPreviousServer("JVM exit");
}
private static void closeStoppedServer() {
Throwable failure = null;
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
failure = runStopStage(failure, "dimension manager", ModdedDimensionManager::clear);
@@ -226,10 +288,14 @@ public final class ModdedEngineBootstrap {
spawnCaptureServer = null;
initialSpawnWasDefault = false;
});
reportStopFailure("stopped", failure);
}
private static void reportStopFailure(String phase, Throwable failure) {
if (failure != null) {
// The shutdown path must not propagate: propagating aborts the remaining loader stop handlers and
// can leave the level unsaved. Every stage already logged its own failure.
ModdedIrisLog.error("Iris modded shutdown completed with failures", failure);
ModdedIrisLog.error("Iris modded shutdown ({}) completed with failures", phase, failure);
}
}
@@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.terrain.feature.NativeFeatureTable;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedImportedFeatures;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
@@ -163,6 +164,7 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
GenerationFailures.rethrowEngineFailure(error);
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
error.toString(), error);
markInert(runtimeIdentity, generation);
@@ -177,6 +179,7 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
built = nativeFeatures.buildTable(new ModdedImportedFeaturePolicy(engine), control, generation);
} catch (Throwable error) {
GenerationFailures.rethrowEngineFailure(error);
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
dimensionKey(engine), error.toString(), error);
markInert(runtimeIdentity, generation);
@@ -16,6 +16,7 @@ public final class ModdedLifecycleCallbacks {
ModdedEngineBootstrap::start,
ModdedEngineBootstrap::serverStarted,
ModdedEngineBootstrap::stop,
ModdedEngineBootstrap::stopped,
ModdedEngineBootstrap::levelLoaded,
ModdedEngineBootstrap::levelUnloaded,
ModdedProtocolHandler::onPlayerJoin,
@@ -40,29 +40,31 @@ import java.util.function.BooleanSupplier;
* references and is safe on a dedicated server.
*/
public final class ModdedMixinAudit {
private static final String ENTITY_CONFIG = "volmlib.entity.mixins.json";
private static final String CLIENT_CONFIG = "volmlib.client.mixins.json";
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
private static final List<ExpectedMixin> EXPECTED = List.of(
new ExpectedMixin("EntityPersistenceMixin", "entity",
static final List<ExpectedMixin> EXPECTED = List.of(
new ExpectedMixin(ENTITY_CONFIG, "EntityPersistenceMixin",
NativeMixinTarget.ENTITY, "iris$applyGeneratedPersistence",
false, NativeMixinFlags::entityPersistenceRan),
new ExpectedMixin("LivingEntityLootMixin", "entity",
new ExpectedMixin(ENTITY_CONFIG, "LivingEntityLootMixin",
NativeMixinTarget.LIVING_ENTITY, "iris$replaceBaseLoot",
false, NativeMixinFlags::livingEntityLootRan),
new ExpectedMixin("MobAwarenessMixin", "entity",
new ExpectedMixin(ENTITY_CONFIG, "MobAwarenessMixin",
NativeMixinTarget.MOB, "iris$tickUnawareMob",
false, NativeMixinFlags::mobAwarenessRan),
new ExpectedMixin("StructureTemplatePaletteConcurrencyMixin", "common",
new ExpectedMixin(ENTITY_CONFIG, "StructureTemplatePaletteConcurrencyMixin",
NativeMixinTarget.STRUCTURE_PALETTE,
"iris$installConcurrentBlockCache",
false, NativeMixinFlags::structureTemplatePaletteRan),
new ExpectedMixin("NativeWorldOpenFlowsMixin", "client",
new ExpectedMixin(CLIENT_CONFIG, "NativeWorldOpenFlowsMixin",
NativeMixinTarget.WORLD_OPEN_FLOWS,
"iris$openWorldCheckWorldStemCompatibility",
"volmlib$openWorldCheckWorldStemCompatibility",
true, NativeMixinFlags::worldOpenFlowsRan),
new ExpectedMixin("NativeWorldTypeEntryMixin", "client",
new ExpectedMixin(CLIENT_CONFIG, "NativeWorldTypeEntryMixin",
NativeMixinTarget.WORLD_TYPE_ENTRY,
"iris$describePreset",
"volmlib$describePreset",
true, NativeMixinFlags::worldTypeEntryRan));
private ModdedMixinAudit() {
@@ -90,7 +92,7 @@ public final class ModdedMixinAudit {
if (isApplied(expected)) {
applied.add(expected.mixinName() + (expected.ran().getAsBoolean() ? "" : " (not yet exercised)"));
} else {
missing.add(expected.config() + '/' + expected.mixinName() + " -> " + expected.targetClass());
missing.add(expected.config() + '/' + expected.mixinName() + " -> " + expected.targetClass().className());
}
}
if (missing.isEmpty()) {
@@ -119,7 +121,7 @@ public final class ModdedMixinAudit {
}
}
private record ExpectedMixin(String mixinName, String config, NativeMixinTarget targetClass, String handlerMethod,
boolean clientOnly, BooleanSupplier ran) {
record ExpectedMixin(String config, String mixinName, NativeMixinTarget targetClass, String handlerMethod,
boolean clientOnly, BooleanSupplier ran) {
}
}
@@ -195,7 +195,8 @@ public final class ModdedScheduler implements PlatformScheduler {
mainQueue.clear();
delayedQueue.clear();
mainThread = null;
// Shutdown stage that always runs (ModdedEngineBootstrap.stop): release the level snapshot with it.
// Runs from ModdedEngineBootstrap.stopped, or its fallback at the next server start or JVM exit when the
// loader skipped it: release the level snapshot with it.
ModdedServerLevels.forget();
}
@@ -426,9 +426,9 @@ public final class ModdedWorldEngines {
Engine engine = entry.getValue();
try {
// Latch the generator's unloading flag BEFORE closing (unbindEngine sets it,
// then evicts): chunk-system drain work running after this stage would
// otherwise see a closed engine and silently rebuild a fresh engine + Mantle
// that no teardown stage ever closes, writing plates after the final save.
// then evicts): late chunk work would otherwise see a closed engine and
// silently rebuild a fresh engine + Mantle that no teardown stage ever
// closes, writing plates after the final save.
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(level, IrisModdedChunkGenerator.class);
if (generator != null) {
generator.unbindEngine(level);
@@ -60,12 +60,16 @@ import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
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 +137,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(10L)));
System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_DEADLINE_SECONDS)));
teleport.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success) && failure == null) {
return;
@@ -142,14 +146,42 @@ public final class IrisModdedCommands {
ModdedIrisLog.error("Iris teleport into '{}' failed for {}",
dimensionId, player.id(), failure);
}
server.execute(() -> fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
MessageArgument.untrusted("dimensionId", dimensionId))));
server.execute(() -> fail(source, teleportFailure(dimensionId, player.name(), failure,
ModdedDimensionManager.level(server, dimensionId) != null, server.player(player.id()) != null)));
});
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.name()), MessageArgument.untrusted("dimensionId", dimensionId)));
return 1;
}
static String teleportFailure(String dimensionId, String playerName, Throwable failure,
boolean dimensionLoaded, boolean playerOnline) {
if (failure != null) {
return IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
MessageArgument.untrusted("dimensionId", dimensionId),
MessageArgument.untrusted("reason", failureReason(failure)));
}
if (!dimensionLoaded) {
return IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
MessageArgument.untrusted("dimensionId", dimensionId));
}
if (!playerOnline) {
return IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_CANCELLED_PLAYER_OFFLINE,
MessageArgument.untrusted("dimensionId", dimensionId),
MessageArgument.untrusted("value", playerName));
}
return IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_REFUSED,
MessageArgument.untrusted("dimensionId", dimensionId));
}
private static String failureReason(Throwable failure) {
Throwable cause = failure;
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
String message = cause.getMessage();
return message == null || message.isBlank() ? cause.getClass().getSimpleName() : message;
}
static int evacuate(NativeCommandSource source, NativeWorld target) {
NativeModdedServer server = source.server();
NativeWorld level = target != null ? target : source.world();
@@ -71,6 +71,18 @@ public final class ModdedCommandMessages {
"iris.modded.irismoddedcommands.teleport_failed_dimension_is_not_loaded",
"Teleport failed: dimension " + "{dimensionId}" + " is not loaded."
);
public static final TextKey IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON = TextKey.of(
"iris.modded.irismoddedcommands.teleport_failed_reason",
"Teleport into " + "{dimensionId}" + " failed: " + "{reason}"
);
public static final TextKey IRIS_MODDED_COMMANDS_TELEPORT_CANCELLED_PLAYER_OFFLINE = TextKey.of(
"iris.modded.irismoddedcommands.teleport_cancelled_player_offline",
"Teleport into " + "{dimensionId}" + " cancelled: " + "{value}" + " is no longer online."
);
public static final TextKey IRIS_MODDED_COMMANDS_TELEPORT_REFUSED = TextKey.of(
"iris.modded.irismoddedcommands.teleport_refused",
"Teleport into " + "{dimensionId}" + " was refused by the server."
);
public static final TextKey IRIS_MODDED_COMMANDS_TELEPORTING = TextKey.of(
"iris.modded.irismoddedcommands.teleporting",
"Teleporting " + "{value}" + " to " + "{dimensionId}" + "..."
@@ -1266,6 +1278,9 @@ public final class ModdedCommandMessages {
IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_PLAYER_IRIS_TP_DIMENSION_PLAYER,
IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS,
IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
IRIS_MODDED_COMMANDS_TELEPORT_CANCELLED_PLAYER_OFFLINE,
IRIS_MODDED_COMMANDS_TELEPORT_REFUSED,
IRIS_MODDED_COMMANDS_TELEPORTING,
IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_2,
IRIS_MODDED_COMMANDS_CANNOT_EVACUATE_PRIMARY_WORLD_THERE_IS_NOWHERE_SEND_PLAYERS,
@@ -30,6 +30,7 @@ import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockResolution;
@@ -38,6 +39,7 @@ import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -47,6 +49,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -149,7 +152,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
int centerZ = player.z() >> 4;
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
updateChunk(engine, level, centerX + dx, centerZ + dz);
updatePassChunk(engine, level, centerX + dx, centerZ + dz);
}
}
});
@@ -157,15 +160,29 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
private void updateForcedChunks(Engine engine, NativeWorld level) {
new NativeWorldMaintenance(level).forEachForcedChunk(chunkKey ->
updateChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
updatePassChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
}
public void updateRegeneratedChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
updateChunk(engine, level, chunkX, chunkZ, false);
}
private void updateChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
updateChunk(engine, level, chunkX, chunkZ, true);
/**
* One chunk of a level pass. A failed chunk keeps its flags and retries on a later pass without stopping the
* chunks after it; only a closed engine ends the level's pass.
*/
private void updatePassChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
try {
updateChunk(engine, level, chunkX, chunkZ, true);
} catch (Throwable failure) {
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
throw failure;
}
if (IrisLogging.errorOnce("chunk-update:" + level.name() + ":" + chunkX + "," + chunkZ + ":" + failure,
"Iris chunk update failed at " + chunkX + "," + chunkZ + " in " + level.name() + ", retrying each pass: " + failure)) {
IrisLogging.reportError(failure);
}
}
}
private void updateChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ, boolean requireNeighbors) {
@@ -195,6 +212,11 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> runCustomPass(engine, level, chunkX, chunkZ, chunk));
chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk));
});
} catch (SavedBiomeUnavailableException unavailable) {
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
IrisLogging.debug("Iris chunk update deferred at " + chunkX + "," + chunkZ + ": saved biomes loading");
} finally {
chunk.release();
}
@@ -315,15 +337,40 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
}
}
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
if (v != null && v.isUpdate()) {
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk);
}
});
IntArrayList completed = new IntArrayList();
try {
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
if (v != null && v.isUpdate()) {
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk);
completed.add((yf << 8) | ((x & 15) << 4) | (z & 15));
}
});
} catch (RuntimeException | Error failure) {
forgetCompletedUpdates(chunk, completed, failure);
throw failure;
}
chunk.deleteSlices(MatterUpdate.class);
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
}
private static void forgetCompletedUpdates(MantleChunk<Matter> chunk, IntArrayList completed, Throwable failure) {
try {
for (int index = 0; index < completed.size(); index++) {
int position = completed.getInt(index);
int y = position >>> 8;
Matter section = chunk.get(y >> 4);
if (section != null && section.hasSlice(MatterUpdate.class)) {
section.<MatterUpdate>getSlice(MatterUpdate.class)
.set((position >> 4) & 15, y & 15, position & 15, null);
}
}
} catch (RuntimeException | Error cleanupFailure) {
if (cleanupFailure != failure) {
failure.addSuppressed(cleanupFailure);
}
}
}
private void update(Engine engine, NativeWorld level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk) {
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
return;
@@ -337,7 +384,12 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
}
try {
ModdedLootApplier.apply(engine, level, pos, state, chunk);
} catch (SavedBiomeUnavailableException unavailable) {
GenerationFailures.rethrowEngineFailure(unavailable);
IrisLogging.warnOnce("chunk-update-loot:" + level.name() + ":" + unavailable.getMessage(),
"Iris left the container at " + pos + " in " + level.name() + " without loot: " + unavailable.getMessage());
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
} else {