mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +00:00
Hardened takeover state recording
- A changed level whose existing Iris generation history already uses the configured pack is recorded as applied instead of quarantined - New and changed takeovers are recorded only once their level generates from the configured pack; removals are recorded at reconciliation - A changed takeover keeps its previous applied state until the new pack generates, so a later staging failure still sees the level as taken over - Failing to write the applied takeover state stops the server instead of only logging
This commit is contained in:
1 parent
a0c9d48601
commit
cf0587c112
2 files changed
+222
-14
No files matched your search
+82
-11
@@ -18,13 +18,19 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryPaths;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldPaths;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldPaths;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -33,7 +39,9 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
public final class ModdedTakeoverReconciler {
|
public final class ModdedTakeoverReconciler {
|
||||||
private static final String STATE_FILE = "data/irisworldgen/dimension-takeovers.json";
|
private static final String STATE_FILE = "data/irisworldgen/dimension-takeovers.json";
|
||||||
@@ -56,10 +64,16 @@ public final class ModdedTakeoverReconciler {
|
|||||||
}
|
}
|
||||||
Path recoveryParent = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen");
|
Path recoveryParent = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen");
|
||||||
Result result = reconcile(worldRoot, configured, config.takeoverProblems(), recoveryParent);
|
Result result = reconcile(worldRoot, configured, config.takeoverProblems(), recoveryParent);
|
||||||
if (result.changedLevels().isEmpty()) {
|
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()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ModdedIrisLog.warn("Iris dimension takeover changed for {}: {}", result.changedLevels(),
|
ModdedIrisLog.warn("Iris dimension takeover changed for {}: {}", quarantined,
|
||||||
result.recovery() == null
|
result.recovery() == null
|
||||||
? "no existing chunk data to quarantine"
|
? "no existing chunk data to quarantine"
|
||||||
: "moved their chunk, entity, POI, and Iris Mantle data to " + result.recovery()
|
: "moved their chunk, entity, POI, and Iris Mantle data to " + result.recovery()
|
||||||
@@ -77,34 +91,91 @@ public final class ModdedTakeoverReconciler {
|
|||||||
}
|
}
|
||||||
Map<String, String> configured = ModdedModConfig.get().dimensionTakeovers();
|
Map<String, String> configured = ModdedModConfig.get().dimensionTakeovers();
|
||||||
try {
|
try {
|
||||||
if (readApplied(worldRoot.get()).equals(configured)) {
|
recordApplied(worldRoot.get(), configured,
|
||||||
return;
|
(String levelKey, String packRef) -> generatesFrom(server, levelKey, packRef));
|
||||||
}
|
|
||||||
writeApplied(worldRoot.get(), configured);
|
|
||||||
} catch (IOException | RuntimeException failure) {
|
} catch (IOException | RuntimeException failure) {
|
||||||
ModdedIrisLog.error("Iris could not record the applied dimension takeovers under {}", worldRoot.get(), 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",
|
||||||
|
worldRoot.get(), failure);
|
||||||
|
server.halt(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Map<String, String> recordApplied(Path worldRoot, Map<String, String> configured,
|
||||||
|
BiPredicate<String, String> generating) throws IOException {
|
||||||
|
Map<String, String> applied = readApplied(worldRoot);
|
||||||
|
Map<String, String> recorded = new TreeMap<>(applied);
|
||||||
|
for (Map.Entry<String, String> entry : new TreeMap<>(configured).entrySet()) {
|
||||||
|
if (generating.test(entry.getKey(), entry.getValue())) {
|
||||||
|
recorded.put(entry.getKey(), entry.getValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ModdedIrisLog.warn("Iris dimension takeover of {} is configured but the level does not generate from {}; it stays unapplied",
|
||||||
|
entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (!recorded.equals(applied)) {
|
||||||
|
writeApplied(worldRoot, recorded);
|
||||||
|
}
|
||||||
|
return Map.copyOf(recorded);
|
||||||
|
}
|
||||||
|
|
||||||
static Result reconcile(Path worldRoot, Map<String, String> configured, List<String> configProblems,
|
static Result reconcile(Path worldRoot, Map<String, String> configured, List<String> configProblems,
|
||||||
Path recoveryParent) throws IOException {
|
Path recoveryParent) throws IOException {
|
||||||
requireReadableConfig(worldRoot, configProblems);
|
requireReadableConfig(worldRoot, configProblems);
|
||||||
Map<String, String> applied = readApplied(worldRoot);
|
Map<String, String> applied = readApplied(worldRoot);
|
||||||
Set<String> changed = ModdedDimensionTakeovers.changedLevels(applied, configured);
|
Set<String> changed = ModdedDimensionTakeovers.changedLevels(applied, configured);
|
||||||
if (changed.isEmpty()) {
|
if (changed.isEmpty()) {
|
||||||
return new Result(changed, null);
|
return new Result(changed, Set.of(), null);
|
||||||
}
|
}
|
||||||
|
Map<String, String> state = new TreeMap<>(applied);
|
||||||
|
Set<String> adopted = new TreeSet<>();
|
||||||
List<Path> sources = new ArrayList<>();
|
List<Path> sources = new ArrayList<>();
|
||||||
for (String level : changed) {
|
for (String level : changed) {
|
||||||
Path storage = ModdedDimensionTakeover.storageFolder(worldRoot, level);
|
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;
|
||||||
|
}
|
||||||
|
if (packRef == null) {
|
||||||
|
state.remove(level);
|
||||||
|
}
|
||||||
for (String folder : ModdedDimensionStorage.chunkDataFolders()) {
|
for (String folder : ModdedDimensionStorage.chunkDataFolders()) {
|
||||||
sources.add(storage.resolve(folder));
|
sources.add(storage.resolve(folder));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Path recovery = recoveryParent.resolve("takeover-recovery-" + UUID.randomUUID());
|
Path recovery = recoveryParent.resolve("takeover-recovery-" + UUID.randomUUID());
|
||||||
List<Path> moved = ModdedWorldQuarantine.move(worldRoot, sources, recovery);
|
List<Path> moved = ModdedWorldQuarantine.move(worldRoot, sources, recovery);
|
||||||
writeApplied(worldRoot, configured);
|
if (!state.equals(applied)) {
|
||||||
return new Result(changed, moved.isEmpty() ? null : recovery);
|
writeApplied(worldRoot, state);
|
||||||
|
}
|
||||||
|
return new Result(changed, Set.copyOf(adopted), moved.isEmpty() ? null : recovery);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean historyMatches(Path storage, ModdedDimensionTakeover takeover) throws IOException {
|
||||||
|
if (!Files.exists(GenerationHistoryPaths.forDimension(storage).generationRoot(), LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GenerationEpoch epoch = GenerationHistory.inspectPacks(storage).manifest().activeEpoch();
|
||||||
|
GenerationEpoch.DimensionContract contract = epoch.dimensionContract();
|
||||||
|
if (!contract.dimensionKey().equals(takeover.packDimension())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Optional<ModdedWorldgenIds.ScopedDimension> scoped = ModdedWorldgenIds.scopedDimensionType(contract.dimensionTypeKey());
|
||||||
|
if (scoped.isPresent()) {
|
||||||
|
return scoped.get().equals(new ModdedWorldgenIds.ScopedDimension(takeover.pack(), takeover.packDimension()));
|
||||||
|
}
|
||||||
|
Set<String> packs = ModdedWorldgenIds.aliasPacks(epoch.registryContract(), contract.dimensionKey());
|
||||||
|
return packs.isEmpty() || packs.contains(takeover.pack());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean generatesFrom(NativeModdedServer server, String levelKey, String packRef) {
|
||||||
|
NativeWorld world = server.world(levelKey);
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class);
|
||||||
|
return generator != null && packRef.equals(generator.dimensionKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requireReadableConfig(Path worldRoot, List<String> configProblems) throws IOException {
|
private static void requireReadableConfig(Path worldRoot, List<String> configProblems) throws IOException {
|
||||||
@@ -154,6 +225,6 @@ public final class ModdedTakeoverReconciler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record Result(Set<String> changedLevels, Path recovery) {
|
record Result(Set<String> changedLevels, Set<String> adoptedLevels, Path recovery) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+140
-3
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.world.history.GenerationPackFingerprint;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -76,7 +79,7 @@ public class ModdedTakeoverReconcilerTest {
|
|||||||
assertTrue(Files.isRegularFile(skylands.resolve("data/payload.bin")));
|
assertTrue(Files.isRegularFile(skylands.resolve("data/payload.bin")));
|
||||||
assertTrue(Files.isRegularFile(worldRoot.resolve("dimensions/minecraft/overworld/region/r.0.0.mca")));
|
assertTrue(Files.isRegularFile(worldRoot.resolve("dimensions/minecraft/overworld/region/r.0.0.mca")));
|
||||||
assertTrue(Files.isRegularFile(worldRoot.resolve("players/data/player.dat")));
|
assertTrue(Files.isRegularFile(worldRoot.resolve("players/data/player.dat")));
|
||||||
assertEquals(Map.of("iristest:skylands", "overworld"), ModdedTakeoverReconciler.readApplied(worldRoot));
|
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
} finally {
|
} finally {
|
||||||
deleteTree(temporary);
|
deleteTree(temporary);
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,7 @@ public class ModdedTakeoverReconcilerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void changeWithoutChunkDataRecordsStateWithoutARecoveryFolder() throws IOException {
|
public void newTakeoverWithoutChunkDataStaysPendingWithoutARecoveryFolder() throws IOException {
|
||||||
Path temporary = Files.createTempDirectory("iris-takeover-empty");
|
Path temporary = Files.createTempDirectory("iris-takeover-empty");
|
||||||
Path worldRoot = temporary.resolve("world");
|
Path worldRoot = temporary.resolve("world");
|
||||||
Path recoveryParent = temporary.resolve("config");
|
Path recoveryParent = temporary.resolve("config");
|
||||||
@@ -125,7 +128,7 @@ public class ModdedTakeoverReconcilerTest {
|
|||||||
assertEquals(Set.of("aether:the_aether"), result.changedLevels());
|
assertEquals(Set.of("aether:the_aether"), result.changedLevels());
|
||||||
assertNull(result.recovery());
|
assertNull(result.recovery());
|
||||||
assertFalse(Files.exists(recoveryParent));
|
assertFalse(Files.exists(recoveryParent));
|
||||||
assertEquals(Map.of("aether:the_aether", "overworld"), ModdedTakeoverReconciler.readApplied(worldRoot));
|
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
} finally {
|
} finally {
|
||||||
deleteTree(temporary);
|
deleteTree(temporary);
|
||||||
}
|
}
|
||||||
@@ -184,6 +187,140 @@ public class ModdedTakeoverReconcilerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changedTakeoverKeepsThePreviousStateUntilTheNewPackGenerates() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-change");
|
||||||
|
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);
|
||||||
|
|
||||||
|
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
|
||||||
|
worldRoot, Map.of("iristest:skylands", "skypack"), List.of(), temporary.resolve("config"));
|
||||||
|
|
||||||
|
assertEquals(Set.of("iristest:skylands"), result.changedLevels());
|
||||||
|
assertFalse(Files.exists(region));
|
||||||
|
assertEquals(Map.of("iristest:skylands", "overworld"), ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void existingHistoryOfTheConfiguredPackIsAdoptedWithoutQuarantine() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-history-match");
|
||||||
|
Path worldRoot = temporary.resolve("world");
|
||||||
|
try {
|
||||||
|
Path skylands = worldRoot.resolve("dimensions/iristest/skylands");
|
||||||
|
createHistory(temporary, skylands, ModdedWorldgenIds.dimensionTypeRef("skypack", "sky"), "sky", List.of());
|
||||||
|
touch(skylands.resolve("region/r.0.0.mca"));
|
||||||
|
|
||||||
|
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
|
||||||
|
worldRoot, Map.of("iristest:skylands", "skypack:sky"), List.of(), temporary.resolve("config"));
|
||||||
|
|
||||||
|
assertEquals(Set.of("iristest:skylands"), result.adoptedLevels());
|
||||||
|
assertNull(result.recovery());
|
||||||
|
assertTrue(Files.isRegularFile(skylands.resolve("region/r.0.0.mca")));
|
||||||
|
assertEquals(Map.of("iristest:skylands", "skypack:sky"), ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void historyOfAnotherPackIsQuarantined() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-history-other");
|
||||||
|
Path worldRoot = temporary.resolve("world");
|
||||||
|
try {
|
||||||
|
Path skylands = worldRoot.resolve("dimensions/iristest/skylands");
|
||||||
|
createHistory(temporary, skylands, ModdedWorldgenIds.dimensionTypeRef("otherpack", "sky"), "sky", List.of());
|
||||||
|
touch(skylands.resolve("region/r.0.0.mca"));
|
||||||
|
|
||||||
|
ModdedTakeoverReconciler.Result result = ModdedTakeoverReconciler.reconcile(
|
||||||
|
worldRoot, Map.of("iristest:skylands", "skypack:sky"), List.of(), temporary.resolve("config"));
|
||||||
|
|
||||||
|
assertEquals(Set.of(), result.adoptedLevels());
|
||||||
|
assertFalse(Files.exists(skylands.resolve("region")));
|
||||||
|
assertFalse(Files.exists(skylands.resolve("iris")));
|
||||||
|
assertEquals(Map.of(), ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void borrowedHistoryMatchesOnItsRecordedAliasPack() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-history-borrowed");
|
||||||
|
Path worldRoot = temporary.resolve("world");
|
||||||
|
try {
|
||||||
|
Path aether = worldRoot.resolve("dimensions/aether/the_aether");
|
||||||
|
createHistory(temporary, aether, "aether:the_aether", "sky",
|
||||||
|
List.of(ModdedWorldgenIds.biomeRef("skypack", "sky", "meadow")));
|
||||||
|
touch(aether.resolve("region/r.0.0.mca"));
|
||||||
|
|
||||||
|
ModdedTakeoverReconciler.Result other = ModdedTakeoverReconciler.reconcile(
|
||||||
|
worldRoot, Map.of("aether:the_aether", "otherpack:sky"), List.of(), temporary.resolve("config"));
|
||||||
|
|
||||||
|
assertEquals(Set.of(), other.adoptedLevels());
|
||||||
|
assertFalse(Files.exists(aether.resolve("region")));
|
||||||
|
|
||||||
|
createHistory(temporary, aether, "aether:the_aether", "sky",
|
||||||
|
List.of(ModdedWorldgenIds.biomeRef("skypack", "sky", "meadow")));
|
||||||
|
ModdedTakeoverReconciler.Result matching = ModdedTakeoverReconciler.reconcile(
|
||||||
|
worldRoot, Map.of("aether:the_aether", "skypack:sky"), List.of(), temporary.resolve("config"));
|
||||||
|
|
||||||
|
assertEquals(Set.of("aether:the_aether"), matching.adoptedLevels());
|
||||||
|
assertTrue(Files.isDirectory(aether.resolve("iris")));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recordAppliedStoresOnlyTakeoversThatGenerateFromTheirPack() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-record");
|
||||||
|
Path worldRoot = temporary.resolve("world");
|
||||||
|
try {
|
||||||
|
ModdedTakeoverReconciler.writeApplied(worldRoot, Map.of("iristest:kept", "overworld"));
|
||||||
|
|
||||||
|
Map<String, String> recorded = ModdedTakeoverReconciler.recordApplied(worldRoot,
|
||||||
|
Map.of("iristest:kept", "overworld", "iristest:live", "skypack", "iristest:skipped", "skypack"),
|
||||||
|
(String level, String packRef) -> !level.equals("iristest:skipped"));
|
||||||
|
|
||||||
|
Map<String, String> expected = Map.of("iristest:kept", "overworld", "iristest:live", "skypack");
|
||||||
|
assertEquals(expected, recorded);
|
||||||
|
assertEquals(expected, ModdedTakeoverReconciler.readApplied(worldRoot));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recordAppliedFailsWhenTheStateCannotBeWritten() throws IOException {
|
||||||
|
Path temporary = Files.createTempDirectory("iris-takeover-record-fail");
|
||||||
|
Path worldRoot = temporary.resolve("world");
|
||||||
|
try {
|
||||||
|
touch(worldRoot.resolve("data/irisworldgen/dimension-takeovers.json/blocker"));
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> ModdedTakeoverReconciler.recordApplied(worldRoot,
|
||||||
|
Map.of("iristest:live", "skypack"), (String level, String packRef) -> true));
|
||||||
|
} finally {
|
||||||
|
deleteTree(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createHistory(Path temporary, Path dimensionRoot, String dimensionTypeKey, String dimensionKey,
|
||||||
|
List<String> biomeKeys) throws IOException {
|
||||||
|
Path source = Files.createTempDirectory(temporary, "pack");
|
||||||
|
touch(source.resolve("dimensions/" + dimensionKey + ".json"));
|
||||||
|
GenerationEpoch epoch = ModdedRetainedPackNameTest.epoch(dimensionTypeKey, dimensionKey, biomeKeys);
|
||||||
|
Files.createDirectories(dimensionRoot);
|
||||||
|
GenerationHistory.create(dimensionRoot, source,
|
||||||
|
GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION), 5L,
|
||||||
|
epoch.dimensionContract(), epoch.registryContract());
|
||||||
|
}
|
||||||
|
|
||||||
private static void touch(Path file) throws IOException {
|
private static void touch(Path file) throws IOException {
|
||||||
Files.createDirectories(file.getParent());
|
Files.createDirectories(file.getParent());
|
||||||
Files.writeString(file, "x", StandardCharsets.UTF_8);
|
Files.writeString(file, "x", StandardCharsets.UTF_8);
|
||||||
|
|||||||
Reference in new issue
Block a user