mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Merged replacement staging fixes
- Replacement progress and admission fixes are integrated with retained biome behavior
This commit is contained in:
commit
3f1448dc33
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()
|
||||
|
||||
Reference in new issue
Block a user