Fixed mining and timeout settings

- Pending biome reads no longer cancel block breaking
- Custom loot resolves saved biome history after accepted breaks
- Native drops survive failed reads and runtime teardown without duplication
- Tree felling captures the original tool for fallback drops
- Modded block loot resolves one historical biome context
- Timeout settings persist atomically through the actual server configuration paths
- Tests cover deferred loot, teardown and timeout persistence
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-12 09:27:26 -04:00
1 parent 2145e90b1c
commit e3673afb1e
17 files changed
+1220 -329

No files matched your search

@@ -539,9 +539,20 @@ public class IrisEngine implements Engine {
if (saved.isPresent()) {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryReadScope(x, z, "resolve a biome environment")) {
return Engine.super.getBiomeEnvironment(x, y, z);
return resolveUnsavedBiomeEnvironment(x, y, z);
}
public CompletableFuture<BiomeEnvironment> getBiomeEnvironmentAsync(int x, int y, int z) {
try {
if (!usesSavedBiomeEnvironment()) {
return CompletableFuture.completedFuture(getBiomeEnvironment(x, y, z));
}
int worldY = y + getWorld().minHeight();
return generationHistoryRuntimeRouter.biomes().readChunkAsync(Math.floorDiv(x, 16), Math.floorDiv(z, 16))
.thenApply(session -> session.resolve(x, worldY, z, false)
.orElseGet(() -> resolveUnsavedBiomeEnvironment(x, y, z)));
} catch (RuntimeException | Error failure) {
return CompletableFuture.failedFuture(failure);
}
}
@@ -577,6 +588,13 @@ public class IrisEngine implements Engine {
return binding;
}
private BiomeEnvironment resolveUnsavedBiomeEnvironment(int x, int y, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryReadScope(x, z, "resolve a biome environment")) {
return Engine.super.getBiomeEnvironment(x, y, z);
}
}
private Optional<BiomeEnvironment> resolveSavedBiomeEnvironment(int x, int y, int z, boolean surface) {
if (!usesSavedBiomeEnvironment()) {
return Optional.empty();
@@ -38,11 +38,15 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Chunk;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -267,24 +271,38 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
blockDropRouter.onBlockBreak(e);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockDropItems(BlockDropItemEvent event) {
runManagerTask("bukkit_world_manager_block_drops", () -> blockDropRouter.onBlockDropItems(event));
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
blockDropRouter.onBlockPlace(e);
}
public CompletableFuture<Void> flushPendingBlockDropsAsync() {
return blockDropRouter.flushAsync();
}
@Override
public synchronized void close() {
Throwable failure = null;
try {
chunkMaintenance.close();
} catch (Throwable e) {
failure = e;
failure = appendCloseFailure(failure, e);
}
try {
super.close();
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
try {
blockDropRouter.close();
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
if (!looperStopped) {
try {
if (looper != null) {
@@ -20,13 +20,8 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.tree.BlockDropRouter;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.world.loot.IrisBlockDrops;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
@@ -35,81 +30,140 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Item;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
/**
* Biome, region and dimension block drops for a Bukkit Iris world. The drop list is resolved on the
* event thread, the marker cleanup runs asynchronously and the drops themselves are handed to the
* event's own router when it offers one so a deferred break keeps its inventory destination.
* Biome, region and dimension block drops for a Bukkit Iris world. Native loot is retained while
* saved biome definitions load, then finalized on the block's owning thread after the break.
*/
final class WorldBlockDropRouter {
private final IrisWorldManager manager;
private final Set<DeferredDrops> pending = ConcurrentHashMap.newKeySet();
private boolean closed;
WorldBlockDropRouter(IrisWorldManager manager) {
this.manager = manager;
}
void onBlockBreak(BlockBreakEvent e) {
if (e.isCancelled()) {
void onBlockBreak(BlockBreakEvent event) {
if (event.isCancelled() || !isManagedWorld(event.getBlock().getWorld())) {
return;
}
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) {
int blockX = e.getBlock().getX();
int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight());
int blockZ = e.getBlock().getZ();
List<IrisBlockDrops> dropProviders;
try {
dropProviders = resolveDropProviders(e);
} catch (SavedBiomeUnavailableException unavailable) {
e.setCancelled(true);
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
Location location = event.getBlock().getLocation();
int mantleY = toMantleY(location.getBlockY(), manager.getEngine().getWorld().minHeight());
Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> {
if (event.isCancelled()) {
return;
}
KList<ItemStack> d = new KList<>();
dropProviders.forEach(provider -> provider.fillDrops(false, d));
if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) {
e.setDropItems(false);
}
World w = e.getBlock().getWorld();
Location blockLocation = e.getBlock().getLocation();
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> {
if (e.isCancelled()) {
J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> {
MatterMarker marker = manager.getMantle().get(location.getBlockX(), mantleY, location.getBlockZ(), MatterMarker.class);
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> {
MatterMarker marker = manager.getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
manager.getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}));
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
});
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
J.s(finalizedBreak, 1);
IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
manager.getMantle().remove(location.getBlockX(), mantleY, location.getBlockZ(), MatterMarker.class);
}
}));
});
schedule(location, finalizedBreak);
if (!(event instanceof BlockDropRouter router) || !event.isDropItems()) {
return;
}
synchronized (pending) {
if (closed) {
return;
}
BlockData state = event.getBlock().getBlockData().clone();
if (!mayHaveBlockDropRules(state)) {
return;
}
Location dropLocation = location.clone().add(.5, .5, .5);
List<NativeDrop> originals = new ArrayList<>();
for (Object nativeDrop : router.captureNativeDrops()) {
if (nativeDrop instanceof ItemStack stack) {
originals.add(new NativeDrop(dropLocation, stack.clone()));
}
}
CompletableFuture<BiomeEnvironment> environment = environment(location);
if (defer(new DeferredDrops(location, state, router, () -> !event.isCancelled(), originals, environment, new AtomicBoolean()))) {
event.setDropItems(false);
}
}
}
void onBlockPlace(BlockPlaceEvent e) {
void onBlockDropItems(BlockDropItemEvent event) {
if (event.isCancelled() || !isManagedWorld(event.getBlock().getWorld())) {
return;
}
synchronized (pending) {
if (closed) {
return;
}
BlockData state = event.getBlockState().getBlockData().clone();
if (!mayHaveBlockDropRules(state)) {
return;
}
Location location = event.getBlockState().getLocation();
CompletableFuture<BiomeEnvironment> environment = environment(location);
List<NativeDrop> originals = new ArrayList<>(event.getItems().size());
for (Item item : event.getItems()) {
originals.add(new NativeDrop(location.clone().add(.5, .5, .5), item.getItemStack().clone()));
}
if (defer(new DeferredDrops(location, state, null, () -> !event.isCancelled(), originals, environment, new AtomicBoolean()))) {
event.getItems().clear();
}
}
}
void onBlockPlace(BlockPlaceEvent event) {
}
CompletableFuture<Void> flushAsync() {
List<DeferredDrops> outstanding;
synchronized (pending) {
outstanding = List.copyOf(pending);
}
List<CompletableFuture<Void>> restores = new ArrayList<>(outstanding.size());
for (DeferredDrops drops : outstanding) {
Location location = drops.location;
restores.add(J.runRegionFuture(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4,
() -> finish(drops, null, null)));
}
return CompletableFuture.allOf(restores.toArray(CompletableFuture<?>[]::new));
}
void close() {
synchronized (pending) {
closed = true;
}
try {
flushAsync().get(5L, TimeUnit.SECONDS);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while preserving pending Iris block loot.", interrupted);
} catch (ExecutionException | TimeoutException failure) {
throw new IllegalStateException("Unable to preserve pending Iris block loot before world shutdown.", failure);
}
}
static int toMantleY(int worldY, int minHeight) {
@@ -136,32 +190,137 @@ final class WorldBlockDropRouter {
return mantleY + minHeight;
}
private List<IrisBlockDrops> filterDrops(KList<IrisBlockDrops> drops, BlockBreakEvent e, IrisData data) {
return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList());
private boolean isManagedWorld(World world) {
return world.equals(BukkitWorldBinding.world(manager.getTarget().getWorld()));
}
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) {
if (!mayHaveBlockDropRules(BukkitBlockState.of(event.getBlock().getBlockData()).materialKey())) {
return List.of();
private CompletableFuture<BiomeEnvironment> environment(Location location) {
Engine engine = manager.getEngine();
int relativeY = toMantleY(location.getBlockY(), engine.getWorld().minHeight());
if (engine instanceof IrisEngine iris) {
return iris.getBiomeEnvironmentAsync(location.getBlockX(), relativeY, location.getBlockZ());
}
IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
try {
return CompletableFuture.completedFuture(engine.getBiomeEnvironment(location.getBlockX(), relativeY, location.getBlockZ()));
} catch (RuntimeException failure) {
return CompletableFuture.failedFuture(failure);
}
}
private boolean defer(DeferredDrops drops) {
pending.add(drops);
if (J.runAt(drops.location, () -> drops.environment.orTimeout(5L, TimeUnit.SECONDS).whenComplete((environment, failure) -> {
if (drops.finished.get()) {
return;
}
J.runRegionFuture(drops.location.getWorld(), drops.location.getBlockX() >> 4, drops.location.getBlockZ() >> 4,
() -> finish(drops, environment, failure)).whenComplete((ignored, schedulingFailure) -> {
if (schedulingFailure != null) {
IrisLogging.reportError("Unable to finalize pending Iris block loot on its owning thread.", schedulingFailure);
}
});
}), 1)) {
return true;
}
pending.remove(drops);
IrisLogging.reportError("Unable to schedule Iris block loot. Preserving normal block loot.",
new IllegalStateException("The owning region did not accept the block-drop task."));
return false;
}
private void schedule(Location location, Runnable task) {
if (!J.runAt(location, task, 1) && !J.isFolia()) {
J.s(task, 1);
}
}
private void finish(DeferredDrops drops, BiomeEnvironment environment, Throwable failure) {
if (!drops.finished.compareAndSet(false, true)) {
return;
}
pending.remove(drops);
if (!drops.allowed.getAsBoolean()) {
return;
}
KList<ItemStack> custom = new KList<>();
boolean replaceVanilla = false;
if (failure == null && environment != null) {
try {
DropResult result = EngineLifecycleTasks.call(manager.getEngine(), "bukkit_world_manager_block_drop_roll",
() -> rollDrops(drops.state, environment), null);
if (result != null) {
custom = result.items();
replaceVanilla = result.replaceVanilla();
}
} catch (Throwable error) {
failure = error;
}
}
if (failure != null) {
Throwable cause = failure instanceof CompletionException && failure.getCause() != null ? failure.getCause() : failure;
String context = "Unable to resolve Iris block drops at " + drops.location + ". Preserving normal block loot.";
if (IrisLogging.warnOnce("block-drops:" + drops.location.getWorld().getUID() + ":" + cause.getMessage(), context)) {
IrisLogging.reportError(context, cause);
}
}
if (!replaceVanilla) {
for (NativeDrop original : drops.originals) {
routeDrops(List.of(original.stack()), drops.router, stack -> original.location().getWorld().dropItemNaturally(original.location(), stack));
}
}
Location location = drops.location.clone().add(.5, .5, .5);
routeDrops(custom, drops.router, stack -> location.getWorld().dropItemNaturally(location, stack));
}
private DropResult rollDrops(BlockData state, BiomeEnvironment environment) {
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(environment)) {
KList<ItemStack> items = new KList<>();
boolean replaceVanilla = false;
for (IrisBlockDrops provider : resolveDropProviders(state, environment)) {
provider.fillDrops(false, items);
replaceVanilla |= provider.isReplaceVanillaDrops();
}
return new DropResult(items, replaceVanilla);
}
}
private List<IrisBlockDrops> resolveDropProviders(BlockData state, BiomeEnvironment environment) {
List<IrisBlockDrops> providers = new ArrayList<>();
addMatching(providers, environment.biome().getBlockDrops(), state, environment.data());
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
IrisRegion region = EngineBukkitOps.getRegion(manager.getEngine(), event.getBlock().getLocation());
providers.addAll(filterDrops(region.getBlockDrops(), event, manager.getData()));
providers.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), event, manager.getData()));
addMatching(providers, environment.region().getBlockDrops(), state, environment.data());
addMatching(providers, environment.dimension().getBlockDrops(), state, environment.data());
}
return providers;
}
private boolean mayHaveBlockDropRules(String material) {
private void addMatching(List<IrisBlockDrops> providers, KList<IrisBlockDrops> candidates, BlockData state, IrisData data) {
for (IrisBlockDrops candidate : candidates) {
if (candidate.shouldDropFor(state, data)) {
providers.add(candidate);
}
}
}
private boolean mayHaveBlockDropRules(BlockData state) {
String material = BukkitBlockState.of(state).materialKey();
if (manager.getData().hasBlockDropRules(material)) {
return true;
}
if (manager.getEngine() instanceof IrisEngine engine) {
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
return router != null && router.biomes().mayHaveBlockDropRules(material);
return engine.getGenerationHistoryRuntimeRouter().map(router -> router.biomes().mayHaveBlockDropRules(material)).orElse(false);
}
return false;
}
private record DropResult(KList<ItemStack> items, boolean replaceVanilla) {
}
private record NativeDrop(Location location, ItemStack stack) {
}
private record DeferredDrops(Location location, BlockData state, BlockDropRouter router, BooleanSupplier allowed,
List<NativeDrop> originals, CompletableFuture<BiomeEnvironment> environment,
AtomicBoolean finished) {
}
}
@@ -44,14 +44,15 @@ import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.io.AtomicFileIO;
import art.arcane.iris.platform.bootstrap.ServerProperties;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
@@ -99,11 +100,17 @@ public class ServerConfigurator {
}
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
if (s.isConfigureSpigotTimeoutTime()) {
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
Throwable failure = J.attemptCatch(ServerConfigurator::increaseKeepAliveSpigot);
if (failure != null) {
IrisLogging.reportError("Unable to persist the Spigot timeout setting.", failure);
}
}
if (s.isConfigurePaperWatchdogDelay()) {
J.attempt(ServerConfigurator::increasePaperWatchdog);
if (s.isConfigurePaperWatchdogDelay() && BukkitPlatform.isPaperServer()) {
Throwable failure = J.attemptCatch(ServerConfigurator::increasePaperWatchdog);
if (failure != null) {
IrisLogging.reportError("Unable to persist the Paper watchdog setting.", failure);
}
}
if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) {
@@ -230,35 +237,61 @@ public class ServerConfigurator {
}
}
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
File spigotConfig = new File("spigot.yml");
FileConfiguration f = new YamlConfiguration();
f.load(spigotConfig);
long tt = f.getLong("settings.timeout-time");
private static void increaseKeepAliveSpigot() throws IOException, ReflectiveOperationException {
increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig());
}
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration) throws IOException {
long previous = configuration.getLong("settings.timeout-time");
long minimum = TimeUnit.MINUTES.toSeconds(5);
if (previous >= minimum) {
return;
}
persistTimeout(file, configuration, "settings.timeout-time", minimum);
IrisLogging.warn("Updated " + file + " settings.timeout-time: " + previous + " -> " + minimum
+ " seconds. Takes effect after a server restart.");
IrisLogging.warn("Disable autoConfiguration.configureSpigotTimeoutTime in iris.json to manage this setting yourself.");
}
if (tt < spigotTimeout) {
IrisLogging.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
f.set("settings.timeout-time", spigotTimeout);
f.save(spigotConfig);
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException, ReflectiveOperationException {
increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml"));
}
static void increasePaperWatchdog(Path file) throws IOException, InvalidConfigurationException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.load(file.toFile());
long previous = configuration.getLong("watchdog.early-warning-delay");
long minimum = TimeUnit.MINUTES.toMillis(3);
if (previous >= minimum) {
return;
}
persistTimeout(file, configuration, "watchdog.early-warning-delay", minimum);
IrisLogging.warn("Updated " + file + " watchdog.early-warning-delay: " + previous + " -> " + minimum
+ " milliseconds. Takes effect after a server restart.");
IrisLogging.warn("Disable autoConfiguration.configurePaperWatchdogDelay in iris.json to manage this setting yourself.");
}
private static void persistTimeout(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException {
Path target = file.toRealPath();
Object previous = configuration.isSet(key) ? configuration.get(key) : null;
configuration.set(key, minimum);
try {
AtomicFileIO.writeString(target, configuration.saveToString());
} catch (IOException | RuntimeException failure) {
configuration.set(key, previous);
throw failure;
}
}
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
File spigotConfig = new File("config/paper-global.yml");
FileConfiguration f = new YamlConfiguration();
f.load(spigotConfig);
long tt = f.getLong("watchdog.early-warning-delay");
long watchdog = TimeUnit.MINUTES.toMillis(3);
if (tt < watchdog) {
IrisLogging.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
f.set("watchdog.early-warning-delay", watchdog);
f.save(spigotConfig);
static Path serverConfigurationOption(String name) throws ReflectiveOperationException, IOException {
Server server = Bukkit.getServer();
Object console = server.getClass().getMethod("getServer").invoke(server);
Object options = console.getClass().getField("options").get(console);
Object value = options.getClass().getMethod("valueOf", String.class).invoke(options, name);
if (!(value instanceof File file)) {
throw new IOException("Server option " + name + " did not provide a configuration path.");
}
return file.toPath().toAbsolutePath().normalize();
}
public static KList<File> getDatapacksFolder() {
@@ -42,6 +42,7 @@ import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.IrisWorldManager;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.chunk.TerrainChunk;
import art.arcane.iris.generation.runtime.Engine;
@@ -928,29 +929,36 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
boolean alreadyClosing = closing;
closing = true;
cancelWatcherHotload();
AtomicBoolean dispatchAccepted = new AtomicBoolean();
CompletableFuture<Void> operation;
try {
operation = withExclusiveControlFuture(() -> {
Looper activeHotloader = hotloader;
hotloader = null;
if (isStudio() && activeHotloader != null) {
activeHotloader.interrupt();
try {
activeHotloader.join(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
CompletableFuture<Void> preservedDrops = engine != null && engine.getWorldManager() instanceof IrisWorldManager manager
? manager.flushPendingBlockDropsAsync() : CompletableFuture.completedFuture(null);
operation = preservedDrops.thenCompose(ignored -> {
CompletableFuture<Void> dispatched = withExclusiveControlFuture(() -> {
Looper activeHotloader = hotloader;
hotloader = null;
if (isStudio() && activeHotloader != null) {
activeHotloader.interrupt();
try {
activeHotloader.join(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
}
}
}
Engine currentEngine = engine;
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
folder.clear();
populators.clear();
closeStartupTarget();
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
Engine currentEngine = engine;
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
folder.clear();
populators.clear();
closeStartupTarget();
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
});
dispatchAccepted.set(true);
return dispatched;
});
} catch (Throwable throwable) {
if (!alreadyClosing) {
@@ -969,9 +977,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (throwable == null) {
future.complete(null);
} else {
// The close body already ran and tore the engine down; unlike the pre-dispatch
// failure above, resetting the gate here would advertise a healthy generator
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
// An accepted close may have torn the engine down. Reset the gate only when
// exclusive cleanup was never dispatched, or a CLOSED or FAILED engine would
// be advertised as a healthy generator.
if (!alreadyClosing && !dispatchAccepted.get()) {
closing = false;
}
closeFuture.compareAndSet(future, null);
future.completeExceptionally(throwable);
}
@@ -1,6 +1,9 @@
package art.arcane.iris.world.tree;
@FunctionalInterface
import java.util.Collection;
public interface BlockDropRouter {
Collection<?> captureNativeDrops();
boolean routeDrop(Object drop);
}