mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Fix Iris replacement
This commit is contained in:
1 parent
4568b3fff1
commit
8297e49ed2
52 files changed
+4748
-198
No files matched your search
@@ -647,9 +647,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
J.s(() -> {
|
||||
pendingWorldReplacements.captureVanillaLevelContext();
|
||||
// Off-main: the verify body takes the replacement-manager monitor and SHA-hashes
|
||||
// whole pack trees; neither belongs on the tick thread.
|
||||
J.a(pendingWorldReplacements::verifyLoadedPublishedWorlds);
|
||||
pendingWorldReplacements.verifyLoadedPublishedWorlds();
|
||||
J.a(this::bstats);
|
||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
||||
J.sr(this::tickQueue, 0);
|
||||
|
||||
+194
-45
@@ -40,6 +40,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -50,6 +51,7 @@ import java.util.concurrent.TimeoutException;
|
||||
public final class PendingWorldReplacementManager implements Listener {
|
||||
private final Iris plugin;
|
||||
private final Set<UUID> cleanupInFlight = new HashSet<>();
|
||||
private final Set<UUID> verificationInFlight = new HashSet<>();
|
||||
|
||||
public PendingWorldReplacementManager(Iris plugin) {
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
@@ -67,12 +69,16 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
public synchronized StagedReplacement stageReplacement(
|
||||
VolmitSender sender,
|
||||
NamespacedKey worldKey,
|
||||
IrisDimension dimension
|
||||
IrisDimension dimension,
|
||||
Long requestedSeed
|
||||
) throws IOException {
|
||||
VolmitSender requiredSender = Objects.requireNonNull(sender, "sender");
|
||||
NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey");
|
||||
WorldSlotKey requiredWorldSlotKey = toWorldSlotKey(requiredWorldKey);
|
||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||
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.");
|
||||
@@ -93,7 +99,6 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
UUID transactionId = UUID.randomUUID();
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId);
|
||||
WorldReplacementFilesystem.requireExistingTarget(paths);
|
||||
long effectiveSeed = WorldReplacementSeed.readAuthoritativeSeed(paths.target());
|
||||
String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey);
|
||||
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
||||
if (!datapacks.succeeded()) {
|
||||
@@ -119,6 +124,11 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
throw new IOException("Iris could not stage the dimension pack.");
|
||||
}
|
||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
||||
paths.target(),
|
||||
paths.stage(),
|
||||
seedSelection
|
||||
);
|
||||
File stagedPack = paths.stage().resolve("iris/pack").toFile();
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
||||
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
||||
@@ -227,68 +237,160 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void verifyLoadedPublishedWorlds() {
|
||||
public void verifyLoadedPublishedWorlds() {
|
||||
J.a(this::discoverLoadedPublishedWorlds);
|
||||
}
|
||||
|
||||
private void discoverLoadedPublishedWorlds() {
|
||||
List<Transaction> transactions;
|
||||
try {
|
||||
for (Transaction transaction : loadTransactions()) {
|
||||
if (transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||
scheduleCommittedCleanup(transaction);
|
||||
} else if (transaction.phase() == Phase.PUBLISHED) {
|
||||
WorldIdentity.resolve(toNamespacedKey(transaction.worldKey()))
|
||||
.ifPresent(world -> verifyPublishedWorld(world, transaction));
|
||||
}
|
||||
synchronized (this) {
|
||||
transactions = loadTransactions();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to inspect published Iris world replacements.", failure);
|
||||
return;
|
||||
}
|
||||
for (Transaction transaction : transactions) {
|
||||
if (transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||
scheduleCommittedCleanup(transaction);
|
||||
} else if (transaction.phase() == Phase.PUBLISHED) {
|
||||
scheduleRuntimeCapture(transaction, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
World world = event.getWorld();
|
||||
// One tick to let the load settle, then verify off-main: the body takes the manager
|
||||
// monitor (held across pack staging by async threads) and SHA-hashes the whole pack
|
||||
// tree — blocking the main thread on either froze the server.
|
||||
J.s(() -> J.a(() -> verifyPublishedWorldIfPending(world)), 1);
|
||||
WorldSlotKey worldKey;
|
||||
try {
|
||||
worldKey = toWorldSlotKey(WorldIdentity.key(event.getWorld()));
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to capture a loaded world identity for replacement verification.", failure);
|
||||
return;
|
||||
}
|
||||
J.a(() -> discoverLoadedWorldTransaction(worldKey));
|
||||
}
|
||||
|
||||
private synchronized void verifyPublishedWorldIfPending(World world) {
|
||||
private void discoverLoadedWorldTransaction(WorldSlotKey worldKey) {
|
||||
Transaction transaction;
|
||||
try {
|
||||
Transaction transaction = findTransaction(WorldIdentity.key(world));
|
||||
if (transaction != null && transaction.phase() == Phase.PUBLISHED) {
|
||||
verifyPublishedWorld(world, transaction);
|
||||
} else if (transaction != null && transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||
scheduleCommittedCleanup(transaction);
|
||||
synchronized (this) {
|
||||
transaction = findTransaction(worldKey);
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to verify a published Iris world replacement.", failure);
|
||||
Iris.reportError("Failed to inspect a loaded Iris world replacement.", failure);
|
||||
return;
|
||||
}
|
||||
if (transaction == null) {
|
||||
return;
|
||||
}
|
||||
if (transaction.phase() == Phase.PUBLISHED) {
|
||||
scheduleRuntimeCapture(transaction, 1);
|
||||
} else if (transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||
scheduleCommittedCleanup(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPublishedWorld(World world, Transaction transaction) {
|
||||
private void scheduleRuntimeCapture(Transaction transaction, int delayTicks) {
|
||||
synchronized (this) {
|
||||
if (!verificationInFlight.add(transaction.id())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
J.s(() -> captureLoadedPublishedWorldOnGlobal(transaction), delayTicks);
|
||||
} catch (Throwable failure) {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
Iris.reportError("Could not schedule runtime verification for " + transaction.worldKey() + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void captureLoadedPublishedWorldOnGlobal(Transaction transaction) {
|
||||
World world;
|
||||
try {
|
||||
world = WorldIdentity.resolve(toNamespacedKey(transaction.worldKey())).orElse(null);
|
||||
} catch (Throwable failure) {
|
||||
dispatchRuntimeCaptureFailure(transaction, failure);
|
||||
return;
|
||||
}
|
||||
if (world == null) {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
return;
|
||||
}
|
||||
PublishedWorldRuntimeState runtimeState;
|
||||
try {
|
||||
runtimeState = capturePublishedWorldRuntime(world);
|
||||
} catch (Throwable failure) {
|
||||
dispatchRuntimeCaptureFailure(transaction, failure);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
J.a(() -> runPublishedWorldVerification(runtimeState, transaction));
|
||||
} catch (Throwable failure) {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
Iris.reportError("Could not dispatch runtime verification for " + transaction.worldKey() + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchRuntimeCaptureFailure(Transaction transaction, Throwable failure) {
|
||||
try {
|
||||
J.a(() -> runPublishedWorldCaptureFailure(transaction, failure));
|
||||
} catch (Throwable dispatchFailure) {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
dispatchFailure.addSuppressed(failure);
|
||||
Iris.reportError("Could not dispatch a failed runtime capture for "
|
||||
+ transaction.worldKey() + ".", dispatchFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private void runPublishedWorldCaptureFailure(Transaction transaction, Throwable failure) {
|
||||
try {
|
||||
initiateRollback(transaction, failure);
|
||||
} finally {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
}
|
||||
}
|
||||
|
||||
private void runPublishedWorldVerification(
|
||||
PublishedWorldRuntimeState runtimeState,
|
||||
Transaction transaction
|
||||
) {
|
||||
try {
|
||||
verifyPublishedWorld(runtimeState, transaction);
|
||||
} finally {
|
||||
finishRuntimeVerification(transaction.id());
|
||||
}
|
||||
}
|
||||
|
||||
static PublishedWorldRuntimeState capturePublishedWorldRuntime(World world) {
|
||||
World requiredWorld = Objects.requireNonNull(world, "world");
|
||||
WorldSlotKey worldKey = toWorldSlotKey(WorldIdentity.key(requiredWorld));
|
||||
boolean irisWorld = IrisToolbelt.isIrisWorld(requiredWorld);
|
||||
long seed = requiredWorld.getSeed();
|
||||
World.Environment bukkitEnvironment = requiredWorld.getEnvironment();
|
||||
PlatformChunkGenerator generator = irisWorld ? IrisToolbelt.access(requiredWorld) : null;
|
||||
String dimension = null;
|
||||
IrisEnvironment dimensionEnvironment = null;
|
||||
if (generator != null) {
|
||||
IrisDimension runtimeDimension = generator.getTarget().getDimension();
|
||||
dimension = runtimeDimension.getLoadKey();
|
||||
dimensionEnvironment = runtimeDimension.getEnvironment();
|
||||
}
|
||||
return new PublishedWorldRuntimeState(
|
||||
worldKey,
|
||||
irisWorld,
|
||||
seed,
|
||||
bukkitEnvironment,
|
||||
dimension,
|
||||
dimensionEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
private void verifyPublishedWorld(PublishedWorldRuntimeState runtimeState, Transaction transaction) {
|
||||
try {
|
||||
if (!transaction.worldKey().equals(toWorldSlotKey(WorldIdentity.key(world)))) {
|
||||
throw new IOException("Loaded world identity does not match the replacement journal.");
|
||||
}
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
throw new IOException("The replaced world did not load with an Iris generator.");
|
||||
}
|
||||
if (world.getSeed() != transaction.seed()) {
|
||||
throw new IOException("The replaced world loaded with an unexpected seed.");
|
||||
}
|
||||
World.Environment expectedEnvironment = expectedEnvironment(transaction.worldKey());
|
||||
if (expectedEnvironment != null && world.getEnvironment() != expectedEnvironment) {
|
||||
throw new IOException("The replaced world loaded with an unexpected environment.");
|
||||
}
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator == null || !transaction.dimension().equals(
|
||||
generator.getTarget().getDimension().getLoadKey())) {
|
||||
throw new IOException("The replaced world loaded an unexpected Iris dimension.");
|
||||
}
|
||||
ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction);
|
||||
requireCompatibleEnvironment(
|
||||
target.slotKind(),
|
||||
generator.getTarget().getDimension().getEnvironment()
|
||||
);
|
||||
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||
paths.target().resolve("iris/pack"));
|
||||
@@ -318,6 +420,39 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
static void validatePublishedWorldRuntime(
|
||||
PublishedWorldRuntimeState runtimeState,
|
||||
Transaction transaction,
|
||||
SlotKind slotKind
|
||||
) throws IOException {
|
||||
PublishedWorldRuntimeState requiredRuntimeState = Objects.requireNonNull(runtimeState, "runtimeState");
|
||||
Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction");
|
||||
SlotKind requiredSlotKind = Objects.requireNonNull(slotKind, "slotKind");
|
||||
if (!requiredTransaction.worldKey().equals(requiredRuntimeState.worldKey())) {
|
||||
throw new IOException("Loaded world identity does not match the replacement journal.");
|
||||
}
|
||||
if (!requiredRuntimeState.irisWorld()) {
|
||||
throw new IOException("The replaced world did not load with an Iris generator.");
|
||||
}
|
||||
if (requiredRuntimeState.seed() != requiredTransaction.seed()) {
|
||||
throw new IOException("The replaced world loaded with an unexpected seed.");
|
||||
}
|
||||
World.Environment expectedEnvironment = expectedEnvironment(requiredTransaction.worldKey());
|
||||
if (expectedEnvironment != null && requiredRuntimeState.bukkitEnvironment() != expectedEnvironment) {
|
||||
throw new IOException("The replaced world loaded with an unexpected environment.");
|
||||
}
|
||||
if (requiredRuntimeState.dimension() == null
|
||||
|| requiredRuntimeState.dimensionEnvironment() == null
|
||||
|| !requiredTransaction.dimension().equals(requiredRuntimeState.dimension())) {
|
||||
throw new IOException("The replaced world loaded an unexpected Iris dimension.");
|
||||
}
|
||||
requireCompatibleEnvironment(requiredSlotKind, requiredRuntimeState.dimensionEnvironment());
|
||||
}
|
||||
|
||||
private synchronized void finishRuntimeVerification(UUID transactionId) {
|
||||
verificationInFlight.remove(transactionId);
|
||||
}
|
||||
|
||||
private void initiateRollback(Transaction transaction, Throwable failure) {
|
||||
Iris.reportError("Iris world replacement verification failed for " + transaction.worldKey()
|
||||
+ "; the retained world will be restored on restart.", failure);
|
||||
@@ -608,4 +743,18 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
record PublishedWorldRuntimeState(
|
||||
WorldSlotKey worldKey,
|
||||
boolean irisWorld,
|
||||
long seed,
|
||||
World.Environment bukkitEnvironment,
|
||||
String dimension,
|
||||
IrisEnvironment dimensionEnvironment
|
||||
) {
|
||||
PublishedWorldRuntimeState {
|
||||
Objects.requireNonNull(worldKey, "worldKey");
|
||||
Objects.requireNonNull(bukkitEnvironment, "bukkitEnvironment");
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-7
@@ -79,12 +79,14 @@ import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.IrisMessages;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
private static final String NO_DOWNLOAD_SOURCE = "__none__";
|
||||
private static final String PRESERVE_REPLACEMENT_SEED = "preserve";
|
||||
private static final long WORLD_UNLOAD_TIMEOUT_SECONDS = 150L;
|
||||
|
||||
private CommandStudio studio;
|
||||
@@ -202,7 +204,15 @@ public class CommandIris implements DirectorExecutor {
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type
|
||||
String type,
|
||||
@Param(
|
||||
name = "seed",
|
||||
aliases = "s",
|
||||
description = "The replacement seed; omit to preserve the target world's seed",
|
||||
defaultValue = PRESERVE_REPLACEMENT_SEED,
|
||||
customHandler = ReplacementSeedHandler.class
|
||||
)
|
||||
Long seed
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
@@ -227,9 +237,13 @@ public class CommandIris implements DirectorExecutor {
|
||||
try {
|
||||
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
|
||||
.pendingWorldReplacements()
|
||||
.stageReplacement(sender(), worldKey, dimension);
|
||||
.stageReplacement(sender(), worldKey, dimension, seed);
|
||||
String seedDetail = seed == null
|
||||
? " preserving seed " + staged.seed()
|
||||
: " using seed " + staged.seed();
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ ". Restart once to publish it. The current dimension is retained until Iris verifies the replacement.");
|
||||
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
|
||||
+ "verifies the replacement.");
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
@@ -539,19 +553,17 @@ public class CommandIris implements DirectorExecutor {
|
||||
String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack;
|
||||
String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link;
|
||||
if ((builtInPack == null) == (directLink == null)) {
|
||||
sender().sendMessage("Use exactly one source: /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>.");
|
||||
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_SOURCE));
|
||||
return;
|
||||
}
|
||||
if (builtInPack != null) {
|
||||
sender().sendMessage("Downloading built-in Iris pack '" + builtInPack + "' from its beta release.");
|
||||
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack);
|
||||
return;
|
||||
}
|
||||
if (!PackDownloader.isDirectZipUrl(directLink)) {
|
||||
sender().sendMessage("Iris requires link= to contain a valid HTTP or HTTPS .zip URL.");
|
||||
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage("Downloading Iris pack from " + directLink + ".");
|
||||
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink);
|
||||
}
|
||||
|
||||
@@ -894,6 +906,41 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReplacementSeedHandler implements DirectorParameterHandler<Long> {
|
||||
@Override
|
||||
public KList<Long> getPossibilities() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Long value) {
|
||||
return value == null ? PRESERVE_REPLACEMENT_SEED : Long.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long parse(String in, boolean force) throws DirectorParsingException {
|
||||
String seed = in == null ? "" : in.trim();
|
||||
if (PRESERVE_REPLACEMENT_SEED.equalsIgnoreCase(seed)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(seed);
|
||||
} catch (NumberFormatException failure) {
|
||||
throw new DirectorParsingException("Seed must be a signed 64-bit integer");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == Long.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRandomDefault() {
|
||||
return "1337";
|
||||
}
|
||||
}
|
||||
|
||||
public static class DownloadPackHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
|
||||
Reference in new issue
Block a user