minecraft-src/net/minecraft/world/level/storage/LevelStorageSource.java
2025-07-04 03:45:38 +03:00

652 lines
23 KiB
Java

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.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
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.ZipOutputStream;
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.PackConfig;
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.WorldGenSettings;
import net.minecraft.world.level.levelgen.WorldDimensions.Complete;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess.1;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess.2;
import net.minecraft.world.level.storage.LevelSummary.CorruptedLevelSummary;
import net.minecraft.world.level.storage.LevelSummary.SymlinkLevelSummary;
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.jetbrains.annotations.Nullable;
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 = path -> 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 baseDir, Path backupDir, DirectoryValidator worldDirValidator, DataFixer fixerUpper) {
this.fixerUpper = fixerUpper;
try {
FileUtil.createDirectoriesSafe(baseDir);
} catch (IOException var6) {
throw new UncheckedIOException(var6);
}
this.baseDir = baseDir;
this.backupDir = backupDir;
this.worldDirValidator = worldDirValidator;
}
public static DirectoryValidator parseValidator(Path validator) {
if (Files.exists(validator, new LinkOption[0])) {
try {
BufferedReader bufferedReader = Files.newBufferedReader(validator);
DirectoryValidator var2;
try {
var2 = new DirectoryValidator(PathAllowList.readPlain(bufferedReader));
} catch (Throwable var5) {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
}
throw var5;
}
if (bufferedReader != null) {
bufferedReader.close();
}
return var2;
} catch (Exception var6) {
LOGGER.error("Failed to parse {}, disallowing all symbolic links", "allowed_symlinks.txt", var6);
}
}
return new DirectoryValidator(NO_SYMLINKS_ALLOWED);
}
public static LevelStorageSource createDefault(Path savesDir) {
DirectoryValidator directoryValidator = parseValidator(savesDir.resolve("allowed_symlinks.txt"));
return new LevelStorageSource(savesDir, savesDir.resolve("../backups"), directoryValidator, DataFixers.getDataFixer());
}
public static WorldDataConfiguration readDataConfig(Dynamic<?> dynamic) {
return (WorldDataConfiguration)WorldDataConfiguration.CODEC.parse(dynamic).resultOrPartial(LOGGER::error).orElse(WorldDataConfiguration.DEFAULT);
}
public static PackConfig getPackConfig(Dynamic<?> dynamic, PackRepository packRepository, boolean safeMode) {
return new PackConfig(packRepository, readDataConfig(dynamic), safeMode, false);
}
public static LevelDataAndDimensions getLevelDataAndDimensions(
Dynamic<?> levelData, WorldDataConfiguration dataConfiguration, Registry<LevelStem> levelStemRegistry, HolderLookup.Provider registries
) {
Dynamic<?> dynamic = RegistryOps.injectRegistryContext(levelData, registries);
Dynamic<?> dynamic2 = dynamic.get("WorldGenSettings").orElseEmptyMap();
WorldGenSettings worldGenSettings = WorldGenSettings.CODEC.parse(dynamic2).getOrThrow();
LevelSettings levelSettings = LevelSettings.parse(dynamic, dataConfiguration);
Complete complete = worldGenSettings.dimensions().bake(levelStemRegistry);
Lifecycle lifecycle = complete.lifecycle().add(registries.allRegistriesLifecycle());
PrimaryLevelData primaryLevelData = PrimaryLevelData.parse(dynamic, levelSettings, complete.specialWorldProperty(), worldGenSettings.options(), lifecycle);
return new LevelDataAndDimensions(primaryLevelData, complete);
}
public String getName() {
return "Anvil";
}
public LevelStorageSource.LevelCandidates findLevelCandidates() throws LevelStorageException {
if (!Files.isDirectory(this.baseDir, new LinkOption[0])) {
throw new LevelStorageException(Component.translatable("selectWorld.load_folder_access"));
} else {
try {
Stream<Path> stream = Files.list(this.baseDir);
LevelStorageSource.LevelCandidates var3;
try {
List<LevelStorageSource.LevelDirectory> list = stream.filter(path -> Files.isDirectory(path, new LinkOption[0]))
.map(LevelStorageSource.LevelDirectory::new)
.filter(
levelDirectory -> Files.isRegularFile(levelDirectory.dataFile(), new LinkOption[0])
|| Files.isRegularFile(levelDirectory.oldDataFile(), new LinkOption[0])
)
.toList();
var3 = new LevelStorageSource.LevelCandidates(list);
} catch (Throwable var5) {
if (stream != null) {
try {
stream.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
}
throw var5;
}
if (stream != null) {
stream.close();
}
return var3;
} catch (IOException var6) {
throw new LevelStorageException(Component.translatable("selectWorld.load_folder_access"));
}
}
}
public CompletableFuture<List<LevelSummary>> loadLevelSummaries(LevelStorageSource.LevelCandidates candidates) {
List<CompletableFuture<LevelSummary>> list = new ArrayList(candidates.levels.size());
for (LevelStorageSource.LevelDirectory levelDirectory : candidates.levels) {
list.add(CompletableFuture.supplyAsync(() -> {
boolean bl;
try {
bl = DirectoryLock.isLocked(levelDirectory.path());
} catch (Exception var13) {
LOGGER.warn("Failed to read {} lock", levelDirectory.path(), var13);
return null;
}
try {
return this.readLevelSummary(levelDirectory, bl);
} catch (OutOfMemoryError var12) {
MemoryReserve.release();
String string = "Ran out of memory trying to read summary of world folder \"" + levelDirectory.directoryName() + "\"";
LOGGER.error(LogUtils.FATAL_MARKER, string);
OutOfMemoryError outOfMemoryError2 = new OutOfMemoryError("Ran out of memory reading level data");
outOfMemoryError2.initCause(var12);
CrashReport crashReport = CrashReport.forThrowable(outOfMemoryError2, string);
CrashReportCategory crashReportCategory = crashReport.addCategory("World details");
crashReportCategory.setDetail("Folder Name", levelDirectory.directoryName());
try {
long l = Files.size(levelDirectory.dataFile());
crashReportCategory.setDetail("level.dat size", l);
} catch (IOException var11) {
crashReportCategory.setDetailError("level.dat size", var11);
}
throw new ReportedException(crashReport);
}
}, Util.backgroundExecutor().forName("loadLevelSummaries")));
}
return Util.sequenceFailFastAndCancel(list).thenApply(listx -> listx.stream().filter(Objects::nonNull).sorted().toList());
}
private int getStorageVersion() {
return 19133;
}
static CompoundTag readLevelDataTagRaw(Path levelPath) throws IOException {
return NbtIo.readCompressed(levelPath, NbtAccounter.create(104857600L));
}
static Dynamic<?> readLevelDataTagFixed(Path levelPath, DataFixer dataFixer) throws IOException {
CompoundTag compoundTag = readLevelDataTagRaw(levelPath);
CompoundTag compoundTag2 = compoundTag.getCompoundOrEmpty("Data");
int i = NbtUtils.getDataVersion(compoundTag2, -1);
Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(dataFixer, new Dynamic<>(NbtOps.INSTANCE, compoundTag2), i);
dynamic = dynamic.update("Player", dynamicx -> DataFixTypes.PLAYER.updateToCurrentVersion(dataFixer, dynamicx, i));
return dynamic.update("WorldGenSettings", dynamicx -> DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(dataFixer, dynamicx, i));
}
private LevelSummary readLevelSummary(LevelStorageSource.LevelDirectory levelDirectory, boolean locked) {
Path path = levelDirectory.dataFile();
if (Files.exists(path, new LinkOption[0])) {
try {
if (Files.isSymbolicLink(path)) {
List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateSymlink(path);
if (!list.isEmpty()) {
LOGGER.warn("{}", ContentValidationException.getMessage(path, list));
return new SymlinkLevelSummary(levelDirectory.directoryName(), levelDirectory.iconFile());
}
}
if (readLightweightData(path) instanceof CompoundTag compoundTag) {
CompoundTag compoundTag2 = compoundTag.getCompoundOrEmpty("Data");
int i = NbtUtils.getDataVersion(compoundTag2, -1);
Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(this.fixerUpper, new Dynamic<>(NbtOps.INSTANCE, compoundTag2), i);
return this.makeLevelSummary(dynamic, levelDirectory, locked);
}
LOGGER.warn("Invalid root tag in {}", path);
} catch (Exception var9) {
LOGGER.error("Exception reading {}", path, var9);
}
}
return new CorruptedLevelSummary(levelDirectory.directoryName(), levelDirectory.iconFile(), getFileModificationTime(levelDirectory));
}
private static long getFileModificationTime(LevelStorageSource.LevelDirectory levelDirectory) {
Instant instant = getFileModificationTime(levelDirectory.dataFile());
if (instant == null) {
instant = getFileModificationTime(levelDirectory.oldDataFile());
}
return instant == null ? -1L : instant.toEpochMilli();
}
@Nullable
static Instant getFileModificationTime(Path dataFilePath) {
try {
return Files.getLastModifiedTime(dataFilePath).toInstant();
} catch (IOException var2) {
return null;
}
}
LevelSummary makeLevelSummary(Dynamic<?> dynamic, LevelStorageSource.LevelDirectory levelDirectory, boolean locked) {
LevelVersion levelVersion = LevelVersion.parse(dynamic);
int i = levelVersion.levelDataVersion();
if (i != 19132 && i != 19133) {
throw new NbtFormatException("Unknown data version: " + Integer.toHexString(i));
} else {
boolean bl = i != this.getStorageVersion();
Path path = levelDirectory.iconFile();
WorldDataConfiguration worldDataConfiguration = readDataConfig(dynamic);
LevelSettings levelSettings = LevelSettings.parse(dynamic, worldDataConfiguration);
FeatureFlagSet featureFlagSet = parseFeatureFlagsFromSummary(dynamic);
boolean bl2 = FeatureFlags.isExperimental(featureFlagSet);
return new LevelSummary(levelSettings, levelVersion, levelDirectory.directoryName(), bl, locked, bl2, path);
}
}
private static FeatureFlagSet parseFeatureFlagsFromSummary(Dynamic<?> dataDynamic) {
Set<ResourceLocation> set = (Set<ResourceLocation>)dataDynamic.get("enabled_features")
.asStream()
.flatMap(dynamic -> dynamic.asString().result().map(ResourceLocation::tryParse).stream())
.collect(Collectors.toSet());
return FeatureFlags.REGISTRY.fromNames(set, resourceLocation -> {});
}
@Nullable
private static Tag readLightweightData(Path file) throws IOException {
SkipFields skipFields = new SkipFields(new FieldSelector("Data", CompoundTag.TYPE, "Player"), new FieldSelector("Data", CompoundTag.TYPE, "WorldGenSettings"));
NbtIo.parseCompressed(file, skipFields, NbtAccounter.create(104857600L));
return skipFields.getResult();
}
public boolean isNewLevelIdAcceptable(String saveName) {
try {
Path path = this.getLevelPath(saveName);
Files.createDirectory(path);
Files.deleteIfExists(path);
return true;
} catch (IOException var3) {
return false;
}
}
/**
* Return whether the given world can be loaded.
*/
public boolean levelExists(String saveName) {
try {
return Files.isDirectory(this.getLevelPath(saveName), new LinkOption[0]);
} catch (InvalidPathException var3) {
return false;
}
}
public Path getLevelPath(String saveName) {
return this.baseDir.resolve(saveName);
}
public Path getBaseDir() {
return this.baseDir;
}
/**
* Gets the folder where backups are stored
*/
public Path getBackupPath() {
return this.backupDir;
}
public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String saveName) throws IOException, ContentValidationException {
Path path = this.getLevelPath(saveName);
List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateDirectory(path, true);
if (!list.isEmpty()) {
throw new ContentValidationException(path, list);
} else {
return new LevelStorageSource.LevelStorageAccess(saveName, path);
}
}
public LevelStorageSource.LevelStorageAccess createAccess(String saveName) throws IOException {
Path path = this.getLevelPath(saveName);
return new LevelStorageSource.LevelStorageAccess(saveName, 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();
}
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 dateTime) {
return this.path.resolve(LevelResource.LEVEL_DATA_FILE.getId() + "_corrupted_" + dateTime.format(LevelStorageSource.FORMATTER));
}
public Path rawDataFile(LocalDateTime dateTime) {
return this.path.resolve(LevelResource.LEVEL_DATA_FILE.getId() + "_raw_" + dateTime.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 resource) {
return this.path.resolve(resource.getId());
}
}
public class LevelStorageAccess implements AutoCloseable {
final DirectoryLock lock;
final LevelStorageSource.LevelDirectory levelDirectory;
private final String levelId;
private final Map<LevelResource, Path> resources = Maps.<LevelResource, Path>newHashMap();
LevelStorageAccess(final String levelId, final Path levelDir) throws IOException {
this.levelId = levelId;
this.levelDirectory = new LevelStorageSource.LevelDirectory(levelDir);
this.lock = DirectoryLock.create(levelDir);
}
public long estimateDiskSpace() {
try {
return Files.getFileStore(this.levelDirectory.path).getUsableSpace();
} catch (Exception var2) {
return Long.MAX_VALUE;
}
}
public boolean checkForLowDiskSpace() {
return this.estimateDiskSpace() < 67108864L;
}
public void safeClose() {
try {
this.close();
} catch (IOException var2) {
LevelStorageSource.LOGGER.warn("Failed to unlock access to level {}", this.getLevelId(), var2);
}
}
public LevelStorageSource parent() {
return LevelStorageSource.this;
}
public LevelStorageSource.LevelDirectory getLevelDirectory() {
return this.levelDirectory;
}
public String getLevelId() {
return this.levelId;
}
public Path getLevelPath(LevelResource folderName) {
return (Path)this.resources.computeIfAbsent(folderName, this.levelDirectory::resourcePath);
}
public Path getDimensionPath(ResourceKey<Level> dimensionPath) {
return DimensionType.getStorageFolder(dimensionPath, 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<?> dynamic) {
this.checkLock();
return LevelStorageSource.this.makeLevelSummary(dynamic, 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 useFallback) throws IOException {
this.checkLock();
return LevelStorageSource.readLevelDataTagFixed(
useFallback ? this.levelDirectory.oldDataFile() : this.levelDirectory.dataFile(), LevelStorageSource.this.fixerUpper
);
}
public void saveDataTag(RegistryAccess registries, WorldData serverConfiguration) {
this.saveDataTag(registries, serverConfiguration, null);
}
public void saveDataTag(RegistryAccess registries, WorldData serverConfiguration, @Nullable CompoundTag hostPlayerNBT) {
CompoundTag compoundTag = serverConfiguration.createTag(registries, hostPlayerNBT);
CompoundTag compoundTag2 = new CompoundTag();
compoundTag2.put("Data", compoundTag);
this.saveLevelData(compoundTag2);
}
private void saveLevelData(CompoundTag tag) {
Path path = this.levelDirectory.path();
try {
Path path2 = Files.createTempFile(path, "level", ".dat");
NbtIo.writeCompressed(tag, path2);
Path path3 = this.levelDirectory.oldDataFile();
Path path4 = this.levelDirectory.dataFile();
Util.safeReplaceFile(path4, path2, path3);
} catch (Exception var6) {
LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6);
}
}
public Optional<Path> getIconFile() {
return !this.lock.isValid() ? Optional.empty() : Optional.of(this.levelDirectory.iconFile());
}
public void deleteLevel() throws IOException {
this.checkLock();
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 1(this, path));
break;
} catch (IOException var6) {
if (i >= 5) {
throw var6;
}
LevelStorageSource.LOGGER.warn("Failed to delete {}", this.levelDirectory.path(), var6);
try {
Thread.sleep(500L);
} catch (InterruptedException var5) {
}
}
}
}
public void renameLevel(String saveName) throws IOException {
this.modifyLevelDataWithoutDatafix(compoundTag -> compoundTag.putString("LevelName", saveName.trim()));
}
public void renameAndDropPlayer(String saveName) throws IOException {
this.modifyLevelDataWithoutDatafix(compoundTag -> {
compoundTag.putString("LevelName", saveName.trim());
compoundTag.remove("Player");
});
}
private void modifyLevelDataWithoutDatafix(Consumer<CompoundTag> modifier) throws IOException {
this.checkLock();
CompoundTag compoundTag = LevelStorageSource.readLevelDataTagRaw(this.levelDirectory.dataFile());
modifier.accept(compoundTag.getCompoundOrEmpty("Data"));
this.saveLevelData(compoundTag);
}
public long makeWorldBackup() throws IOException {
this.checkLock();
String string = LocalDateTime.now().format(LevelStorageSource.FORMATTER) + "_" + this.levelId;
Path path = LevelStorageSource.this.getBackupPath();
try {
FileUtil.createDirectoriesSafe(path);
} catch (IOException var9) {
throw new RuntimeException(var9);
}
Path path2 = path.resolve(FileUtil.findAvailableName(path, string, ".zip"));
ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(path2)));
try {
Path path3 = Paths.get(this.levelId);
Files.walkFileTree(this.levelDirectory.path(), new 2(this, path3, zipOutputStream));
} catch (Throwable var8) {
try {
zipOutputStream.close();
} catch (Throwable var7) {
var8.addSuppressed(var7);
}
throw var8;
}
zipOutputStream.close();
return Files.size(path2);
}
public boolean hasWorldData() {
return Files.exists(this.levelDirectory.dataFile(), new LinkOption[0]) || Files.exists(this.levelDirectory.oldDataFile(), new LinkOption[0]);
}
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 useFallback) {
return LevelStorageSource.getFileModificationTime(useFallback ? this.levelDirectory.oldDataFile() : this.levelDirectory.dataFile());
}
}
}