487 lines
18 KiB
Java
487 lines
18 KiB
Java
package net.minecraft.util.worldupdate;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
|
import com.mojang.datafixers.DataFixer;
|
|
import com.mojang.logging.LogUtils;
|
|
import it.unimi.dsi.fastutil.objects.Reference2FloatMap;
|
|
import it.unimi.dsi.fastutil.objects.Reference2FloatMaps;
|
|
import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionException;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.SharedConstants;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.core.Registry;
|
|
import net.minecraft.core.RegistryAccess;
|
|
import net.minecraft.core.registries.Registries;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.ListTag;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.util.datafix.DataFixTypes;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
|
import net.minecraft.world.level.chunk.storage.ChunkStorage;
|
|
import net.minecraft.world.level.chunk.storage.RecreatingChunkStorage;
|
|
import net.minecraft.world.level.chunk.storage.RecreatingSimpleRegionStorage;
|
|
import net.minecraft.world.level.chunk.storage.RegionFile;
|
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
|
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
|
|
import net.minecraft.world.level.dimension.LevelStem;
|
|
import net.minecraft.world.level.saveddata.SavedData;
|
|
import net.minecraft.world.level.storage.DimensionDataStorage;
|
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
|
import net.minecraft.world.level.storage.WorldData;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class WorldUpgrader implements AutoCloseable {
|
|
static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder().setDaemon(true).build();
|
|
private static final String NEW_DIRECTORY_PREFIX = "new_";
|
|
static final Component STATUS_UPGRADING_POI = Component.translatable("optimizeWorld.stage.upgrading.poi");
|
|
static final Component STATUS_FINISHED_POI = Component.translatable("optimizeWorld.stage.finished.poi");
|
|
static final Component STATUS_UPGRADING_ENTITIES = Component.translatable("optimizeWorld.stage.upgrading.entities");
|
|
static final Component STATUS_FINISHED_ENTITIES = Component.translatable("optimizeWorld.stage.finished.entities");
|
|
static final Component STATUS_UPGRADING_CHUNKS = Component.translatable("optimizeWorld.stage.upgrading.chunks");
|
|
static final Component STATUS_FINISHED_CHUNKS = Component.translatable("optimizeWorld.stage.finished.chunks");
|
|
final Registry<LevelStem> dimensions;
|
|
final Set<ResourceKey<Level>> levels;
|
|
final boolean eraseCache;
|
|
final boolean recreateRegionFiles;
|
|
final LevelStorageSource.LevelStorageAccess levelStorage;
|
|
private final Thread thread;
|
|
final DataFixer dataFixer;
|
|
volatile boolean running = true;
|
|
private volatile boolean finished;
|
|
volatile float progress;
|
|
volatile int totalChunks;
|
|
volatile int totalFiles;
|
|
volatile int converted;
|
|
volatile int skipped;
|
|
final Reference2FloatMap<ResourceKey<Level>> progressMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap<>());
|
|
volatile Component status = Component.translatable("optimizeWorld.stage.counting");
|
|
static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$");
|
|
final DimensionDataStorage overworldDataStorage;
|
|
|
|
public WorldUpgrader(
|
|
LevelStorageSource.LevelStorageAccess levelStorage,
|
|
DataFixer dataFixer,
|
|
WorldData worldData,
|
|
RegistryAccess registryAccess,
|
|
boolean eraseCache,
|
|
boolean recreateRegionFiles
|
|
) {
|
|
this.dimensions = registryAccess.lookupOrThrow(Registries.LEVEL_STEM);
|
|
this.levels = (Set<ResourceKey<Level>>)this.dimensions.registryKeySet().stream().map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet());
|
|
this.eraseCache = eraseCache;
|
|
this.dataFixer = dataFixer;
|
|
this.levelStorage = levelStorage;
|
|
SavedData.Context context = new SavedData.Context(null, worldData.worldGenOptions().seed());
|
|
this.overworldDataStorage = new DimensionDataStorage(context, this.levelStorage.getDimensionPath(Level.OVERWORLD).resolve("data"), dataFixer, registryAccess);
|
|
this.recreateRegionFiles = recreateRegionFiles;
|
|
this.thread = THREAD_FACTORY.newThread(this::work);
|
|
this.thread.setUncaughtExceptionHandler((thread, throwable) -> {
|
|
LOGGER.error("Error upgrading world", throwable);
|
|
this.status = Component.translatable("optimizeWorld.stage.failed");
|
|
this.finished = true;
|
|
});
|
|
this.thread.start();
|
|
}
|
|
|
|
public void cancel() {
|
|
this.running = false;
|
|
|
|
try {
|
|
this.thread.join();
|
|
} catch (InterruptedException var2) {
|
|
}
|
|
}
|
|
|
|
private void work() {
|
|
long l = Util.getMillis();
|
|
LOGGER.info("Upgrading entities");
|
|
new WorldUpgrader.EntityUpgrader().upgrade();
|
|
LOGGER.info("Upgrading POIs");
|
|
new WorldUpgrader.PoiUpgrader().upgrade();
|
|
LOGGER.info("Upgrading blocks");
|
|
new WorldUpgrader.ChunkUpgrader().upgrade();
|
|
this.overworldDataStorage.saveAndJoin();
|
|
l = Util.getMillis() - l;
|
|
LOGGER.info("World optimizaton finished after {} seconds", l / 1000L);
|
|
this.finished = true;
|
|
}
|
|
|
|
public boolean isFinished() {
|
|
return this.finished;
|
|
}
|
|
|
|
public Set<ResourceKey<Level>> levels() {
|
|
return this.levels;
|
|
}
|
|
|
|
public float dimensionProgress(ResourceKey<Level> level) {
|
|
return this.progressMap.getFloat(level);
|
|
}
|
|
|
|
public float getProgress() {
|
|
return this.progress;
|
|
}
|
|
|
|
public int getTotalChunks() {
|
|
return this.totalChunks;
|
|
}
|
|
|
|
public int getConverted() {
|
|
return this.converted;
|
|
}
|
|
|
|
public int getSkipped() {
|
|
return this.skipped;
|
|
}
|
|
|
|
public Component getStatus() {
|
|
return this.status;
|
|
}
|
|
|
|
public void close() {
|
|
this.overworldDataStorage.close();
|
|
}
|
|
|
|
static Path resolveRecreateDirectory(Path path) {
|
|
return path.resolveSibling("new_" + path.getFileName().toString());
|
|
}
|
|
|
|
abstract class AbstractUpgrader<T extends AutoCloseable> {
|
|
private final Component upgradingStatus;
|
|
private final Component finishedStatus;
|
|
private final String type;
|
|
private final String folderName;
|
|
@Nullable
|
|
protected CompletableFuture<Void> previousWriteFuture;
|
|
protected final DataFixTypes dataFixType;
|
|
|
|
AbstractUpgrader(final DataFixTypes dataFixType, final String type, final String folderName, final Component upgradingStatus, final Component finishedStatus) {
|
|
this.dataFixType = dataFixType;
|
|
this.type = type;
|
|
this.folderName = folderName;
|
|
this.upgradingStatus = upgradingStatus;
|
|
this.finishedStatus = finishedStatus;
|
|
}
|
|
|
|
public void upgrade() {
|
|
WorldUpgrader.this.totalFiles = 0;
|
|
WorldUpgrader.this.totalChunks = 0;
|
|
WorldUpgrader.this.converted = 0;
|
|
WorldUpgrader.this.skipped = 0;
|
|
List<WorldUpgrader.DimensionToUpgrade<T>> list = this.getDimensionsToUpgrade();
|
|
if (WorldUpgrader.this.totalChunks != 0) {
|
|
float f = WorldUpgrader.this.totalFiles;
|
|
WorldUpgrader.this.status = this.upgradingStatus;
|
|
|
|
while (WorldUpgrader.this.running) {
|
|
boolean bl = false;
|
|
float g = 0.0F;
|
|
|
|
for (WorldUpgrader.DimensionToUpgrade<T> dimensionToUpgrade : list) {
|
|
ResourceKey<Level> resourceKey = dimensionToUpgrade.dimensionKey;
|
|
ListIterator<WorldUpgrader.FileToUpgrade> listIterator = dimensionToUpgrade.files;
|
|
T autoCloseable = dimensionToUpgrade.storage;
|
|
if (listIterator.hasNext()) {
|
|
WorldUpgrader.FileToUpgrade fileToUpgrade = (WorldUpgrader.FileToUpgrade)listIterator.next();
|
|
boolean bl2 = true;
|
|
|
|
for (ChunkPos chunkPos : fileToUpgrade.chunksToUpgrade) {
|
|
bl2 = bl2 && this.processOnePosition(resourceKey, autoCloseable, chunkPos);
|
|
bl = true;
|
|
}
|
|
|
|
if (WorldUpgrader.this.recreateRegionFiles) {
|
|
if (bl2) {
|
|
this.onFileFinished(fileToUpgrade.file);
|
|
} else {
|
|
WorldUpgrader.LOGGER.error("Failed to convert region file {}", fileToUpgrade.file.getPath());
|
|
}
|
|
}
|
|
}
|
|
|
|
float h = listIterator.nextIndex() / f;
|
|
WorldUpgrader.this.progressMap.put(resourceKey, h);
|
|
g += h;
|
|
}
|
|
|
|
WorldUpgrader.this.progress = g;
|
|
if (!bl) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
WorldUpgrader.this.status = this.finishedStatus;
|
|
|
|
for (WorldUpgrader.DimensionToUpgrade<T> dimensionToUpgrade2 : list) {
|
|
try {
|
|
dimensionToUpgrade2.storage.close();
|
|
} catch (Exception var14) {
|
|
WorldUpgrader.LOGGER.error("Error upgrading chunk", (Throwable)var14);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<WorldUpgrader.DimensionToUpgrade<T>> getDimensionsToUpgrade() {
|
|
List<WorldUpgrader.DimensionToUpgrade<T>> list = Lists.<WorldUpgrader.DimensionToUpgrade<T>>newArrayList();
|
|
|
|
for (ResourceKey<Level> resourceKey : WorldUpgrader.this.levels) {
|
|
RegionStorageInfo regionStorageInfo = new RegionStorageInfo(WorldUpgrader.this.levelStorage.getLevelId(), resourceKey, this.type);
|
|
Path path = WorldUpgrader.this.levelStorage.getDimensionPath(resourceKey).resolve(this.folderName);
|
|
T autoCloseable = this.createStorage(regionStorageInfo, path);
|
|
ListIterator<WorldUpgrader.FileToUpgrade> listIterator = this.getFilesToProcess(regionStorageInfo, path);
|
|
list.add(new WorldUpgrader.DimensionToUpgrade(resourceKey, autoCloseable, listIterator));
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
protected abstract T createStorage(RegionStorageInfo regionStorageInfo, Path path);
|
|
|
|
private ListIterator<WorldUpgrader.FileToUpgrade> getFilesToProcess(RegionStorageInfo regionStorageInfo, Path path) {
|
|
List<WorldUpgrader.FileToUpgrade> list = getAllChunkPositions(regionStorageInfo, path);
|
|
WorldUpgrader.this.totalFiles = WorldUpgrader.this.totalFiles + list.size();
|
|
WorldUpgrader.this.totalChunks = WorldUpgrader.this.totalChunks + list.stream().mapToInt(fileToUpgrade -> fileToUpgrade.chunksToUpgrade.size()).sum();
|
|
return list.listIterator();
|
|
}
|
|
|
|
private static List<WorldUpgrader.FileToUpgrade> getAllChunkPositions(RegionStorageInfo regionStorageInfo, Path path) {
|
|
File[] files = path.toFile().listFiles((filex, string) -> string.endsWith(".mca"));
|
|
if (files == null) {
|
|
return List.of();
|
|
} else {
|
|
List<WorldUpgrader.FileToUpgrade> list = Lists.<WorldUpgrader.FileToUpgrade>newArrayList();
|
|
|
|
for (File file : files) {
|
|
Matcher matcher = WorldUpgrader.REGEX.matcher(file.getName());
|
|
if (matcher.matches()) {
|
|
int i = Integer.parseInt(matcher.group(1)) << 5;
|
|
int j = Integer.parseInt(matcher.group(2)) << 5;
|
|
List<ChunkPos> list2 = Lists.<ChunkPos>newArrayList();
|
|
|
|
try (RegionFile regionFile = new RegionFile(regionStorageInfo, file.toPath(), path, true)) {
|
|
for (int k = 0; k < 32; k++) {
|
|
for (int l = 0; l < 32; l++) {
|
|
ChunkPos chunkPos = new ChunkPos(k + i, l + j);
|
|
if (regionFile.doesChunkExist(chunkPos)) {
|
|
list2.add(chunkPos);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!list2.isEmpty()) {
|
|
list.add(new WorldUpgrader.FileToUpgrade(regionFile, list2));
|
|
}
|
|
} catch (Throwable var18) {
|
|
WorldUpgrader.LOGGER.error("Failed to read chunks from region file {}", file.toPath(), var18);
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
}
|
|
|
|
private boolean processOnePosition(ResourceKey<Level> dimesion, T storage, ChunkPos chunkPos) {
|
|
boolean bl = false;
|
|
|
|
try {
|
|
bl = this.tryProcessOnePosition(storage, chunkPos, dimesion);
|
|
} catch (CompletionException | ReportedException var7) {
|
|
Throwable throwable = var7.getCause();
|
|
if (!(throwable instanceof IOException)) {
|
|
throw var7;
|
|
}
|
|
|
|
WorldUpgrader.LOGGER.error("Error upgrading chunk {}", chunkPos, throwable);
|
|
}
|
|
|
|
if (bl) {
|
|
WorldUpgrader.this.converted++;
|
|
} else {
|
|
WorldUpgrader.this.skipped++;
|
|
}
|
|
|
|
return bl;
|
|
}
|
|
|
|
protected abstract boolean tryProcessOnePosition(T chunkStorage, ChunkPos chunkPos, ResourceKey<Level> dimension);
|
|
|
|
private void onFileFinished(RegionFile regionFile) {
|
|
if (WorldUpgrader.this.recreateRegionFiles) {
|
|
if (this.previousWriteFuture != null) {
|
|
this.previousWriteFuture.join();
|
|
}
|
|
|
|
Path path = regionFile.getPath();
|
|
Path path2 = path.getParent();
|
|
Path path3 = WorldUpgrader.resolveRecreateDirectory(path2).resolve(path.getFileName().toString());
|
|
|
|
try {
|
|
if (path3.toFile().exists()) {
|
|
Files.delete(path);
|
|
Files.move(path3, path);
|
|
} else {
|
|
WorldUpgrader.LOGGER.error("Failed to replace an old region file. New file {} does not exist.", path3);
|
|
}
|
|
} catch (IOException var6) {
|
|
WorldUpgrader.LOGGER.error("Failed to replace an old region file", (Throwable)var6);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChunkUpgrader extends WorldUpgrader.AbstractUpgrader<ChunkStorage> {
|
|
ChunkUpgrader() {
|
|
super(DataFixTypes.CHUNK, "chunk", "region", WorldUpgrader.STATUS_UPGRADING_CHUNKS, WorldUpgrader.STATUS_FINISHED_CHUNKS);
|
|
}
|
|
|
|
protected boolean tryProcessOnePosition(ChunkStorage chunkStorage, ChunkPos chunkPos, ResourceKey<Level> resourceKey) {
|
|
CompoundTag compoundTag = (CompoundTag)((Optional)chunkStorage.read(chunkPos).join()).orElse(null);
|
|
if (compoundTag != null) {
|
|
int i = ChunkStorage.getVersion(compoundTag);
|
|
ChunkGenerator chunkGenerator = WorldUpgrader.this.dimensions.getValueOrThrow(Registries.levelToLevelStem(resourceKey)).generator();
|
|
CompoundTag compoundTag2 = chunkStorage.upgradeChunkTag(
|
|
resourceKey, () -> WorldUpgrader.this.overworldDataStorage, compoundTag, chunkGenerator.getTypeNameForDataFixer()
|
|
);
|
|
ChunkPos chunkPos2 = new ChunkPos(compoundTag2.getIntOr("xPos", 0), compoundTag2.getIntOr("zPos", 0));
|
|
if (!chunkPos2.equals(chunkPos)) {
|
|
WorldUpgrader.LOGGER.warn("Chunk {} has invalid position {}", chunkPos, chunkPos2);
|
|
}
|
|
|
|
boolean bl = i < SharedConstants.getCurrentVersion().getDataVersion().getVersion();
|
|
if (WorldUpgrader.this.eraseCache) {
|
|
bl = bl || compoundTag2.contains("Heightmaps");
|
|
compoundTag2.remove("Heightmaps");
|
|
bl = bl || compoundTag2.contains("isLightOn");
|
|
compoundTag2.remove("isLightOn");
|
|
ListTag listTag = compoundTag2.getListOrEmpty("sections");
|
|
|
|
for (int j = 0; j < listTag.size(); j++) {
|
|
Optional<CompoundTag> optional = listTag.getCompound(j);
|
|
if (!optional.isEmpty()) {
|
|
CompoundTag compoundTag3 = (CompoundTag)optional.get();
|
|
bl = bl || compoundTag3.contains("BlockLight");
|
|
compoundTag3.remove("BlockLight");
|
|
bl = bl || compoundTag3.contains("SkyLight");
|
|
compoundTag3.remove("SkyLight");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bl || WorldUpgrader.this.recreateRegionFiles) {
|
|
if (this.previousWriteFuture != null) {
|
|
this.previousWriteFuture.join();
|
|
}
|
|
|
|
this.previousWriteFuture = chunkStorage.write(chunkPos, () -> compoundTag2);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected ChunkStorage createStorage(RegionStorageInfo regionStorageInfo, Path path) {
|
|
return (ChunkStorage)(WorldUpgrader.this.recreateRegionFiles
|
|
? new RecreatingChunkStorage(
|
|
regionStorageInfo.withTypeSuffix("source"),
|
|
path,
|
|
regionStorageInfo.withTypeSuffix("target"),
|
|
WorldUpgrader.resolveRecreateDirectory(path),
|
|
WorldUpgrader.this.dataFixer,
|
|
true
|
|
)
|
|
: new ChunkStorage(regionStorageInfo, path, WorldUpgrader.this.dataFixer, true));
|
|
}
|
|
}
|
|
|
|
record DimensionToUpgrade<T>(ResourceKey<Level> dimensionKey, T storage, ListIterator<WorldUpgrader.FileToUpgrade> files) {
|
|
}
|
|
|
|
class EntityUpgrader extends WorldUpgrader.SimpleRegionStorageUpgrader {
|
|
EntityUpgrader() {
|
|
super(DataFixTypes.ENTITY_CHUNK, "entities", WorldUpgrader.STATUS_UPGRADING_ENTITIES, WorldUpgrader.STATUS_FINISHED_ENTITIES);
|
|
}
|
|
|
|
@Override
|
|
protected CompoundTag upgradeTag(SimpleRegionStorage regionStorage, CompoundTag chunkTag) {
|
|
return regionStorage.upgradeChunkTag(chunkTag, -1);
|
|
}
|
|
}
|
|
|
|
record FileToUpgrade(RegionFile file, List<ChunkPos> chunksToUpgrade) {
|
|
}
|
|
|
|
class PoiUpgrader extends WorldUpgrader.SimpleRegionStorageUpgrader {
|
|
PoiUpgrader() {
|
|
super(DataFixTypes.POI_CHUNK, "poi", WorldUpgrader.STATUS_UPGRADING_POI, WorldUpgrader.STATUS_FINISHED_POI);
|
|
}
|
|
|
|
@Override
|
|
protected CompoundTag upgradeTag(SimpleRegionStorage regionStorage, CompoundTag chunkTag) {
|
|
return regionStorage.upgradeChunkTag(chunkTag, 1945);
|
|
}
|
|
}
|
|
|
|
abstract class SimpleRegionStorageUpgrader extends WorldUpgrader.AbstractUpgrader<SimpleRegionStorage> {
|
|
SimpleRegionStorageUpgrader(final DataFixTypes dataFixType, final String type, final Component upgradingStatus, final Component finishedStatus) {
|
|
super(dataFixType, type, type, upgradingStatus, finishedStatus);
|
|
}
|
|
|
|
protected SimpleRegionStorage createStorage(RegionStorageInfo regionStorageInfo, Path path) {
|
|
return (SimpleRegionStorage)(WorldUpgrader.this.recreateRegionFiles
|
|
? new RecreatingSimpleRegionStorage(
|
|
regionStorageInfo.withTypeSuffix("source"),
|
|
path,
|
|
regionStorageInfo.withTypeSuffix("target"),
|
|
WorldUpgrader.resolveRecreateDirectory(path),
|
|
WorldUpgrader.this.dataFixer,
|
|
true,
|
|
this.dataFixType
|
|
)
|
|
: new SimpleRegionStorage(regionStorageInfo, path, WorldUpgrader.this.dataFixer, true, this.dataFixType));
|
|
}
|
|
|
|
protected boolean tryProcessOnePosition(SimpleRegionStorage simpleRegionStorage, ChunkPos chunkPos, ResourceKey<Level> resourceKey) {
|
|
CompoundTag compoundTag = (CompoundTag)((Optional)simpleRegionStorage.read(chunkPos).join()).orElse(null);
|
|
if (compoundTag != null) {
|
|
int i = ChunkStorage.getVersion(compoundTag);
|
|
CompoundTag compoundTag2 = this.upgradeTag(simpleRegionStorage, compoundTag);
|
|
boolean bl = i < SharedConstants.getCurrentVersion().getDataVersion().getVersion();
|
|
if (bl || WorldUpgrader.this.recreateRegionFiles) {
|
|
if (this.previousWriteFuture != null) {
|
|
this.previousWriteFuture.join();
|
|
}
|
|
|
|
this.previousWriteFuture = simpleRegionStorage.write(chunkPos, compoundTag2);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected abstract CompoundTag upgradeTag(SimpleRegionStorage regionStorage, CompoundTag chunkTag);
|
|
}
|
|
}
|