mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 08:30:34 +00:00
Fixed replacement staging stalls
- Concurrent replacement requests report busy before waiting for the manager lock - Replacement staging reports localized phases and elapsed time - Player progress messages use entity scheduling - Pack copies flush completed files after traversal - Replacement admission and progress tests cover scheduling and cleanup - Language editor tests use a local catalog response
This commit is contained in:
1 parent
f2887632fe
commit
27d511d99a
25 files changed
+671
-128
No files matched your search
+127
-116
@@ -15,6 +15,7 @@ import art.arcane.iris.core.lifecycle.WorldReplacementJournal;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
@@ -115,7 +116,7 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
return worldKey;
|
||||
}
|
||||
|
||||
public synchronized StagedReplacement stageReplacement(
|
||||
public StagedReplacement stageReplacement(
|
||||
VolmitSender sender,
|
||||
NamespacedKey worldKey,
|
||||
IrisDimension dimension,
|
||||
@@ -128,132 +129,142 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
OptionalLong seedSelection = requestedSeed == null
|
||||
? OptionalLong.empty()
|
||||
: OptionalLong.of(requestedSeed.longValue());
|
||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||
if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) {
|
||||
throw new IOException("Exact world replacement requires a full Paper-family startup bootstrap.");
|
||||
}
|
||||
PackValidationRegistry.requireLoadable(requiredDimension.getLoader().getDataFolder().getName());
|
||||
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||
try (LifecycleOperationCoordinator.Lease ignored = coordinator.acquire(
|
||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
||||
requiredWorldKey.toString()
|
||||
)) {
|
||||
if (findTransaction(requiredWorldSlotKey) != null) {
|
||||
throw new IOException("A replacement is already pending for " + requiredWorldKey + ".");
|
||||
}
|
||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(requiredWorldSlotKey);
|
||||
requireCompatibleEnvironment(target.slotKind(), requiredDimension.getEnvironment());
|
||||
requireVanillaSlotEnabled(target.slotKind());
|
||||
UUID transactionId = UUID.randomUUID();
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId);
|
||||
WorldReplacementFilesystem.requireExistingTarget(paths);
|
||||
String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey);
|
||||
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
||||
if (!datapacks.succeeded()) {
|
||||
throw new IOException("Iris could not compile the dimension datapacks.");
|
||||
}
|
||||
); WorldReplacementProgress progress = WorldReplacementProgress.start(
|
||||
requiredSender, requiredWorldKey.toString())) {
|
||||
synchronized (this) {
|
||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||
if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) {
|
||||
throw new IOException("Exact world replacement requires a full Paper-family startup bootstrap.");
|
||||
}
|
||||
PackValidationRegistry.requireLoadable(requiredDimension.getLoader().getDataFolder().getName());
|
||||
if (findTransaction(requiredWorldSlotKey) != null) {
|
||||
throw new IOException("A replacement is already pending for " + requiredWorldKey + ".");
|
||||
}
|
||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(requiredWorldSlotKey);
|
||||
requireCompatibleEnvironment(target.slotKind(), requiredDimension.getEnvironment());
|
||||
requireVanillaSlotEnabled(target.slotKind());
|
||||
UUID transactionId = UUID.randomUUID();
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId);
|
||||
WorldReplacementFilesystem.requireExistingTarget(paths);
|
||||
String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey);
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_DATAPACKS);
|
||||
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
||||
if (!datapacks.succeeded()) {
|
||||
throw new IOException("Iris could not compile the dimension datapacks.");
|
||||
}
|
||||
|
||||
boolean targetPresent = true;
|
||||
WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName
|
||||
);
|
||||
Transaction transaction = null;
|
||||
boolean journalWritten = false;
|
||||
boolean configurationApplied = false;
|
||||
try {
|
||||
Files.createDirectory(paths.stage());
|
||||
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
||||
paths.target(),
|
||||
paths.stage(),
|
||||
seedSelection
|
||||
);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||
requiredSender,
|
||||
requiredDimension,
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
if (installed == null) {
|
||||
throw new IOException("Iris could not stage the dimension pack.");
|
||||
}
|
||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
|
||||
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
|
||||
}
|
||||
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
||||
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
||||
transaction = new Transaction(
|
||||
transactionId,
|
||||
requiredWorldSlotKey,
|
||||
worldName,
|
||||
target.levelRoot(),
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed,
|
||||
packFingerprint,
|
||||
originalConfiguration,
|
||||
targetPresent,
|
||||
Phase.PREPARED
|
||||
);
|
||||
writeTransaction(transaction);
|
||||
journalWritten = true;
|
||||
GeneratorReplacement replacement = BukkitWorldConfiguration.replaceIfMatching(
|
||||
boolean targetPresent = true;
|
||||
WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName,
|
||||
originalConfiguration,
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed
|
||||
worldName
|
||||
);
|
||||
if (!replacement.applied()) {
|
||||
throw new IOException("bukkit.yml changed while the replacement was being staged.");
|
||||
}
|
||||
configurationApplied = true;
|
||||
transaction = transaction.withPhase(Phase.ARMED);
|
||||
writeTransaction(transaction);
|
||||
return new StagedReplacement(
|
||||
requiredWorldKey,
|
||||
worldName,
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed,
|
||||
targetPresent,
|
||||
datapacks.restartRequired()
|
||||
);
|
||||
} catch (Throwable failure) {
|
||||
if (configurationApplied && transaction != null) {
|
||||
try {
|
||||
WorldGeneratorSnapshot replacement = WorldReplacementBootstrap.replacementSnapshot(transaction);
|
||||
if (!BukkitWorldConfiguration.restoreIfMatching(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName,
|
||||
replacement,
|
||||
originalConfiguration
|
||||
)) {
|
||||
failure.addSuppressed(new IOException(
|
||||
"bukkit.yml changed before the failed replacement could be restored."));
|
||||
}
|
||||
} catch (Throwable restoreFailure) {
|
||||
failure.addSuppressed(restoreFailure);
|
||||
Transaction transaction = null;
|
||||
boolean journalWritten = false;
|
||||
boolean configurationApplied = false;
|
||||
try {
|
||||
Files.createDirectory(paths.stage());
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_SEED);
|
||||
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
||||
paths.target(),
|
||||
paths.stage(),
|
||||
seedSelection
|
||||
);
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PACK);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||
requiredSender,
|
||||
requiredDimension,
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
if (installed == null) {
|
||||
throw new IOException("Iris could not stage the dimension pack.");
|
||||
}
|
||||
}
|
||||
if (!configurationApplied || configurationMatches(originalConfiguration, worldName)) {
|
||||
try {
|
||||
WorldReplacementFilesystem.discardStage(paths);
|
||||
if (journalWritten) {
|
||||
deleteJournal(transactionId);
|
||||
}
|
||||
} catch (Throwable cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PLAYERS);
|
||||
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
|
||||
}
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_VERIFY);
|
||||
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
||||
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
||||
transaction = new Transaction(
|
||||
transactionId,
|
||||
requiredWorldSlotKey,
|
||||
worldName,
|
||||
target.levelRoot(),
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed,
|
||||
packFingerprint,
|
||||
originalConfiguration,
|
||||
targetPresent,
|
||||
Phase.PREPARED
|
||||
);
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_SAVE);
|
||||
writeTransaction(transaction);
|
||||
journalWritten = true;
|
||||
GeneratorReplacement replacement = BukkitWorldConfiguration.replaceIfMatching(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName,
|
||||
originalConfiguration,
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed
|
||||
);
|
||||
if (!replacement.applied()) {
|
||||
throw new IOException("bukkit.yml changed while the replacement was being staged.");
|
||||
}
|
||||
configurationApplied = true;
|
||||
transaction = transaction.withPhase(Phase.ARMED);
|
||||
writeTransaction(transaction);
|
||||
return new StagedReplacement(
|
||||
requiredWorldKey,
|
||||
worldName,
|
||||
installed.getLoadKey(),
|
||||
effectiveSeed,
|
||||
targetPresent,
|
||||
datapacks.restartRequired()
|
||||
);
|
||||
} catch (Throwable failure) {
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_CLEANUP);
|
||||
if (configurationApplied && transaction != null) {
|
||||
try {
|
||||
WorldGeneratorSnapshot replacement = WorldReplacementBootstrap.replacementSnapshot(transaction);
|
||||
if (!BukkitWorldConfiguration.restoreIfMatching(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName,
|
||||
replacement,
|
||||
originalConfiguration
|
||||
)) {
|
||||
failure.addSuppressed(new IOException(
|
||||
"bukkit.yml changed before the failed replacement could be restored."));
|
||||
}
|
||||
} catch (Throwable restoreFailure) {
|
||||
failure.addSuppressed(restoreFailure);
|
||||
}
|
||||
}
|
||||
if (!configurationApplied || configurationMatches(originalConfiguration, worldName)) {
|
||||
try {
|
||||
WorldReplacementFilesystem.discardStage(paths);
|
||||
if (journalWritten) {
|
||||
deleteJournal(transactionId);
|
||||
}
|
||||
} catch (Throwable cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
if (failure instanceof IOException ioFailure) {
|
||||
throw ioFailure;
|
||||
}
|
||||
throw new IOException("Failed to stage replacement for " + requiredWorldKey + ".", failure);
|
||||
}
|
||||
if (failure instanceof IOException ioFailure) {
|
||||
throw ioFailure;
|
||||
}
|
||||
throw new IOException("Failed to stage replacement for " + requiredWorldKey + ".", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import art.arcane.volmlib.util.localization.MessageArgs;
|
||||
import art.arcane.volmlib.util.localization.MessageKey;
|
||||
import art.arcane.volmlib.util.localization.TextKey;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class WorldReplacementProgress implements AutoCloseable {
|
||||
private static final int HEARTBEAT_INTERVAL_TICKS = 200;
|
||||
|
||||
private final VolmitSender sender;
|
||||
private final String worldKey;
|
||||
private final Player player;
|
||||
private final UUID languageAudience;
|
||||
private final long startedAtNanos;
|
||||
private String detail;
|
||||
private int taskId = -1;
|
||||
private boolean firstTick = true;
|
||||
private boolean closed;
|
||||
|
||||
private WorldReplacementProgress(VolmitSender sender, String worldKey) {
|
||||
this.sender = Objects.requireNonNull(sender, "sender");
|
||||
this.worldKey = Objects.requireNonNull(worldKey, "worldKey");
|
||||
player = sender.isPlayer() ? sender.player() : null;
|
||||
languageAudience = player == null ? LanguageAudience.current() : player.getUniqueId();
|
||||
startedAtNanos = System.nanoTime();
|
||||
detail = IrisLanguage.text(
|
||||
languageAudience,
|
||||
RuntimeProgressMessages.WORLD_CREATE_STAGE_INITIALIZING,
|
||||
MessageArgs.empty()
|
||||
);
|
||||
}
|
||||
|
||||
public static WorldReplacementProgress start(VolmitSender sender, String worldKey) {
|
||||
WorldReplacementProgress progress = new WorldReplacementProgress(sender, worldKey);
|
||||
progress.send(RuntimeProgressMessages.WORLD_REPLACE_START);
|
||||
try {
|
||||
progress.taskId = J.ar(progress::heartbeat, HEARTBEAT_INTERVAL_TICKS);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Could not start world replacement progress updates for " + worldKey + ".", failure);
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (taskId >= 0) {
|
||||
try {
|
||||
J.car(taskId);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Could not cancel world replacement progress updates for " + worldKey + ".", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stage(TextKey stage) {
|
||||
if (closed || stage == null) {
|
||||
return;
|
||||
}
|
||||
String detail = IrisLanguage.text(languageAudience, stage, MessageArgs.empty());
|
||||
if (detail.equals(this.detail)) {
|
||||
return;
|
||||
}
|
||||
this.detail = detail;
|
||||
send(RuntimeProgressMessages.WORLD_REPLACE_PROGRESS);
|
||||
}
|
||||
|
||||
private synchronized void heartbeat() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (firstTick) {
|
||||
firstTick = false;
|
||||
return;
|
||||
}
|
||||
send(RuntimeProgressMessages.WORLD_REPLACE_PROGRESS);
|
||||
}
|
||||
|
||||
private void send(MessageKey key) {
|
||||
try {
|
||||
MessageArgs.Builder arguments = MessageArgs.builder().untrusted("world", worldKey);
|
||||
if (key == RuntimeProgressMessages.WORLD_REPLACE_PROGRESS) {
|
||||
arguments.untrusted("stage", detail)
|
||||
.untrusted("elapsed", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startedAtNanos));
|
||||
}
|
||||
String message = IrisLanguage.text(languageAudience, key, arguments.build());
|
||||
if (player == null) {
|
||||
sender.sendMessage(message);
|
||||
} else {
|
||||
J.runEntity(player, () -> deliverToPlayer(message));
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Could not report world replacement progress for " + worldKey + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void deliverToPlayer(String message) {
|
||||
if (!closed) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,8 @@ public class CommandIris implements DirectorExecutor {
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
|
||||
+ "verifies the replacement.");
|
||||
} catch (LifecycleOperationCoordinator.BusyException busy) {
|
||||
sender().sendMessage(C.YELLOW + busy.getMessage());
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator.BusyException;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator.Domain;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator.Lease;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator.OperationKind;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class PendingWorldReplacementAdmissionTest {
|
||||
@Test
|
||||
public void busyReplacementRejectsBeforeWaitingForTheManagerMonitor() throws Exception {
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
NamespacedKey worldKey = NamespacedKey.minecraft("overworld");
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try (Lease lease = LifecycleOperationCoordinator.get().acquire(
|
||||
Domain.WORLD_MUTATION, OperationKind.WORLD_REPLACE, worldKey.toString())) {
|
||||
synchronized (manager) {
|
||||
Future<BusyException> rejected = executor.submit(() -> assertThrows(
|
||||
BusyException.class,
|
||||
() -> manager.stageReplacement(sender, worldKey, dimension, null)
|
||||
));
|
||||
assertEquals(lease.operation(), rejected.get(2L, TimeUnit.SECONDS).currentOperation());
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.localization.MessageArgs;
|
||||
import art.arcane.volmlib.util.localization.MessageKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldReplacementProgressTest {
|
||||
@Test
|
||||
public void reportsStartStageAndHeartbeatUntilClosed() {
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
List<String> messages = new ArrayList<>();
|
||||
doAnswer(invocation -> messages.add(invocation.getArgument(0))).when(sender).sendMessage(anyString());
|
||||
AtomicReference<Runnable> heartbeat = new AtomicReference<>();
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
|
||||
scheduler.when(() -> J.ar(any(Runnable.class), eq(200))).thenAnswer(invocation -> {
|
||||
heartbeat.set(invocation.getArgument(0));
|
||||
return 42;
|
||||
});
|
||||
|
||||
WorldReplacementProgress progress = WorldReplacementProgress.start(sender, "minecraft:overworld");
|
||||
assertEquals(1, messages.size());
|
||||
assertTrue(messages.getFirst().contains("Preparing Iris replacement for minecraft:overworld"));
|
||||
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PACK);
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PACK);
|
||||
assertEquals(2, messages.size());
|
||||
assertTrue(messages.getLast().contains("Copying and validating the dimension pack"));
|
||||
assertTrue(messages.getLast().matches(".*\\([0-9]+s elapsed\\).*"));
|
||||
|
||||
heartbeat.get().run();
|
||||
assertEquals(2, messages.size());
|
||||
heartbeat.get().run();
|
||||
assertEquals(3, messages.size());
|
||||
assertTrue(messages.getLast().contains("Copying and validating the dimension pack"));
|
||||
|
||||
progress.close();
|
||||
progress.close();
|
||||
heartbeat.get().run();
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_SAVE);
|
||||
assertEquals(3, messages.size());
|
||||
scheduler.verify(() -> J.car(42), times(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schedulingFailureDoesNotPreventPhaseReports() {
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
List<String> messages = new ArrayList<>();
|
||||
doAnswer(invocation -> messages.add(invocation.getArgument(0))).when(sender).sendMessage(anyString());
|
||||
IllegalStateException failure = new IllegalStateException("Scheduler stopped");
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
scheduler.when(() -> J.ar(any(Runnable.class), eq(200))).thenThrow(failure);
|
||||
try (WorldReplacementProgress progress = WorldReplacementProgress.start(sender, "minecraft:overworld")) {
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PACK);
|
||||
}
|
||||
|
||||
assertEquals(2, messages.size());
|
||||
logging.verify(() -> IrisLogging.reportError(anyString(), eq(failure)));
|
||||
scheduler.verify(() -> J.car(anyInt()), times(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableTimerLeavesPhaseReportsUsable() {
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
List<String> messages = new ArrayList<>();
|
||||
doAnswer(invocation -> messages.add(invocation.getArgument(0))).when(sender).sendMessage(anyString());
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
|
||||
scheduler.when(() -> J.ar(any(Runnable.class), eq(200))).thenReturn(-1);
|
||||
try (WorldReplacementProgress progress = WorldReplacementProgress.start(sender, "minecraft:overworld")) {
|
||||
progress.stage(RuntimeProgressMessages.WORLD_REPLACE_STAGE_PACK);
|
||||
}
|
||||
|
||||
assertEquals(2, messages.size());
|
||||
scheduler.verify(() -> J.car(anyInt()), times(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playerReportsUseEntitySchedulingAndRetainAudience() {
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
Player player = mock(Player.class);
|
||||
UUID playerId = UUID.randomUUID();
|
||||
when(sender.isPlayer()).thenReturn(true);
|
||||
when(sender.player()).thenReturn(player);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
List<String> messages = new ArrayList<>();
|
||||
List<Runnable> deliveries = new ArrayList<>();
|
||||
List<UUID> audiences = new ArrayList<>();
|
||||
AtomicReference<Runnable> heartbeat = new AtomicReference<>();
|
||||
doAnswer(invocation -> messages.add(invocation.getArgument(0))).when(sender).sendMessage(anyString());
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class);
|
||||
MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class)) {
|
||||
language.when(() -> IrisLanguage.text(any(UUID.class), any(MessageKey.class), any(MessageArgs.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
audiences.add(invocation.getArgument(0));
|
||||
return "Replacement progress";
|
||||
});
|
||||
scheduler.when(() -> J.ar(any(Runnable.class), eq(200))).thenAnswer(invocation -> {
|
||||
heartbeat.set(invocation.getArgument(0));
|
||||
return 42;
|
||||
});
|
||||
scheduler.when(() -> J.runEntity(eq(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
deliveries.add(invocation.getArgument(1));
|
||||
return true;
|
||||
});
|
||||
|
||||
WorldReplacementProgress progress = WorldReplacementProgress.start(sender, "minecraft:overworld");
|
||||
assertTrue(messages.isEmpty());
|
||||
deliveries.getFirst().run();
|
||||
assertEquals(1, messages.size());
|
||||
heartbeat.get().run();
|
||||
heartbeat.get().run();
|
||||
assertEquals(2, deliveries.size());
|
||||
assertEquals(List.of(playerId, playerId, playerId), audiences);
|
||||
|
||||
progress.close();
|
||||
deliveries.getLast().run();
|
||||
assertEquals(1, messages.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,15 @@ public final class RuntimeProgressMessages {
|
||||
public static final TextKey WORLD_CREATE_STAGE_PREGENERATE = TextKey.of("iris.runtime.world_create.stage.pregenerate", "Pregenerating");
|
||||
public static final TextKey WORLD_CREATE_STAGE_FINALIZE = TextKey.of("iris.runtime.world_create.stage.finalize", "Finalizing");
|
||||
public static final TextKey WORLD_CREATE_STAGE_COMPLETE = TextKey.of("iris.runtime.world_create.stage.complete", "World ready");
|
||||
public static final TextKey WORLD_REPLACE_START = TextKey.of("iris.runtime.world_replace.start", C.YELLOW + "Preparing Iris replacement for {world}. The current world remains active until restart.");
|
||||
public static final TextKey WORLD_REPLACE_PROGRESS = TextKey.of("iris.runtime.world_replace.progress", C.YELLOW + "Replacement for {world}: {stage} ({elapsed}s elapsed)");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_DATAPACKS = TextKey.of("iris.runtime.world_replace.stage.datapacks", "Checking dimension datapacks");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_SEED = TextKey.of("iris.runtime.world_replace.stage.seed", "Preparing the replacement seed");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_PACK = TextKey.of("iris.runtime.world_replace.stage.pack", "Copying and validating the dimension pack");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_PLAYERS = TextKey.of("iris.runtime.world_replace.stage.players", "Recording player entry protection");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_VERIFY = TextKey.of("iris.runtime.world_replace.stage.verify", "Verifying the staged replacement");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_SAVE = TextKey.of("iris.runtime.world_replace.stage.save", "Saving the replacement for restart");
|
||||
public static final TextKey WORLD_REPLACE_STAGE_CLEANUP = TextKey.of("iris.runtime.world_replace.stage.cleanup", "Cleaning up the failed replacement");
|
||||
public static final TextKey WORLD_PREGEN_ACTION = TextKey.of("iris.runtime.world_create.pregen.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "Pregenerating");
|
||||
public static final TextKey WORLD_PREGEN_CONSOLE = TextKey.of("iris.runtime.world_create.pregen.console", C.GOLD + "Pregenerating " + C.YELLOW + "{percent}%");
|
||||
public static final TextKey CHUNK_TITLE_REGEN = TextKey.of("iris.runtime.chunk_job.title.regen", "Regen");
|
||||
@@ -140,6 +149,15 @@ public final class RuntimeProgressMessages {
|
||||
WORLD_CREATE_STAGE_PREGENERATE,
|
||||
WORLD_CREATE_STAGE_FINALIZE,
|
||||
WORLD_CREATE_STAGE_COMPLETE,
|
||||
WORLD_REPLACE_START,
|
||||
WORLD_REPLACE_PROGRESS,
|
||||
WORLD_REPLACE_STAGE_DATAPACKS,
|
||||
WORLD_REPLACE_STAGE_SEED,
|
||||
WORLD_REPLACE_STAGE_PACK,
|
||||
WORLD_REPLACE_STAGE_PLAYERS,
|
||||
WORLD_REPLACE_STAGE_VERIFY,
|
||||
WORLD_REPLACE_STAGE_SAVE,
|
||||
WORLD_REPLACE_STAGE_CLEANUP,
|
||||
WORLD_PREGEN_ACTION,
|
||||
WORLD_PREGEN_CONSOLE,
|
||||
CHUNK_TITLE_REGEN,
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -220,6 +222,7 @@ public final class GenerationPackRepository {
|
||||
+ normalizedSource + " and " + normalizedTarget);
|
||||
}
|
||||
|
||||
List<Path> copiedFiles = new ArrayList<>();
|
||||
Files.walkFileTree(normalizedSource, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
|
||||
@@ -250,11 +253,8 @@ public final class GenerationPackRepository {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
Path destination = copyDestination(normalizedSource, normalizedTarget, file);
|
||||
Files.createDirectories(Objects.requireNonNull(destination.getParent(), "Pack entry parent"));
|
||||
Files.copy(file, destination, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
try (FileChannel channel = FileChannel.open(destination, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
copiedFiles.add(destination);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@@ -263,6 +263,11 @@ public final class GenerationPackRepository {
|
||||
throw new IOException("Unable to copy pack entry: " + file, failure);
|
||||
}
|
||||
});
|
||||
for (Path copiedFile : copiedFiles) {
|
||||
try (FileChannel channel = FileChannel.open(copiedFile, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path copyDestination(Path source, Path target, Path entry) throws IOException {
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eDie Iris-Ersetzung für {world} wird vorbereitet. Die aktuelle Welt bleibt bis zum Neustart aktiv.",
|
||||
"iris.runtime.world_replace.progress": "§eErsetzung für {world}: {stage} ({elapsed}s vergangen)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Datapacks der Dimension werden geprüft",
|
||||
"iris.runtime.world_replace.stage.seed": "Seed für die Ersetzung wird vorbereitet",
|
||||
"iris.runtime.world_replace.stage.pack": "Dimensionspaket wird kopiert und geprüft",
|
||||
"iris.runtime.world_replace.stage.players": "Schutz beim Welteintritt der Spieler wird gespeichert",
|
||||
"iris.runtime.world_replace.stage.verify": "Vorbereitete Ersetzung wird geprüft",
|
||||
"iris.runtime.world_replace.stage.save": "Ersetzung wird für den Neustart gespeichert",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Fehlgeschlagene Ersetzung wird bereinigt",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fVorgenerierung",
|
||||
"iris.runtime.world_create.pregen.console": "§6Vorgenerierung §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Regenerieren",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§ePreparando el reemplazo de {world} con Iris. El mundo actual permanece activo hasta el reinicio.",
|
||||
"iris.runtime.world_replace.progress": "§eReemplazo de {world}: {stage} ({elapsed}s transcurridos)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Comprobando los paquetes de datos de la dimensión",
|
||||
"iris.runtime.world_replace.stage.seed": "Preparando la semilla del reemplazo",
|
||||
"iris.runtime.world_replace.stage.pack": "Copiando y validando el pack de la dimensión",
|
||||
"iris.runtime.world_replace.stage.players": "Guardando la protección de entrada de los jugadores",
|
||||
"iris.runtime.world_replace.stage.verify": "Verificando el reemplazo preparado",
|
||||
"iris.runtime.world_replace.stage.save": "Guardando el reemplazo para el reinicio",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Limpiando el reemplazo fallido",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregenerando",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregenerando §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Regenerar",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eIris valmistelee maailman {world} korvaamista. Nykyinen maailma pysyy aktiivisena uudelleenkäynnistykseen asti.",
|
||||
"iris.runtime.world_replace.progress": "§eMaailman {world} korvaaminen: {stage} ({elapsed}s kulunut)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Tarkistetaan ulottuvuuden datapaketteja",
|
||||
"iris.runtime.world_replace.stage.seed": "Valmistellaan korvaavan maailman siementä",
|
||||
"iris.runtime.world_replace.stage.pack": "Kopioidaan ja tarkistetaan ulottuvuuspakettia",
|
||||
"iris.runtime.world_replace.stage.players": "Tallennetaan pelaajien turvallisen maailmaan saapumisen suojaus",
|
||||
"iris.runtime.world_replace.stage.verify": "Tarkistetaan valmisteltua korvaavaa maailmaa",
|
||||
"iris.runtime.world_replace.stage.save": "Tallennetaan korvaava maailma uudelleenkäynnistystä varten",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Siivotaan epäonnistunutta korvausyritystä",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fEsituotanto",
|
||||
"iris.runtime.world_create.pregen.console": "§6Esituotanto §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Regin",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§ePréparation du remplacement de {world} par Iris. Le monde actuel reste actif jusqu’au redémarrage.",
|
||||
"iris.runtime.world_replace.progress": "§eRemplacement de {world} : {stage} ({elapsed}s écoulées)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Vérification des packs de données de la dimension",
|
||||
"iris.runtime.world_replace.stage.seed": "Préparation de la graine du remplacement",
|
||||
"iris.runtime.world_replace.stage.pack": "Copie et validation du pack de la dimension",
|
||||
"iris.runtime.world_replace.stage.players": "Enregistrement de la protection à l’entrée des joueurs",
|
||||
"iris.runtime.world_replace.stage.verify": "Vérification du remplacement préparé",
|
||||
"iris.runtime.world_replace.stage.save": "Enregistrement du remplacement pour le redémarrage",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Nettoyage du remplacement ayant échoué",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPrégénération",
|
||||
"iris.runtime.world_create.pregen.console": "§6Prégénération §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Régénérer",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eמכין החלפה באמצעות Iris עבור {world}. העולם הנוכחי נשאר פעיל עד להפעלה מחדש.",
|
||||
"iris.runtime.world_replace.progress": "§eהחלפה עבור {world}: {stage} ({elapsed} שניות חלפו)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "בודק את חבילות הנתונים של הממד",
|
||||
"iris.runtime.world_replace.stage.seed": "מכין את זרע העולם החלופי",
|
||||
"iris.runtime.world_replace.stage.pack": "מעתיק ומאמת את חבילת הממד",
|
||||
"iris.runtime.world_replace.stage.players": "שומר את ההגנה על כניסת השחקנים",
|
||||
"iris.runtime.world_replace.stage.verify": "מאמת את ההחלפה שהוכנה",
|
||||
"iris.runtime.world_replace.stage.save": "שומר את ההחלפה להפעלה מחדש",
|
||||
"iris.runtime.world_replace.stage.cleanup": "מנקה את ההחלפה שנכשלה",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fהפקה מוקדמת",
|
||||
"iris.runtime.world_create.pregen.console": "§6הפקה מוקדמת §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "מחדש",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§ePreparazione della sostituzione di {world} con Iris. Il mondo attuale rimane attivo fino al riavvio.",
|
||||
"iris.runtime.world_replace.progress": "§eSostituzione di {world}: {stage} ({elapsed}s trascorsi)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Verifica dei pacchetti di dati della dimensione",
|
||||
"iris.runtime.world_replace.stage.seed": "Preparazione del seme del mondo sostitutivo",
|
||||
"iris.runtime.world_replace.stage.pack": "Copia e convalida del pacchetto della dimensione",
|
||||
"iris.runtime.world_replace.stage.players": "Salvataggio della protezione all’ingresso dei giocatori",
|
||||
"iris.runtime.world_replace.stage.verify": "Verifica della sostituzione preparata",
|
||||
"iris.runtime.world_replace.stage.save": "Salvataggio della sostituzione per il riavvio",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Pulizia della sostituzione non riuscita",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregenerazione",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregenerazione §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Rigenera",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§e{world} を Iris で置き換える準備をしています。現在のワールドは再起動まで稼働し続けます。",
|
||||
"iris.runtime.world_replace.progress": "§e{world} の置き換え: {stage} ({elapsed}秒経過)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "ディメンションのデータパックを確認しています",
|
||||
"iris.runtime.world_replace.stage.seed": "置き換え先のシード値を準備しています",
|
||||
"iris.runtime.world_replace.stage.pack": "ディメンションパックをコピーして検証しています",
|
||||
"iris.runtime.world_replace.stage.players": "プレイヤーのワールド入場時の保護を記録しています",
|
||||
"iris.runtime.world_replace.stage.verify": "準備した置き換え内容を検証しています",
|
||||
"iris.runtime.world_replace.stage.save": "再起動時の置き換え内容を保存しています",
|
||||
"iris.runtime.world_replace.stage.cleanup": "失敗した置き換えのデータを整理しています",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f事前生成中",
|
||||
"iris.runtime.world_create.pregen.console": "§6事前生成中 §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "再生成",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§e{world}를 Iris로 교체할 준비를 하고 있습니다. 현재 월드는 재시작 전까지 계속 활성화됩니다.",
|
||||
"iris.runtime.world_replace.progress": "§e{world} 교체: {stage} ({elapsed}초 경과)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "차원 데이터 팩 확인 중",
|
||||
"iris.runtime.world_replace.stage.seed": "교체할 월드의 시드 준비 중",
|
||||
"iris.runtime.world_replace.stage.pack": "차원 팩 복사 및 검증 중",
|
||||
"iris.runtime.world_replace.stage.players": "플레이어의 월드 진입 보호 기록 중",
|
||||
"iris.runtime.world_replace.stage.verify": "준비된 교체 내용 검증 중",
|
||||
"iris.runtime.world_replace.stage.save": "재시작 시 적용할 교체 내용 저장 중",
|
||||
"iris.runtime.world_replace.stage.cleanup": "실패한 교체 작업 정리 중",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f연구분야",
|
||||
"iris.runtime.world_create.pregen.console": "§6연구분야 §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "재생성",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eRuošiamas {world} pakeitimas naudojant Iris. Dabartinis pasaulis liks aktyvus iki paleidimo iš naujo.",
|
||||
"iris.runtime.world_replace.progress": "§e{world} pakeitimas: {stage} (praėjo {elapsed}s)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Tikrinami dimensijos duomenų paketai",
|
||||
"iris.runtime.world_replace.stage.seed": "Ruošiama pakaitinio pasaulio sėkla",
|
||||
"iris.runtime.world_replace.stage.pack": "Kopijuojamas ir tikrinamas dimensijos paketas",
|
||||
"iris.runtime.world_replace.stage.players": "Įrašoma žaidėjų įėjimo apsauga",
|
||||
"iris.runtime.world_replace.stage.verify": "Tikrinamas paruoštas pakeitimas",
|
||||
"iris.runtime.world_replace.stage.save": "Pakeitimas išsaugomas paleidimui iš naujo",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Valomi nepavykusio pakeitimo duomenys",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fRegeneravimas",
|
||||
"iris.runtime.world_create.pregen.console": "§6Regeneravimas §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Režen",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eDe vervanging van {world} met Iris wordt voorbereid. De huidige wereld blijft actief tot de herstart.",
|
||||
"iris.runtime.world_replace.progress": "§eVervanging van {world}: {stage} ({elapsed}s verstreken)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Datapacks van de dimensie controleren",
|
||||
"iris.runtime.world_replace.stage.seed": "Seed voor de vervangende wereld voorbereiden",
|
||||
"iris.runtime.world_replace.stage.pack": "Dimensiepakket kopiëren en valideren",
|
||||
"iris.runtime.world_replace.stage.players": "Bescherming bij het betreden van de wereld vastleggen",
|
||||
"iris.runtime.world_replace.stage.verify": "Voorbereide vervanging controleren",
|
||||
"iris.runtime.world_replace.stage.save": "Vervanging opslaan voor de herstart",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Mislukte vervanging opruimen",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneren",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregeneren §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "RegenName",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§ePrzygotowywanie zastąpienia {world} przez Iris. Obecny świat pozostaje aktywny do ponownego uruchomienia.",
|
||||
"iris.runtime.world_replace.progress": "§eZastąpienie {world}: {stage} (upłynęło {elapsed}s)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Sprawdzanie pakietów danych wymiaru",
|
||||
"iris.runtime.world_replace.stage.seed": "Przygotowywanie ziarna nowego świata",
|
||||
"iris.runtime.world_replace.stage.pack": "Kopiowanie i sprawdzanie pakietu wymiaru",
|
||||
"iris.runtime.world_replace.stage.players": "Zapisywanie ochrony przy wejściu graczy do świata",
|
||||
"iris.runtime.world_replace.stage.verify": "Sprawdzanie przygotowanego zastąpienia",
|
||||
"iris.runtime.world_replace.stage.save": "Zapisywanie zastąpienia do zastosowania po ponownym uruchomieniu",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Usuwanie pozostałości nieudanego zastąpienia",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneracja",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregeneracja §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Regen.",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eA preparar a substituição de {world} pelo Iris. O mundo atual permanece ativo até ao reinício.",
|
||||
"iris.runtime.world_replace.progress": "§eSubstituição de {world}: {stage} ({elapsed}s decorridos)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "A verificar os pacotes de dados da dimensão",
|
||||
"iris.runtime.world_replace.stage.seed": "A preparar a semente do mundo de substituição",
|
||||
"iris.runtime.world_replace.stage.pack": "A copiar e validar o pacote da dimensão",
|
||||
"iris.runtime.world_replace.stage.players": "A guardar a proteção de entrada dos jogadores",
|
||||
"iris.runtime.world_replace.stage.verify": "A verificar a substituição preparada",
|
||||
"iris.runtime.world_replace.stage.save": "A guardar a substituição para o reinício",
|
||||
"iris.runtime.world_replace.stage.cleanup": "A limpar a substituição falhada",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneração",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregeneração §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Regen.",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eПодготовка замены {world} с помощью Iris. Текущий мир останется активным до перезапуска.",
|
||||
"iris.runtime.world_replace.progress": "§eЗамена {world}: {stage} (прошло {elapsed}с)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Проверка пакетов данных измерения",
|
||||
"iris.runtime.world_replace.stage.seed": "Подготовка сида заменяющего мира",
|
||||
"iris.runtime.world_replace.stage.pack": "Копирование и проверка пакета измерения",
|
||||
"iris.runtime.world_replace.stage.players": "Сохранение защиты при входе игроков в мир",
|
||||
"iris.runtime.world_replace.stage.verify": "Проверка подготовленной замены",
|
||||
"iris.runtime.world_replace.stage.save": "Сохранение замены для перезапуска",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Очистка данных неудавшейся замены",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fгенерирующий",
|
||||
"iris.runtime.world_create.pregen.console": "§6генерирующий §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Реген",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§e{world} için Iris ile değiştirme hazırlanıyor. Mevcut dünya yeniden başlatılana kadar etkin kalır.",
|
||||
"iris.runtime.world_replace.progress": "§e{world} değişimi: {stage} ({elapsed} saniye geçti)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Boyut veri paketleri kontrol ediliyor",
|
||||
"iris.runtime.world_replace.stage.seed": "Yeni dünyanın tohumu hazırlanıyor",
|
||||
"iris.runtime.world_replace.stage.pack": "Boyut paketi kopyalanıp doğrulanıyor",
|
||||
"iris.runtime.world_replace.stage.players": "Oyuncuların dünyaya giriş koruması kaydediliyor",
|
||||
"iris.runtime.world_replace.stage.verify": "Hazırlanan değişim doğrulanıyor",
|
||||
"iris.runtime.world_replace.stage.save": "Değişim yeniden başlatma için kaydediliyor",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Başarısız değişim temizleniyor",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneating",
|
||||
"iris.runtime.world_create.pregen.console": "§6Pregeneating §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Rejen",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§eĐang chuẩn bị thay thế {world} bằng Iris. Thế giới hiện tại vẫn hoạt động cho đến khi máy chủ khởi động lại.",
|
||||
"iris.runtime.world_replace.progress": "§eThay thế {world}: {stage} (đã qua {elapsed} giây)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "Đang kiểm tra các gói dữ liệu của chiều không gian",
|
||||
"iris.runtime.world_replace.stage.seed": "Đang chuẩn bị hạt giống của thế giới thay thế",
|
||||
"iris.runtime.world_replace.stage.pack": "Đang sao chép và xác thực gói chiều không gian",
|
||||
"iris.runtime.world_replace.stage.players": "Đang lưu biện pháp bảo vệ khi người chơi vào thế giới",
|
||||
"iris.runtime.world_replace.stage.verify": "Đang xác minh bản thay thế đã chuẩn bị",
|
||||
"iris.runtime.world_replace.stage.save": "Đang lưu bản thay thế để áp dụng khi khởi động lại",
|
||||
"iris.runtime.world_replace.stage.cleanup": "Đang dọn dẹp bản thay thế bị lỗi",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fTạo ra",
|
||||
"iris.runtime.world_create.pregen.console": "§6Tạo ra §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "Bản sao",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§e正在准备使用 Iris 替换 {world}。当前世界将在重启前保持运行。",
|
||||
"iris.runtime.world_replace.progress": "§e替换 {world}:{stage}(已用时 {elapsed} 秒)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "正在检查维度数据包",
|
||||
"iris.runtime.world_replace.stage.seed": "正在准备替换世界的种子",
|
||||
"iris.runtime.world_replace.stage.pack": "正在复制并验证维度包",
|
||||
"iris.runtime.world_replace.stage.players": "正在记录玩家进入世界时的保护信息",
|
||||
"iris.runtime.world_replace.stage.verify": "正在验证已准备的替换内容",
|
||||
"iris.runtime.world_replace.stage.save": "正在保存替换内容以供重启时应用",
|
||||
"iris.runtime.world_replace.stage.cleanup": "正在清理失败的替换内容",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f预生成",
|
||||
"iris.runtime.world_create.pregen.console": "§6预生成 §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "瑞根,你好吗?",
|
||||
|
||||
@@ -1209,6 +1209,15 @@
|
||||
"iris.runtime.world_create.stage.pregenerate": "Pregenerating",
|
||||
"iris.runtime.world_create.stage.finalize": "Finalizing",
|
||||
"iris.runtime.world_create.stage.complete": "World ready",
|
||||
"iris.runtime.world_replace.start": "§e正在準備使用 Iris 替換 {world}。目前世界將在重新啟動前持續運作。",
|
||||
"iris.runtime.world_replace.progress": "§e替換 {world}:{stage}(已耗時 {elapsed} 秒)",
|
||||
"iris.runtime.world_replace.stage.datapacks": "正在檢查維度資料包",
|
||||
"iris.runtime.world_replace.stage.seed": "正在準備替換世界的種子",
|
||||
"iris.runtime.world_replace.stage.pack": "正在複製並驗證維度包",
|
||||
"iris.runtime.world_replace.stage.players": "正在記錄玩家進入世界時的保護資訊",
|
||||
"iris.runtime.world_replace.stage.verify": "正在驗證已準備的替換內容",
|
||||
"iris.runtime.world_replace.stage.save": "正在儲存替換內容以供重新啟動時套用",
|
||||
"iris.runtime.world_replace.stage.cleanup": "正在清理失敗的替換內容",
|
||||
"iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f預生成",
|
||||
"iris.runtime.world_create.pregen.console": "§6預生成 §e{percent}%",
|
||||
"iris.runtime.chunk_job.title.regen": "瑞根,你好嗎?",
|
||||
|
||||
@@ -5,8 +5,11 @@ import art.arcane.volmlib.util.localization.LocalizationSnapshot;
|
||||
import art.arcane.volmlib.util.localization.PluginLanguageEditor;
|
||||
import art.arcane.volmlib.util.localization.PluginLanguageService;
|
||||
import art.arcane.volmlib.util.localization.PluralSelector;
|
||||
import art.arcane.volmlib.util.localization.RemoteLanguageCatalog;
|
||||
import art.arcane.volmlib.util.localization.TextValue;
|
||||
import art.arcane.volmlib.util.localization.VolmitLocales;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -14,11 +17,16 @@ import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@@ -98,13 +106,51 @@ public class IrisLanguageEditorTest {
|
||||
|
||||
@Test
|
||||
public void incompleteDownloadedLocaleCanBeEditedWithoutBeingSelected() throws Exception {
|
||||
Files.writeString(IrisLanguage.remote(directory).cacheFile("fr_FR"), "{\"locale\":\"fr_FR\",\"messages\":{}}");
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
TextValue value = new TextValue("Langue {locale}");
|
||||
PluginLanguageEditor.Document saved = editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), value)).get(5, TimeUnit.SECONDS);
|
||||
assertEquals(value, saved.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals("en_US", languages.defaultLocale());
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
byte[] complete = Files.readAllBytes(Path.of("src/main/resources/languages/fr_FR.json"));
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
Field remoteField = IrisLanguage.class.getDeclaredField("remote");
|
||||
remoteField.setAccessible(true);
|
||||
RemoteLanguageCatalog previous = IrisLanguage.remote(directory);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
try (RemoteLanguageCatalog catalog = RemoteLanguageCatalog.load(new RemoteLanguageCatalog.Options(
|
||||
"Iris",
|
||||
URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"),
|
||||
"languages",
|
||||
".json",
|
||||
"iris-language-source.properties",
|
||||
directory.toPath().resolve("languages/downloaded"),
|
||||
IrisLanguage.class.getClassLoader()
|
||||
))) {
|
||||
server.createContext(catalog.sourceUri("fr_FR").getPath(),
|
||||
exchange -> respond(exchange, complete, requests));
|
||||
server.start();
|
||||
remoteField.set(null, catalog);
|
||||
Path cache = catalog.cacheFile("fr_FR");
|
||||
Files.writeString(cache, "{\"locale\":\"fr_FR\",\"messages\":{}}");
|
||||
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
TextValue value = new TextValue("Langue {locale}");
|
||||
PluginLanguageEditor.Document saved = editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), value)).get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals(value, saved.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals("en_US", languages.defaultLocale());
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
assertArrayEquals(complete, Files.readAllBytes(cache));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
remoteField.set(null, previous);
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void respond(HttpExchange exchange, byte[] response, AtomicInteger requests) throws IOException {
|
||||
requests.incrementAndGet();
|
||||
try {
|
||||
exchange.sendResponseHeaders(200, response.length);
|
||||
exchange.getResponseBody().write(response);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user