mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
dwa
This commit is contained in:
@@ -492,7 +492,9 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener {
|
||||
}
|
||||
|
||||
long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds());
|
||||
EventQueue.invokeLater(() -> {
|
||||
// Pace on a worker, not the EDT: a sleep queued on the event thread blocks painting
|
||||
// and input for the whole frame budget. repaint() marshals itself back.
|
||||
J.a(() -> {
|
||||
J.sleep(sleepMs);
|
||||
repaint();
|
||||
});
|
||||
|
||||
@@ -124,7 +124,16 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
service.shutdown();
|
||||
throw new IllegalStateException("An Iris pregeneration job is already running; stop it first.");
|
||||
}
|
||||
worker.start();
|
||||
try {
|
||||
worker.start();
|
||||
} catch (Throwable startFailure) {
|
||||
// Un-publish: a worker that never started can never run onClose(), so nothing
|
||||
// else would ever clear this instance via the normal path.
|
||||
instance.compareAndSet(this, null);
|
||||
monitor.close();
|
||||
service.shutdown();
|
||||
throw startFailure;
|
||||
}
|
||||
}
|
||||
|
||||
private void computeBounds() {
|
||||
|
||||
@@ -528,14 +528,16 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
});
|
||||
return;
|
||||
}
|
||||
double frameMs = 0;
|
||||
try {
|
||||
paintBody(gx);
|
||||
frameMs = paintBody(gx);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.debug("Vision paint failed: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
} finally {
|
||||
// The repaint loop is entirely self-driven from here; it must survive any render
|
||||
// exception or the window freezes on a stale frame permanently.
|
||||
long sleepMs = eco ? 32 : 16;
|
||||
// exception or the window freezes on a stale frame permanently. Frame-time
|
||||
// compensated so a slow frame does not stack a full sleep on top of itself.
|
||||
long sleepMs = Math.max(1, (eco ? 32 : 16) - (long) frameMs);
|
||||
J.a(() -> {
|
||||
J.sleep(sleepMs);
|
||||
repaint();
|
||||
@@ -543,7 +545,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
}
|
||||
}
|
||||
|
||||
private void paintBody(Graphics gx) {
|
||||
private double paintBody(Graphics gx) {
|
||||
|
||||
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
|
||||
oxp = lerp(oxp, ox, 0.36);
|
||||
@@ -630,11 +632,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
|
||||
handleFollow();
|
||||
renderOverlays(g, p.getMilliseconds());
|
||||
|
||||
if (!isVisible() || !getParent().isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return p.getMilliseconds();
|
||||
}
|
||||
|
||||
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
|
||||
|
||||
@@ -255,6 +255,13 @@ public final class BukkitWorldConfiguration {
|
||||
}
|
||||
Files.move(staged, absoluteTarget, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
// bukkit.yml is the commit record the bootstrap reconciles against; its rename
|
||||
// needs the same directory-durability barrier the journal and world moves get.
|
||||
if (requireAtomicReplacement) {
|
||||
DirectoryDurability.forceDirectoryRequired(parent);
|
||||
} else {
|
||||
DirectoryDurability.forceDirectoryAfterCommit(parent, "A bukkit.yml world configuration change");
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(staged);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
/**
|
||||
* Directory-level durability barrier shared by every atomic publication in the
|
||||
* world-replacement protocol (journal writes, world-directory moves, bukkit.yml
|
||||
* saves). A rename is only durable once its parent directory has been fsynced;
|
||||
* skipped on Windows, where directory handles cannot be forced.
|
||||
*/
|
||||
final class DirectoryDurability {
|
||||
private DirectoryDurability() {
|
||||
}
|
||||
|
||||
static void forceDirectoryRequired(Path directory) throws IOException {
|
||||
if (File.separatorChar == '\\') {
|
||||
return;
|
||||
}
|
||||
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
|
||||
channel.force(true);
|
||||
} catch (UnsupportedOperationException failure) {
|
||||
throw new IOException("Directory durability sync is unavailable for " + directory + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
static void forceDirectoryAfterCommit(Path directory, String context) {
|
||||
try {
|
||||
forceDirectoryRequired(directory);
|
||||
} catch (IOException failure) {
|
||||
IrisLogging.reportError(
|
||||
context + " completed, but its parent directory could not be durability-synced.",
|
||||
failure
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
import java.io.File;
|
||||
@@ -479,6 +480,10 @@ final class WorldLifecycleSupport {
|
||||
}
|
||||
}
|
||||
|
||||
if (!announceManualWorldUnload(world)) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
if (save) {
|
||||
world.save();
|
||||
}
|
||||
@@ -496,6 +501,12 @@ final class WorldLifecycleSupport {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean announceManualWorldUnload(World world) {
|
||||
WorldUnloadEvent unloadEvent = new WorldUnloadEvent(world);
|
||||
Bukkit.getPluginManager().callEvent(unloadEvent);
|
||||
return !unloadEvent.isCancelled();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) {
|
||||
if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) {
|
||||
return null;
|
||||
@@ -597,9 +608,11 @@ final class WorldLifecycleSupport {
|
||||
Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds");
|
||||
Object rawWorlds = worldsField.get(bukkitServer);
|
||||
if (rawWorlds instanceof Map map) {
|
||||
map.remove(WorldIdentity.key(world));
|
||||
map.remove(WorldIdentity.serialize(world));
|
||||
map.remove(world.getName());
|
||||
boolean removed = map.values().removeIf(candidate -> candidate == world);
|
||||
if (!removed) {
|
||||
throw new IllegalStateException(
|
||||
"CraftServer world registry did not contain \"" + world.getName() + "\".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,20 @@ public final class WorldReplacementBootstrap {
|
||||
int published = 0;
|
||||
int rolledBack = 0;
|
||||
int retained = 0;
|
||||
int skipped = 0;
|
||||
for (Transaction transaction : transactions) {
|
||||
// A journal recorded against another level root or logical world name is not this
|
||||
// boot's transaction; skip it with a pointer instead of aborting the whole startup.
|
||||
if (!WorldReplacementJournal.appliesTo(transaction, requiredLevelRoot)) {
|
||||
skipped++;
|
||||
requiredFeedback.accept("Skipping pending world replacement " + transaction.id()
|
||||
+ " for " + transaction.worldKey() + ": it was staged against level root "
|
||||
+ transaction.levelRoot() + " but this server now uses " + requiredLevelRoot
|
||||
+ ". Remove " + requiredDataDirectory.resolve(WorldReplacementJournal.DIRECTORY_NAME)
|
||||
.resolve(transaction.id() + ".properties")
|
||||
+ " or restore the previous level-name to resolve it.");
|
||||
continue;
|
||||
}
|
||||
ReconcileAction action = reconcileTransaction(
|
||||
requiredDataDirectory,
|
||||
requiredLevelRoot,
|
||||
@@ -52,7 +65,7 @@ public final class WorldReplacementBootstrap {
|
||||
case RETAINED -> retained++;
|
||||
}
|
||||
}
|
||||
return new ReconcileResult(transactions.size(), published, rolledBack, retained);
|
||||
return new ReconcileResult(transactions.size(), published, rolledBack, retained, skipped);
|
||||
}
|
||||
|
||||
public static WorldGeneratorSnapshot replacementSnapshot(Transaction transaction) {
|
||||
@@ -124,11 +137,20 @@ public final class WorldReplacementBootstrap {
|
||||
}
|
||||
throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state.");
|
||||
}
|
||||
WorldReplacementFilesystem.publish(
|
||||
paths,
|
||||
active.originalTargetPresent(),
|
||||
active.packFingerprint()
|
||||
);
|
||||
try {
|
||||
WorldReplacementFilesystem.publish(
|
||||
paths,
|
||||
active.originalTargetPresent(),
|
||||
active.packFingerprint()
|
||||
);
|
||||
} catch (IOException publishFailure) {
|
||||
throw new IOException("Pending replacement for " + active.worldKey()
|
||||
+ " cannot be published: " + publishFailure.getMessage()
|
||||
+ " To recover, restore the original generator entry for \"" + active.worldName()
|
||||
+ "\" in bukkit.yml (the next boot rolls the replacement back), or delete the journal at "
|
||||
+ dataDirectory.resolve(WorldReplacementJournal.DIRECTORY_NAME).resolve(active.id() + ".properties")
|
||||
+ ".", publishFailure);
|
||||
}
|
||||
active = active.withPhase(Phase.PUBLISHED);
|
||||
WorldReplacementJournal.write(dataDirectory, active);
|
||||
feedback.accept("Published Iris world replacement for " + active.worldKey()
|
||||
@@ -227,7 +249,7 @@ public final class WorldReplacementBootstrap {
|
||||
return new IOException("Pending replacement for " + transaction.worldKey() + " is blocked: " + detail);
|
||||
}
|
||||
|
||||
public record ReconcileResult(int transactions, int published, int rolledBack, int retained) {
|
||||
public record ReconcileResult(int transactions, int published, int rolledBack, int retained, int skipped) {
|
||||
}
|
||||
|
||||
private enum ReconcileAction {
|
||||
|
||||
@@ -2,9 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -13,6 +11,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
@@ -61,6 +60,26 @@ public final class WorldReplacementFilesystem {
|
||||
if (state.stagePresent() || state.backupPresent()) {
|
||||
throw new IOException("A replacement artifact already exists for this transaction.");
|
||||
}
|
||||
requireMigratedRetainedWorld(requiredPaths.target());
|
||||
}
|
||||
|
||||
public static void requireMigratedRetainedWorld(Path retainedWorld) throws IOException {
|
||||
try {
|
||||
requireDirectory(retainedWorld, "retained world");
|
||||
requireDirectory(retainedWorld.resolve("data"), "retained world data");
|
||||
requireDirectory(retainedWorld.resolve("data/paper"), "retained Paper data");
|
||||
requireDirectory(retainedWorld.resolve("data/minecraft"), "retained Minecraft data");
|
||||
for (Path relative : PAPER_WORLD_METADATA) {
|
||||
BasicFileAttributes sourceAttributes = requireSafeEntry(retainedWorld.resolve(relative));
|
||||
if (!sourceAttributes.isRegularFile()) {
|
||||
throw new IOException("Retained Paper world metadata is not a regular file: " + relative);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
throw new IOException("World slot " + retainedWorld.getFileName()
|
||||
+ " is missing Paper world metadata (" + e.getFile()
|
||||
+ "); load the world once on this server before replacing it.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void publish(
|
||||
@@ -328,17 +347,8 @@ public final class WorldReplacementFilesystem {
|
||||
}
|
||||
|
||||
private static void preservePaperWorldMetadata(Path retainedWorld, Path replacementWorld) throws IOException {
|
||||
requireDirectory(retainedWorld, "retained world");
|
||||
requireMigratedRetainedWorld(retainedWorld);
|
||||
requireDirectory(replacementWorld, "replacement world");
|
||||
requireDirectory(retainedWorld.resolve("data"), "retained world data");
|
||||
requireDirectory(retainedWorld.resolve("data/paper"), "retained Paper data");
|
||||
requireDirectory(retainedWorld.resolve("data/minecraft"), "retained Minecraft data");
|
||||
for (Path relative : PAPER_WORLD_METADATA) {
|
||||
BasicFileAttributes sourceAttributes = requireSafeEntry(retainedWorld.resolve(relative));
|
||||
if (!sourceAttributes.isRegularFile()) {
|
||||
throw new IOException("Retained Paper world metadata is not a regular file: " + relative);
|
||||
}
|
||||
}
|
||||
ensureDirectory(replacementWorld.resolve("data"), "replacement world data");
|
||||
ensureDirectory(replacementWorld.resolve("data/paper"), "replacement Paper data");
|
||||
ensureDirectory(replacementWorld.resolve("data/minecraft"), "replacement Minecraft data");
|
||||
@@ -420,25 +430,11 @@ public final class WorldReplacementFilesystem {
|
||||
}
|
||||
|
||||
private static void forceDirectoryRequired(Path directory) throws IOException {
|
||||
if (File.separatorChar == '\\') {
|
||||
return;
|
||||
}
|
||||
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
|
||||
channel.force(true);
|
||||
} catch (UnsupportedOperationException failure) {
|
||||
throw new IOException("Directory durability sync is unavailable for " + directory + ".", failure);
|
||||
}
|
||||
DirectoryDurability.forceDirectoryRequired(directory);
|
||||
}
|
||||
|
||||
private static void forceDirectoryAfterCommit(Path directory) {
|
||||
try {
|
||||
forceDirectoryRequired(directory);
|
||||
} catch (IOException failure) {
|
||||
IrisLogging.reportError(
|
||||
"A world-replacement move completed, but its parent directory could not be durability-synced.",
|
||||
failure
|
||||
);
|
||||
}
|
||||
DirectoryDurability.forceDirectoryAfterCommit(directory, "A world-replacement move");
|
||||
}
|
||||
|
||||
public record ReplacementPaths(Path target, Path stage, Path backup) {
|
||||
|
||||
@@ -3,10 +3,8 @@ package art.arcane.iris.core.lifecycle;
|
||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -46,19 +44,50 @@ public final class WorldReplacementJournal {
|
||||
if (Files.isSymbolicLink(file) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Replacement journal entry is unsafe: " + file);
|
||||
}
|
||||
transactions.add(read(file, currentLevelRoot));
|
||||
try {
|
||||
transactions.add(read(file));
|
||||
} catch (IOException failure) {
|
||||
throw new IOException("Invalid replacement journal " + file + ": " + failure.getMessage(), failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
transactions.sort(Comparator.comparing(transaction -> transaction.id().toString()));
|
||||
// Only transactions that target the current level root compete for a slot; a stale
|
||||
// journal recorded against another level root must not collide with a live one.
|
||||
Set<WorldSlotKey> worldKeys = new HashSet<>();
|
||||
for (Transaction transaction : transactions) {
|
||||
if (!worldKeys.add(transaction.worldKey())) {
|
||||
if (appliesTo(transaction, currentLevelRoot) && !worldKeys.add(transaction.worldKey())) {
|
||||
throw new IOException("Multiple replacement journals target " + transaction.worldKey() + ".");
|
||||
}
|
||||
}
|
||||
return List.copyOf(transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this journal entry targets the current level root and logical world name. A
|
||||
* mismatch means the operator changed level-name or world container after the replacement
|
||||
* was staged; such entries are skipped at bootstrap instead of aborting the whole boot.
|
||||
*/
|
||||
public static boolean appliesTo(Transaction transaction, Path currentLevelRoot) {
|
||||
Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction");
|
||||
ExactWorldSlotPathPolicy.Target target;
|
||||
try {
|
||||
target = ExactWorldSlotPathPolicy.resolve(currentLevelRoot, requiredTransaction.worldKey());
|
||||
} catch (RuntimeException failure) {
|
||||
return false;
|
||||
}
|
||||
if (!target.levelRoot().equals(requiredTransaction.levelRoot())) {
|
||||
return false;
|
||||
}
|
||||
String expectedWorldName;
|
||||
try {
|
||||
expectedWorldName = logicalWorldName(target.levelRoot(), requiredTransaction.worldKey());
|
||||
} catch (IllegalArgumentException failure) {
|
||||
return false;
|
||||
}
|
||||
return expectedWorldName.equals(requiredTransaction.worldName());
|
||||
}
|
||||
|
||||
public static void write(Path dataDirectory, Transaction transaction) throws IOException {
|
||||
Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction");
|
||||
Path directory = Objects.requireNonNull(directory(dataDirectory, true));
|
||||
@@ -145,7 +174,7 @@ public final class WorldReplacementJournal {
|
||||
throw new IllegalArgumentException("World key is not an exact replaceable world slot: " + requiredWorldKey);
|
||||
}
|
||||
|
||||
private static Transaction read(Path file, Path currentLevelRoot) throws IOException {
|
||||
private static Transaction read(Path file) throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
properties.load(input);
|
||||
@@ -196,7 +225,6 @@ public final class WorldReplacementJournal {
|
||||
originalTargetPresent,
|
||||
phase
|
||||
);
|
||||
resolveTarget(transaction, currentLevelRoot);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@@ -342,25 +370,11 @@ public final class WorldReplacementJournal {
|
||||
}
|
||||
|
||||
private static void forceDirectoryRequired(Path directory) throws IOException {
|
||||
if (File.separatorChar == '\\') {
|
||||
return;
|
||||
}
|
||||
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
|
||||
channel.force(true);
|
||||
} catch (UnsupportedOperationException failure) {
|
||||
throw new IOException("Directory durability sync is unavailable for " + directory + ".", failure);
|
||||
}
|
||||
DirectoryDurability.forceDirectoryRequired(directory);
|
||||
}
|
||||
|
||||
private static void forceDirectoryAfterCommit(Path directory) {
|
||||
try {
|
||||
forceDirectoryRequired(directory);
|
||||
} catch (IOException failure) {
|
||||
IrisLogging.reportError(
|
||||
"A world-replacement journal change completed, but its parent directory could not be durability-synced.",
|
||||
failure
|
||||
);
|
||||
}
|
||||
DirectoryDurability.forceDirectoryAfterCommit(directory, "A world-replacement journal change");
|
||||
}
|
||||
|
||||
public record Transaction(
|
||||
|
||||
+10
@@ -507,6 +507,14 @@ public final class BukkitCommandMessagesExtended {
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks",
|
||||
C.RED + "Pregen radius must be greater than zero blocks."
|
||||
);
|
||||
public static final TextKey COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE = TextKey.of(
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range",
|
||||
C.RED + "Radius must be between 1 and 2048 chunks."
|
||||
);
|
||||
public static final TextKey COMMAND_STUDIO_REGIONS_SCAN_FAILED = TextKey.of(
|
||||
"iris.bukkit.commandstudio.regions_scan_failed",
|
||||
C.RED + "Region scan failed: {value}"
|
||||
);
|
||||
public static final TextKey COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER = TextKey.of(
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server",
|
||||
C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server."
|
||||
@@ -965,6 +973,8 @@ public final class BukkitCommandMessagesExtended {
|
||||
COMMAND_OBJECT_NO_AREA_SELECTED_4,
|
||||
COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2,
|
||||
COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS,
|
||||
COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE,
|
||||
COMMAND_STUDIO_REGIONS_SCAN_FAILED,
|
||||
COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER,
|
||||
COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL,
|
||||
COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE,
|
||||
|
||||
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -270,13 +271,17 @@ public class TreeSVC implements IrisService {
|
||||
IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom());
|
||||
} else block.setBlockData(d, false);
|
||||
int mantleY = block.getY() - event.getWorld().getMinHeight();
|
||||
engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker);
|
||||
engine.getMantle().getMantle().set(
|
||||
block.getX(),
|
||||
mantleY,
|
||||
block.getZ(),
|
||||
TreeBlockMaterial.of(block.getBlockData().getAsString())
|
||||
);
|
||||
// Lease-gated: the grow task can land after the engine started closing, and
|
||||
// an unleased mantle write would race the region flush.
|
||||
EngineLifecycleTasks.run(engine, "tree_grow_mantle", () -> {
|
||||
engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker);
|
||||
engine.getMantle().getMantle().set(
|
||||
block.getX(),
|
||||
mantleY,
|
||||
block.getZ(),
|
||||
TreeBlockMaterial.of(block.getBlockData().getAsString())
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core.splash;
|
||||
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
@@ -51,12 +52,13 @@ public final class IrisSplashPackScanner {
|
||||
|
||||
try (FileReader reader = new FileReader(dimensionFile)) {
|
||||
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
if (!json.has("version")) {
|
||||
JsonElement version = json.get("version");
|
||||
if (version == null || !version.isJsonPrimitive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SplashPackMetadata(dimName, json.get("version").getAsString());
|
||||
} catch (IOException | JsonParseException | IllegalStateException error) {
|
||||
return new SplashPackMetadata(dimName, version.getAsString());
|
||||
} catch (IOException | JsonParseException | IllegalStateException | UnsupportedOperationException error) {
|
||||
report(reporter, "Failed to read splash metadata for dimension pack \"" + dimName + "\".", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -198,6 +198,9 @@ public class IrisConverter {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS));
|
||||
}
|
||||
});
|
||||
// Single-use pool: retire the worker once the queued conversion finishes instead of
|
||||
// leaking a non-daemon thread per invocation.
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
private static int resolveVersion(CompoundTag compound) throws Exception {
|
||||
|
||||
@@ -39,6 +39,7 @@ import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod;
|
||||
import art.arcane.iris.core.service.GlobalCacheSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
@@ -643,7 +644,10 @@ public class IrisToolbelt {
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
return e.getEngine().getMantle().getMantle().get(x, y - world.getMinHeight(), z, of);
|
||||
// Lease-gated so the engine shutdown drain covers these public accessors; a mantle
|
||||
// mid-close refuses the lease instead of racing the region flush.
|
||||
return EngineLifecycleTasks.call(e.getEngine(), "api_mantle_get",
|
||||
() -> e.getEngine().getMantle().getMantle().get(x, y - world.getMinHeight(), z, of), null);
|
||||
}
|
||||
|
||||
public static <T> void deleteMantleData(World world, int x, int y, int z, Class<T> of) {
|
||||
@@ -651,7 +655,8 @@ public class IrisToolbelt {
|
||||
if (e == null) {
|
||||
return;
|
||||
}
|
||||
e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of);
|
||||
EngineLifecycleTasks.run(e.getEngine(), "api_mantle_delete",
|
||||
() -> e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of));
|
||||
}
|
||||
|
||||
public static <T> void setMantleData(World world, int x, int y, int z, T data) {
|
||||
@@ -659,7 +664,8 @@ public class IrisToolbelt {
|
||||
if (e == null || data == null) {
|
||||
return;
|
||||
}
|
||||
e.getEngine().getMantle().getMantle().set(x, y - world.getMinHeight(), z, data);
|
||||
EngineLifecycleTasks.run(e.getEngine(), "api_mantle_set",
|
||||
() -> e.getEngine().getMantle().getMantle().set(x, y - world.getMinHeight(), z, data));
|
||||
}
|
||||
|
||||
public static boolean removeWorld(World world) throws IOException {
|
||||
|
||||
@@ -24,9 +24,9 @@ public final class WorldMaintenance {
|
||||
return;
|
||||
}
|
||||
|
||||
int depth = worldMaintenanceDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
|
||||
int depth = incrementDepth(worldMaintenanceDepth, worldName);
|
||||
if (bypassMantleStages) {
|
||||
worldMaintenanceMantleBypassDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
|
||||
incrementDepth(worldMaintenanceMantleBypassDepth, worldName);
|
||||
}
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
IrisLogging.info("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
|
||||
@@ -44,29 +44,17 @@ public final class WorldMaintenance {
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicInteger depthCounter = worldMaintenanceDepth.get(worldName);
|
||||
if (depthCounter == null) {
|
||||
if (!worldMaintenanceDepth.containsKey(worldName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int depth = depthCounter.decrementAndGet();
|
||||
if (depth <= 0) {
|
||||
worldMaintenanceDepth.remove(worldName, depthCounter);
|
||||
depth = 0;
|
||||
}
|
||||
int depth = decrementDepth(worldMaintenanceDepth, worldName);
|
||||
|
||||
// Only a bypass-begin's paired end releases bypass credit: a plain end overlapping a
|
||||
// bypassing operation stole its credit and re-enabled mantle stages under it.
|
||||
int bypassDepth = 0;
|
||||
if (bypassMantleStages) {
|
||||
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(worldName);
|
||||
if (bypassCounter != null) {
|
||||
bypassDepth = bypassCounter.decrementAndGet();
|
||||
if (bypassDepth <= 0) {
|
||||
worldMaintenanceMantleBypassDepth.remove(worldName, bypassCounter);
|
||||
bypassDepth = 0;
|
||||
}
|
||||
}
|
||||
bypassDepth = decrementDepth(worldMaintenanceMantleBypassDepth, worldName);
|
||||
}
|
||||
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
@@ -76,6 +64,22 @@ public final class WorldMaintenance {
|
||||
}
|
||||
}
|
||||
|
||||
// Depth mutations run inside compute() so an identity-based remove can never clear a
|
||||
// registration a concurrent begin just re-incremented.
|
||||
private static int incrementDepth(Map<String, AtomicInteger> depths, String worldName) {
|
||||
return depths.compute(worldName, (key, current) -> {
|
||||
AtomicInteger counter = current == null ? new AtomicInteger() : current;
|
||||
counter.incrementAndGet();
|
||||
return counter;
|
||||
}).get();
|
||||
}
|
||||
|
||||
private static int decrementDepth(Map<String, AtomicInteger> depths, String worldName) {
|
||||
AtomicInteger remaining = depths.computeIfPresent(
|
||||
worldName, (key, current) -> current.decrementAndGet() <= 0 ? null : current);
|
||||
return remaining == null ? 0 : Math.max(0, remaining.get());
|
||||
}
|
||||
|
||||
public static boolean isWorldMaintenanceActive(String worldName) {
|
||||
if (worldName == null) {
|
||||
return false;
|
||||
|
||||
@@ -34,7 +34,6 @@ import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
|
||||
|
||||
@@ -64,7 +63,7 @@ final class EngineHotloader {
|
||||
IrisComplex nextComplex = null;
|
||||
try {
|
||||
engine.sealForTransition("complex hotload", false);
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), previous.target());
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
||||
engine.runtimeAssembly.set(assembly);
|
||||
EngineRuntime next;
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||
|
||||
@@ -40,10 +40,10 @@ import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static art.arcane.iris.engine.EngineShutdownSequence.propagate;
|
||||
|
||||
@@ -65,7 +65,7 @@ final class EngineRuntimeBuilder {
|
||||
}
|
||||
|
||||
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), runtimeTarget);
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), runtimeTarget);
|
||||
engine.runtimeAssembly.set(assembly);
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||
IrisLogging.debug("Setup Engine " + assembly.cacheId);
|
||||
@@ -279,8 +279,14 @@ final class EngineRuntimeBuilder {
|
||||
}
|
||||
|
||||
static final class RuntimeAssembly {
|
||||
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
|
||||
|
||||
final int cacheId;
|
||||
final EngineTarget target;
|
||||
|
||||
static int nextRuntimeId() {
|
||||
return RUNTIME_IDS.incrementAndGet();
|
||||
}
|
||||
IrisComplex complex;
|
||||
UpperDimensionContext upperContext;
|
||||
EngineEffects effects;
|
||||
|
||||
@@ -97,7 +97,11 @@ public class LinkedTerrainChunk implements TerrainChunk {
|
||||
|
||||
@Override
|
||||
public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, PlatformBlockState state) {
|
||||
rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, (BlockData) state.nativeHandle());
|
||||
BlockData blockData = (BlockData) state.nativeHandle();
|
||||
if (blockData instanceof IrisCustomData data) {
|
||||
blockData = data.getBase();
|
||||
}
|
||||
rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, blockData);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -33,7 +33,44 @@ public record NativeStructureVolume(
|
||||
int maxY,
|
||||
int maxZ
|
||||
) {
|
||||
public static final KList<NativeStructureVolume> NONE = new KList<>();
|
||||
// Shared empty sentinel handed to every engine and memoized by the volume caches; a silent
|
||||
// add()/clear() here would corrupt them all globally, so mutation fails loudly instead.
|
||||
public static final KList<NativeStructureVolume> NONE = new KList<>() {
|
||||
@Override
|
||||
public boolean add(NativeStructureVolume volume) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, NativeStructureVolume volume) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(java.util.Collection<? extends NativeStructureVolume> volumes) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, java.util.Collection<? extends NativeStructureVolume> volumes) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeStructureVolume remove(int index) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object volume) {
|
||||
throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable");
|
||||
}
|
||||
};
|
||||
|
||||
public static NativeStructureVolume of(String structure, int aX, int aY, int aZ, int bX, int bY, int bZ) {
|
||||
return new NativeStructureVolume(
|
||||
|
||||
@@ -231,6 +231,10 @@ public final class IrisObjectIO {
|
||||
|
||||
static void write(IrisObject self, OutputStream o) throws IOException {
|
||||
validateWritable(self);
|
||||
writeValidated(self, o);
|
||||
}
|
||||
|
||||
private static void writeValidated(IrisObject self, OutputStream o) throws IOException {
|
||||
DataOutputStream dos = new DataOutputStream(o);
|
||||
dos.writeInt(self.w);
|
||||
dos.writeInt(self.h);
|
||||
@@ -270,6 +274,10 @@ public final class IrisObjectIO {
|
||||
|
||||
static void write(IrisObject self, OutputStream o, VolmitSender sender) throws IOException {
|
||||
validateWritable(self);
|
||||
writeValidated(self, o, sender);
|
||||
}
|
||||
|
||||
private static void writeValidated(IrisObject self, OutputStream o, VolmitSender sender) throws IOException {
|
||||
AtomicReference<IOException> ref = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Job() {
|
||||
@@ -365,7 +373,7 @@ public final class IrisObjectIO {
|
||||
// object must leave the existing .iob untouched.
|
||||
validateWritable(self);
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
write(self, out);
|
||||
writeValidated(self, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +384,7 @@ public final class IrisObjectIO {
|
||||
|
||||
validateWritable(self);
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
write(self, out, sender);
|
||||
writeValidated(self, out, sender);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -482,9 +482,10 @@ 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.
|
||||
future.completeExceptionally(throwable);
|
||||
closeFuture.compareAndSet(future, null);
|
||||
closing = false;
|
||||
}
|
||||
});
|
||||
return future;
|
||||
|
||||
@@ -75,19 +75,4 @@ public class VectorMath extends art.arcane.volmlib.util.math.VectorMath {
|
||||
return v;
|
||||
}
|
||||
|
||||
public static Vector getAxis(Direction current, Direction to) {
|
||||
if (current.equals(Direction.U) || current.equals(Direction.D)) {
|
||||
if (to.equals(Direction.U) || to.equals(Direction.D)) {
|
||||
return new Vector(1, 0, 0);
|
||||
} else {
|
||||
if (current.equals(Direction.N) || current.equals(Direction.S)) {
|
||||
return Direction.E.toVector();
|
||||
} else {
|
||||
return Direction.S.toVector();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Vector(0, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@ package art.arcane.iris.util.common.parallel;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.parallel.BurstExecutorSupport;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class BurstExecutor extends BurstExecutorSupport {
|
||||
public BurstExecutor(ExecutorService executor, int burstSizeEstimate) {
|
||||
super(executor, burstSizeEstimate, IrisLogging::reportError);
|
||||
}
|
||||
|
||||
public BurstExecutor(Supplier<ExecutorService> executorSource, int burstSizeEstimate) {
|
||||
super(executorSource, burstSizeEstimate, IrisLogging::reportError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
|
||||
@Override
|
||||
public BurstExecutor burst(int estimate) {
|
||||
return new BurstExecutor(service(), estimate);
|
||||
return new BurstExecutor(this::service, estimate);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.util.project.hunk.view;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.project.hunk.storage.AtomicHunk;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
@@ -88,6 +89,11 @@ public class ChunkDataHunkHolder extends AtomicHunk<PlatformBlockState> {
|
||||
for (int y = 0; y < height; y++) {
|
||||
PlatformBlockState state = super.getRaw(x, y, z);
|
||||
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
|
||||
// Custom wrappers are not real Bukkit data; write the vanilla base like the
|
||||
// NMS fast path (NMSBinding.applyChunkDataBlocks) does.
|
||||
if (block instanceof IrisCustomData custom) {
|
||||
block = custom.getBase();
|
||||
}
|
||||
if (block == null) {
|
||||
flushRun(x, z, runStart, y, activeBlock);
|
||||
activeBlock = null;
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Kein Bereich ausgewählt.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAutomatische Auswahl abgeschlossen!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cDer Vorgenerierungsradius muss größer als null Blöcke sein.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cDer Radius muss zwischen 1 und 2048 Chunks liegen.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cRegion-Scan fehlgeschlagen: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cDie strikt serielle Vorgenerierung erfordert Paper oder einen Paper-kompatiblen Server.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cDer Engine-Zugriff für diese Welt ist null!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cStelle sicher, dass die Welt geladen und die Engine initialisiert ist. Generiere zum Beispiel einen neuen Chunk.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "No hay ningún área seleccionada.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§a¡Selección automática completada!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cEl radio de pregen debe ser mayor que cero bloques.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cEl radio debe estar entre 1 y 2048 chunks.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cError al escanear regiones: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cLa pregeneración estrictamente secuencial requiere Paper o un servidor compatible con Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§c¡El acceso al motor de este mundo es nulo!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cAsegúrate de que el mundo esté cargado y el motor inicializado. Por ejemplo, genera un chunk nuevo.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Aluetta ei ole valittu.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAutomaattivalinta valmis!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen-säteen on oltava suurempi kuin nolla lohkoa.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cSäteen on oltava 1 ja 2048 chunkin välillä.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cAlueskannaus epäonnistui: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cTiukasti sarjallinen esigenerointi vaatii Paperin tai Paper-yhteensopivan palvelimen.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cTämän maailman moottorit eivät toimi!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cVarmista, että maailma on ladattu. & Moottori on alustettu. Luo esimerkiksi uusi pala.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Aucune zone sélectionnée.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aSélection automatique terminée !",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cLe rayon de pregen doit être supérieur à zéro bloc.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cLe rayon doit être compris entre 1 et 2048 chunks.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cÉchec de l'analyse des régions : {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cLa prégénération strictement séquentielle nécessite Paper ou un serveur compatible avec Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cL'accès au moteur de ce monde est nul !",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cVérifiez que le monde est chargé et que le moteur est initialisé. Générez par exemple un nouveau chunk.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "שום אזור לא נבחר.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aבחירה אוטומטית הושלם!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cרדיוס Pregen חייב להיות גדול יותר מאשר אפס בלוקים.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cהרדיוס חייב להיות בין 1 ל-2048 צ'אנקים.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cסריקת האזורים נכשלה: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cקדם-יצירה טורית קפדנית דורשת Paper או שרת תואם Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cהגישה של המנוע לעולם הזה היא אפס!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cודא שהעולם טעון & המנוע הוא ראשוני. ליצור נתח חדש, למשל.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Nessuna area selezionata.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAuto-selezione completa!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cIl raggio di pregenerazione deve essere maggiore di zero blocchi.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cIl raggio deve essere compreso tra 1 e 2048 chunk.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cScansione delle regioni non riuscita: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cLa pregenerazione strettamente seriale richiede Paper o un server compatibile con Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cL'accesso all'engine per questo mondo è nullo!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cAssicurati che il mondo sia caricato e che l'engine sia inizializzato. Ad esempio, genera un nuovo chunk.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "範囲が選択されていません。",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§a自動選択完了!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§c事前生成の半径は 0 ブロックより大きくなければなりません。",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§c半径は1から2048チャンクの範囲で指定してください。",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cリージョンスキャンに失敗しました: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§c厳密な直列事前生成には Paper または Paper 互換サーバーが必要です。",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cこのワールドのエンジンにアクセスできません!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cワールドが読み込まれ、エンジンが初期化されていることを確認してください。たとえば、新しいチャンクを生成してください。",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "선택 없음.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§a자동 선택 완료!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen 반경은 0 개 이상의 블록이 있어야합니다.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§c반경은 1~2048 청크 사이여야 합니다.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§c지역 스캔에 실패했습니다: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§c엄격한 순차 사전 생성에는 Paper 또는 Paper 호환 서버가 필요합니다.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§c이 세상의 엔진 접근은 null입니다!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§c세상이 로드되었는지 확인하십시오. & 엔진은 초기화됩니다. 새로운 청크 생성, 예를 들어.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Nepasirinkta sritis.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAuto-pasirinkite baigtas!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen spindulys turi būti didesnis nei nulis blokų.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cSpindulys turi būti nuo 1 iki 2048 gabalų.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cRegionų nuskaitymas nepavyko: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cGriežtai nuosekliam išankstiniam generavimui reikia Paper arba su Paper suderinamo serverio.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cVariklio pasiekiamumas šiam pasauliui nulinis!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cĮsitikinkite, kad pasaulis įkeltas & variklis įjungiamas. Pavyzdžiui, generuoti naują gabalą.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Geen gebied geselecteerd.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAutomatisch selecteren voltooid!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen radius moet groter zijn dan nul blokken.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cDe radius moet tussen 1 en 2048 chunks liggen.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cRegioscan mislukt: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cStrikt seriële pregeneratie vereist Paper of een Paper-compatibele server.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cDe toegang tot de motor voor deze wereld is nul!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cZorg ervoor dat de wereld geladen is & de motor is geïnitialiseerd. Genereer bijvoorbeeld een nieuwe chunk.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Nie wybrano obszaru.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aAuto- select zakończone!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPromień pregen musi być większy niż zero bloków.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cPromień musi mieścić się w zakresie od 1 do 2048 chunków.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cSkanowanie regionów nie powiodło się: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cŚciśle szeregowa pregeneracja wymaga Paper lub serwera zgodnego z Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cSilnik dostępu dla tego świata jest zerowy!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cUpewnij się, że świat jest załadowany. & silnik jest inicjalizowany. Na przykład wygenerować nowy kawałek.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Nenhuma área selecionada.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aSelecção automática completa!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cO raio de Pregen deve ser superior a zero blocos.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cO raio deve estar entre 1 e 2048 chunks.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cFalha ao analisar regiões: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cA pré-geração estritamente sequencial requer Paper ou um servidor compatível com Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cO acesso do motor para este mundo é nulo!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cPor favor, certifique-se que o mundo está carregado & O motor está inicializado. Gerar um novo chunk, por exemplo.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Никакой области не выбрано.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aАвтовыбор завершен!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cРадиус прегена должен быть больше нуля блоков.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cРадиус должен быть от 1 до 2048 чанков.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cНе удалось просканировать регионы: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cСтрого последовательная прегенерация требует Paper или Paper-совместимый сервер.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cДоступ к двигателю для этого мира недействителен!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cУбедитесь, что мир загружен & Двигатель инициализируется. Например, создать новый чанк.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Hiçbir alan seçilmiş değildir.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aotomatik seçim tamamen!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen radius sıfır bloklardan daha büyük olmalıdır.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cYarıçap 1 ile 2048 chunk arasında olmalıdır.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cBölge taraması başarısız oldu: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cKesin sıralı ön oluşturma Paper veya Paper uyumlu bir sunucu gerektirir.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cBu dünya için motor erişimi çıplak!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cLütfen dünyanın yüklendiğinden emin olun & Motor başlangıçlı. Örneğin yeni bir chunk.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "Chưa chọn diện tích.",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§aTự động chọn hoàn tất!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cBán kính Pregen phải lớn hơn không khối.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§cBán kính phải nằm trong khoảng từ 1 đến 2048 chunk.",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§cQuét khu vực thất bại: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§cTiền tạo tuần tự nghiêm ngặt yêu cầu Paper hoặc máy chủ tương thích với Paper.",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§cĐộng cơ truy cập thế giới này là vô ích!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§cHãy chắc chắn rằng thế giới đã lên đạn. & Động cơ đã khởi động. Chẳng hạn như tạo ra một mảng mới.",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "未选择区域 。",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§a自动选择完成!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen半径必须大于零块.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§c半径必须在 1 到 2048 区块之间。",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§c区域扫描失败: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§c严格的序列预生成要求 Paper 或一个 Paper- 兼容服务器。",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§c这个世界的引擎是无效的!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§c请确保世界充满 & 引擎已经初始化。 例如, 生成一个新的块 。",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"iris.bukkit.commandobject.no_area_selected_4": "未選擇區域 。",
|
||||
"iris.bukkit.commandobject.auto_select_complete_2": "§a自動選擇完成!",
|
||||
"iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks": "§cPregen半徑必須大於零塊.",
|
||||
"iris.bukkit.commandstudio.regions_radius_out_of_range": "§c半徑必須介於 1 到 2048 區塊之間。",
|
||||
"iris.bukkit.commandstudio.regions_scan_failed": "§c區域掃描失敗: {value}",
|
||||
"iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server": "§c嚴格的序列預生成要求 Paper 或一個 Paper- 相容伺服器。",
|
||||
"iris.bukkit.commandpregen.engine_access_this_world_is_null": "§c這個世界的引擎是無效的!",
|
||||
"iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate": "§c請確保世界充滿 & 引擎已經初始化。 例如, 生成一個新的塊 。",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -16,10 +20,43 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldLifecycleUnloadAsyncTest {
|
||||
@Test
|
||||
public void manualUnloadFallbackDispatchesWorldUnloadEvent() {
|
||||
World world = mock(World.class);
|
||||
PluginManager pluginManager = mock(PluginManager.class);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager);
|
||||
assertTrue(WorldLifecycleSupport.announceManualWorldUnload(world));
|
||||
}
|
||||
|
||||
verify(pluginManager).callEvent(any(WorldUnloadEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualUnloadFallbackHonorsCancelledWorldUnloadEvent() {
|
||||
World world = mock(World.class);
|
||||
PluginManager pluginManager = mock(PluginManager.class);
|
||||
doAnswer(invocation -> {
|
||||
WorldUnloadEvent event = invocation.getArgument(0);
|
||||
event.setCancelled(true);
|
||||
return null;
|
||||
}).when(pluginManager).callEvent(any(WorldUnloadEvent.class));
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager);
|
||||
assertFalse(WorldLifecycleSupport.announceManualWorldUnload(world));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reflectedAsyncUnloadWaitsForTrueCallback() throws Exception {
|
||||
CallbackServer server = new CallbackServer();
|
||||
|
||||
+32
-10
@@ -192,22 +192,22 @@ public class WorldReplacementBootstrapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsChangedLevelRootBeforeTouchingStagedStorage() throws Exception {
|
||||
public void skipsChangedLevelRootWithoutTouchingStagedStorage() throws Exception {
|
||||
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
|
||||
configureReplacement(transaction);
|
||||
Path otherLevelRoot = Files.createDirectories(serverRoot.resolve("renamed-world"));
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementBootstrap.reconcile(
|
||||
dataDirectory,
|
||||
otherLevelRoot,
|
||||
bukkitConfiguration,
|
||||
ignored -> {
|
||||
}
|
||||
)
|
||||
WorldReplacementBootstrap.ReconcileResult result = WorldReplacementBootstrap.reconcile(
|
||||
dataDirectory,
|
||||
otherLevelRoot,
|
||||
bukkitConfiguration,
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(1, result.skipped());
|
||||
assertEquals(0, result.published());
|
||||
assertEquals(0, result.rolledBack());
|
||||
assertEquals("original", Files.readString(target.worldDirectory().resolve("original.txt")));
|
||||
assertTrue(Files.isDirectory(paths(transaction).stage()));
|
||||
assertFalse(Files.exists(paths(transaction).backup()));
|
||||
@@ -280,6 +280,28 @@ public class WorldReplacementBootstrapTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsJournalStagedAgainstAnotherLevelRootInsteadOfAborting() throws Exception {
|
||||
Transaction transaction = stagedTransaction(Phase.CLEANUP_PENDING, true, "original");
|
||||
Path otherLevelRoot = Files.createDirectories(serverRoot.resolve("renamed-world"));
|
||||
|
||||
WorldReplacementBootstrap.ReconcileResult result = WorldReplacementBootstrap.reconcile(
|
||||
dataDirectory,
|
||||
otherLevelRoot,
|
||||
bukkitConfiguration,
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(1, result.skipped());
|
||||
assertEquals(0, result.published());
|
||||
assertEquals(0, result.rolledBack());
|
||||
assertEquals(0, result.retained());
|
||||
assertTrue(Files.exists(dataDirectory
|
||||
.resolve(WorldReplacementJournal.DIRECTORY_NAME)
|
||||
.resolve(transaction.id() + ".properties")));
|
||||
}
|
||||
|
||||
private WorldReplacementBootstrap.ReconcileResult reconcile() throws Exception {
|
||||
return WorldReplacementBootstrap.reconcile(
|
||||
dataDirectory,
|
||||
|
||||
@@ -108,6 +108,23 @@ public class WorldReplacementFilesystemTest {
|
||||
assertFalse(Files.exists(paths.backup()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnmigratedRetainedWorldAtAdmission() throws Exception {
|
||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("admit-missing-paper-metadata", TRANSACTION_ID);
|
||||
Files.createDirectories(paths.target());
|
||||
Files.writeString(paths.target().resolve("original.txt"), "original");
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementFilesystem.requireExistingTarget(paths)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("missing Paper world metadata"));
|
||||
assertTrue(failure.getMessage().contains("load the world once on this server"));
|
||||
assertFalse(Files.exists(paths.stage()));
|
||||
assertFalse(Files.exists(paths.backup()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishesReplacementWithoutCreatingBackupForAbsentTarget() throws Exception {
|
||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("publish-absent", TRANSACTION_ID);
|
||||
|
||||
@@ -8,7 +8,9 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -25,7 +27,7 @@ public class IrisLoggingTest {
|
||||
|
||||
@Test
|
||||
public void contextualReportPrintsFullStacktraceWithBoundPlatform() {
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class, CALLS_REAL_METHODS);
|
||||
IrisPlatforms.bind(platform);
|
||||
IllegalStateException failure = new IllegalStateException("outer", new IllegalArgumentException("inner"));
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
@@ -38,10 +40,29 @@ public class IrisLoggingTest {
|
||||
}
|
||||
|
||||
verify(platform).log(LogLevel.ERROR, "Runtime world creation failed.");
|
||||
verify(platform).reportError("Runtime world creation failed.", failure);
|
||||
verify(platform).reportError(failure);
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("IllegalStateException"));
|
||||
assertTrue(text.contains("IllegalArgumentException"));
|
||||
assertTrue(text.contains("inner"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextualReportHonorsPlatformOverrideSuppression() {
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
IrisPlatforms.bind(platform);
|
||||
IllegalStateException failure = new IllegalStateException("suppressed");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
try {
|
||||
IrisLogging.reportError("Throttled failure.", failure);
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
verify(platform).reportError("Throttled failure.", failure);
|
||||
assertFalse(output.toString(StandardCharsets.UTF_8).contains("suppressed"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user