Code/net/minecraft/world/level/storage/LevelStorageSource.java

636 lines
27 KiB
Java
Raw Normal View History

2025-07-01 06:20:03 +00:00
package net.minecraft.world.level.storage;
import com.google.common.collect.Maps;
import com.mojang.datafixers.DataFixer;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.Lifecycle;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.Nullable;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.FileUtil;
import net.minecraft.ReportedException;
import net.minecraft.Util;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtFormatException;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.visitors.FieldSelector;
import net.minecraft.nbt.visitors.SkipFields;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.WorldLoader;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.util.DirectoryLock;
import net.minecraft.util.MemoryReserve;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelSettings;
import net.minecraft.world.level.WorldDataConfiguration;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.WorldDimensions;
import net.minecraft.world.level.levelgen.WorldGenSettings;
import net.minecraft.world.level.validation.ContentValidationException;
import net.minecraft.world.level.validation.DirectoryValidator;
import net.minecraft.world.level.validation.ForbiddenSymlinkInfo;
import net.minecraft.world.level.validation.PathAllowList;
import org.slf4j.Logger;
public class LevelStorageSource {
static final Logger LOGGER = LogUtils.getLogger();
static final DateTimeFormatter FORMATTER = FileNameDateFormatter.create();
public static final String TAG_DATA = "Data";
private static final PathMatcher NO_SYMLINKS_ALLOWED = p_296993_ -> false;
public static final String ALLOWED_SYMLINKS_CONFIG_NAME = "allowed_symlinks.txt";
private static final int UNCOMPRESSED_NBT_QUOTA = 104857600;
private static final int DISK_SPACE_WARNING_THRESHOLD = 67108864;
private final Path baseDir;
private final Path backupDir;
final DataFixer fixerUpper;
private final DirectoryValidator worldDirValidator;
public LevelStorageSource(Path p_289985_, Path p_289978_, DirectoryValidator p_289922_, DataFixer p_289940_) {
this.fixerUpper = p_289940_;
try {
FileUtil.createDirectoriesSafe(p_289985_);
} catch (IOException ioexception) {
throw new UncheckedIOException(ioexception);
}
this.baseDir = p_289985_;
this.backupDir = p_289978_;
this.worldDirValidator = p_289922_;
}
public static DirectoryValidator parseValidator(Path p_289968_) {
if (Files.exists(p_289968_)) {
try {
DirectoryValidator directoryvalidator;
try (BufferedReader bufferedreader = Files.newBufferedReader(p_289968_)) {
directoryvalidator = new DirectoryValidator(PathAllowList.readPlain(bufferedreader));
}
return directoryvalidator;
} catch (Exception exception) {
LOGGER.error("Failed to parse {}, disallowing all symbolic links", "allowed_symlinks.txt", exception);
}
}
return new DirectoryValidator(NO_SYMLINKS_ALLOWED);
}
public static LevelStorageSource createDefault(Path p_78243_) {
DirectoryValidator directoryvalidator = parseValidator(p_78243_.resolve("allowed_symlinks.txt"));
return new LevelStorageSource(p_78243_, p_78243_.resolve("../backups"), directoryvalidator, DataFixers.getDataFixer());
}
public static WorldDataConfiguration readDataConfig(Dynamic<?> p_250884_) {
return WorldDataConfiguration.CODEC.parse(p_250884_).resultOrPartial(LOGGER::error).orElse(WorldDataConfiguration.DEFAULT);
}
public static WorldLoader.PackConfig getPackConfig(Dynamic<?> p_312675_, PackRepository p_309764_, boolean p_310223_) {
return new WorldLoader.PackConfig(p_309764_, readDataConfig(p_312675_), p_310223_, false);
}
public static LevelDataAndDimensions getLevelDataAndDimensions(
Dynamic<?> p_311362_, WorldDataConfiguration p_311014_, Registry<LevelStem> p_311619_, HolderLookup.Provider p_365798_
) {
Dynamic<?> dynamic = RegistryOps.injectRegistryContext(p_311362_, p_365798_);
Dynamic<?> dynamic1 = dynamic.get("WorldGenSettings").orElseEmptyMap();
WorldGenSettings worldgensettings = WorldGenSettings.CODEC.parse(dynamic1).getOrThrow();
LevelSettings levelsettings = LevelSettings.parse(dynamic, p_311014_);
WorldDimensions.Complete worlddimensions$complete = worldgensettings.dimensions().bake(p_311619_);
Lifecycle lifecycle = worlddimensions$complete.lifecycle().add(p_365798_.allRegistriesLifecycle());
PrimaryLevelData primaryleveldata = PrimaryLevelData.parse(
dynamic, levelsettings, worlddimensions$complete.specialWorldProperty(), worldgensettings.options(), lifecycle
);
return new LevelDataAndDimensions(primaryleveldata, worlddimensions$complete);
}
public String getName() {
return "Anvil";
}
public LevelStorageSource.LevelCandidates findLevelCandidates() throws LevelStorageException {
if (!Files.isDirectory(this.baseDir)) {
throw new LevelStorageException(Component.translatable("selectWorld.load_folder_access"));
} else {
try {
LevelStorageSource.LevelCandidates levelstoragesource$levelcandidates;
try (Stream<Path> stream = Files.list(this.baseDir)) {
List<LevelStorageSource.LevelDirectory> list = stream.filter(p_230839_ -> Files.isDirectory(p_230839_))
.map(LevelStorageSource.LevelDirectory::new)
.filter(p_230835_ -> Files.isRegularFile(p_230835_.dataFile()) || Files.isRegularFile(p_230835_.oldDataFile()))
.toList();
levelstoragesource$levelcandidates = new LevelStorageSource.LevelCandidates(list);
}
return levelstoragesource$levelcandidates;
} catch (IOException ioexception) {
throw new LevelStorageException(Component.translatable("selectWorld.load_folder_access"));
}
}
}
public CompletableFuture<List<LevelSummary>> loadLevelSummaries(LevelStorageSource.LevelCandidates p_230814_) {
List<CompletableFuture<LevelSummary>> list = new ArrayList<>(p_230814_.levels.size());
for (LevelStorageSource.LevelDirectory levelstoragesource$leveldirectory : p_230814_.levels) {
list.add(CompletableFuture.supplyAsync(() -> {
boolean flag;
try {
flag = DirectoryLock.isLocked(levelstoragesource$leveldirectory.path());
} catch (Exception exception) {
LOGGER.warn("Failed to read {} lock", levelstoragesource$leveldirectory.path(), exception);
return null;
}
try {
return this.readLevelSummary(levelstoragesource$leveldirectory, flag);
} catch (OutOfMemoryError outofmemoryerror1) {
MemoryReserve.release();
String s = "Ran out of memory trying to read summary of world folder \"" + levelstoragesource$leveldirectory.directoryName() + "\"";
LOGGER.error(LogUtils.FATAL_MARKER, s);
OutOfMemoryError outofmemoryerror = new OutOfMemoryError("Ran out of memory reading level data");
outofmemoryerror.initCause(outofmemoryerror1);
CrashReport crashreport = CrashReport.forThrowable(outofmemoryerror, s);
CrashReportCategory crashreportcategory = crashreport.addCategory("World details");
crashreportcategory.setDetail("Folder Name", levelstoragesource$leveldirectory.directoryName());
try {
long i = Files.size(levelstoragesource$leveldirectory.dataFile());
crashreportcategory.setDetail("level.dat size", i);
} catch (IOException ioexception) {
crashreportcategory.setDetailError("level.dat size", ioexception);
}
throw new ReportedException(crashreport);
}
}, Util.backgroundExecutor().forName("loadLevelSummaries")));
}
return Util.sequenceFailFastAndCancel(list).thenApply(p_230832_ -> p_230832_.stream().filter(Objects::nonNull).sorted().toList());
}
private int getStorageVersion() {
return 19133;
}
static CompoundTag readLevelDataTagRaw(Path p_312857_) throws IOException {
return NbtIo.readCompressed(p_312857_, NbtAccounter.create(104857600L));
}
static Dynamic<?> readLevelDataTagFixed(Path p_309458_, DataFixer p_312702_) throws IOException {
CompoundTag compoundtag = readLevelDataTagRaw(p_309458_);
CompoundTag compoundtag1 = compoundtag.getCompoundOrEmpty("Data");
int i = NbtUtils.getDataVersion(compoundtag1, -1);
Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(p_312702_, new Dynamic<>(NbtOps.INSTANCE, compoundtag1), i);
dynamic = dynamic.update("Player", p_327540_ -> DataFixTypes.PLAYER.updateToCurrentVersion(p_312702_, p_327540_, i));
return dynamic.update("WorldGenSettings", p_327543_ -> DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(p_312702_, p_327543_, i));
}
private LevelSummary readLevelSummary(LevelStorageSource.LevelDirectory p_313112_, boolean p_312081_) {
Path path = p_313112_.dataFile();
if (Files.exists(path)) {
try {
if (Files.isSymbolicLink(path)) {
List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateSymlink(path);
if (!list.isEmpty()) {
LOGGER.warn("{}", ContentValidationException.getMessage(path, list));
return new LevelSummary.SymlinkLevelSummary(p_313112_.directoryName(), p_313112_.iconFile());
}
}
if (readLightweightData(path) instanceof CompoundTag compoundtag) {
CompoundTag compoundtag1 = compoundtag.getCompoundOrEmpty("Data");
int i = NbtUtils.getDataVersion(compoundtag1, -1);
Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(this.fixerUpper, new Dynamic<>(NbtOps.INSTANCE, compoundtag1), i);
return this.makeLevelSummary(dynamic, p_313112_, p_312081_);
}
LOGGER.warn("Invalid root tag in {}", path);
} catch (Exception exception) {
LOGGER.error("Exception reading {}", path, exception);
}
}
return new LevelSummary.CorruptedLevelSummary(p_313112_.directoryName(), p_313112_.iconFile(), getFileModificationTime(p_313112_));
}
private static long getFileModificationTime(LevelStorageSource.LevelDirectory p_311230_) {
Instant instant = getFileModificationTime(p_311230_.dataFile());
if (instant == null) {
instant = getFileModificationTime(p_311230_.oldDataFile());
}
return instant == null ? -1L : instant.toEpochMilli();
}
@Nullable
static Instant getFileModificationTime(Path p_313101_) {
try {
return Files.getLastModifiedTime(p_313101_).toInstant();
} catch (IOException ioexception) {
return null;
}
}
LevelSummary makeLevelSummary(Dynamic<?> p_310955_, LevelStorageSource.LevelDirectory p_309842_, boolean p_310644_) {
LevelVersion levelversion = LevelVersion.parse(p_310955_);
int i = levelversion.levelDataVersion();
if (i != 19132 && i != 19133) {
throw new NbtFormatException("Unknown data version: " + Integer.toHexString(i));
} else {
boolean flag = i != this.getStorageVersion();
Path path = p_309842_.iconFile();
WorldDataConfiguration worlddataconfiguration = readDataConfig(p_310955_);
LevelSettings levelsettings = LevelSettings.parse(p_310955_, worlddataconfiguration);
FeatureFlagSet featureflagset = parseFeatureFlagsFromSummary(p_310955_);
boolean flag1 = FeatureFlags.isExperimental(featureflagset);
return new LevelSummary(levelsettings, levelversion, p_309842_.directoryName(), flag, p_310644_, flag1, path);
}
}
private static FeatureFlagSet parseFeatureFlagsFromSummary(Dynamic<?> p_249466_) {
Set<ResourceLocation> set = p_249466_.get("enabled_features")
.asStream()
.flatMap(p_327537_ -> p_327537_.asString().result().map(ResourceLocation::tryParse).stream())
.collect(Collectors.toSet());
return FeatureFlags.REGISTRY.fromNames(set, p_248503_ -> {});
}
@Nullable
private static Tag readLightweightData(Path p_230837_) throws IOException {
SkipFields skipfields = new SkipFields(
new FieldSelector("Data", CompoundTag.TYPE, "Player"), new FieldSelector("Data", CompoundTag.TYPE, "WorldGenSettings")
);
NbtIo.parseCompressed(p_230837_, skipfields, NbtAccounter.create(104857600L));
return skipfields.getResult();
}
public boolean isNewLevelIdAcceptable(String p_78241_) {
try {
Path path = this.getLevelPath(p_78241_);
Files.createDirectory(path);
Files.deleteIfExists(path);
return true;
} catch (IOException ioexception) {
return false;
}
}
public boolean levelExists(String p_78256_) {
try {
return Files.isDirectory(this.getLevelPath(p_78256_));
} catch (InvalidPathException invalidpathexception) {
return false;
}
}
public Path getLevelPath(String p_289974_) {
return this.baseDir.resolve(p_289974_);
}
public Path getBaseDir() {
return this.baseDir;
}
public Path getBackupPath() {
return this.backupDir;
}
public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String p_289980_) throws IOException, ContentValidationException {
Path path = this.getLevelPath(p_289980_);
List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateDirectory(path, true);
if (!list.isEmpty()) {
throw new ContentValidationException(path, list);
} else {
return new LevelStorageSource.LevelStorageAccess(p_289980_, path);
}
}
public LevelStorageSource.LevelStorageAccess createAccess(String p_78261_) throws IOException {
Path path = this.getLevelPath(p_78261_);
return new LevelStorageSource.LevelStorageAccess(p_78261_, path);
}
public DirectoryValidator getWorldDirValidator() {
return this.worldDirValidator;
}
public record LevelCandidates(List<LevelStorageSource.LevelDirectory> levels) implements Iterable<LevelStorageSource.LevelDirectory> {
public boolean isEmpty() {
return this.levels.isEmpty();
}
@Override
public Iterator<LevelStorageSource.LevelDirectory> iterator() {
return this.levels.iterator();
}
}
public record LevelDirectory(Path path) {
public String directoryName() {
return this.path.getFileName().toString();
}
public Path dataFile() {
return this.resourcePath(LevelResource.LEVEL_DATA_FILE);
}
public Path oldDataFile() {
return this.resourcePath(LevelResource.OLD_LEVEL_DATA_FILE);
}
public Path corruptedDataFile(LocalDateTime p_230857_) {
return this.path.resolve(LevelResource.LEVEL_DATA_FILE.getId() + "_corrupted_" + p_230857_.format(LevelStorageSource.FORMATTER));
}
public Path rawDataFile(LocalDateTime p_310027_) {
return this.path.resolve(LevelResource.LEVEL_DATA_FILE.getId() + "_raw_" + p_310027_.format(LevelStorageSource.FORMATTER));
}
public Path iconFile() {
return this.resourcePath(LevelResource.ICON_FILE);
}
public Path lockFile() {
return this.resourcePath(LevelResource.LOCK_FILE);
}
public Path resourcePath(LevelResource p_230855_) {
return this.path.resolve(p_230855_.getId());
}
}
public class LevelStorageAccess implements AutoCloseable {
final DirectoryLock lock;
final LevelStorageSource.LevelDirectory levelDirectory;
private final String levelId;
private final Map<LevelResource, Path> resources = Maps.newHashMap();
LevelStorageAccess(final String p_289967_, final Path p_289988_) throws IOException {
this.levelId = p_289967_;
this.levelDirectory = new LevelStorageSource.LevelDirectory(p_289988_);
this.lock = DirectoryLock.create(p_289988_);
}
public long estimateDiskSpace() {
try {
return Files.getFileStore(this.levelDirectory.path).getUsableSpace();
} catch (Exception exception) {
return Long.MAX_VALUE;
}
}
public boolean checkForLowDiskSpace() {
return this.estimateDiskSpace() < 67108864L;
}
public void safeClose() {
try {
this.close();
} catch (IOException ioexception) {
LevelStorageSource.LOGGER.warn("Failed to unlock access to level {}", this.getLevelId(), ioexception);
}
}
public LevelStorageSource parent() {
return LevelStorageSource.this;
}
public LevelStorageSource.LevelDirectory getLevelDirectory() {
return this.levelDirectory;
}
public String getLevelId() {
return this.levelId;
}
public Path getLevelPath(LevelResource p_78284_) {
return this.resources.computeIfAbsent(p_78284_, this.levelDirectory::resourcePath);
}
public Path getDimensionPath(ResourceKey<Level> p_197395_) {
return DimensionType.getStorageFolder(p_197395_, this.levelDirectory.path());
}
private void checkLock() {
if (!this.lock.isValid()) {
throw new IllegalStateException("Lock is no longer valid");
}
}
public PlayerDataStorage createPlayerStorage() {
this.checkLock();
return new PlayerDataStorage(this, LevelStorageSource.this.fixerUpper);
}
public LevelSummary getSummary(Dynamic<?> p_310283_) {
this.checkLock();
return LevelStorageSource.this.makeLevelSummary(p_310283_, this.levelDirectory, false);
}
public Dynamic<?> getDataTag() throws IOException {
return this.getDataTag(false);
}
public Dynamic<?> getDataTagFallback() throws IOException {
return this.getDataTag(true);
}
private Dynamic<?> getDataTag(boolean p_310699_) throws IOException {
this.checkLock();
return LevelStorageSource.readLevelDataTagFixed(p_310699_ ? this.levelDirectory.oldDataFile() : this.levelDirectory.dataFile(), LevelStorageSource.this.fixerUpper);
}
public void saveDataTag(RegistryAccess p_78288_, WorldData p_78289_) {
this.saveDataTag(p_78288_, p_78289_, null);
}
public void saveDataTag(RegistryAccess p_78291_, WorldData p_78292_, @Nullable CompoundTag p_78293_) {
CompoundTag compoundtag = p_78292_.createTag(p_78291_, p_78293_);
CompoundTag compoundtag1 = new CompoundTag();
compoundtag1.put("Data", compoundtag);
this.saveLevelData(compoundtag1);
}
private void saveLevelData(CompoundTag p_312575_) {
Path path = this.levelDirectory.path();
try {
Path path1 = Files.createTempFile(path, "level", ".dat");
NbtIo.writeCompressed(p_312575_, path1);
Path path2 = this.levelDirectory.oldDataFile();
Path path3 = this.levelDirectory.dataFile();
Util.safeReplaceFile(path3, path1, path2);
} catch (Exception exception) {
LevelStorageSource.LOGGER.error("Failed to save level {}", path, exception);
}
}
public Optional<Path> getIconFile() {
return !this.lock.isValid() ? Optional.empty() : Optional.of(this.levelDirectory.iconFile());
}
public void deleteLevel() throws IOException {
this.checkLock();
final Path path = this.levelDirectory.lockFile();
LevelStorageSource.LOGGER.info("Deleting level {}", this.levelId);
for (int i = 1; i <= 5; i++) {
LevelStorageSource.LOGGER.info("Attempt {}...", i);
try {
Files.walkFileTree(this.levelDirectory.path(), new SimpleFileVisitor<Path>() {
public FileVisitResult visitFile(Path p_78323_, BasicFileAttributes p_78324_) throws IOException {
if (!p_78323_.equals(path)) {
LevelStorageSource.LOGGER.debug("Deleting {}", p_78323_);
Files.delete(p_78323_);
}
return FileVisitResult.CONTINUE;
}
public FileVisitResult postVisitDirectory(Path p_78320_, @Nullable IOException p_78321_) throws IOException {
if (p_78321_ != null) {
throw p_78321_;
} else {
if (p_78320_.equals(LevelStorageAccess.this.levelDirectory.path())) {
LevelStorageAccess.this.lock.close();
Files.deleteIfExists(path);
}
Files.delete(p_78320_);
return FileVisitResult.CONTINUE;
}
}
});
break;
} catch (IOException ioexception) {
if (i >= 5) {
throw ioexception;
}
LevelStorageSource.LOGGER.warn("Failed to delete {}", this.levelDirectory.path(), ioexception);
try {
Thread.sleep(500L);
} catch (InterruptedException interruptedexception) {
}
}
}
}
public void renameLevel(String p_78298_) throws IOException {
this.modifyLevelDataWithoutDatafix(p_313219_ -> p_313219_.putString("LevelName", p_78298_.trim()));
}
public void renameAndDropPlayer(String p_309798_) throws IOException {
this.modifyLevelDataWithoutDatafix(p_312160_ -> {
p_312160_.putString("LevelName", p_309798_.trim());
p_312160_.remove("Player");
});
}
private void modifyLevelDataWithoutDatafix(Consumer<CompoundTag> p_310066_) throws IOException {
this.checkLock();
CompoundTag compoundtag = LevelStorageSource.readLevelDataTagRaw(this.levelDirectory.dataFile());
p_310066_.accept(compoundtag.getCompoundOrEmpty("Data"));
this.saveLevelData(compoundtag);
}
public long makeWorldBackup() throws IOException {
this.checkLock();
String s = LocalDateTime.now().format(LevelStorageSource.FORMATTER) + "_" + this.levelId;
Path path = LevelStorageSource.this.getBackupPath();
try {
FileUtil.createDirectoriesSafe(path);
} catch (IOException ioexception) {
throw new RuntimeException(ioexception);
}
Path path1 = path.resolve(FileUtil.findAvailableName(path, s, ".zip"));
try (final ZipOutputStream zipoutputstream = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(path1)))) {
final Path path2 = Paths.get(this.levelId);
Files.walkFileTree(this.levelDirectory.path(), new SimpleFileVisitor<Path>() {
public FileVisitResult visitFile(Path p_78339_, BasicFileAttributes p_78340_) throws IOException {
if (p_78339_.endsWith("session.lock")) {
return FileVisitResult.CONTINUE;
} else {
String s1 = path2.resolve(LevelStorageAccess.this.levelDirectory.path().relativize(p_78339_)).toString().replace('\\', '/');
ZipEntry zipentry = new ZipEntry(s1);
zipoutputstream.putNextEntry(zipentry);
com.google.common.io.Files.asByteSource(p_78339_.toFile()).copyTo(zipoutputstream);
zipoutputstream.closeEntry();
return FileVisitResult.CONTINUE;
}
}
});
}
return Files.size(path1);
}
public boolean hasWorldData() {
return Files.exists(this.levelDirectory.dataFile()) || Files.exists(this.levelDirectory.oldDataFile());
}
@Override
public void close() throws IOException {
this.lock.close();
}
public boolean restoreLevelDataFromOld() {
return Util.safeReplaceOrMoveFile(this.levelDirectory.dataFile(), this.levelDirectory.oldDataFile(), this.levelDirectory.corruptedDataFile(LocalDateTime.now()), true);
}
@Nullable
public Instant getFileModificationTime(boolean p_311251_) {
return LevelStorageSource.getFileModificationTime(p_311251_ ? this.levelDirectory.oldDataFile() : this.levelDirectory.dataFile());
}
}
}