mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 22:44:09 +00:00
Merge branch 'client-fixes' into review
This commit is contained in:
+1
-1
@@ -189,7 +189,7 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBypassMantleStages(Engine engine) {
|
||||
public boolean shouldRefuseGeneration(Engine engine) {
|
||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+76
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+36
-4
@@ -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();
|
||||
|
||||
+15
@@ -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,
|
||||
|
||||
+61
-9
@@ -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 {
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeServerSpawn;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
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.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
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 ModdedEngineBootstrapShutdownTest {
|
||||
private IrisSettings previousSettings;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void openRuntime() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
reopenBurstPools();
|
||||
}
|
||||
|
||||
@After
|
||||
public void restoreRuntime() {
|
||||
reopenBurstPools();
|
||||
IrisModdedCommands.openDownloadAdmission();
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStoppingKeepsEnginesAndWorkersOpenUntilServerStopped() throws Exception {
|
||||
NativeWorld level = level();
|
||||
Engine engine = mock(Engine.class);
|
||||
ModdedWorldEngines.installReplacement(level, engine);
|
||||
Callable<Thread> worker = Thread::currentThread;
|
||||
|
||||
ModdedEngineBootstrap.stop();
|
||||
|
||||
verify(engine, never()).close();
|
||||
assertTrue(ModdedWorldEngines.activeEngines().contains(engine));
|
||||
assertNotSame(Thread.currentThread(), MultiBurst.burst.submit(worker).get(10L, TimeUnit.SECONDS));
|
||||
|
||||
ModdedEngineBootstrap.stopped();
|
||||
|
||||
verify(engine).close();
|
||||
assertFalse(ModdedWorldEngines.activeEngines().contains(engine));
|
||||
assertSame(Thread.currentThread(), MultiBurst.burst.submit(worker).get(10L, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nextServerStartClosesTheEnginesOfAServerThatNeverReachedStopped() {
|
||||
NativeWorld level = level();
|
||||
Engine engine = mock(Engine.class);
|
||||
ModdedWorldEngines.installReplacement(level, engine);
|
||||
ModdedEngineBootstrap.stop();
|
||||
|
||||
try (MockedStatic<ModdedStartup> startup = mockStatic(ModdedStartup.class);
|
||||
MockedConstruction<NativeServerSpawn> spawn = mockConstruction(NativeServerSpawn.class)) {
|
||||
ModdedEngineBootstrap.serverAboutToStart(mock(NativeModdedServer.class));
|
||||
}
|
||||
|
||||
verify(engine).close();
|
||||
assertFalse(ModdedWorldEngines.activeEngines().contains(engine));
|
||||
}
|
||||
|
||||
private static NativeWorld level() {
|
||||
ServerLevel serverLevel = mock(ServerLevel.class);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
when(serverLevel.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.getGenerator()).thenReturn(mock(ChunkGenerator.class));
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||
when(level.name()).thenReturn("example:shutdown");
|
||||
return level;
|
||||
}
|
||||
|
||||
private static void reopenBurstPools() {
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
import org.objectweb.asm.AnnotationVisitor;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedMixinAuditTest {
|
||||
private static final String MIXIN_ANNOTATION = "Lorg/spongepowered/asm/mixin/Mixin;";
|
||||
private static final Set<String> INJECTOR_ANNOTATIONS = Set.of(
|
||||
"Lorg/spongepowered/asm/mixin/injection/Inject;",
|
||||
"Lorg/spongepowered/asm/mixin/injection/Redirect;",
|
||||
"Lorg/spongepowered/asm/mixin/injection/ModifyArg;",
|
||||
"Lorg/spongepowered/asm/mixin/injection/ModifyArgs;",
|
||||
"Lorg/spongepowered/asm/mixin/injection/ModifyVariable;",
|
||||
"Lorg/spongepowered/asm/mixin/injection/ModifyConstant;");
|
||||
private static final Pattern NEOFORGE_MIXIN_CONFIG = Pattern.compile(
|
||||
"\\[\\[mixins]]\\s*config\\s*=\\s*\"([^\"]+)\"");
|
||||
// One applied mixin per config proves the config was registered, which is all the audit detects.
|
||||
private static final Set<String> UNAUDITED = Set.of(
|
||||
"volmlib.entity.mixins.json/WorldGenerationWriteGuardMixin",
|
||||
"volmlib.entity.mixins.json/ChunkMapTerrainReceiptAccess",
|
||||
"volmlib.entity.mixins.json/ChunkTerrainReceiptMixin",
|
||||
"volmlib.entity.mixins.json/LevelChunkTerrainReceiptMixin",
|
||||
"volmlib.entity.mixins.json/SerializableTerrainReceiptMixin",
|
||||
"volmlib.fabric.mixins.json/BlockItemMixin",
|
||||
"volmlib.fabric.mixins.json/BlockMixin",
|
||||
"volmlib.fabric.mixins.json/PackRepositoryMixin");
|
||||
|
||||
@Test
|
||||
public void everyExpectedHandlerIsAnInjectorDeclaredByItsMixin() throws IOException {
|
||||
for (ModdedMixinAudit.ExpectedMixin expected : ModdedMixinAudit.EXPECTED) {
|
||||
MixinConfig config = MixinConfig.read(expected.config());
|
||||
String section = config.sections().get(expected.mixinName());
|
||||
assertNotNull(expected.config() + " does not list " + expected.mixinName(), section);
|
||||
assertEquals(expected.mixinName() + " client-only flag disagrees with its config section",
|
||||
"client".equals(section), expected.clientOnly());
|
||||
|
||||
MixinClass mixin = MixinClass.read(config.packageName() + '.' + expected.mixinName());
|
||||
assertTrue(expected.mixinName() + " does not target " + expected.targetClass().className()
|
||||
+ " (targets " + mixin.targets() + ")",
|
||||
mixin.targets().contains(expected.targetClass().className()));
|
||||
assertTrue(expected.mixinName() + " declares no injector handler named " + expected.handlerMethod()
|
||||
+ " (injectors " + mixin.injectors() + ")",
|
||||
mixin.injectors().contains(expected.handlerMethod()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyRegisteredMixinIsAuditedOrDeliberatelyUnaudited() throws IOException {
|
||||
Set<String> registered = registeredConfigs();
|
||||
Assume.assumeFalse("loader metadata on this classpath declares no mixin configs", registered.isEmpty());
|
||||
Set<String> audited = new LinkedHashSet<>();
|
||||
for (ModdedMixinAudit.ExpectedMixin expected : ModdedMixinAudit.EXPECTED) {
|
||||
audited.add(expected.config() + '/' + expected.mixinName());
|
||||
}
|
||||
|
||||
Set<String> listed = new TreeSet<>();
|
||||
for (String configName : registered) {
|
||||
for (String mixinName : MixinConfig.read(configName).sections().keySet()) {
|
||||
listed.add(configName + '/' + mixinName);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> uncovered = new TreeSet<>(listed);
|
||||
uncovered.removeAll(audited);
|
||||
uncovered.removeAll(UNAUDITED);
|
||||
assertEquals("mixins neither audited nor deliberately unaudited", Set.of(), uncovered);
|
||||
|
||||
Set<String> unregisteredAudits = new TreeSet<>(audited);
|
||||
unregisteredAudits.removeAll(listed);
|
||||
assertEquals("audited mixins missing from the registered configs", Set.of(), unregisteredAudits);
|
||||
}
|
||||
|
||||
private static Set<String> registeredConfigs() throws IOException {
|
||||
Set<String> configs = new LinkedHashSet<>();
|
||||
String fabric = readResource("fabric.mod.json");
|
||||
if (fabric != null) {
|
||||
for (JsonElement entry : JsonParser.parseString(fabric).getAsJsonObject().getAsJsonArray("mixins")) {
|
||||
configs.add(entry.isJsonObject()
|
||||
? entry.getAsJsonObject().get("config").getAsString()
|
||||
: entry.getAsString());
|
||||
}
|
||||
}
|
||||
String neoforge = readResource("META-INF/neoforge.mods.toml");
|
||||
if (neoforge != null) {
|
||||
Matcher matcher = NEOFORGE_MIXIN_CONFIG.matcher(neoforge);
|
||||
while (matcher.find()) {
|
||||
configs.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
private static String readResource(String name) throws IOException {
|
||||
try (InputStream input = ModdedMixinAuditTest.class.getClassLoader().getResourceAsStream(name)) {
|
||||
return input == null ? null : new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private record MixinConfig(String packageName, Map<String, String> sections) {
|
||||
static MixinConfig read(String name) throws IOException {
|
||||
String json = readResource(name);
|
||||
assertNotNull("Missing mixin config resource " + name, json);
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
Map<String, String> sections = new LinkedHashMap<>();
|
||||
for (String section : new String[]{"mixins", "client", "server"}) {
|
||||
JsonArray entries = root.getAsJsonArray(section);
|
||||
if (entries == null) {
|
||||
continue;
|
||||
}
|
||||
for (JsonElement entry : entries) {
|
||||
sections.put(entry.getAsString(), section);
|
||||
}
|
||||
}
|
||||
return new MixinConfig(root.get("package").getAsString(), sections);
|
||||
}
|
||||
}
|
||||
|
||||
private record MixinClass(Set<String> targets, Set<String> injectors) {
|
||||
static MixinClass read(String className) throws IOException {
|
||||
String resource = className.replace('.', '/') + ".class";
|
||||
byte[] bytes;
|
||||
try (InputStream input = ModdedMixinAuditTest.class.getClassLoader().getResourceAsStream(resource)) {
|
||||
assertNotNull("Missing mixin class " + className, input);
|
||||
bytes = input.readAllBytes();
|
||||
}
|
||||
Set<String> targets = new TreeSet<>();
|
||||
Set<String> injectors = new TreeSet<>();
|
||||
new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
|
||||
if (!MIXIN_ANNOTATION.equals(descriptor)) {
|
||||
return null;
|
||||
}
|
||||
return new AnnotationVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public AnnotationVisitor visitArray(String name) {
|
||||
return new AnnotationVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public void visit(String ignored, Object value) {
|
||||
if (value instanceof Type type) {
|
||||
targets.add(type.getClassName());
|
||||
} else if (value instanceof String target) {
|
||||
targets.add(target.replace('/', '.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
|
||||
String[] exceptions) {
|
||||
return new MethodVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
|
||||
if (INJECTOR_ANNOTATIONS.contains(annotation)) {
|
||||
injectors.add(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
return new MixinClass(targets, injectors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisModdedCommandsTeleportTest {
|
||||
private static final String DIMENSION = "irisworldgen:1";
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportWaitsForAColdDestinationWithinABoundedDeadline() {
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
NativeCommandSource source = mock(NativeCommandSource.class);
|
||||
when(source.server()).thenReturn(server);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.name()).thenReturn(DIMENSION);
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
when(player.name()).thenReturn("Tester");
|
||||
AtomicReference<NativeWorldTeleport.Destination> requested = new AtomicReference<>();
|
||||
|
||||
long before = System.nanoTime();
|
||||
try (MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
|
||||
MockedStatic<ModdedDimensionManager> dimensions = mockStatic(ModdedDimensionManager.class);
|
||||
MockedStatic<NativeWorldTeleport> teleport = mockStatic(NativeWorldTeleport.class)) {
|
||||
generators.when(() -> NativeWorldGenerators.find(level, IrisModdedChunkGenerator.class))
|
||||
.thenReturn(mock(IrisModdedChunkGenerator.class));
|
||||
dimensions.when(() -> ModdedDimensionManager.level(server, DIMENSION)).thenReturn(level);
|
||||
teleport.when(() -> NativeWorldTeleport.teleport(any(), any())).thenAnswer((InvocationOnMock invocation) -> {
|
||||
requested.set(invocation.getArgument(1));
|
||||
return new CompletableFuture<Boolean>();
|
||||
});
|
||||
|
||||
assertEquals(1, IrisModdedCommands.tp(source, level, player));
|
||||
}
|
||||
long after = System.nanoTime();
|
||||
|
||||
long deadline = requested.get().deadlineNanos();
|
||||
assertTrue("deadline must outlast a cold first chunk", deadline - before >= TimeUnit.SECONDS.toNanos(60L));
|
||||
assertTrue("deadline must be bounded", deadline - after <= TimeUnit.SECONDS.toNanos(120L));
|
||||
}
|
||||
|
||||
@Test(timeout = 15_000L)
|
||||
public void expiredTeleportReportsTheCallersDeadline() throws Exception {
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
when(server.isSameThread()).thenReturn(true);
|
||||
ResourceKey<Level> dimension = ResourceKey.create(Registries.DIMENSION, Identifier.parse(DIMENSION));
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
when(chunks.addTicketAndLoadWithRadius(any(), any(), anyInt())).thenReturn(new CompletableFuture<>());
|
||||
ServerLevel serverLevel = mock(ServerLevel.class);
|
||||
when(serverLevel.getServer()).thenReturn(server);
|
||||
when(serverLevel.dimension()).thenReturn(dimension);
|
||||
when(serverLevel.getChunkSource()).thenReturn(chunks);
|
||||
when(server.getLevel(dimension)).thenReturn(serverLevel);
|
||||
ServerPlayer serverPlayer = mock(ServerPlayer.class);
|
||||
when(serverPlayer.getUUID()).thenReturn(UUID.randomUUID());
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||
|
||||
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(NativeProtocolPlayer.fromHandle(serverPlayer),
|
||||
new NativeWorldTeleport.Destination(NativeModdedServer.fromHandle(server), level, 8.5D,
|
||||
Double.MIN_VALUE, 8.5D, System.nanoTime() + TimeUnit.SECONDS.toNanos(2L)));
|
||||
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> teleport.get(10L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof TimeoutException);
|
||||
assertTrue(failure.getCause().getMessage(), failure.getCause().getMessage().contains("within 2 seconds"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureReportsItsRootCauseInsteadOfAnUnloadedDimension() {
|
||||
String message = IrisModdedCommands.teleportFailure(DIMENSION, "Tester",
|
||||
new CompletionException(new IllegalStateException("Exception generating new chunk")), true, true);
|
||||
|
||||
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
|
||||
MessageArgument.untrusted("dimensionId", DIMENSION),
|
||||
MessageArgument.untrusted("reason", "Exception generating new chunk")), message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureWithoutMessageNamesTheFailureType() {
|
||||
String message = IrisModdedCommands.teleportFailure(DIMENSION, "Tester", new TimeoutException(), true, true);
|
||||
|
||||
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_REASON,
|
||||
MessageArgument.untrusted("dimensionId", DIMENSION),
|
||||
MessageArgument.untrusted("reason", "TimeoutException")), message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadedDimensionIsReportedOnlyWhenItIsGone() {
|
||||
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
|
||||
MessageArgument.untrusted("dimensionId", DIMENSION)),
|
||||
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, false, true));
|
||||
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_CANCELLED_PLAYER_OFFLINE,
|
||||
MessageArgument.untrusted("dimensionId", DIMENSION),
|
||||
MessageArgument.untrusted("value", "Tester")),
|
||||
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, true, false));
|
||||
assertEquals(IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_REFUSED,
|
||||
MessageArgument.untrusted("dimensionId", DIMENSION)),
|
||||
IrisModdedCommands.teleportFailure(DIMENSION, "Tester", null, true, true));
|
||||
}
|
||||
}
|
||||
+259
@@ -2,15 +2,30 @@ package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedPlatformWorld;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedLootApplier;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldMaintenance;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -24,19 +39,33 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FlowingFluid;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
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 ModdedChunkUpdateServiceTest {
|
||||
@@ -70,6 +99,148 @@ public class ModdedChunkUpdateServiceTest {
|
||||
assertTrue(chunk.deleted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadingSavedBiomeDefersThePassAndForgetsOnlyCompletedLoot() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getMinY()).thenReturn(-64);
|
||||
when(level.getMaxY()).thenReturn(319);
|
||||
when(level.getHeight()).thenReturn(384);
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.CHEST.defaultBlockState());
|
||||
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
||||
SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException(
|
||||
"Saved biome information is loading at chunk 0,0. Try again shortly.", true);
|
||||
|
||||
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class)) {
|
||||
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
|
||||
NativeBlockPoint position = invocation.getArgument(2);
|
||||
if (position.x() == 2) {
|
||||
throw loading;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
SavedBiomeUnavailableException thrown = assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> new ModdedChunkUpdateService().runUpdatePass(
|
||||
engine, new ModdedPlatformWorld(level), 0, 0, chunk));
|
||||
|
||||
assertSame(loading, thrown);
|
||||
}
|
||||
assertFalse(chunk.deleted());
|
||||
verify(chunk.slice).set(1, 5, 1, null);
|
||||
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadingSavedBiomeLeavesTheChunkUnetchedWithoutAnErrorAndRetriesOnTheNextPass() {
|
||||
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
||||
Engine engine = updateEngine(chunk);
|
||||
NativeWorld level = chestLevel();
|
||||
AtomicInteger loadingAttempts = new AtomicInteger();
|
||||
ModdedChunkUpdateService service = new ModdedChunkUpdateService();
|
||||
|
||||
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
|
||||
NativeBlockPoint position = invocation.getArgument(2);
|
||||
if (position.x() == 2 && loadingAttempts.getAndIncrement() == 0) {
|
||||
throw new SavedBiomeUnavailableException("Saved biome information is loading at chunk 0,0.", true);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(chunk.deleted());
|
||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void permanentSavedBiomeUnavailabilityCompletesThePassWithAWarning() {
|
||||
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
||||
Engine engine = updateEngine(chunk);
|
||||
NativeWorld level = chestLevel();
|
||||
String reason = "This chunk has no exact saved Iris biome assignment for generation 3.";
|
||||
|
||||
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer((InvocationOnMock invocation) -> {
|
||||
NativeBlockPoint position = invocation.getArgument(2);
|
||||
if (position.x() == 2) {
|
||||
throw new SavedBiomeUnavailableException(reason, false);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
logging.verify(() -> IrisLogging.warnOnce(anyString(), contains(reason)));
|
||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedChunkDoesNotStopTheLevelPassForItsOtherChunks() {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
FailingMantleChunk failing = new FailingMantleChunk();
|
||||
ChestUpdateMantleChunk healthy = new ChestUpdateMantleChunk();
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> failing.use());
|
||||
when(mantle.useChunk(4, 0)).thenAnswer((InvocationOnMock invocation) -> healthy.use());
|
||||
Engine engine = engineWith(mantle);
|
||||
NativeWorld level = chestLevel();
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
when(server.worlds()).thenReturn(List.of(level));
|
||||
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
|
||||
when(generator.engineIfBound()).thenReturn(engine);
|
||||
|
||||
try (MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
|
||||
MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
|
||||
MockedConstruction<NativeWorldMaintenance> maintenance = mockConstruction(NativeWorldMaintenance.class,
|
||||
(NativeWorldMaintenance mocked, MockedConstruction.Context context) -> {
|
||||
when(mocked.hasForcedChunks()).thenReturn(true);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
LongConsumer action = invocation.getArgument(0);
|
||||
action.accept(0L);
|
||||
action.accept(4L);
|
||||
return null;
|
||||
}).when(mocked).forEachForcedChunk(any(LongConsumer.class));
|
||||
})) {
|
||||
generators.when(() -> NativeWorldGenerators.find(level, IrisModdedChunkGenerator.class)).thenReturn(generator);
|
||||
|
||||
new ModdedChunkUpdateService().onServerTick(server);
|
||||
|
||||
assertFalse(failing.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(healthy.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(healthy.deleted());
|
||||
logging.verify(() -> IrisLogging.errorOnce(anyString(), anyString()));
|
||||
} finally {
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scansWhenPlayersArePresent() {
|
||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
||||
@@ -166,6 +337,94 @@ public class ModdedChunkUpdateServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ChestUpdateMantleChunk extends MantleChunk<Matter> {
|
||||
@SuppressWarnings("unchecked")
|
||||
private final MatterSlice<MatterUpdate> slice = mock(MatterSlice.class);
|
||||
private final Matter section = mock(Matter.class);
|
||||
private boolean deleted;
|
||||
|
||||
private ChestUpdateMantleChunk() {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
when(section.hasSlice(MatterUpdate.class)).thenReturn(true);
|
||||
when(section.getSlice(MatterUpdate.class)).thenReturn(slice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matter get(int index) {
|
||||
return section;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(
|
||||
Class<T> type,
|
||||
Consumer4<Integer, Integer, Integer, T> iterator
|
||||
) {
|
||||
if (type == MatterUpdate.class) {
|
||||
iterator.accept(1, 5, 1, type.cast(UpdateMatter.ON));
|
||||
iterator.accept(2, 5, 2, type.cast(UpdateMatter.ON));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSlices(Class<?> type) {
|
||||
if (type == MatterUpdate.class) {
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleted() {
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine updateEngine(ChestUpdateMantleChunk chunk) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(mantle.hasFlag(0, 0, MantleFlag.ETCHED)).thenAnswer((InvocationOnMock invocation) -> chunk.isFlagged(MantleFlag.ETCHED));
|
||||
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> chunk.use());
|
||||
return engineWith(mantle);
|
||||
}
|
||||
|
||||
private static Engine engineWith(Mantle<Matter> mantle) {
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static NativeWorld chestLevel() {
|
||||
NativeBlockState chest = mock(NativeBlockState.class);
|
||||
when(chest.isStorage()).thenReturn(true);
|
||||
when(chest.isStorageChest()).thenReturn(true);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.name()).thenReturn("example:updates");
|
||||
when(level.minHeight()).thenReturn(-64);
|
||||
when(level.maxHeight()).thenReturn(320);
|
||||
when(level.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(level.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(chest);
|
||||
return level;
|
||||
}
|
||||
|
||||
private static final class FailingMantleChunk extends MantleChunk<Matter> {
|
||||
private FailingMantleChunk() {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(
|
||||
Class<T> type,
|
||||
Consumer4<Integer, Integer, Integer, T> iterator
|
||||
) {
|
||||
throw new IllegalStateException("corrupt " + type.getSimpleName() + " slice");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||
|
||||
@@ -24,6 +24,7 @@ import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -233,6 +234,7 @@ final class IrisBiomeLayerGenerator {
|
||||
data.add(layer.get(random, layerIndex + offset,
|
||||
(wx + offset) / zoom, offset, (wz - offset) / zoom, rdata));
|
||||
} catch (Throwable error) {
|
||||
GenerationFailures.rethrowEngineFailure(error);
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
}
|
||||
@@ -330,6 +332,7 @@ final class IrisBiomeLayerGenerator {
|
||||
blocks.add(layer.get(random, layerIndex + offset,
|
||||
(x + offset) / zoom, offset, (z - offset) / zoom, data));
|
||||
} catch (Throwable error) {
|
||||
GenerationFailures.rethrowEngineFailure(error);
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
@@ -89,6 +90,7 @@ final class DecoratorCore {
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -163,6 +165,7 @@ final class DecoratorCore {
|
||||
data.set(x, lowerY, z, lower);
|
||||
data.set(x, upperY, z, upper);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
return;
|
||||
@@ -356,6 +359,7 @@ final class DecoratorCore {
|
||||
data.set(xf, lowerY, zf, lower);
|
||||
data.set(xf, upperY, zf, upper);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
@@ -713,7 +714,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
|
||||
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("Hydrology tile cache is closed.");
|
||||
throw new GenerationClosedException("Hydrology tile cache is closed.");
|
||||
}
|
||||
sharedCacheScope = Objects.requireNonNull(scope, "scope");
|
||||
persistentStore = persistentRoot == null
|
||||
|
||||
+7
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
import art.arcane.iris.generation.biome.FloatingIslandSample;
|
||||
@@ -231,6 +232,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -326,6 +328,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -438,6 +441,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -611,11 +615,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
return maxObjectExtent;
|
||||
@@ -649,6 +655,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.UpperDimensionContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -475,6 +476,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
nullObjects += result.nullObjects();
|
||||
errors += result.errors();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place objects in the following biome: " + surfaceBiome.getName());
|
||||
@@ -508,6 +510,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
nullObjects += result.nullObjects();
|
||||
errors += result.errors();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place cave objects in the following biome: " + caveBiome.getName());
|
||||
@@ -538,6 +541,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
nullObjects += result.nullObjects();
|
||||
errors += result.errors();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place objects in the following region: " + region.getName());
|
||||
@@ -571,6 +575,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
nullObjects += result.nullObjects();
|
||||
errors += result.errors();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place cave objects in the following region: " + region.getName());
|
||||
@@ -760,6 +765,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " commit=" + commitResult);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place procedural object '" + p.getName() + "' in " + scope);
|
||||
}
|
||||
@@ -988,6 +994,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " density=" + density);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Regen object placement exception: chunk=" + chunkX + "," + chunkZ
|
||||
@@ -1192,6 +1199,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " density=" + density);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
errors++;
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Regen cave object placement exception: chunk=" + metricChunkX + "," + metricChunkZ
|
||||
@@ -1251,6 +1259,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
try {
|
||||
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-biome-surface");
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place upper-dimension objects in biome " + upperBiome.getName()
|
||||
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
|
||||
@@ -1266,6 +1275,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
try {
|
||||
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-region-surface");
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to place upper-dimension objects in region " + upperRegion.getName()
|
||||
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
|
||||
@@ -1790,6 +1800,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
||||
@@ -1495,6 +1496,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
try {
|
||||
setData(pos.getX(), pos.getY(), pos.getZ(), data);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
// Reached per position while an object writes past the edge of the writer window, which is
|
||||
// how a large object is clipped. Nothing here is actionable from outside the engine.
|
||||
IrisLogging.debug("No set? " + data.toString() + " for " + pos.toString());
|
||||
|
||||
@@ -23,7 +23,6 @@ import art.arcane.iris.generation.stage.IrisDecorantActuator;
|
||||
import art.arcane.iris.generation.stage.IrisTerrainNormalActuator;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMode;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.runtime.EngineStage;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMode;
|
||||
import art.arcane.iris.generation.stage.IrisCarveModifier;
|
||||
@@ -32,17 +31,10 @@ import art.arcane.iris.generation.stage.IrisDepositModifier;
|
||||
import art.arcane.iris.generation.stage.IrisFloatingChildBiomeModifier;
|
||||
import art.arcane.iris.generation.stage.IrisPerfectionModifier;
|
||||
import art.arcane.iris.generation.stage.IrisPostModifier;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
private static final AtomicLong lastMaintenanceBypassLog = new AtomicLong(0L);
|
||||
private final EnginePlatformHooks platformHooks;
|
||||
|
||||
public ModeOverworld(Engine engine) {
|
||||
super(engine);
|
||||
platformHooks = engine.getPlatformHooks();
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
IrisDecorantActuator decorant = new IrisDecorantActuator(getEngine());
|
||||
@@ -53,51 +45,17 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
IrisCustomModifier custom = new IrisCustomModifier(getEngine());
|
||||
IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
|
||||
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
|
||||
EngineStage sGenMatter = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
generateTerrainMatter(
|
||||
x >> 4,
|
||||
z >> 4,
|
||||
m || getEngine().isStudio(),
|
||||
c);
|
||||
};
|
||||
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
|
||||
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
|
||||
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
|
||||
EngineStage sCave = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
cave.modify(x >> 4, z >> 4, k, m, c);
|
||||
};
|
||||
EngineStage sDeposit = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
deposit.modify(x, z, k, m, c);
|
||||
};
|
||||
EngineStage sPost = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
post.modify(x, z, k, m, c);
|
||||
};
|
||||
EngineStage sInsertMatter = (x, z, K, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m, c);
|
||||
};
|
||||
EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c);
|
||||
EngineStage sDeposit = (x, z, k, p, m, c) -> deposit.modify(x, z, k, m, c);
|
||||
EngineStage sPost = (x, z, k, p, m, c) -> post.modify(x, z, k, m, c);
|
||||
EngineStage sInsertMatter = (x, z, K, p, m, c) -> getMantle().insertMatter(x >> 4, z >> 4, K, m, c);
|
||||
EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c);
|
||||
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
|
||||
EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c);
|
||||
EngineStage sCustom = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
custom.modify(x, z, k, m, c);
|
||||
};
|
||||
EngineStage sCustom = (x, z, k, p, m, c) -> custom.modify(x, z, k, m, c);
|
||||
|
||||
// Matter runs on the calling thread so its window fans out across the burst pool (a pool
|
||||
// thread would run every chunk of the window inline) while biome and terrain run alongside.
|
||||
@@ -114,11 +72,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
// the surface sInsertMatter writes), so parallel order is scheduler-dependent. The
|
||||
// production path already runs them inline in this order; sequential registration
|
||||
// makes studio (the only multicore path) match production and the goldenhash baseline.
|
||||
registerStage((x, z, k, p, m, c) -> {
|
||||
if (!shouldBypassMantleStages()) {
|
||||
generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
|
||||
}
|
||||
});
|
||||
registerStage((x, z, k, p, m, c) -> generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c));
|
||||
registerStage(sDeposit);
|
||||
registerStage(sInsertMatter);
|
||||
registerStage(sDecorant);
|
||||
@@ -128,17 +82,4 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldBypassMantleStages() {
|
||||
boolean active = platformHooks.shouldBypassMantleStages(getEngine());
|
||||
if (active) {
|
||||
long now = System.currentTimeMillis();
|
||||
long last = lastMaintenanceBypassLog.get();
|
||||
if (now - last >= 5000L && lastMaintenanceBypassLog.compareAndSet(last, now)) {
|
||||
IrisLogging.info("Maintenance regen bypass: skipping mantle-backed overworld stages for Folia safety.");
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
}
|
||||
return new NoiseBounds(minimum, maximum);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,11 @@ public interface EnginePlatformHooks {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean shouldBypassMantleStages(Engine engine) {
|
||||
/**
|
||||
* True while world maintenance forbids the mantle-backed stages. Generating without them would persist a
|
||||
* chunk with no objects, caves or deposits, so the engine refuses the chunk instead.
|
||||
*/
|
||||
default boolean shouldRefuseGeneration(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
public final class GenerationClosedException extends IllegalStateException {
|
||||
public GenerationClosedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Separates failures of the engine, its mantle, generation history, executors or the JVM from failures of one
|
||||
* piece of content. Content loops (objects, decorators, layers) may skip a failed item because every rebuild
|
||||
* skips it the same way. An engine failure is transient, so skipping would cache and persist terrain that a
|
||||
* healthy engine never produces: it must fail the whole chunk instead.
|
||||
*/
|
||||
public final class GenerationFailures {
|
||||
private static final int MAXIMUM_CAUSE_DEPTH = 32;
|
||||
|
||||
private GenerationFailures() {
|
||||
}
|
||||
|
||||
public static boolean isEngineFailure(Throwable failure) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
return true;
|
||||
}
|
||||
Throwable current = failure;
|
||||
for (int depth = 0; current != null && depth < MAXIMUM_CAUSE_DEPTH; depth++, current = current.getCause()) {
|
||||
if (current instanceof Error
|
||||
|| current instanceof GenerationClosedException
|
||||
|| current instanceof MantleClosedException
|
||||
|| current instanceof WrongEngineBroException
|
||||
|| current instanceof SavedBiomeUnavailableException unavailable && unavailable.isLoading()
|
||||
|| current instanceof InterruptedException
|
||||
|| current instanceof InterruptedIOException
|
||||
|| current instanceof ClosedChannelException
|
||||
|| current instanceof RejectedExecutionException
|
||||
|| current instanceof CancellationException
|
||||
|| current instanceof TimeoutException) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void rethrowEngineFailure(Throwable failure) {
|
||||
if (!isEngineFailure(failure)) {
|
||||
return;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtimeFailure) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
throw new IllegalStateException("Iris generation stopped: " + failure, failure);
|
||||
}
|
||||
}
|
||||
@@ -2044,6 +2044,7 @@ public class IrisComplex implements DataProvider {
|
||||
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
|
||||
return bounds.noiseBounds;
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
long now = System.currentTimeMillis();
|
||||
long last = lastBoundsFailureLog.get();
|
||||
// The five second gate keeps this off the hot path; the once key keeps it out of a log
|
||||
|
||||
@@ -86,6 +86,8 @@ import java.util.function.Supplier;
|
||||
@Data
|
||||
public class IrisEngine implements Engine {
|
||||
static final long SESSION_DRAIN_TIMEOUT_MILLIS = 15000L;
|
||||
private static final long MAINTENANCE_REFUSAL_WARNING_MILLIS = 5000L;
|
||||
private static final AtomicLong lastMaintenanceRefusalWarning = new AtomicLong();
|
||||
|
||||
private final AtomicInteger bud;
|
||||
private final AtomicInteger buds;
|
||||
@@ -1083,10 +1085,10 @@ public class IrisEngine implements Engine {
|
||||
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
|
||||
}
|
||||
if (retiringGenerationRuntimes.contains(required.runtime)) {
|
||||
throw new IllegalStateException("Iris generation runtime binding is retiring.");
|
||||
throw new GenerationClosedException("Iris generation runtime binding is retiring.");
|
||||
}
|
||||
if (!isGenerationRuntimeBindingLive(required)) {
|
||||
throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned.");
|
||||
throw new GenerationClosedException("Iris generation runtime binding is closed or no longer owned.");
|
||||
}
|
||||
return threadState.open(required);
|
||||
}
|
||||
@@ -1485,6 +1487,9 @@ public class IrisEngine implements Engine {
|
||||
@Override
|
||||
public void generate(int x, int z, Hunk<NativeBlockState> vblocks, Hunk<NativeBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
|
||||
awaitGenerationCacheWarm();
|
||||
if (platformHooks.shouldRefuseGeneration(this)) {
|
||||
refuseMaintenanceGeneration(x >> 4, z >> 4);
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openGenerationHistoryCoordinateScope(x, z);
|
||||
GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
|
||||
@@ -1505,20 +1510,17 @@ public class IrisEngine implements Engine {
|
||||
activeMode.generate(x, z, blocks, vbiomes, multicore, lease.sessionId());
|
||||
}
|
||||
|
||||
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
|
||||
if (!skipRealFlag) {
|
||||
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
Matter section = chunk.get(0);
|
||||
if (section != null) {
|
||||
section.deleteSlice(ObjectContinuationBundle.class);
|
||||
}
|
||||
chunk.flag(MantleFlag.REAL, true);
|
||||
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
Matter section = chunk.get(0);
|
||||
if (section != null) {
|
||||
section.deleteSlice(ObjectContinuationBundle.class);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
chunk.flag(MantleFlag.REAL, true);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
getMetrics().getTotal().put(p.getMilliseconds());
|
||||
generated.incrementAndGet();
|
||||
@@ -1538,6 +1540,17 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
}
|
||||
|
||||
private void refuseMaintenanceGeneration(int chunkX, int chunkZ) throws GenerationSessionException {
|
||||
long now = System.currentTimeMillis();
|
||||
long last = lastMaintenanceRefusalWarning.get();
|
||||
if (now - last >= MAINTENANCE_REFUSAL_WARNING_MILLIS && lastMaintenanceRefusalWarning.compareAndSet(last, now)) {
|
||||
IrisLogging.warn("Iris refused chunk " + chunkX + "," + chunkZ + " in " + getWorld().name()
|
||||
+ ": world maintenance forbids mantle generation, the chunk stays ungenerated");
|
||||
}
|
||||
throw new GenerationSessionException("Iris refused chunk " + chunkX + "," + chunkZ + " in "
|
||||
+ getWorld().name() + " during world maintenance.", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenerationSessionManager getGenerationSessions() {
|
||||
return generationSessions;
|
||||
@@ -1682,7 +1695,7 @@ public class IrisEngine implements Engine {
|
||||
|
||||
void requireRunning(String operation) {
|
||||
if (closed || closing.get() || lifecycleState != LifecycleState.RUNNING || runtime == null) {
|
||||
throw new IllegalStateException("Cannot " + operation + " while Iris engine " + getWorld().name()
|
||||
throw new GenerationClosedException("Cannot " + operation + " while Iris engine " + getWorld().name()
|
||||
+ " is " + lifecycleState.name().toLowerCase() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.decoration.FloatingDecorator;
|
||||
import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator;
|
||||
@@ -100,6 +101,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
try {
|
||||
generated.add(layer.get(random.nextParallelRNG(i + j), (wx + j) / layer.getZoom(), j, (wz - j) / layer.getZoom(), data));
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -345,6 +347,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz));
|
||||
FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, NOOP_DECORATION_MISS);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -377,6 +380,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
try {
|
||||
seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
@@ -432,6 +436,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.structure.nativegen;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
@@ -165,7 +166,7 @@ public final class NativeStructureOwnershipStore {
|
||||
lifecycleLock.writeLock().lock();
|
||||
try {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Native structure ownership store is closed");
|
||||
throw new GenerationClosedException("Native structure ownership store is closed");
|
||||
}
|
||||
flushDirty();
|
||||
} finally {
|
||||
@@ -214,7 +215,7 @@ public final class NativeStructureOwnershipStore {
|
||||
private void requireOpen() {
|
||||
engine();
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Native structure ownership store is closed");
|
||||
throw new GenerationClosedException("Native structure ownership store is closed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
@@ -519,6 +520,7 @@ final class IrisObjectPlacementRunner {
|
||||
tile = states.get(g);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)");
|
||||
d = IrisObject.States.air();
|
||||
@@ -713,6 +715,7 @@ final class IrisObjectPlacementRunner {
|
||||
try {
|
||||
sourceData = stiltCursor.value();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)");
|
||||
sourceData = IrisObject.States.air();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.pack.value.IrisDirection;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
|
||||
@@ -450,6 +451,7 @@ public class IrisObjectRotation {
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -137,7 +139,7 @@ public final class GenerationAdmission {
|
||||
|
||||
private void requireOpen() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation admission belongs to a closed runtime.");
|
||||
throw new GenerationClosedException("Generation admission belongs to a closed runtime.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.pack.AtomicDirectoryPublisher;
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
@@ -1386,7 +1387,7 @@ public final class GenerationHistory {
|
||||
throw new IllegalArgumentException("Generation stage belongs to a different history.");
|
||||
}
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation stage is closed.");
|
||||
throw new GenerationClosedException("Generation stage is closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -9,6 +9,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
|
||||
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterContext;
|
||||
@@ -749,7 +750,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
throw propagate(entry.failure, "Saved mantle for activation " + entry.activationId + " is unavailable.");
|
||||
}
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime router is closed.");
|
||||
throw new GenerationClosedException("Generation-history runtime router is closed.");
|
||||
}
|
||||
try {
|
||||
inactive.await();
|
||||
@@ -800,7 +801,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
private void awaitRetirementLocked(long activationId) throws IOException {
|
||||
while (true) {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime router is closed.");
|
||||
throw new GenerationClosedException("Generation-history runtime router is closed.");
|
||||
}
|
||||
RuntimeRetirement retirement = retiringBindings.get(activationId);
|
||||
if (retirement == null) {
|
||||
@@ -1032,7 +1033,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
stateLock.lock();
|
||||
try {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime router is closed.");
|
||||
throw new GenerationClosedException("Generation-history runtime router is closed.");
|
||||
}
|
||||
activeOperations++;
|
||||
operationDepth.set(operationDepth.get() + 1);
|
||||
@@ -1045,7 +1046,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
stateLock.lock();
|
||||
try {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime router is closed.");
|
||||
throw new GenerationClosedException("Generation-history runtime router is closed.");
|
||||
}
|
||||
activeOperations++;
|
||||
} finally {
|
||||
@@ -1202,7 +1203,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
public Scope openScope() {
|
||||
synchronized (this) {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Saved chunk mantle is closed.");
|
||||
throw new GenerationClosedException("Saved chunk mantle is closed.");
|
||||
}
|
||||
activeScopes++;
|
||||
}
|
||||
@@ -1535,7 +1536,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
public RuntimeScope openRuntimeScope() {
|
||||
synchronized (this) {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime route is closed.");
|
||||
throw new GenerationClosedException("Generation-history runtime route is closed.");
|
||||
}
|
||||
activeScopes++;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
@@ -365,7 +366,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
if (!refresh) {
|
||||
if (closed || prepared == null) {
|
||||
loading.result.completeExceptionally(new IllegalStateException("Saved biome runtime is closed."));
|
||||
loading.result.completeExceptionally(new GenerationClosedException("Saved biome runtime is closed."));
|
||||
} else {
|
||||
loading.result.complete(prepared);
|
||||
}
|
||||
@@ -480,7 +481,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
|
||||
private void requireOpen() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Saved biome runtime is closed.");
|
||||
throw new GenerationClosedException("Saved biome runtime is closed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Öffne {value} {value2} in deinem Editor."
|
||||
console_must_name_player_iris_tp_dimension_player = "Die Konsole muss einen Spieler benennen: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} wird nicht von Iris generiert."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleportation fehlgeschlagen: Dimension {dimensionId} ist nicht geladen."
|
||||
teleport_failed_reason = "Teleport nach {dimensionId} fehlgeschlagen: {reason}"
|
||||
teleport_cancelled_player_offline = "Teleport nach {dimensionId} abgebrochen: {value} ist nicht mehr online."
|
||||
teleport_refused = "Teleport nach {dimensionId} wurde vom Server abgelehnt."
|
||||
teleporting = "Teleportiere {value} nach {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} wird nicht von Iris generiert."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Die primäre Welt kann nicht evakuiert werden, da es kein Ziel für die Spieler gibt."
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Abriendo {value} {value2} en tu editor."
|
||||
console_must_name_player_iris_tp_dimension_player = "La consola debe indicar un jugador: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} no se genera por Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "El teletransporte falló: la dimensión {dimensionId} no está cargada."
|
||||
teleport_failed_reason = "El teletransporte a {dimensionId} falló: {reason}"
|
||||
teleport_cancelled_player_offline = "Teletransporte a {dimensionId} cancelado: {value} ya no está conectado."
|
||||
teleport_refused = "El servidor rechazó el teletransporte a {dimensionId}."
|
||||
teleporting = "Teletransportando a {value} a {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} no se genera por Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "No se puede evacuar el mundo principal porque no hay adónde enviar a los jugadores."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "Avaaminen {value} {value2} Päätoimittajassasi."
|
||||
console_must_name_player_iris_tp_dimension_player = "Konsolin on nimettävä pelaaja: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} ei ole tuotettu Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleportti epäonnistui: ulottuvuus {dimensionId} ei ole ladattu."
|
||||
teleport_failed_reason = "Teleporttaus ulottuvuuteen {dimensionId} epäonnistui: {reason}"
|
||||
teleport_cancelled_player_offline = "Teleporttaus ulottuvuuteen {dimensionId} peruttiin: {value} ei ole enää paikalla."
|
||||
teleport_refused = "Palvelin hylkäsi teleporttauksen ulottuvuuteen {dimensionId}."
|
||||
teleporting = "Telesiirtäminen {value} - {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} ei ole tuotettu Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Ensisijaista maailmaa ei voida evakuoida; pelaajia ei voi lähettää minnekään."
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Ouverture de {value} {value2} dans votre éditeur."
|
||||
console_must_name_player_iris_tp_dimension_player = "La console doit indiquer un joueur : /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} n'est pas généré par Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Échec de la téléportation : la dimension {dimensionId} n'est pas chargée."
|
||||
teleport_failed_reason = "Échec de la téléportation vers {dimensionId} : {reason}"
|
||||
teleport_cancelled_player_offline = "Téléportation vers {dimensionId} annulée : {value} n'est plus connecté."
|
||||
teleport_refused = "Le serveur a refusé la téléportation vers {dimensionId}."
|
||||
teleporting = "Téléportation de {value} vers {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} n'est pas généré par Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Impossible d'évacuer le monde principal, car aucun autre monde ne peut accueillir les joueurs."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "פתיחה {value} {value2} בעורך שלך."
|
||||
console_must_name_player_iris_tp_dimension_player = "הקונסולה חייבת לקרוא לשחקן: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} לא נוצר על ידי Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "טלפורט נכשל: ממד {dimensionId} לא טעון."
|
||||
teleport_failed_reason = "השיגור אל {dimensionId} נכשל: {reason}"
|
||||
teleport_cancelled_player_offline = "השיגור אל {dimensionId} בוטל: {value} כבר לא מחובר."
|
||||
teleport_refused = "השרת דחה את השיגור אל {dimensionId}."
|
||||
teleporting = "תקשורת {value} כדי {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} לא נוצר על ידי Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "לא ניתן לפנות לעולם הראשי, אין מקום לשלוח שחקנים."
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Apertura di {value} {value2} nel tuo editor."
|
||||
console_must_name_player_iris_tp_dimension_player = "La console deve specificare un giocatore: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} non è generato da Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teletrasporto non riuscito: la dimensione {dimensionId} non è caricata."
|
||||
teleport_failed_reason = "Teletrasporto verso {dimensionId} non riuscito: {reason}"
|
||||
teleport_cancelled_player_offline = "Teletrasporto verso {dimensionId} annullato: {value} non è più online."
|
||||
teleport_refused = "Il server ha rifiutato il teletrasporto verso {dimensionId}."
|
||||
teleporting = "Teletrasporto di {value} verso {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} non è generato da Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Impossibile evacuare il mondo principale: non esiste una destinazione per i giocatori."
|
||||
|
||||
@@ -772,6 +772,9 @@ opening_your_editor = "エディターで {value} {value2} を開いています
|
||||
console_must_name_player_iris_tp_dimension_player = "コンソールではプレイヤーを指定してください: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} は Iris によって生成されていません。"
|
||||
teleport_failed_dimension_is_not_loaded = "テレポートに失敗しました: ディメンション {dimensionId} は読み込まれていません。"
|
||||
teleport_failed_reason = "{dimensionId} へのテレポートに失敗しました: {reason}"
|
||||
teleport_cancelled_player_offline = "{dimensionId} へのテレポートを中止しました: {value} はオフラインです。"
|
||||
teleport_refused = "サーバーが {dimensionId} へのテレポートを拒否しました。"
|
||||
teleporting = "{value} を {dimensionId} にテレポートしています..."
|
||||
is_not_generated_by_iris_2 = "{value} は Iris によって生成されていません。"
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "プライマリーワールドからは退避できません。プレイヤーの移動先がありません。"
|
||||
|
||||
@@ -772,6 +772,9 @@ opening_your_editor = "편집기에서 {value} {value2} 여는 중입니다."
|
||||
console_must_name_player_iris_tp_dimension_player = "콘솔은 플레이어를 지정해야 합니다: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} 에 의해 생성되지 않음 Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleport 실패: 차원 {dimensionId} 로드 되지 않습니다."
|
||||
teleport_failed_reason = "{dimensionId}(으)로 순간이동 실패: {reason}"
|
||||
teleport_cancelled_player_offline = "{dimensionId}(으)로 순간이동 취소됨: {value} 님이 더 이상 접속해 있지 않습니다."
|
||||
teleport_refused = "서버가 {dimensionId}(으)로의 순간이동을 거부했습니다."
|
||||
teleporting = "팟캐스트 {value} 으로 {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} 에 의해 생성되지 않음 Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "1 차 세계를 피할 수 없습니다. 플레이어에게 보내는 곳이 없습니다."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "Atidaryti {value} {value2} redaktoriuje."
|
||||
console_must_name_player_iris_tp_dimension_player = "Konsolė turi pavadinti žaidėją: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} generuoja ne Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleportas nepavyko: dimensija {dimensionId} neįkrauta."
|
||||
teleport_failed_reason = "Teleportavimas į {dimensionId} nepavyko: {reason}"
|
||||
teleport_cancelled_player_offline = "Teleportavimas į {dimensionId} atšauktas: {value} nebėra prisijungęs."
|
||||
teleport_refused = "Serveris atmetė teleportavimą į {dimensionId}."
|
||||
teleporting = "Teleportavimas {value} iki {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} generuoja ne Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Negalima evakuoti pirminio pasaulio; nėra kur siųsti žaidėjų."
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Openen {value} {value2} in uw redacteur."
|
||||
console_must_name_player_iris_tp_dimension_player = "Console moet een speler een naam geven: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} wordt niet gegenereerd door Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleporteren mislukt: dimensie {dimensionId} is niet geladen."
|
||||
teleport_failed_reason = "Teleporteren naar {dimensionId} mislukt: {reason}"
|
||||
teleport_cancelled_player_offline = "Teleporteren naar {dimensionId} geannuleerd: {value} is niet meer online."
|
||||
teleport_refused = "De server heeft het teleporteren naar {dimensionId} geweigerd."
|
||||
teleporting = "Teleporteren {value} tot {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} wordt niet gegenereerd door Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Kan de primaire wereld niet evacueren; er is geen plek om spelers te sturen."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "Otwarcie {value} {value2} w twoim wydawcy."
|
||||
console_must_name_player_iris_tp_dimension_player = "Konsola musi nazwać gracza: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} nie jest generowany przez Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleport nie powiódł się: wymiar {dimensionId} nie jest załadowany."
|
||||
teleport_failed_reason = "Teleportacja do {dimensionId} nie powiodła się: {reason}"
|
||||
teleport_cancelled_player_offline = "Teleportacja do {dimensionId} anulowana: {value} nie jest już online."
|
||||
teleport_refused = "Serwer odrzucił teleportację do {dimensionId}."
|
||||
teleporting = "Teleportowanie {value} do {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} nie jest generowany przez Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Nie można ewakuować głównego świata; nie ma gdzie wysłać graczy."
|
||||
|
||||
@@ -769,6 +769,9 @@ opening_your_editor = "Abertura {value} {value2} no seu editor."
|
||||
console_must_name_player_iris_tp_dimension_player = "A consola deve nomear um jogador: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} não é gerado por Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Falha no teletransporte: dimensão {dimensionId} não está carregada."
|
||||
teleport_failed_reason = "Falha no teletransporte para {dimensionId}: {reason}"
|
||||
teleport_cancelled_player_offline = "Teletransporte para {dimensionId} cancelado: {value} já não está online."
|
||||
teleport_refused = "O servidor recusou o teletransporte para {dimensionId}."
|
||||
teleporting = "Teletransportando {value} para {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} não é gerado por Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Não é possível evacuar o mundo primário; não há nenhum lugar para enviar jogadores."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "Открытие {value} {value2} В вашем реда
|
||||
console_must_name_player_iris_tp_dimension_player = "Консоль должна называть игрока: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} не генерируется путем Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Телепорт провалился: измерение {dimensionId} не загружается."
|
||||
teleport_failed_reason = "Не удалось телепортироваться в {dimensionId}: {reason}"
|
||||
teleport_cancelled_player_offline = "Телепортация в {dimensionId} отменена: {value} больше не в сети."
|
||||
teleport_refused = "Сервер отклонил телепортацию в {dimensionId}."
|
||||
teleporting = "телепортация {value} то {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} не генерируется путем Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Нельзя эвакуировать основной мир, негде отправить игроков."
|
||||
|
||||
@@ -774,6 +774,9 @@ opening_your_editor = "Açılış {value} {value2} editörünüzde."
|
||||
console_must_name_player_iris_tp_dimension_player = "Konsol bir oyuncu adı olmalıdır: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} tarafından üretilmiyor Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleport başarısız oldu: Boyut {dimensionId} yüklenemez."
|
||||
teleport_failed_reason = "{dimensionId} boyutuna ışınlanma başarısız oldu: {reason}"
|
||||
teleport_cancelled_player_offline = "{dimensionId} boyutuna ışınlanma iptal edildi: {value} artık çevrimiçi değil."
|
||||
teleport_refused = "Sunucu {dimensionId} boyutuna ışınlanmayı reddetti."
|
||||
teleporting = "Telporting {value} tom için {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} tarafından üretilmiyor Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "birincil dünyayı tahliye edemez; oyuncuları göndermek için hiçbir yer yoktur."
|
||||
|
||||
@@ -772,6 +772,9 @@ opening_your_editor = "Mở {value} {value2} trong biên tập của anh."
|
||||
console_must_name_player_iris_tp_dimension_player = "Cần phải đặt tên người chơi: /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} không được tạo ra bởi Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Chuyển tiếp thất bại: chiều {dimensionId} không nạp đạn."
|
||||
teleport_failed_reason = "Dịch chuyển tới {dimensionId} thất bại: {reason}"
|
||||
teleport_cancelled_player_offline = "Đã hủy dịch chuyển tới {dimensionId}: {value} không còn trực tuyến."
|
||||
teleport_refused = "Máy chủ đã từ chối dịch chuyển tới {dimensionId}."
|
||||
teleporting = "Chuyển tiếp {value} phải {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} không được tạo ra bởi Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "Không thể sơ tán thế giới chính; không có nơi nào để gửi cầu thủ."
|
||||
|
||||
@@ -772,6 +772,9 @@ opening_your_editor = "开幕 {value} {value2} 在你的编辑。"
|
||||
console_must_name_player_iris_tp_dimension_player = "控制台必须给玩家命名 : /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} 不由 Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleport 失败: 维度 {dimensionId} 未加载。"
|
||||
teleport_failed_reason = "传送到 {dimensionId} 失败:{reason}"
|
||||
teleport_cancelled_player_offline = "已取消传送到 {dimensionId}:{value} 已不在线。"
|
||||
teleport_refused = "服务器拒绝了传送到 {dimensionId}。"
|
||||
teleporting = "远程传送 {value} 改为: {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} 不由 Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "无法撤离主世界; 没有地方可以发送玩家 。"
|
||||
|
||||
@@ -772,6 +772,9 @@ opening_your_editor = "開幕 {value} {value2} 在你的編輯。"
|
||||
console_must_name_player_iris_tp_dimension_player = "控制檯必須給玩家命名 : /iris tp <dimension> <player>"
|
||||
is_not_generated_by_iris = "{value} 不由 Iris."
|
||||
teleport_failed_dimension_is_not_loaded = "Teleport 失敗: 維度 {dimensionId} 未載入。"
|
||||
teleport_failed_reason = "傳送到 {dimensionId} 失敗:{reason}"
|
||||
teleport_cancelled_player_offline = "已取消傳送到 {dimensionId}:{value} 已不在線上。"
|
||||
teleport_refused = "伺服器拒絕了傳送到 {dimensionId}。"
|
||||
teleporting = "遠端傳送 {value} 改為: {dimensionId}..."
|
||||
is_not_generated_by_iris_2 = "{value} 不由 Iris."
|
||||
cannot_evacuate_primary_world_there_is_nowhere_send_players = "無法撤離主世界; 沒有地方可以傳送玩家 。"
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.cave.CarvingMode;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleObjectComponentEngineFailureTest {
|
||||
private IrisSettings previousSettings;
|
||||
|
||||
@Before
|
||||
public void bindSettings() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
}
|
||||
|
||||
@After
|
||||
public void restoreSettings() {
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closedHistoryRouterDuringPlacementFailsTheChunkAndCachesNoPlan() {
|
||||
GenerationClosedException closed = new GenerationClosedException("Generation-history runtime router is closed.");
|
||||
Fixture fixture = new Fixture(closed);
|
||||
|
||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
fixture.generate();
|
||||
fail("object placement swallowed a closed generation-history router");
|
||||
} catch (GenerationClosedException failure) {
|
||||
assertSame(closed, failure);
|
||||
}
|
||||
assertEquals(attempt, fixture.placements.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectSpecificFailureSkipsOnlyThatObject() {
|
||||
Fixture fixture = new Fixture(new IllegalArgumentException("malformed object"));
|
||||
|
||||
fixture.generate();
|
||||
|
||||
assertEquals(1, fixture.placements.get());
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final AtomicInteger placements = new AtomicInteger();
|
||||
private final MantleObjectComponent component;
|
||||
private final MantleWriter writer;
|
||||
private final ChunkContext context;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture(RuntimeException placementFailure) {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
|
||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
IrisObject object = mock(IrisObject.class);
|
||||
when(object.getLoadKey()).thenReturn("trees/closed-router");
|
||||
when(object.getW()).thenReturn(1);
|
||||
when(object.getH()).thenReturn(1);
|
||||
when(object.getD()).thenReturn(1);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
placements.incrementAndGet();
|
||||
throw placementFailure;
|
||||
}).when(object).place(anyInt(), anyInt(), anyInt(), any(IObjectPlacer.class), any(IrisObjectPlacement.class),
|
||||
any(RNG.class), any(), any(), any());
|
||||
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisObjectPlacement placement = mock(IrisObjectPlacement.class);
|
||||
when(placement.getChance()).thenReturn(2D);
|
||||
when(placement.getCarvingSupport()).thenReturn(CarvingMode.SURFACE_ONLY);
|
||||
when(placement.getPlace()).thenReturn(new KList<>("trees/closed-router"));
|
||||
when(placement.getAllowedCollisions()).thenReturn(new KList<>());
|
||||
when(placement.getForbiddenCollisions()).thenReturn(new KList<>());
|
||||
when(placement.getDensity(any(RNG.class), anyDouble(), anyDouble(), any())).thenReturn(1);
|
||||
when(placement.getObject(any(), any(RNG.class))).thenReturn(object);
|
||||
when(placement.scaleObject(any(RNG.class), any(), any())).thenReturn(object);
|
||||
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey("closed-router-biome");
|
||||
biome.setObjects(new KList<>(placement));
|
||||
IrisRegion region = new IrisRegion();
|
||||
region.setLoadKey("closed-router-region");
|
||||
|
||||
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
|
||||
when(regions.get(anyDouble(), anyDouble())).thenReturn(region);
|
||||
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
|
||||
when(biomes.get(anyDouble(), anyDouble())).thenReturn(biome);
|
||||
ProceduralStream<Integer> heights = mock(ProceduralStream.class);
|
||||
when(heights.get(anyDouble(), anyDouble())).thenReturn(64);
|
||||
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
|
||||
when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(true);
|
||||
when(complex.getRegionStream()).thenReturn(regions);
|
||||
when(complex.getTrueBiomeStream()).thenReturn(biomes);
|
||||
when(complex.getRoundedHeighteightStream()).thenReturn(heights);
|
||||
context = mock(ChunkContext.class);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.getWorldHeight()).thenReturn(256);
|
||||
writer = mock(MantleWriter.class);
|
||||
when(writer.getMantle()).thenReturn(mantle);
|
||||
when(writer.getEngine()).thenReturn(engine);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
Runnable task = invocation.getArgument(2);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
|
||||
component = new MantleObjectComponent(engineMantle);
|
||||
}
|
||||
|
||||
private void generate() {
|
||||
component.generateLayer(writer, 0, 0, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class GenerationFailuresTest {
|
||||
@Test
|
||||
public void engineAndLifecycleFailuresAreRecognizedThroughWrappers() {
|
||||
assertTrue(GenerationFailures.isEngineFailure(new GenerationClosedException("router closed")));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new MantleClosedException("Tectonic Plate is closed!")));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new RejectedExecutionException("pool shut down")));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new ClosedByInterruptException()));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new SavedBiomeUnavailableException("loading", true)));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new GenerationSessionException("sealed", true)));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new OutOfMemoryError()));
|
||||
assertTrue(GenerationFailures.isEngineFailure(new CompletionException(
|
||||
new IllegalStateException("placement failed", new GenerationClosedException("router closed")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentFailuresRemainSkippable() {
|
||||
assertFalse(GenerationFailures.isEngineFailure(new IllegalArgumentException("malformed object")));
|
||||
assertFalse(GenerationFailures.isEngineFailure(new NullPointerException("missing block")));
|
||||
assertFalse(GenerationFailures.isEngineFailure(new IllegalStateException("unrelated")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void permanentSavedBiomeUnavailabilityIsAContentFailure() {
|
||||
assertFalse(GenerationFailures.isEngineFailure(new SavedBiomeUnavailableException(
|
||||
"This chunk has no exact saved Iris biome assignment for generation 3.", false)));
|
||||
assertFalse(GenerationFailures.isEngineFailure(new SavedBiomeUnavailableException(
|
||||
"Unable to read the saved biome at chunk 0,0.", new IllegalArgumentException("bad header"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anyFailureOnAnInterruptedThreadIsAnEngineFailure() {
|
||||
Thread.currentThread().interrupt();
|
||||
try {
|
||||
assertTrue(GenerationFailures.isEngineFailure(new IllegalArgumentException("malformed object")));
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rethrowPropagatesEngineFailuresAndReturnsForContentFailures() {
|
||||
GenerationClosedException closed = new GenerationClosedException("router closed");
|
||||
try {
|
||||
GenerationFailures.rethrowEngineFailure(closed);
|
||||
fail("engine failure was not rethrown");
|
||||
} catch (GenerationClosedException rethrown) {
|
||||
assertSame(closed, rethrown);
|
||||
}
|
||||
|
||||
GenerationSessionException sealed = new GenerationSessionException("sealed", true);
|
||||
try {
|
||||
GenerationFailures.rethrowEngineFailure(sealed);
|
||||
fail("checked engine failure was not rethrown");
|
||||
} catch (IllegalStateException wrapped) {
|
||||
assertSame(sealed, wrapped.getCause());
|
||||
}
|
||||
|
||||
GenerationFailures.rethrowEngineFailure(new IllegalArgumentException("malformed object"));
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisEngineMaintenanceRefusalTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void maintenanceThatCannotRunMantleStagesRefusesTheChunkInsteadOfGeneratingIt() throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
|
||||
when(hooks.shouldRefuseGeneration(engine)).thenReturn(true);
|
||||
Field platformHooks = IrisEngine.class.getDeclaredField("platformHooks");
|
||||
platformHooks.setAccessible(true);
|
||||
platformHooks.set(engine, hooks);
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.name()).thenReturn("world");
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
doCallRealMethod().when(engine).generate(anyInt(), anyInt(), any(), any(), anyBoolean());
|
||||
Hunk<NativeBlockState> blocks = mock(Hunk.class);
|
||||
Hunk<NativeBiome> biomes = mock(Hunk.class);
|
||||
|
||||
GenerationSessionException refused = assertThrows(GenerationSessionException.class,
|
||||
() -> engine.generate(32, 48, blocks, biomes, false));
|
||||
|
||||
assertTrue(refused.isExpectedTeardown());
|
||||
verify(engine, never()).acquireGenerationLease(anyString());
|
||||
verify(engine, never()).getMode();
|
||||
verifyNoInteractions(blocks, biomes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user