Refused unstageable takeovers

- Datapack staging refuses a takeover it cannot stage when the level has applied takeover state or Iris generation history, instead of silently leaving the mod generator in place
- A level's retained generation history now stages its takeover even when the installed pack is gone
- Takeover pack names and dimension keys are resolved to the installed folder and file names before staging, reconciliation, and recording, and the takeover command stores that form
- Reconciliation refuses to switch an applied level to an uninstalled pack before moving any data, and leaves a never-applied level untouched until its pack is installed
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-14 20:59:09 -04:00
1 parent cf0587c112
commit 038833e614
7 files changed
+361 -71

No files matched your search

@@ -18,13 +18,16 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -36,6 +39,8 @@ public final class ModdedDimensionTakeovers {
private static final String GENERATOR_TYPE = "irisworldgen:iris";
private static final String PLACEHOLDER_BIOME_SOURCE = "minecraft:fixed";
private static final String PLACEHOLDER_BIOME = "minecraft:plains";
private static final String DIMENSIONS = "dimensions";
private static final String JSON = ".json";
private ModdedDimensionTakeovers() {
}
@@ -80,13 +85,12 @@ public final class ModdedDimensionTakeovers {
return output;
}
public static void writeLevelStems(Path datapackRoot, Map<String, String> configured,
Map<String, String> stagedDimensionTypes) throws IOException {
public static void writeLevelStems(Path datapackRoot, Map<String, String> configured, StagedTypes staged,
Path worldRoot) throws IOException {
for (ModdedDimensionTakeover takeover : parseAll(configured)) {
String typeRef = stagedDimensionTypes.get(takeover.pack() + ":" + takeover.packDimension());
String typeRef = staged.typeRef(takeover, worldRoot);
if (typeRef == null) {
ModdedIrisLog.error("Iris skipped the dimension takeover of {}: pack '{}' dimension '{}' is not installed or failed validation, so the dimension keeps its own generator",
takeover.levelKey(), takeover.pack(), takeover.packDimension());
skipUnstaged(takeover, staged, worldRoot);
continue;
}
writeLevelStem(datapackRoot, takeover, typeRef);
@@ -95,6 +99,40 @@ public final class ModdedDimensionTakeovers {
}
}
public static Map<String, String> configured() throws IOException {
return canonical(ModdedModConfig.get().dimensionTakeovers(), IrisPlatforms.get().packsFolderNoCreate().toPath());
}
static Map<String, String> canonical(Map<String, String> configured, Path packsRoot) throws IOException {
Map<String, String> canonical = new TreeMap<>();
for (ModdedDimensionTakeover takeover : parseAll(configured)) {
canonical.put(takeover.levelKey(), canonical(takeover, packsRoot).packRef());
}
return Map.copyOf(canonical);
}
public static ModdedDimensionTakeover canonical(ModdedDimensionTakeover takeover, Path packsRoot) throws IOException {
Path pack = packsRoot.resolve(takeover.pack());
if (!Files.isDirectory(pack)) {
return takeover;
}
String packName = sameIgnoringCase(pack.toRealPath().getFileName().toString(), takeover.pack());
Path dimensions = pack.resolve(DIMENSIONS);
Path dimensionFile = dimensions.resolve(takeover.packDimension() + JSON);
String dimension = takeover.packDimension();
if (Files.isRegularFile(dimensionFile)) {
String relative = dimensions.toRealPath().relativize(dimensionFile.toRealPath()).toString()
.replace(File.separatorChar, '/');
dimension = sameIgnoringCase(relative.substring(0, relative.length() - JSON.length()), takeover.packDimension());
}
return new ModdedDimensionTakeover(takeover.levelKey(), packName, dimension);
}
public static boolean installed(ModdedDimensionTakeover takeover, Path packsRoot) {
return Files.isRegularFile(packsRoot.resolve(takeover.pack()).resolve(DIMENSIONS)
.resolve(takeover.packDimension() + JSON));
}
public static Set<String> changedLevels(Map<String, String> applied, Map<String, String> configured) {
Set<String> levels = new TreeSet<>(applied.keySet());
levels.addAll(configured.keySet());
@@ -107,6 +145,32 @@ public final class ModdedDimensionTakeovers {
return changed;
}
private static void skipUnstaged(ModdedDimensionTakeover takeover, StagedTypes staged, Path worldRoot) throws IOException {
String reason = "pack '" + takeover.pack() + "' dimension '" + takeover.packDimension()
+ "' is not installed or failed validation" + caseHint(takeover, staged);
if (worldRoot != null && ModdedTakeoverReconciler.hasTakeoverRecord(worldRoot, takeover.levelKey())) {
throw new IOException("Iris refused to stage the dimension takeover of " + takeover.levelKey() + ": " + reason
+ ", and the level already has an applied takeover or Iris generation history. Restore the pack, or run /iris world takeover "
+ takeover.levelKey() + " off to hand the level back to its own generator.");
}
ModdedIrisLog.error("Iris skipped the dimension takeover of {}: {}, so the dimension keeps its own generator and the takeover stays unapplied",
takeover.levelKey(), reason);
}
private static String caseHint(ModdedDimensionTakeover takeover, StagedTypes staged) {
String wanted = takeover.pack() + ":" + takeover.packDimension();
for (String installed : staged.installed().keySet()) {
if (installed.equalsIgnoreCase(wanted)) {
return "; pack names and dimension keys are case-sensitive, the installed dimension is " + installed;
}
}
return "";
}
private static String sameIgnoringCase(String actual, String configured) {
return actual.equalsIgnoreCase(configured) ? actual : configured;
}
public static String fingerprint(Map<String, String> configured) {
StringBuilder canonical = new StringBuilder();
for (Map.Entry<String, String> entry : new TreeMap<>(configured).entrySet()) {
@@ -114,4 +178,20 @@ public final class ModdedDimensionTakeovers {
}
return canonical.toString();
}
public record StagedTypes(Map<String, String> installed, Map<Path, String> retained) {
public static StagedTypes create() {
return new StagedTypes(new LinkedHashMap<>(), new LinkedHashMap<>());
}
String typeRef(ModdedDimensionTakeover takeover, Path worldRoot) {
if (worldRoot != null) {
String retainedType = retained.get(takeover.storageFolder(worldRoot).toAbsolutePath().normalize());
if (retainedType != null) {
return retainedType;
}
}
return installed.get(takeover.pack() + ":" + takeover.packDimension());
}
}
}
@@ -324,7 +324,7 @@ public final class ModdedForcedDatapack {
false
);
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
digest.update(ModdedDimensionTakeovers.fingerprint(ModdedModConfig.get().dimensionTakeovers())
digest.update(ModdedDimensionTakeovers.fingerprint(ModdedDimensionTakeovers.configured())
.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest.digest());
}
@@ -335,7 +335,7 @@ public final class ModdedForcedDatapack {
int packCount = 0;
KList<String> presetIds = new KList<>();
Map<String, String> stagedDimensionTypes = new LinkedHashMap<>();
ModdedDimensionTakeovers.StagedTypes stagedDimensionTypes = ModdedDimensionTakeovers.StagedTypes.create();
Path worldRoot = worldRootOrNull();
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
List<File> packRoots = compilationPackRoots(worldRoot);
@@ -347,7 +347,7 @@ public final class ModdedForcedDatapack {
}
if (!ModdedEngineBootstrap.loader().clientEnvironment()) {
ModdedDimensionTakeovers.writeLevelStems(stagingDirectory,
ModdedModConfig.get().dimensionTakeovers(), stagedDimensionTypes);
ModdedDimensionTakeovers.configured(), stagedDimensionTypes, worldRoot);
}
IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer);
@@ -370,7 +370,7 @@ public final class ModdedForcedDatapack {
private static boolean stagePack(PackStageContext context, IDataFixer fixer, Path stagingDirectory,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds,
Map<String, String> stagedDimensionTypes) throws IOException {
ModdedDimensionTakeovers.StagedTypes stagedDimensionTypes) throws IOException {
File sourcePack = context.packRoot().toFile();
PackValidationResult validation;
try {
@@ -400,7 +400,7 @@ public final class ModdedForcedDatapack {
stagingDirectory.getParent(), PACK_FOLDER + ".pack-" + sourcePack.getName() + "-");
Map<String, KSet<String>> packBiomes = new LinkedHashMap<>();
KList<String> packPresetIds = new KList<>();
Map<String, String> packDimensionTypes = new LinkedHashMap<>();
ModdedDimensionTakeovers.StagedTypes packDimensionTypes = ModdedDimensionTakeovers.StagedTypes.create();
KList<File> packFolders = new KList<>();
packFolders.add(packStagingDirectory.toFile());
boolean installed;
@@ -423,7 +423,8 @@ public final class ModdedForcedDatapack {
mergeDirectory(packStagingDirectory, stagingDirectory);
mergeBiomes(seenBiomes, packBiomes);
presetIds.addAll(packPresetIds);
stagedDimensionTypes.putAll(packDimensionTypes);
stagedDimensionTypes.installed().putAll(packDimensionTypes.installed());
stagedDimensionTypes.retained().putAll(packDimensionTypes.retained());
return true;
} finally {
try {
@@ -483,7 +484,7 @@ public final class ModdedForcedDatapack {
private static boolean installPack(PackStageContext context, IDataFixer fixer, KList<File> folders,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds,
Map<String, String> dimensionTypes) throws IOException {
ModdedDimensionTakeovers.StagedTypes dimensionTypes) throws IOException {
File packFolder = context.packRoot().toFile();
String packName = context.packName();
File dimensionsDirectory = new File(packFolder, "dimensions");
@@ -531,8 +532,11 @@ public final class ModdedForcedDatapack {
context
);
}
if (context.retainedDimensionRoot() != null) {
dimensionTypes.retained().put(context.retainedDimensionRoot(), typeRef);
}
if (context.emitPresets()) {
dimensionTypes.put(packName + ":" + dimensionKey, typeRef);
dimensionTypes.installed().put(packName + ":" + dimensionKey, typeRef);
if (!dimension.hasBorrowedDimensionType()) {
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
writeWorldPreset(folders, packName, dimensionKey, presetRef);
@@ -830,13 +834,13 @@ public final class ModdedForcedDatapack {
GenerationEpoch.DimensionContract contract = retained.epoch().dimensionContract();
Optional<ModdedWorldgenIds.ScopedDimension> scoped = ModdedWorldgenIds.scopedDimensionType(contract.dimensionTypeKey());
if (scoped.isEmpty()) {
return PackStageContext.retained(normalizedRoot, retainedPackName(retained.epoch(), normalizedRoot), retained.epoch());
return PackStageContext.retained(normalizedRoot, retainedPackName(retained.epoch(), normalizedRoot), retained);
}
ModdedWorldgenIds.ScopedDimension identity = scoped.get();
if (!identity.dimension().equals(contract.dimensionKey())) {
throw new IOException("Retained Iris dimension type identity does not match its epoch contract.");
}
return PackStageContext.retained(normalizedRoot, identity.pack(), retained.epoch());
return PackStageContext.retained(normalizedRoot, identity.pack(), retained);
}
String packName = normalizedRoot.getFileName().toString();
@@ -918,7 +922,8 @@ public final class ModdedForcedDatapack {
boolean emitLegacyDimensionTypeAlias,
boolean required,
Set<String> legacyDimensions,
GenerationRegistryContract retainedContract
GenerationRegistryContract retainedContract,
Path retainedDimensionRoot
) implements IrisCustomBiomeAliasResolver {
private static PackStageContext installed(
Path packRoot,
@@ -934,6 +939,7 @@ public final class ModdedForcedDatapack {
true,
false,
Set.copyOf(legacyDimensions),
null,
null
);
}
@@ -941,8 +947,9 @@ public final class ModdedForcedDatapack {
private static PackStageContext retained(
Path packRoot,
String packName,
GenerationEpoch epoch
RetainedPack retained
) {
GenerationEpoch epoch = retained.epoch();
return new PackStageContext(
packRoot,
packName,
@@ -952,7 +959,8 @@ public final class ModdedForcedDatapack {
true,
true,
Set.of(),
epoch.registryContract()
epoch.registryContract(),
retained.history().paths().dimensionRoot().toAbsolutePath().normalize()
);
}
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.history.GenerationEpoch;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryPaths;
@@ -42,6 +43,7 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
public final class ModdedTakeoverReconciler {
private static final String STATE_FILE = "data/irisworldgen/dimension-takeovers.json";
@@ -53,7 +55,7 @@ public final class ModdedTakeoverReconciler {
public static void reconcileEarly() {
try {
ModdedModConfig config = ModdedModConfig.get();
Map<String, String> configured = config.dimensionTakeovers();
Map<String, String> configured = ModdedDimensionTakeovers.configured();
Path worldRoot = MainWorldService.configuredWorldRootIfPresent();
if (worldRoot == null) {
if (!configured.isEmpty()) {
@@ -63,17 +65,17 @@ public final class ModdedTakeoverReconciler {
return;
}
Path recoveryParent = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen");
Result result = reconcile(worldRoot, configured, config.takeoverProblems(), recoveryParent);
Path packsRoot = IrisPlatforms.get().packsFolderNoCreate().toPath();
Result result = reconcile(new Request(worldRoot, configured, config.takeoverProblems(),
(ModdedDimensionTakeover takeover) -> ModdedDimensionTakeovers.installed(takeover, packsRoot), recoveryParent));
if (!result.adoptedLevels().isEmpty()) {
ModdedIrisLog.info("Iris dimension takeover recorded for {}: their existing Iris generation history already uses the configured pack",
result.adoptedLevels());
}
Set<String> quarantined = new TreeSet<>(result.changedLevels());
quarantined.removeAll(result.adoptedLevels());
if (quarantined.isEmpty()) {
if (result.quarantinedLevels().isEmpty()) {
return;
}
ModdedIrisLog.warn("Iris dimension takeover changed for {}: {}", quarantined,
ModdedIrisLog.warn("Iris dimension takeover changed for {}: {}", result.quarantinedLevels(),
result.recovery() == null
? "no existing chunk data to quarantine"
: "moved their chunk, entity, POI, and Iris Mantle data to " + result.recovery()
@@ -89,9 +91,8 @@ public final class ModdedTakeoverReconciler {
if (worldRoot.isEmpty()) {
return;
}
Map<String, String> configured = ModdedModConfig.get().dimensionTakeovers();
try {
recordApplied(worldRoot.get(), configured,
recordApplied(worldRoot.get(), ModdedDimensionTakeovers.configured(),
(String levelKey, String packRef) -> generatesFrom(server, levelKey, packRef));
} catch (IOException | RuntimeException failure) {
ModdedIrisLog.error("Iris is stopping the server because it could not record the applied dimension takeovers under {}; an unrecorded takeover would be quarantined on the next start",
@@ -118,42 +119,69 @@ public final class ModdedTakeoverReconciler {
return Map.copyOf(recorded);
}
static Result reconcile(Path worldRoot, Map<String, String> configured, List<String> configProblems,
Path recoveryParent) throws IOException {
requireReadableConfig(worldRoot, configProblems);
static Result reconcile(Request request) throws IOException {
Path worldRoot = request.worldRoot();
requireReadableConfig(worldRoot, request.configProblems());
Map<String, String> applied = readApplied(worldRoot);
Set<String> changed = ModdedDimensionTakeovers.changedLevels(applied, configured);
if (changed.isEmpty()) {
return new Result(changed, Set.of(), null);
}
Set<String> changed = ModdedDimensionTakeovers.changedLevels(applied, request.configured());
Map<String, String> state = new TreeMap<>(applied);
Set<String> quarantined = new TreeSet<>();
Set<String> adopted = new TreeSet<>();
List<Path> sources = new ArrayList<>();
for (String level : changed) {
Path storage = ModdedDimensionTakeover.storageFolder(worldRoot, level);
String packRef = configured.get(level);
if (packRef != null && historyMatches(storage, ModdedDimensionTakeover.parse(level, packRef))) {
state.put(level, packRef);
adopted.add(level);
continue;
}
String packRef = request.configured().get(level);
if (packRef == null) {
state.remove(level);
quarantined.add(level);
continue;
}
ModdedDimensionTakeover takeover = ModdedDimensionTakeover.parse(level, packRef);
Path storage = takeover.storageFolder(worldRoot);
if (historyMatches(storage, takeover)) {
state.put(level, packRef);
adopted.add(level);
} else if (request.installed().test(takeover)) {
quarantined.add(level);
} else {
refuseOrSkipUninstalled(worldRoot, applied, takeover);
}
}
List<Path> sources = new ArrayList<>();
for (String level : quarantined) {
Path storage = ModdedDimensionTakeover.storageFolder(worldRoot, level);
for (String folder : ModdedDimensionStorage.chunkDataFolders()) {
sources.add(storage.resolve(folder));
}
}
Path recovery = recoveryParent.resolve("takeover-recovery-" + UUID.randomUUID());
List<Path> moved = ModdedWorldQuarantine.move(worldRoot, sources, recovery);
Path recovery = request.recoveryParent().resolve("takeover-recovery-" + UUID.randomUUID());
List<Path> moved = sources.isEmpty() ? List.of() : ModdedWorldQuarantine.move(worldRoot, sources, recovery);
if (!state.equals(applied)) {
writeApplied(worldRoot, state);
}
return new Result(changed, Set.copyOf(adopted), moved.isEmpty() ? null : recovery);
return new Result(Set.copyOf(quarantined), Set.copyOf(adopted), moved.isEmpty() ? null : recovery);
}
static boolean hasTakeoverRecord(Path worldRoot, String levelKey) throws IOException {
return readApplied(worldRoot).containsKey(levelKey) || hasGenerationHistory(
ModdedDimensionTakeover.storageFolder(worldRoot, levelKey));
}
private static void refuseOrSkipUninstalled(Path worldRoot, Map<String, String> applied,
ModdedDimensionTakeover takeover) throws IOException {
if (applied.containsKey(takeover.levelKey()) || hasGenerationHistory(takeover.storageFolder(worldRoot))) {
throw new IOException("Iris refused to switch the dimension takeover of " + takeover.levelKey() + " to "
+ takeover.packRef() + ": pack '" + takeover.pack() + "' dimension '" + takeover.packDimension()
+ "' is not installed, and the level already has an applied takeover or Iris generation history. Install the pack or restore the previous takeover.");
}
ModdedIrisLog.error("Iris left {} untouched: its takeover pack '{}' dimension '{}' is not installed",
takeover.levelKey(), takeover.pack(), takeover.packDimension());
}
private static boolean hasGenerationHistory(Path storage) {
return Files.exists(GenerationHistoryPaths.forDimension(storage).generationRoot(), LinkOption.NOFOLLOW_LINKS);
}
static boolean historyMatches(Path storage, ModdedDimensionTakeover takeover) throws IOException {
if (!Files.exists(GenerationHistoryPaths.forDimension(storage).generationRoot(), LinkOption.NOFOLLOW_LINKS)) {
if (!hasGenerationHistory(storage)) {
return false;
}
GenerationEpoch epoch = GenerationHistory.inspectPacks(storage).manifest().activeEpoch();
@@ -225,6 +253,10 @@ public final class ModdedTakeoverReconciler {
}
}
record Result(Set<String> changedLevels, Set<String> adoptedLevels, Path recovery) {
record Request(Path worldRoot, Map<String, String> configured, List<String> configProblems,
Predicate<ModdedDimensionTakeover> installed, Path recoveryParent) {
}
record Result(Set<String> quarantinedLevels, Set<String> adoptedLevels, Path recovery) {
}
}
@@ -27,6 +27,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
import art.arcane.iris.modded.MainWorldService;
import art.arcane.iris.modded.ModdedDimensionTakeover;
import art.arcane.iris.modded.ModdedDimensionTakeovers;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
@@ -46,6 +47,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -320,12 +322,20 @@ public final class ModdedWorldCommands {
if (blockIfPackBroken(source, takeover.levelKey(), pack) || !loadPackDimension(source, pack, takeover.packDimension())) {
return 0;
}
ModdedModConfig.setDimensionTakeover(takeover);
ModdedForcedDatapack.scheduleRegeneration("dimension takeover of " + takeover.levelKey());
ModdedDimensionTakeover canonical;
try {
canonical = ModdedDimensionTakeovers.canonical(takeover, ModdedPackCommands.packsRoot().toPath());
} catch (IOException unreadable) {
ModdedIrisLog.error("Iris could not resolve the takeover pack {} for {}", takeover.packRef(), takeover.levelKey(), unreadable);
IrisModdedCommands.fail(source, IrisLanguage.errorDetail(unreadable));
return 0;
}
ModdedModConfig.setDimensionTakeover(canonical);
ModdedForcedDatapack.scheduleRegeneration("dimension takeover of " + canonical.levelKey());
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_TAKEOVER_SET,
MessageArgument.untrusted("dimensionId", takeover.levelKey()),
MessageArgument.untrusted("pack", pack),
MessageArgument.untrusted("packDimension", takeover.packDimension())));
MessageArgument.untrusted("dimensionId", canonical.levelKey()),
MessageArgument.untrusted("pack", canonical.pack()),
MessageArgument.untrusted("packDimension", canonical.packDimension())));
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_TAKEOVER_RESTART_REQUIRED));
return 1;
}
@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -130,4 +131,116 @@ public class ModdedDimensionTakeoversTest {
assertEquals("", ModdedDimensionTakeovers.fingerprint(Map.of()));
assertTrue(ModdedDimensionTakeovers.fingerprint(forward).contains("iristest:b=overworld:sky"));
}
@Test
public void installedPackTypeIsStagedForTheTakeover() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-stage-installed");
try {
Path datapack = temporary.resolve("datapack");
ModdedDimensionTakeovers.StagedTypes staged = ModdedDimensionTakeovers.StagedTypes.create();
staged.installed().put("skypack:sky", "irisworldgen:packs/x/dimensions/y/dimension_type");
ModdedDimensionTakeovers.writeLevelStems(datapack, Map.of("aether:the_aether", "skypack:sky"), staged,
temporary.resolve("world"));
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
assertEquals("irisworldgen:packs/x/dimensions/y/dimension_type", stem.getString("type"));
assertEquals("skypack:sky", stem.getJSONObject("generator").getString("dimension"));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(temporary);
}
}
@Test
public void retainedHistoryOfTheLevelStagesTheTakeoverWithoutTheInstalledPack() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-stage-retained");
try {
Path datapack = temporary.resolve("datapack");
Path worldRoot = temporary.resolve("world");
ModdedDimensionTakeovers.StagedTypes staged = ModdedDimensionTakeovers.StagedTypes.create();
staged.retained().put(worldRoot.resolve("dimensions/aether/the_aether").toAbsolutePath().normalize(),
"aether:the_aether");
ModdedDimensionTakeovers.writeLevelStems(datapack, Map.of("aether:the_aether", "skypack:sky"), staged, worldRoot);
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
assertEquals("aether:the_aether", stem.getString("type"));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(temporary);
}
}
@Test
public void unstageableTakeoverWithoutAnyRecordIsSkipped() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-stage-skip");
try {
Path datapack = temporary.resolve("datapack");
Path worldRoot = temporary.resolve("world");
Files.createDirectories(worldRoot);
ModdedDimensionTakeovers.writeLevelStems(datapack, Map.of("aether:the_aether", "skypack:sky"),
ModdedDimensionTakeovers.StagedTypes.create(), worldRoot);
ModdedDimensionTakeovers.writeLevelStems(datapack, Map.of("aether:the_aether", "skypack:sky"),
ModdedDimensionTakeovers.StagedTypes.create(), null);
assertFalse(Files.exists(datapack.resolve("data/aether/dimension/the_aether.json")));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(temporary);
}
}
@Test
public void unstageableTakeoverOfAnAppliedLevelRefusesStaging() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-stage-applied");
try {
Path worldRoot = temporary.resolve("world");
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of("aether:the_aether", "skypack:sky"));
IOException refused = assertThrows(IOException.class, () -> ModdedDimensionTakeovers.writeLevelStems(
temporary.resolve("datapack"), Map.of("aether:the_aether", "skypack:sky"),
ModdedDimensionTakeovers.StagedTypes.create(), worldRoot));
assertTrue(refused.getMessage(), refused.getMessage().contains("aether:the_aether"));
assertTrue(refused.getMessage(), refused.getMessage().contains("skypack"));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(temporary);
}
}
@Test
public void unstageableTakeoverOfALevelWithIrisHistoryRefusesStaging() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-stage-history");
try {
Path worldRoot = temporary.resolve("world");
Files.createDirectories(worldRoot.resolve("dimensions/aether/the_aether/iris/generation"));
assertThrows(IOException.class, () -> ModdedDimensionTakeovers.writeLevelStems(
temporary.resolve("datapack"), Map.of("aether:the_aether", "skypack:sky"),
ModdedDimensionTakeovers.StagedTypes.create(), worldRoot));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(temporary);
}
}
@Test
public void canonicalTakeoverFollowsTheInstalledPackFolderCase() throws IOException {
Path packsRoot = Files.createTempDirectory("iris-takeover-canonical");
try {
Files.createDirectories(packsRoot.resolve("skypack/dimensions"));
Files.writeString(packsRoot.resolve("skypack/dimensions/sky.json"), "{}");
boolean caseInsensitive = Files.isDirectory(packsRoot.resolve("SKYPACK"));
Map<String, String> canonical = ModdedDimensionTakeovers.canonical(
Map.of("aether:the_aether", "SkyPack:Sky", "aether:other", "missing:sky"), packsRoot);
assertEquals(caseInsensitive ? "skypack:sky" : "SkyPack:Sky", canonical.get("aether:the_aether"));
assertEquals("missing:sky", canonical.get("aether:other"));
assertEquals("skypack:sky", ModdedDimensionTakeovers.canonical(
Map.of("aether:the_aether", "skypack:sky"), packsRoot).get("aether:the_aether"));
assertTrue(ModdedDimensionTakeovers.installed(ModdedDimensionTakeover.parse("aether:the_aether", "skypack:sky"), packsRoot));
assertFalse(ModdedDimensionTakeovers.installed(ModdedDimensionTakeover.parse("aether:the_aether", "skypack:void"), packsRoot));
} finally {
ModdedTakeoverReconcilerTest.deleteTree(packsRoot);
}
}
}
@@ -111,10 +111,10 @@ public class ModdedForcedDatapackTest {
Map.of(key, saved));
Class<?> stageType = Class.forName(ModdedForcedDatapack.class.getName() + "$PackStageContext");
Constructor<?> constructor = stageType.getDeclaredConstructor(Path.class, String.class, String.class,
String.class, boolean.class, boolean.class, boolean.class, Set.class, GenerationRegistryContract.class);
String.class, boolean.class, boolean.class, boolean.class, Set.class, GenerationRegistryContract.class, Path.class);
constructor.setAccessible(true);
IrisCustomBiomeAliasResolver resolver = (IrisCustomBiomeAliasResolver) constructor.newInstance(
Path.of("retained"), "test", "test", "iris:test_type", false, false, true, Set.of(), contract);
Path.of("retained"), "test", "test", "iris:test_type", false, false, true, Set.of(), contract, Path.of("dimension"));
assertEquals(source, resolver.generatedSource(key.registryKey(), key.resourceKey(), "{}", fixer));
assertThrows(IOException.class, () -> resolver.generatedSource(
key.registryKey(), "iris:missing_type", "{}", fixer));
@@ -67,10 +67,10 @@ public class ModdedTakeoverReconcilerTest {
touch(worldRoot.resolve("dimensions/minecraft/overworld/region/r.0.0.mca"));
touch(worldRoot.resolve("players/data/player.dat"));
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of("iristest:skylands", "overworld"), List.of(), recoveryParent);
assertEquals(Set.of("iristest:skylands"), result.changedLevels());
assertEquals(Set.of("iristest:skylands"), result.quarantinedLevels());
for (String folder : List.of("region", "entities", "poi", "mantle-hydrology", "iris")) {
assertFalse(folder, Files.exists(skylands.resolve(folder)));
assertTrue(folder, Files.isRegularFile(result.recovery()
@@ -95,17 +95,17 @@ public class ModdedTakeoverReconcilerTest {
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of("iristest:skylands", "overworld"));
touch(region);
ModdedTakeoverReconciler.Result unchanged = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result unchanged = reconcile(
worldRoot, Map.of("iristest:skylands", "overworld"), List.of(), recoveryParent);
assertEquals(Set.of(), unchanged.changedLevels());
assertEquals(Set.of(), unchanged.quarantinedLevels());
assertNull(unchanged.recovery());
assertTrue(Files.isRegularFile(region));
ModdedTakeoverReconciler.Result removed = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result removed = reconcile(
worldRoot, Map.of(), List.of(), recoveryParent);
assertEquals(Set.of("iristest:skylands"), removed.changedLevels());
assertEquals(Set.of("iristest:skylands"), removed.quarantinedLevels());
assertFalse(Files.exists(region));
assertTrue(Files.isRegularFile(removed.recovery().resolve("dimensions/iristest/skylands/region/r.0.0.mca")));
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
@@ -122,10 +122,10 @@ public class ModdedTakeoverReconcilerTest {
try {
Files.createDirectories(worldRoot);
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of("aether:the_aether", "overworld"), List.of(), recoveryParent);
assertEquals(Set.of("aether:the_aether"), result.changedLevels());
assertEquals(Set.of("aether:the_aether"), result.quarantinedLevels());
assertNull(result.recovery());
assertFalse(Files.exists(recoveryParent));
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
@@ -145,7 +145,7 @@ public class ModdedTakeoverReconcilerTest {
touch(region);
List<String> problems = List.of("Iris could not read /srv/config/irisworldgen/modded.json: Unterminated object");
IOException refused = assertThrows(IOException.class, () -> ModdedTakeoverReconciler.reconcile(
IOException refused = assertThrows(IOException.class, () -> reconcile(
worldRoot, Map.of(), problems, recoveryParent));
assertTrue(refused.getMessage(), refused.getMessage().contains("modded.json"));
@@ -164,7 +164,7 @@ public class ModdedTakeoverReconcilerTest {
try {
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of());
assertThrows(IOException.class, () -> ModdedTakeoverReconciler.reconcile(
assertThrows(IOException.class, () -> reconcile(
worldRoot, Map.of(), List.of("invalid takeover entry"), temporary.resolve("config")));
} finally {
deleteTree(temporary);
@@ -178,10 +178,10 @@ public class ModdedTakeoverReconcilerTest {
try {
Files.createDirectories(worldRoot);
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of(), List.of("invalid takeover entry"), temporary.resolve("config"));
assertEquals(Set.of(), result.changedLevels());
assertEquals(Set.of(), result.quarantinedLevels());
} finally {
deleteTree(temporary);
}
@@ -196,10 +196,10 @@ public class ModdedTakeoverReconcilerTest {
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of("iristest:skylands", "overworld"));
touch(region);
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of("iristest:skylands", "skypack"), List.of(), temporary.resolve("config"));
assertEquals(Set.of("iristest:skylands"), result.changedLevels());
assertEquals(Set.of("iristest:skylands"), result.quarantinedLevels());
assertFalse(Files.exists(region));
assertEquals(Map.of("iristest:skylands", "overworld"), ModdedTakeoverReconciler.readApplied(worldRoot));
} finally {
@@ -216,7 +216,7 @@ public class ModdedTakeoverReconcilerTest {
createHistory(temporary, skylands, ModdedWorldgenIds.dimensionTypeRef("skypack", "sky"), "sky", List.of());
touch(skylands.resolve("region/r.0.0.mca"));
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of("iristest:skylands", "skypack:sky"), List.of(), temporary.resolve("config"));
assertEquals(Set.of("iristest:skylands"), result.adoptedLevels());
@@ -237,7 +237,7 @@ public class ModdedTakeoverReconcilerTest {
createHistory(temporary, skylands, ModdedWorldgenIds.dimensionTypeRef("otherpack", "sky"), "sky", List.of());
touch(skylands.resolve("region/r.0.0.mca"));
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result result = reconcile(
worldRoot, Map.of("iristest:skylands", "skypack:sky"), List.of(), temporary.resolve("config"));
assertEquals(Set.of(), result.adoptedLevels());
@@ -259,7 +259,7 @@ public class ModdedTakeoverReconcilerTest {
List.of(ModdedWorldgenIds.biomeRef("skypack", "sky", "meadow")));
touch(aether.resolve("region/r.0.0.mca"));
ModdedTakeoverReconciler.Result other = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result other = reconcile(
worldRoot, Map.of("aether:the_aether", "otherpack:sky"), List.of(), temporary.resolve("config"));
assertEquals(Set.of(), other.adoptedLevels());
@@ -267,7 +267,7 @@ public class ModdedTakeoverReconcilerTest {
createHistory(temporary, aether, "aether:the_aether", "sky",
List.of(ModdedWorldgenIds.biomeRef("skypack", "sky", "meadow")));
ModdedTakeoverReconciler.Result matching = ModdedTakeoverReconciler.reconcile(
ModdedTakeoverReconciler.Result matching = reconcile(
worldRoot, Map.of("aether:the_aether", "skypack:sky"), List.of(), temporary.resolve("config"));
assertEquals(Set.of("aether:the_aether"), matching.adoptedLevels());
@@ -310,6 +310,53 @@ public class ModdedTakeoverReconcilerTest {
}
}
@Test
public void switchingAnAppliedLevelToAnUninstalledPackRefusesBeforeMovingAnything() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-uninstalled-applied");
Path worldRoot = temporary.resolve("world");
try {
Path region = worldRoot.resolve("dimensions/iristest/skylands/region/r.0.0.mca");
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of("iristest:skylands", "overworld"));
touch(region);
IOException refused = assertThrows(IOException.class, () -> ModdedTakeoverReconciler.reconcile(
new ModdedTakeoverReconciler.Request(worldRoot, Map.of("iristest:skylands", "missing"), List.of(),
(ModdedDimensionTakeover takeover) -> false, temporary.resolve("config"))));
assertTrue(refused.getMessage(), refused.getMessage().contains("missing"));
assertTrue(Files.isRegularFile(region));
assertEquals(Map.of("iristest:skylands", "overworld"), ModdedTakeoverReconciler.readApplied(worldRoot));
} finally {
deleteTree(temporary);
}
}
@Test
public void newTakeoverOfAnUninstalledPackLeavesTheLevelAlone() throws IOException {
Path temporary = Files.createTempDirectory("iris-takeover-uninstalled-new");
Path worldRoot = temporary.resolve("world");
try {
Path region = worldRoot.resolve("dimensions/aether/the_aether/region/r.0.0.mca");
touch(region);
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
new ModdedTakeoverReconciler.Request(worldRoot, Map.of("aether:the_aether", "missing"), List.of(),
(ModdedDimensionTakeover takeover) -> false, temporary.resolve("config")));
assertEquals(Set.of(), result.quarantinedLevels());
assertTrue(Files.isRegularFile(region));
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
} finally {
deleteTree(temporary);
}
}
private static ModdedTakeoverReconciler.Result reconcile(Path worldRoot, Map<String, String> configured,
List<String> problems, Path recoveryParent) throws IOException {
return ModdedTakeoverReconciler.reconcile(new ModdedTakeoverReconciler.Request(
worldRoot, configured, problems, (ModdedDimensionTakeover takeover) -> true, recoveryParent));
}
private static void createHistory(Path temporary, Path dimensionRoot, String dimensionTypeKey, String dimensionKey,
List<String> biomeKeys) throws IOException {
Path source = Files.createTempDirectory(temporary, "pack");