Minestom/src/main/java/net/minestom/server/instance/AnvilLoader.java

311 lines
14 KiB
Java

package net.minestom.server.instance;
import net.minestom.server.MinecraftServer;
import net.minestom.server.exception.ExceptionManager;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.world.biomes.Biome;
import net.minestom.server.world.biomes.BiomeManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.mca.*;
import org.jglrxavpok.hephaistos.nbt.*;
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class AnvilLoader implements IChunkLoader {
private final static Logger LOGGER = LoggerFactory.getLogger(AnvilLoader.class);
private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager();
private static final BiomeManager BIOME_MANAGER = MinecraftServer.getBiomeManager();
private static final ExceptionManager EXCEPTION_MANAGER = MinecraftServer.getExceptionManager();
private static final Biome BIOME = Biome.PLAINS;
private final Map<String, RegionFile> alreadyLoaded = new ConcurrentHashMap<>();
private final Path path;
private final Path levelPath;
private final Path regionPath;
public AnvilLoader(@NotNull Path path) {
this.path = path;
this.levelPath = path.resolve("level.dat");
this.regionPath = path.resolve("region");
}
public AnvilLoader(@NotNull String path) {
this(Path.of(path));
}
@Override
public void loadInstance(@NotNull Instance instance) {
if (!Files.exists(levelPath)) {
return;
}
try (var reader = new NBTReader(Files.newInputStream(levelPath))) {
final NBTCompound tag = (NBTCompound) reader.read();
Files.copy(levelPath, path.resolve("level.dat_old"), StandardCopyOption.REPLACE_EXISTING);
instance.setTag(Tag.NBT, tag);
} catch (IOException | NBTException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
@Override
public @NotNull CompletableFuture<@Nullable Chunk> loadChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
LOGGER.debug("Attempt loading at {} {}", chunkX, chunkZ);
if (!Files.exists(path)) {
// No world folder
return CompletableFuture.completedFuture(null);
}
try {
return loadMCA(instance, chunkX, chunkZ);
} catch (Exception e) {
EXCEPTION_MANAGER.handleException(e);
}
return CompletableFuture.completedFuture(null);
}
private @NotNull CompletableFuture<@Nullable Chunk> loadMCA(Instance instance, int chunkX, int chunkZ) throws IOException, AnvilException {
final RegionFile mcaFile = getMCAFile(instance, chunkX, chunkZ);
if (mcaFile == null)
return CompletableFuture.completedFuture(null);
final ChunkColumn fileChunk = mcaFile.getChunk(chunkX, chunkZ);
if (fileChunk == null)
return CompletableFuture.completedFuture(null);
Chunk chunk = new DynamicChunk(instance, chunkX, chunkZ);
// TODO: Parallelize block, block entities and biome loading
if (fileChunk.getGenerationStatus().compareTo(ChunkColumn.GenerationStatus.Biomes) > 0) {
HashMap<String, Biome> biomeCache = new HashMap<>();
for (ChunkSection section : fileChunk.getSections().values()) {
if (section.getEmpty()) continue;
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
int finalX = fileChunk.getX() * Chunk.CHUNK_SIZE_X + x;
int finalZ = fileChunk.getZ() * Chunk.CHUNK_SIZE_Z + z;
int finalY = section.getY() * Chunk.CHUNK_SECTION_SIZE + y;
String biomeName = section.getBiome(x, y, z);
Biome biome = biomeCache.computeIfAbsent(biomeName, n -> Objects.requireNonNullElse(BIOME_MANAGER.getByName(NamespaceID.from(n)), BIOME));
chunk.setBiome(finalX, finalY, finalZ, biome);
}
}
}
}
}
// Blocks
loadBlocks(chunk, fileChunk);
loadTileEntities(chunk, fileChunk);
// Lights
for (int sectionY = chunk.getMinSection(); sectionY < chunk.getMaxSection(); sectionY++) {
var section = chunk.getSection(sectionY);
var chunkSection = fileChunk.getSection((byte) sectionY);
section.setSkyLight(chunkSection.getSkyLights());
section.setBlockLight(chunkSection.getBlockLights());
}
mcaFile.forget(fileChunk);
return CompletableFuture.completedFuture(chunk);
}
private @Nullable RegionFile getMCAFile(Instance instance, int chunkX, int chunkZ) {
final int regionX = CoordinatesKt.chunkToRegion(chunkX);
final int regionZ = CoordinatesKt.chunkToRegion(chunkZ);
return alreadyLoaded.computeIfAbsent(RegionFile.Companion.createFileName(regionX, regionZ), n -> {
try {
final Path regionPath = this.regionPath.resolve(n);
if (!Files.exists(regionPath)) {
return null;
}
return new RegionFile(new RandomAccessFile(regionPath.toFile(), "rw"), regionX, regionZ, instance.getDimensionType().getMinY(), instance.getDimensionType().getMaxY()-1);
} catch (IOException | AnvilException e) {
EXCEPTION_MANAGER.handleException(e);
return null;
}
});
}
private void loadBlocks(Chunk chunk, ChunkColumn fileChunk) {
for (var section : fileChunk.getSections().values()) {
if (section.getEmpty()) continue;
final int yOffset = Chunk.CHUNK_SECTION_SIZE * section.getY();
for (int x = 0; x < Chunk.CHUNK_SECTION_SIZE; x++) {
for (int z = 0; z < Chunk.CHUNK_SECTION_SIZE; z++) {
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
try {
final BlockState blockState = section.get(x, y, z);
final String blockName = blockState.getName();
if (blockName.equals("minecraft:air")) continue;
Block block = Objects.requireNonNull(Block.fromNamespaceId(blockName));
// Properties
final Map<String, String> properties = blockState.getProperties();
if (!properties.isEmpty()) block = block.withProperties(properties);
// Handler
final BlockHandler handler = MinecraftServer.getBlockManager().getHandler(block.name());
if (handler != null) block = block.withHandler(handler);
chunk.setBlock(x, y + yOffset, z, block);
} catch (Exception e) {
EXCEPTION_MANAGER.handleException(e);
}
}
}
}
}
}
private void loadTileEntities(Chunk loadedChunk, ChunkColumn fileChunk) {
for (NBTCompound te : fileChunk.getTileEntities()) {
final var x = te.getInt("x");
final var y = te.getInt("y");
final var z = te.getInt("z");
if (x == null || y == null || z == null) {
LOGGER.warn("Tile entity has failed to load due to invalid coordinate");
continue;
}
Block block = loadedChunk.getBlock(x, y, z);
final String tileEntityID = te.getString("id");
if (tileEntityID != null) {
final BlockHandler handler = BLOCK_MANAGER.getHandlerOrDummy(tileEntityID);
block = block.withHandler(handler);
}
// Remove anvil tags
MutableNBTCompound mutableCopy = te.toMutableCompound();
mutableCopy.remove("id");
mutableCopy.remove("x");
mutableCopy.remove("y");
mutableCopy.remove("z");
mutableCopy.remove("keepPacked");
// Place block
final var finalBlock = mutableCopy.getSize() > 0 ?
block.withNbt(mutableCopy.toCompound()) : block;
loadedChunk.setBlock(x, y, z, finalBlock);
}
}
@Override
public @NotNull CompletableFuture<Void> saveInstance(@NotNull Instance instance) {
final var nbt = instance.getTag(Tag.NBT);
if (nbt == null) {
// Instance has no data
return AsyncUtils.VOID_FUTURE;
}
try (NBTWriter writer = new NBTWriter(Files.newOutputStream(levelPath))) {
writer.writeNamed("", nbt);
} catch (IOException e) {
e.printStackTrace();
}
return AsyncUtils.VOID_FUTURE;
}
@Override
public @NotNull CompletableFuture<Void> saveChunk(@NotNull Chunk chunk) {
final int chunkX = chunk.getChunkX();
final int chunkZ = chunk.getChunkZ();
RegionFile mcaFile;
synchronized (alreadyLoaded) {
mcaFile = getMCAFile(chunk.instance, chunkX, chunkZ);
if (mcaFile == null) {
final int regionX = CoordinatesKt.chunkToRegion(chunkX);
final int regionZ = CoordinatesKt.chunkToRegion(chunkZ);
final String n = RegionFile.Companion.createFileName(regionX, regionZ);
File regionFile = new File(regionPath.toFile(), n);
try {
if (!regionFile.exists()) {
if (!regionFile.getParentFile().exists()) {
regionFile.getParentFile().mkdirs();
}
regionFile.createNewFile();
}
mcaFile = new RegionFile(new RandomAccessFile(regionFile, "rw"), regionX, regionZ);
alreadyLoaded.put(n, mcaFile);
} catch (AnvilException | IOException e) {
LOGGER.error("Failed to save chunk " + chunkX + ", " + chunkZ, e);
EXCEPTION_MANAGER.handleException(e);
return AsyncUtils.VOID_FUTURE;
}
}
}
ChunkColumn column;
try {
column = mcaFile.getOrCreateChunk(chunkX, chunkZ);
} catch (AnvilException | IOException e) {
LOGGER.error("Failed to save chunk " + chunkX + ", " + chunkZ, e);
EXCEPTION_MANAGER.handleException(e);
return AsyncUtils.VOID_FUTURE;
}
save(chunk, column);
try {
LOGGER.debug("Attempt saving at {} {}", chunk.getChunkX(), chunk.getChunkZ());
mcaFile.writeColumn(column);
mcaFile.forget(column);
} catch (IOException e) {
LOGGER.error("Failed to save chunk " + chunkX + ", " + chunkZ, e);
EXCEPTION_MANAGER.handleException(e);
return AsyncUtils.VOID_FUTURE;
}
return AsyncUtils.VOID_FUTURE;
}
private void save(Chunk chunk, ChunkColumn chunkColumn) {
chunkColumn.changeVersion(SupportedVersion.Companion.getLatest());
chunkColumn.setYRange(chunk.getMinSection()*16, chunk.getMaxSection()*16-1);
List<NBTCompound> tileEntities = new ArrayList<>();
chunkColumn.setGenerationStatus(ChunkColumn.GenerationStatus.Full);
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
for (int y = chunkColumn.getMinY(); y < chunkColumn.getMaxY(); y++) {
final Block block = chunk.getBlock(x, y, z);
// Block
chunkColumn.setBlockState(x, y, z, new BlockState(block.name(), block.properties()));
chunkColumn.setBiome(x, y, z, chunk.getBiome(x, y, z).name().asString());
// Tile entity
final BlockHandler handler = block.handler();
var originalNBT = block.nbt();
if (originalNBT != null || handler != null) {
MutableNBTCompound nbt = originalNBT != null ?
originalNBT.toMutableCompound() : new MutableNBTCompound();
if (handler != null) {
nbt.setString("id", handler.getNamespaceId().asString());
}
nbt.setInt("x", x + Chunk.CHUNK_SIZE_X * chunk.getChunkX());
nbt.setInt("y", y);
nbt.setInt("z", z + Chunk.CHUNK_SIZE_Z * chunk.getChunkZ());
nbt.setByte("keepPacked", (byte) 0);
tileEntities.add(nbt.toCompound());
}
}
}
}
chunkColumn.setTileEntities(NBT.List(NBTType.TAG_Compound, tileEntities));
}
@Override
public boolean supportsParallelLoading() {
return true;
}
@Override
public boolean supportsParallelSaving() {
return true;
}
}