diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index fb8714b03..fb2316332 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -26,6 +26,7 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.LightingChunk; +import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.predicate.BlockPredicate; import net.minestom.server.instance.block.predicate.BlockTypeFilter; @@ -191,7 +192,7 @@ public class PlayerInit { static { InstanceManager instanceManager = MinecraftServer.getInstanceManager(); - InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD); + InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD, new AnvilLoader("/Users/matt/dev/projects/hollowcube/minestom-ce/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample")); instanceContainer.setGenerator(unit -> { unit.modifier().fillHeight(0, 40, Block.STONE); diff --git a/src/main/java/net/minestom/server/instance/AnvilLoader.java b/src/main/java/net/minestom/server/instance/AnvilLoader.java deleted file mode 100644 index 02c96a477..000000000 --- a/src/main/java/net/minestom/server/instance/AnvilLoader.java +++ /dev/null @@ -1,463 +0,0 @@ -package net.minestom.server.instance; - -import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; -import net.minestom.server.MinecraftServer; -import net.minestom.server.utils.NamespaceID; -import net.minestom.server.utils.async.AsyncUtils; -import net.minestom.server.world.biomes.Biome; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; -import java.util.Set; -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 final static Biome PLAINS = MinecraftServer.getBiomeManager().getByName(NamespaceID.from("minecraft:plains")); - -// private final Map alreadyLoaded = new ConcurrentHashMap<>(); - private final Path path; - private final Path levelPath; - private final Path regionPath; - - private static class RegionCache extends ConcurrentHashMap> { - } - - /** - * Represents the chunks currently loaded per region. Used to determine when a region file can be unloaded. - */ - private final RegionCache perRegionLoadedChunks = new RegionCache(); - - // thread local to avoid contention issues with locks -// private final ThreadLocal> blockStateId2ObjectCacheTLS = ThreadLocal.withInitial(Int2ObjectArrayMap::new); - - 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.tagHandler().updateContent(tag); -// } catch (IOException | NBTException e) { -// MinecraftServer.getExceptionManager().handleException(e); -// } - } - - @Override - public @NotNull CompletableFuture<@Nullable Chunk> loadChunk(@NotNull Instance instance, int chunkX, int chunkZ) { -// if (!Files.exists(path)) { -// // No world folder -// return CompletableFuture.completedFuture(null); -// } -// try { -// return loadMCA(instance, chunkX, chunkZ); -// } catch (Exception e) { -// MinecraftServer.getExceptionManager().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 NBTCompound chunkData = mcaFile.getChunkData(chunkX, chunkZ); -// if (chunkData == null) -// return CompletableFuture.completedFuture(null); -// -// final ChunkReader chunkReader = new ChunkReader(chunkData); -// -// Chunk chunk = instance.getChunkSupplier().createChunk(instance, chunkX, chunkZ); -// synchronized (chunk) { -// var yRange = chunkReader.getYRange(); -// if (yRange.getStart() < instance.getDimensionType().getMinY()) { -// throw new AnvilException( -// String.format("Trying to load chunk with minY = %d, but instance dimension type (%s) has a minY of %d", -// yRange.getStart(), -// instance.getDimensionType().getName().asString(), -// instance.getDimensionType().getMinY() -// )); -// } -// if (yRange.getEndInclusive() > instance.getDimensionType().getMaxY()) { -// throw new AnvilException( -// String.format("Trying to load chunk with maxY = %d, but instance dimension type (%s) has a maxY of %d", -// yRange.getEndInclusive(), -// instance.getDimensionType().getName().asString(), -// instance.getDimensionType().getMaxY() -// )); -// } -// -// // TODO: Parallelize block, block entities and biome loading -// // Blocks + Biomes -// loadSections(chunk, chunkReader); -// -// // Block entities -// loadBlockEntities(chunk, chunkReader); -// } -// synchronized (perRegionLoadedChunks) { -// int regionX = CoordinatesKt.chunkToRegion(chunkX); -// int regionZ = CoordinatesKt.chunkToRegion(chunkZ); -// var chunks = perRegionLoadedChunks.computeIfAbsent(new IntIntImmutablePair(regionX, regionZ), r -> new HashSet<>()); // region cache may have been removed on another thread due to unloadChunk -// chunks.add(new IntIntImmutablePair(chunkX, chunkZ)); -// } -// 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; -// } -// synchronized (perRegionLoadedChunks) { -// Set previousVersion = perRegionLoadedChunks.put(new IntIntImmutablePair(regionX, regionZ), new HashSet<>()); -// assert previousVersion == null : "The AnvilLoader cache should not already have data for this region."; -// } -// return new RegionFile(new RandomAccessFile(regionPath.toFile(), "rw"), regionX, regionZ, instance.getDimensionType().getMinY(), instance.getDimensionType().getMaxY() - 1); -// } catch (IOException | AnvilException e) { -// MinecraftServer.getExceptionManager().handleException(e); -// return null; -// } -// }); -// } -// -// private void loadSections(Chunk chunk, ChunkReader chunkReader) { -// final HashMap biomeCache = new HashMap<>(); -// for (NBTCompound sectionNBT : chunkReader.getSections()) { -// ChunkSectionReader sectionReader = new ChunkSectionReader(chunkReader.getMinecraftVersion(), sectionNBT); -// -// if (sectionReader.isSectionEmpty()) continue; -// final int sectionY = sectionReader.getY(); -// final int yOffset = Chunk.CHUNK_SECTION_SIZE * sectionY; -// -// Section section = chunk.getSection(sectionY); -// -// if (sectionReader.getSkyLight() != null) { -// section.setSkyLight(sectionReader.getSkyLight().copyArray()); -// } -// if (sectionReader.getBlockLight() != null) { -// section.setBlockLight(sectionReader.getBlockLight().copyArray()); -// } -// -// // Biomes -// if (chunkReader.getGenerationStatus().compareTo(ChunkColumn.GenerationStatus.Biomes) > 0) { -// SectionBiomeInformation sectionBiomeInformation = chunkReader.readSectionBiomes(sectionReader); -// -// if (sectionBiomeInformation != null && sectionBiomeInformation.hasBiomeInformation()) { -// if (sectionBiomeInformation.isFilledWithSingleBiome()) { -// 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 = chunk.chunkX * Chunk.CHUNK_SIZE_X + x; -// int finalZ = chunk.chunkZ * Chunk.CHUNK_SIZE_Z + z; -// int finalY = sectionY * Chunk.CHUNK_SECTION_SIZE + y; -// String biomeName = sectionBiomeInformation.getBaseBiome(); -// Biome biome = biomeCache.computeIfAbsent(biomeName, n -> -// Objects.requireNonNullElse(MinecraftServer.getBiomeManager().getByName(NamespaceID.from(n)), PLAINS)); -// chunk.setBiome(finalX, finalY, finalZ, biome); -// } -// } -// } -// } else { -// 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 = chunk.chunkX * Chunk.CHUNK_SIZE_X + x; -// int finalZ = chunk.chunkZ * Chunk.CHUNK_SIZE_Z + z; -// int finalY = sectionY * Chunk.CHUNK_SECTION_SIZE + y; -// -// int index = x / 4 + (z / 4) * 4 + (y / 4) * 16; -// String biomeName = sectionBiomeInformation.getBiomes()[index]; -// Biome biome = biomeCache.computeIfAbsent(biomeName, n -> -// Objects.requireNonNullElse(MinecraftServer.getBiomeManager().getByName(NamespaceID.from(n)), PLAINS)); -// chunk.setBiome(finalX, finalY, finalZ, biome); -// } -// } -// } -// } -// } -// } -// -// // Blocks -// final NBTList blockPalette = sectionReader.getBlockPalette(); -// if (blockPalette != null) { -// final int[] blockStateIndices = sectionReader.getUncompressedBlockStateIDs(); -// Block[] convertedPalette = new Block[blockPalette.getSize()]; -// for (int i = 0; i < convertedPalette.length; i++) { -// final NBTCompound paletteEntry = blockPalette.get(i); -// String blockName = Objects.requireNonNull(paletteEntry.getString("Name")); -// if (blockName.equals("minecraft:air")) { -// convertedPalette[i] = Block.AIR; -// } else { -// if (blockName.equals("minecraft:grass")) { -// blockName = "minecraft:short_grass"; -// } -// Block block = Objects.requireNonNull(Block.fromNamespaceId(blockName)); -// // Properties -// final Map properties = new HashMap<>(); -// NBTCompound propertiesNBT = paletteEntry.getCompound("Properties"); -// if (propertiesNBT != null) { -// for (var property : propertiesNBT) { -// if (property.getValue().getID() != NBTType.TAG_String) { -// LOGGER.warn("Fail to parse block state properties {}, expected a TAG_String for {}, but contents were {}", -// propertiesNBT, -// property.getKey(), -// property.getValue().toSNBT()); -// } else { -// properties.put(property.getKey(), ((NBTString) property.getValue()).getValue()); -// } -// } -// } -// -// if (!properties.isEmpty()) block = block.withProperties(properties); -// // Handler -// final BlockHandler handler = MinecraftServer.getBlockManager().getHandler(block.name()); -// if (handler != null) block = block.withHandler(handler); -// -// convertedPalette[i] = block; -// } -// } -// -// for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) { -// for (int z = 0; z < Chunk.CHUNK_SECTION_SIZE; z++) { -// for (int x = 0; x < Chunk.CHUNK_SECTION_SIZE; x++) { -// try { -// final int blockIndex = y * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SECTION_SIZE + z * Chunk.CHUNK_SECTION_SIZE + x; -// final int paletteIndex = blockStateIndices[blockIndex]; -// final Block block = convertedPalette[paletteIndex]; -// -// chunk.setBlock(x, y + yOffset, z, block); -// } catch (Exception e) { -// MinecraftServer.getExceptionManager().handleException(e); -// } -// } -// } -// } -// } -// } -// } -// -// private void loadBlockEntities(Chunk loadedChunk, ChunkReader chunkReader) { -// for (NBTCompound te : chunkReader.getBlockEntities()) { -// 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 = MinecraftServer.getBlockManager().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 saveInstance(@NotNull Instance instance) { -// final NBTCompound nbt = instance.tagHandler().asCompound(); -// if (nbt.isEmpty()) { -// // 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 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); -// MinecraftServer.getExceptionManager().handleException(e); -// return AsyncUtils.VOID_FUTURE; -// } -// } -// } -// ChunkWriter writer = new ChunkWriter(SupportedVersion.Companion.getLatest()); -// save(chunk, writer); -// try { -// LOGGER.debug("Attempt saving at {} {}", chunk.getChunkX(), chunk.getChunkZ()); -// mcaFile.writeColumnData(writer.toNBT(), chunk.getChunkX(), chunk.getChunkZ()); -// } catch (IOException e) { -// LOGGER.error("Failed to save chunk " + chunkX + ", " + chunkZ, e); -// MinecraftServer.getExceptionManager().handleException(e); -// return AsyncUtils.VOID_FUTURE; -// } - return AsyncUtils.VOID_FUTURE; - } - -// private BlockState getBlockState(final Block block) { -// return blockStateId2ObjectCacheTLS.get().computeIfAbsent(block.stateId(), _unused -> new BlockState(block.name(), block.properties())); -// } -// -// private void save(Chunk chunk, ChunkWriter chunkWriter) { -// final int minY = chunk.getMinSection() * Chunk.CHUNK_SECTION_SIZE; -// final int maxY = chunk.getMaxSection() * Chunk.CHUNK_SECTION_SIZE - 1; -// chunkWriter.setYPos(minY); -// List blockEntities = new ArrayList<>(); -// chunkWriter.setStatus(ChunkColumn.GenerationStatus.Full); -// -// List sectionData = new ArrayList<>((maxY - minY + 1) / Chunk.CHUNK_SECTION_SIZE); -// int[] palettedBiomes = new int[ChunkSection.Companion.getBiomeArraySize()]; -// int[] palettedBlockStates = new int[Chunk.CHUNK_SIZE_X * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SIZE_Z]; -// for (int sectionY = chunk.getMinSection(); sectionY < chunk.getMaxSection(); sectionY++) { -// ChunkSectionWriter sectionWriter = new ChunkSectionWriter(SupportedVersion.Companion.getLatest(), (byte) sectionY); -// -// Section section = chunk.getSection(sectionY); -// sectionWriter.setSkyLights(section.skyLight().array()); -// sectionWriter.setBlockLights(section.blockLight().array()); -// -// BiomePalette biomePalette = new BiomePalette(); -// BlockPalette blockPalette = new BlockPalette(); -// for (int sectionLocalY = 0; sectionLocalY < Chunk.CHUNK_SECTION_SIZE; sectionLocalY++) { -// for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) { -// for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) { -// final int y = sectionLocalY + sectionY * Chunk.CHUNK_SECTION_SIZE; -// -// final int blockIndex = x + sectionLocalY * 16 * 16 + z * 16; -// -// final Block block = chunk.getBlock(x, y, z); -// -// final BlockState hephaistosBlockState = getBlockState(block); -// blockPalette.increaseReference(hephaistosBlockState); -// -// palettedBlockStates[blockIndex] = blockPalette.getPaletteIndex(hephaistosBlockState); -// -// // biome are stored for 4x4x4 volumes, avoid unnecessary work -// if (x % 4 == 0 && sectionLocalY % 4 == 0 && z % 4 == 0) { -// int biomeIndex = (x / 4) + (sectionLocalY / 4) * 4 * 4 + (z / 4) * 4; -// final Biome biome = chunk.getBiome(x, y, z); -// final String biomeName = biome.name(); -// -// biomePalette.increaseReference(biomeName); -// palettedBiomes[biomeIndex] = biomePalette.getPaletteIndex(biomeName); -// } -// -// // Block entities -// final BlockHandler handler = block.handler(); -// final NBTCompound 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); -// blockEntities.add(nbt.toCompound()); -// } -// } -// } -// } -// -// sectionWriter.setPalettedBiomes(biomePalette, palettedBiomes); -// sectionWriter.setPalettedBlockStates(blockPalette, palettedBlockStates); -// -// sectionData.add(sectionWriter.toNBT()); -// } -// -// chunkWriter.setSectionsData(NBT.List(NBTType.TAG_Compound, sectionData)); -// chunkWriter.setBlockEntityData(NBT.List(NBTType.TAG_Compound, blockEntities)); -// } -// -// /** -// * Unload a given chunk. Also unloads a region when no chunk from that region is loaded. -// * -// * @param chunk the chunk to unload -// */ -// @Override -// public void unloadChunk(Chunk chunk) { -// final int regionX = CoordinatesKt.chunkToRegion(chunk.chunkX); -// final int regionZ = CoordinatesKt.chunkToRegion(chunk.chunkZ); -// -// final IntIntImmutablePair regionKey = new IntIntImmutablePair(regionX, regionZ); -// synchronized (perRegionLoadedChunks) { -// Set chunks = perRegionLoadedChunks.get(regionKey); -// if (chunks != null) { // if null, trying to unload a chunk from a region that was not created by the AnvilLoader -// // don't check return value, trying to unload a chunk not created by the AnvilLoader is valid -// chunks.remove(new IntIntImmutablePair(chunk.chunkX, chunk.chunkZ)); -// -// if (chunks.isEmpty()) { -// perRegionLoadedChunks.remove(regionKey); -// RegionFile regionFile = alreadyLoaded.remove(RegionFile.Companion.createFileName(regionX, regionZ)); -// if (regionFile != null) { -// try { -// regionFile.close(); -// } catch (IOException e) { -// MinecraftServer.getExceptionManager().handleException(e); -// } -// } -// } -// } -// } -// } - - @Override - public boolean supportsParallelLoading() { - return true; - } - - @Override - public boolean supportsParallelSaving() { - return true; - } -} diff --git a/src/main/java/net/minestom/server/instance/IChunkLoader.java b/src/main/java/net/minestom/server/instance/IChunkLoader.java index bf5029631..31f6f8ee3 100644 --- a/src/main/java/net/minestom/server/instance/IChunkLoader.java +++ b/src/main/java/net/minestom/server/instance/IChunkLoader.java @@ -1,6 +1,7 @@ package net.minestom.server.instance; import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.utils.async.AsyncUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index 3c516a8ab..3f9683f69 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -11,6 +11,7 @@ import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.instance.InstanceChunkLoadEvent; import net.minestom.server.event.instance.InstanceChunkUnloadEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockHandler; diff --git a/src/main/java/net/minestom/server/instance/anvil/AnvilLoader.java b/src/main/java/net/minestom/server/instance/anvil/AnvilLoader.java new file mode 100644 index 000000000..1162e0307 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/anvil/AnvilLoader.java @@ -0,0 +1,482 @@ +package net.minestom.server.instance.anvil; + +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import net.kyori.adventure.nbt.*; +import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.IChunkLoader; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.utils.ArrayUtils; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.utils.async.AsyncUtils; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.utils.validate.Check; +import net.minestom.server.world.DimensionType; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public class AnvilLoader implements IChunkLoader { + private final static Logger LOGGER = LoggerFactory.getLogger(AnvilLoader.class); + private static final BiomeManager BIOME_MANAGER = MinecraftServer.getBiomeManager(); + private final static Biome PLAINS = BIOME_MANAGER.getByName(NamespaceID.from("minecraft:plains")); + + private final Map alreadyLoaded = new ConcurrentHashMap<>(); + private final Path path; + private final Path levelPath; + private final Path regionPath; + + private static class RegionCache extends ConcurrentHashMap> { + } + + /** + * Represents the chunks currently loaded per region. Used to determine when a region file can be unloaded. + */ + private final RegionCache perRegionLoadedChunks = new RegionCache(); + private final ReentrantLock perRegionLoadedChunksLock = new ReentrantLock(); + + // thread local to avoid contention issues with locks +// private final ThreadLocal> blockStateId2ObjectCacheTLS = ThreadLocal.withInitial(Int2ObjectArrayMap::new); + + 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 (InputStream is = Files.newInputStream(levelPath)) { + final CompoundBinaryTag tag = BinaryTagIO.reader().readNamed(is, BinaryTagIO.Compression.GZIP).getValue(); + Files.copy(levelPath, path.resolve("level.dat_old"), StandardCopyOption.REPLACE_EXISTING); + instance.tagHandler().updateContent(tag); + } catch (IOException e) { + MinecraftServer.getExceptionManager().handleException(e); + } + } + + @Override + public @NotNull CompletableFuture<@Nullable Chunk> loadChunk(@NotNull Instance instance, int chunkX, int chunkZ) { + if (!Files.exists(path)) { + // No world folder + return CompletableFuture.completedFuture(null); + } + try { + return loadMCA(instance, chunkX, chunkZ); + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + return CompletableFuture.completedFuture(null); + } + } + + private @NotNull CompletableFuture<@Nullable Chunk> loadMCA(Instance instance, int chunkX, int chunkZ) throws IOException { + final RegionFile mcaFile = getMCAFile(instance, chunkX, chunkZ); + if (mcaFile == null) + return CompletableFuture.completedFuture(null); + final CompoundBinaryTag chunkData = mcaFile.getChunk(chunkX, chunkZ); + if (chunkData == null) + return CompletableFuture.completedFuture(null); + + // Ensure the chunk matches the expected Y range + DimensionType dimensionType = instance.getDimensionType(); + int minY = chunkData.getInt("yPos") * Chunk.CHUNK_SECTION_SIZE; + Check.stateCondition(minY != dimensionType.getMinY(), "Trying to load chunk with minY = {0}, but instance dimension type ({1}) has a minY of {2}", + minY, dimensionType.getName().asString(), dimensionType.getMinY()); + int maxY = minY + (chunkData.getList("sections", BinaryTagTypes.COMPOUND).size() * Chunk.CHUNK_SECTION_SIZE); + Check.stateCondition(maxY != dimensionType.getMaxY(), "Trying to load chunk with maxY = {0}, but instance dimension type ({1}) has a maxY of {2}", + maxY, dimensionType.getName().asString(), dimensionType.getMaxY()); + + // Load the chunk data (assuming it is fully generated) + final Chunk chunk = instance.getChunkSupplier().createChunk(instance, chunkX, chunkZ); + synchronized (chunk) { // todo: boo, synchronized + final String status = chunkData.getString("status"); + + // TODO: Should we handle other states? + if (status.isEmpty() || "minecraft:full".equals(status)) { + // TODO: Parallelize block, block entities and biome loading + // Blocks + Biomes + loadSections(chunk, chunkData); + + // Block entities + loadBlockEntities(chunk, chunkData); + } else { + LOGGER.warn("Skipping partially generated chunk at {}, {} with status {}", chunkX, chunkZ, status); + } + } + + // Cache the index of the loaded chunk + perRegionLoadedChunksLock.lock(); + try { + int regionX = ChunkUtils.toRegionCoordinate(chunkX); + int regionZ = ChunkUtils.toRegionCoordinate(chunkZ); + var chunks = perRegionLoadedChunks.computeIfAbsent(new IntIntImmutablePair(regionX, regionZ), r -> new HashSet<>()); // region cache may have been removed on another thread due to unloadChunk + chunks.add(new IntIntImmutablePair(chunkX, chunkZ)); + } finally { + perRegionLoadedChunksLock.unlock(); + } + return CompletableFuture.completedFuture(chunk); + } + + private @Nullable RegionFile getMCAFile(Instance instance, int chunkX, int chunkZ) { + final int regionX = ChunkUtils.toRegionCoordinate(chunkX); + final int regionZ = ChunkUtils.toRegionCoordinate(chunkZ); + return alreadyLoaded.computeIfAbsent(RegionFile.getFileName(regionX, regionZ), n -> { + final Path regionPath = this.regionPath.resolve(n); + if (!Files.exists(regionPath)) { + return null; + } + perRegionLoadedChunksLock.lock(); + try { + Set previousVersion = perRegionLoadedChunks.put(new IntIntImmutablePair(regionX, regionZ), new HashSet<>()); + assert previousVersion == null : "The AnvilLoader cache should not already have data for this region."; + return new RegionFile(regionPath, regionX, regionZ, instance.getDimensionType()); + } catch (IOException e) { + MinecraftServer.getExceptionManager().handleException(e); + return null; + } finally { + perRegionLoadedChunksLock.unlock(); + } + }); + } + + private void loadSections(@NotNull Chunk chunk, @NotNull CompoundBinaryTag chunkData) { + for (BinaryTag sectionTag : chunkData.getList("sections", BinaryTagTypes.COMPOUND)) { + final CompoundBinaryTag sectionData = (CompoundBinaryTag) sectionTag; + + final int sectionY = sectionData.getInt("Y", Integer.MIN_VALUE); + Check.stateCondition(sectionY == Integer.MIN_VALUE, "Missing section Y value"); + final int yOffset = Chunk.CHUNK_SECTION_SIZE * sectionY; + + final Section section = chunk.getSection(sectionY); + + // Lighting + if (sectionData.get("SkyLight") instanceof ByteArrayBinaryTag skyLightTag && skyLightTag.size() == 2048) { + section.setSkyLight(skyLightTag.value()); + } + if (sectionData.get("BlockLight") instanceof ByteArrayBinaryTag blockLightTag && blockLightTag.size() == 2048) { + section.setBlockLight(blockLightTag.value()); + } + + { // Biomes + final CompoundBinaryTag biomesTag = sectionData.getCompound("biomes"); + final ListBinaryTag biomePaletteTag = biomesTag.getList("palette", BinaryTagTypes.STRING); + Biome[] convertedPalette = loadBiomePalette(biomePaletteTag); + + if (convertedPalette.length == 1) { + // One solid block, no need to check the data + section.biomePalette().fill(BIOME_MANAGER.getId(convertedPalette[0])); + } else if (convertedPalette.length > 1) { + final long[] packedIndices = biomesTag.getLongArray("data"); + Check.stateCondition(packedIndices.length == 0, "Missing packed biomes data"); + int[] biomeIndices = new int[64]; + ArrayUtils.unpack(biomeIndices, packedIndices, packedIndices.length * 64 / biomeIndices.length); + + section.biomePalette().setAll((x, y, z) -> { + final int index = x + z * 4 + y * 16; + final Biome biome = convertedPalette[biomeIndices[index]]; + return BIOME_MANAGER.getId(biome); + }); + } + } + + { // Blocks + final CompoundBinaryTag blockStatesTag = sectionData.getCompound("block_states"); + final ListBinaryTag blockPaletteTag = blockStatesTag.getList("palette", BinaryTagTypes.COMPOUND); + Block[] convertedPalette = loadBlockPalette(blockPaletteTag); + if (blockPaletteTag.size() == 1) { + // One solid block, no need to check the data + section.blockPalette().fill(convertedPalette[0].stateId()); + } else if (blockPaletteTag.size() > 1) { + final long[] packedStates = blockStatesTag.getLongArray("data"); + Check.stateCondition(packedStates.length == 0, "Missing packed states data"); + int[] blockStateIndices = new int[Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SECTION_SIZE]; + ArrayUtils.unpack(blockStateIndices, packedStates, packedStates.length * 64 / blockStateIndices.length); + + for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) { + for (int z = 0; z < Chunk.CHUNK_SECTION_SIZE; z++) { + for (int x = 0; x < Chunk.CHUNK_SECTION_SIZE; x++) { + try { + final int blockIndex = y * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SECTION_SIZE + z * Chunk.CHUNK_SECTION_SIZE + x; + final int paletteIndex = blockStateIndices[blockIndex]; + final Block block = convertedPalette[paletteIndex]; + + chunk.setBlock(x, y + yOffset, z, block); + } catch (Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + } + } + } + } + } + } + } + } + + private Block[] loadBlockPalette(@NotNull ListBinaryTag paletteTag) { + Block[] convertedPalette = new Block[paletteTag.size()]; + for (int i = 0; i < convertedPalette.length; i++) { + CompoundBinaryTag paletteEntry = paletteTag.getCompound(i); + String blockName = paletteEntry.getString("Name"); + if (blockName.equals("minecraft:air")) { + convertedPalette[i] = Block.AIR; + } else { + Block block = Objects.requireNonNull(Block.fromNamespaceId(blockName), "Unknown block " + blockName); + // Properties + final Map properties = new HashMap<>(); + CompoundBinaryTag propertiesNBT = paletteEntry.getCompound("Properties"); + for (var property : propertiesNBT) { + if (property.getValue() instanceof StringBinaryTag propertyValue) { + properties.put(property.getKey(), propertyValue.value()); + } else { + LOGGER.warn("Fail to parse block state properties {}, expected a string for {}, but contents were {}", + propertiesNBT, property.getKey(), TagStringIOExt.writeTag(property.getValue())); + } + } + if (!properties.isEmpty()) block = block.withProperties(properties); + + // Handler + final BlockHandler handler = MinecraftServer.getBlockManager().getHandler(block.name()); + if (handler != null) block = block.withHandler(handler); + + convertedPalette[i] = block; + } + } + return convertedPalette; + } + + private Biome[] loadBiomePalette(@NotNull ListBinaryTag paletteTag) { + Biome[] convertedPalette = new Biome[paletteTag.size()]; + for (int i = 0; i < convertedPalette.length; i++) { + final String name = paletteTag.getString(i); + convertedPalette[i] = Objects.requireNonNullElse(BIOME_MANAGER.getByName(name), PLAINS); + } + return convertedPalette; + } + + private void loadBlockEntities(@NotNull Chunk loadedChunk, @NotNull CompoundBinaryTag chunkData) { + for (BinaryTag blockEntityTag : chunkData.getList("block_entities", BinaryTagTypes.COMPOUND)) { + final CompoundBinaryTag blockEntity = (CompoundBinaryTag) blockEntityTag; + + final int x = blockEntity.getInt("x"); + final int y = blockEntity.getInt("y"); + final int z = blockEntity.getInt("z"); + Block block = loadedChunk.getBlock(x, y, z); + + // Load the block handler if the id is present + if (blockEntity.get("id") instanceof StringBinaryTag blockEntityId) { + final BlockHandler handler = MinecraftServer.getBlockManager().getHandlerOrDummy(blockEntityId.value()); + block = block.withHandler(handler); + } + + // Remove anvil tags + CompoundBinaryTag trimmedTag = CompoundBinaryTag.builder().put(blockEntity) + .remove("id").remove("keepPacked") + .remove("x").remove("y").remove("z") + .build(); + + // Place block + final var finalBlock = trimmedTag.size() > 0 ? block.withNbt(trimmedTag) : block; + loadedChunk.setBlock(x, y, z, finalBlock); + } + } + + @Override + public @NotNull CompletableFuture saveInstance(@NotNull Instance instance) { + final CompoundBinaryTag nbt = instance.tagHandler().asCompound(); + if (nbt.size() == 0) { + // Instance has no data + return AsyncUtils.VOID_FUTURE; + } + try (OutputStream os = Files.newOutputStream(levelPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + BinaryTagIO.writer().writeNamed(Map.entry("", nbt), os, BinaryTagIO.Compression.GZIP); + } catch (IOException e) { + MinecraftServer.getExceptionManager().handleException(e); + } + return AsyncUtils.VOID_FUTURE; + } + + @Override + public @NotNull CompletableFuture 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); +// MinecraftServer.getExceptionManager().handleException(e); +// return AsyncUtils.VOID_FUTURE; +// } +// } +// } +// ChunkWriter writer = new ChunkWriter(SupportedVersion.Companion.getLatest()); +// save(chunk, writer); +// try { +// LOGGER.debug("Attempt saving at {} {}", chunk.getChunkX(), chunk.getChunkZ()); +// mcaFile.writeColumnData(writer.toNBT(), chunk.getChunkX(), chunk.getChunkZ()); +// } catch (IOException e) { +// LOGGER.error("Failed to save chunk " + chunkX + ", " + chunkZ, e); +// MinecraftServer.getExceptionManager().handleException(e); +// return AsyncUtils.VOID_FUTURE; +// } + return AsyncUtils.VOID_FUTURE; + } + +// private BlockState getBlockState(final Block block) { +// return blockStateId2ObjectCacheTLS.get().computeIfAbsent(block.stateId(), _unused -> new BlockState(block.name(), block.properties())); +// } +// +// private void save(Chunk chunk, ChunkWriter chunkWriter) { +// final int minY = chunk.getMinSection() * Chunk.CHUNK_SECTION_SIZE; +// final int maxY = chunk.getMaxSection() * Chunk.CHUNK_SECTION_SIZE - 1; +// chunkWriter.setYPos(minY); +// List blockEntities = new ArrayList<>(); +// chunkWriter.setStatus(ChunkColumn.GenerationStatus.Full); +// +// List sectionData = new ArrayList<>((maxY - minY + 1) / Chunk.CHUNK_SECTION_SIZE); +// int[] palettedBiomes = new int[ChunkSection.Companion.getBiomeArraySize()]; +// int[] palettedBlockStates = new int[Chunk.CHUNK_SIZE_X * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SIZE_Z]; +// for (int sectionY = chunk.getMinSection(); sectionY < chunk.getMaxSection(); sectionY++) { +// ChunkSectionWriter sectionWriter = new ChunkSectionWriter(SupportedVersion.Companion.getLatest(), (byte) sectionY); +// +// Section section = chunk.getSection(sectionY); +// sectionWriter.setSkyLights(section.skyLight().array()); +// sectionWriter.setBlockLights(section.blockLight().array()); +// +// BiomePalette biomePalette = new BiomePalette(); +// BlockPalette blockPalette = new BlockPalette(); +// for (int sectionLocalY = 0; sectionLocalY < Chunk.CHUNK_SECTION_SIZE; sectionLocalY++) { +// for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) { +// for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) { +// final int y = sectionLocalY + sectionY * Chunk.CHUNK_SECTION_SIZE; +// +// final int blockIndex = x + sectionLocalY * 16 * 16 + z * 16; +// +// final Block block = chunk.getBlock(x, y, z); +// +// final BlockState hephaistosBlockState = getBlockState(block); +// blockPalette.increaseReference(hephaistosBlockState); +// +// palettedBlockStates[blockIndex] = blockPalette.getPaletteIndex(hephaistosBlockState); +// +// // biome are stored for 4x4x4 volumes, avoid unnecessary work +// if (x % 4 == 0 && sectionLocalY % 4 == 0 && z % 4 == 0) { +// int biomeIndex = (x / 4) + (sectionLocalY / 4) * 4 * 4 + (z / 4) * 4; +// final Biome biome = chunk.getBiome(x, y, z); +// final String biomeName = biome.name(); +// +// biomePalette.increaseReference(biomeName); +// palettedBiomes[biomeIndex] = biomePalette.getPaletteIndex(biomeName); +// } +// +// // Block entities +// final BlockHandler handler = block.handler(); +// final NBTCompound 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); +// blockEntities.add(nbt.toCompound()); +// } +// } +// } +// } +// +// sectionWriter.setPalettedBiomes(biomePalette, palettedBiomes); +// sectionWriter.setPalettedBlockStates(blockPalette, palettedBlockStates); +// +// sectionData.add(sectionWriter.toNBT()); +// } +// +// chunkWriter.setSectionsData(NBT.List(NBTType.TAG_Compound, sectionData)); +// chunkWriter.setBlockEntityData(NBT.List(NBTType.TAG_Compound, blockEntities)); +// } +// +// /** +// * Unload a given chunk. Also unloads a region when no chunk from that region is loaded. +// * +// * @param chunk the chunk to unload +// */ +// @Override +// public void unloadChunk(Chunk chunk) { +// final int regionX = CoordinatesKt.chunkToRegion(chunk.chunkX); +// final int regionZ = CoordinatesKt.chunkToRegion(chunk.chunkZ); +// +// final IntIntImmutablePair regionKey = new IntIntImmutablePair(regionX, regionZ); +// synchronized (perRegionLoadedChunks) { +// Set chunks = perRegionLoadedChunks.get(regionKey); +// if (chunks != null) { // if null, trying to unload a chunk from a region that was not created by the AnvilLoader +// // don't check return value, trying to unload a chunk not created by the AnvilLoader is valid +// chunks.remove(new IntIntImmutablePair(chunk.chunkX, chunk.chunkZ)); +// +// if (chunks.isEmpty()) { +// perRegionLoadedChunks.remove(regionKey); +// RegionFile regionFile = alreadyLoaded.remove(RegionFile.Companion.createFileName(regionX, regionZ)); +// if (regionFile != null) { +// try { +// regionFile.close(); +// } catch (IOException e) { +// MinecraftServer.getExceptionManager().handleException(e); +// } +// } +// } +// } +// } +// } + + @Override + public boolean supportsParallelLoading() { + return true; + } + + @Override + public boolean supportsParallelSaving() { + return true; + } +} diff --git a/src/main/java/net/minestom/server/instance/anvil/RegionFile.java b/src/main/java/net/minestom/server/instance/anvil/RegionFile.java new file mode 100644 index 000000000..be1cab63d --- /dev/null +++ b/src/main/java/net/minestom/server/instance/anvil/RegionFile.java @@ -0,0 +1,127 @@ +package net.minestom.server.instance.anvil; + +import it.unimi.dsi.fastutil.booleans.BooleanArrayList; +import it.unimi.dsi.fastutil.booleans.BooleanList; +import net.kyori.adventure.nbt.BinaryTagIO; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.world.DimensionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implements a thread-safe reader and writer for Minecraft region files. + * + * @see Region file format + * @see Hephaistos implementation + */ +final class RegionFile implements AutoCloseable { + + private static final int MAX_ENTRY_COUNT = 1024; + private static final int SECTOR_SIZE = 4096; + private static final int SECTOR_1MB = 1024 * 1024 / SECTOR_SIZE; + private static final int HEADER_LENGTH = MAX_ENTRY_COUNT * 2 * 4; // 2 4-byte fields per entry + + private static final BinaryTagIO.Reader TAG_READER = BinaryTagIO.unlimitedReader(); + + public static @NotNull String getFileName(int regionX, int regionZ) { + return "r." + regionX + "." + regionZ + ".mca"; + } + + private final ReentrantLock lock = new ReentrantLock(); + private final RandomAccessFile file; + + private final int[] locations = new int[MAX_ENTRY_COUNT]; + private final int[] timestamps = new int[MAX_ENTRY_COUNT]; + private final BooleanList freeSectors = new BooleanArrayList(2); + + public RegionFile(@NotNull Path path, int regionX, int regionZ, @NotNull DimensionType dimensionType) throws IOException { + this.file = new RandomAccessFile(path.toFile(), "rw"); + + readHeader(); + } + + public boolean hasChunkData(int chunkX, int chunkZ) { + lock.lock(); + try { + return locations[getChunkIndex(chunkX, chunkZ)] != 0; + } finally { + lock.unlock(); + } + } + + public @Nullable CompoundBinaryTag getChunk(int chunkX, int chunkZ) throws IOException { + lock.lock(); + try { + if (!hasChunkData(chunkX, chunkZ)) return null; + + int location = locations[getChunkIndex(chunkX, chunkZ)]; + file.seek((long) (location >> 8) * SECTOR_SIZE); // Move to start of first sector + int length = file.readInt(); + int compressionType = file.readByte(); + BinaryTagIO.Compression compression = switch (compressionType) { + case 1 -> BinaryTagIO.Compression.GZIP; + case 2 -> BinaryTagIO.Compression.ZLIB; + case 3 -> BinaryTagIO.Compression.NONE; + default -> throw new IOException("Unsupported compression type: " + compressionType); + }; + + // Read the raw content + byte[] data = new byte[length - 1]; + file.read(data); + + // Parse it as a compound tag + return TAG_READER.read(new ByteArrayInputStream(data), compression); + } finally { + lock.unlock(); + } + } + + + @Override + public void close() throws Exception { + file.close(); + } + + private void readHeader() throws IOException { + file.seek(0); + if (file.length() < HEADER_LENGTH) { + // new file, fill in data + file.write(new byte[HEADER_LENGTH]); + } + + //todo: addPadding() + + final long totalSectors = file.length() / SECTOR_SIZE; + for (int i = 0; i < totalSectors; i++) freeSectors.add(true); + + // Read locations + file.seek(0); + for (int i = 0; i < MAX_ENTRY_COUNT; i++) { + int location = locations[i] = file.readInt(); + int offset = location >> 8; + int length = location & 0xFF; + + if (location != 0 && offset + length <= freeSectors.size()) { + for (int sectorIndex = 0; sectorIndex < length; sectorIndex++) { + freeSectors.set(sectorIndex + offset, false); + } + } + } + + // Read timestamps + for (int i = 0; i < MAX_ENTRY_COUNT; i++) { + timestamps[i] = file.readInt(); + } + } + + private int getChunkIndex(int chunkX, int chunkZ) { + return (ChunkUtils.toRegionLocal(chunkZ) << 5) | ChunkUtils.toRegionLocal(chunkX); + } +} diff --git a/src/main/java/net/minestom/server/inventory/ContainerInventory.java b/src/main/java/net/minestom/server/inventory/ContainerInventory.java index c9f7d049c..875d364b8 100644 --- a/src/main/java/net/minestom/server/inventory/ContainerInventory.java +++ b/src/main/java/net/minestom/server/inventory/ContainerInventory.java @@ -66,9 +66,11 @@ public non-sealed class ContainerInventory extends InventoryImpl { } } - inventory.update(player); - if (inventory != playerInventory) { - playerInventory.update(player); + if (inventory.isViewer(player)) { + inventory.update(player); + if (inventory != playerInventory) { + playerInventory.update(player); + } } return null; } diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index bbedc7d2f..abc91f86e 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -1,19 +1,23 @@ package net.minestom.server.item; import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; +import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.item.component.CustomData; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagReadable; import net.minestom.server.tag.TagWritable; +import net.minestom.server.utils.nbt.BinaryTagSerializer; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; +import java.io.IOException; import java.util.function.Consumer; import java.util.function.IntUnaryOperator; import java.util.function.UnaryOperator; @@ -33,6 +37,7 @@ public sealed interface ItemStack extends TagReadable, ItemComponentMap, HoverEv @NotNull ItemStack AIR = ItemStack.of(Material.AIR); @NotNull NetworkBuffer.Type NETWORK_TYPE = ItemStackImpl.NETWORK_TYPE; + @NotNull BinaryTagSerializer NBT_TYPE = ItemStackImpl.NBT_TYPE; @Contract(value = "_ -> new", pure = true) static @NotNull Builder builder(@NotNull Material material) { @@ -55,7 +60,7 @@ public sealed interface ItemStack extends TagReadable, ItemComponentMap, HoverEv * @param nbtCompound The nbt representation of the item */ static @NotNull ItemStack fromItemNBT(@NotNull CompoundBinaryTag nbtCompound) { - return ItemStackImpl.NBT_TYPE.read(nbtCompound); + return NBT_TYPE.read(nbtCompound); } @Contract(pure = true) @@ -126,15 +131,12 @@ public sealed interface ItemStack extends TagReadable, ItemComponentMap, HoverEv @Override default @NotNull HoverEvent asHoverEvent(@NotNull UnaryOperator op) { - //todo -// try { -// final BinaryTagHolder tagHolder = BinaryTagHolder.encode(meta().toNBT(), MinestomAdventure.NBT_CODEC); -// return HoverEvent.showItem(op.apply(HoverEvent.ShowItem.showItem(material(), amount(), tagHolder))); -// } catch (IOException e) { -// //todo(matt): revisit, -// throw new RuntimeException(e); -// } - throw new UnsupportedOperationException("todo"); + try { + BinaryTagHolder tagHolder = BinaryTagHolder.encode((CompoundBinaryTag) NBT_TYPE.write(this), MinestomAdventure.NBT_CODEC); + return HoverEvent.showItem(op.apply(HoverEvent.ShowItem.showItem(material(), amount(), tagHolder))); + } catch (IOException e) { + throw new RuntimeException("failed to encode itemstack nbt", e); + } } sealed interface Builder extends TagWritable diff --git a/src/main/java/net/minestom/server/item/Material.java b/src/main/java/net/minestom/server/item/Material.java index 900a3c8b0..43badc2cd 100644 --- a/src/main/java/net/minestom/server/item/Material.java +++ b/src/main/java/net/minestom/server/item/Material.java @@ -47,8 +47,8 @@ import java.util.Collection; public sealed interface Material extends StaticProtocolObject, Materials permits MaterialImpl { - NetworkBuffer.Type NETWORK_TYPE = MaterialImpl.NETWORK_TYPE; - BinaryTagSerializer NBT_TYPE = MaterialImpl.NBT_TYPE; + NetworkBuffer.Type NETWORK_TYPE = NetworkBuffer.lazy(() -> MaterialImpl.NETWORK_TYPE); + BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.lazy(() -> MaterialImpl.NBT_TYPE); /** * Returns the material registry. diff --git a/src/main/java/net/minestom/server/item/component/PotDecorations.java b/src/main/java/net/minestom/server/item/component/PotDecorations.java index 35fd2302a..4d5fb4bd5 100644 --- a/src/main/java/net/minestom/server/item/component/PotDecorations.java +++ b/src/main/java/net/minestom/server/item/component/PotDecorations.java @@ -1,6 +1,5 @@ package net.minestom.server.item.component; -import net.kyori.adventure.nbt.BinaryTag; import net.minestom.server.item.Material; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.nbt.BinaryTagSerializer; @@ -17,24 +16,8 @@ public record PotDecorations( public static final @NotNull Material DEFAULT_ITEM = Material.BRICK; public static final PotDecorations EMPTY = new PotDecorations(DEFAULT_ITEM, DEFAULT_ITEM, DEFAULT_ITEM, DEFAULT_ITEM); - public static NetworkBuffer.Type NETWORK_TYPE = new NetworkBuffer.Type() { - @Override public void write(@NotNull NetworkBuffer buffer, PotDecorations value) { - Material.NETWORK_TYPE.list(4).map(PotDecorations::new, PotDecorations::asList).write(buffer, value); - } - - @Override public PotDecorations read(@NotNull NetworkBuffer buffer) { - return Material.NETWORK_TYPE.list(4).map(PotDecorations::new, PotDecorations::asList).read(buffer); - } - }; - public static BinaryTagSerializer NBT_TYPE = new BinaryTagSerializer() { - @Override public @NotNull BinaryTag write(@NotNull PotDecorations value) { - return Material.NBT_TYPE.list().map(PotDecorations::new, PotDecorations::asList).write(value); - } - - @Override public @NotNull PotDecorations read(@NotNull BinaryTag tag) { - return Material.NBT_TYPE.list().map(PotDecorations::new, PotDecorations::asList).read(tag); - } - }; + public static NetworkBuffer.Type NETWORK_TYPE = Material.NETWORK_TYPE.list(4).map(PotDecorations::new, PotDecorations::asList); + public static BinaryTagSerializer NBT_TYPE = Material.NBT_TYPE.list().map(PotDecorations::new, PotDecorations::asList); public PotDecorations(@NotNull List list) { this(getOrAir(list, 0), getOrAir(list, 1), getOrAir(list, 2), getOrAir(list, 3)); diff --git a/src/main/java/net/minestom/server/utils/ArrayUtils.java b/src/main/java/net/minestom/server/utils/ArrayUtils.java index fb32053f3..de23b16d1 100644 --- a/src/main/java/net/minestom/server/utils/ArrayUtils.java +++ b/src/main/java/net/minestom/server/utils/ArrayUtils.java @@ -76,4 +76,37 @@ public final class ArrayUtils { default -> Map.copyOf(new Object2ObjectArrayMap<>(keys, values, length)); }; } + + public static long[] pack(int[] ints, int bitsPerEntry) { + int intsPerLong = (int) Math.floor(64d / bitsPerEntry); + long[] longs = new long[(int) Math.ceil(ints.length / (double) intsPerLong)]; + + long mask = (1L << bitsPerEntry) - 1L; + for (int i = 0; i < longs.length; i++) { + for (int intIndex = 0; intIndex < intsPerLong; intIndex++) { + int bitIndex = intIndex * bitsPerEntry; + int intActualIndex = intIndex + i * intsPerLong; + if (intActualIndex < ints.length) { + longs[i] |= (ints[intActualIndex] & mask) << bitIndex; + } + } + } + + return longs; + } + + public static void unpack(int[] out, long[] in, int bitsPerEntry) { + assert in.length != 0: "unpack input array is zero"; + + var intsPerLong = Math.floor(64d / bitsPerEntry); + var intsPerLongCeil = (int) Math.ceil(intsPerLong); + + long mask = (1L << bitsPerEntry) - 1L; + for (int i = 0; i < out.length; i++) { + int longIndex = i / intsPerLongCeil; + int subIndex = i % intsPerLongCeil; + + out[i] = (int) ((in[longIndex] >>> (bitsPerEntry * subIndex)) & mask); + } + } } diff --git a/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java b/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java index aaa210ad1..9300de8e6 100644 --- a/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java +++ b/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java @@ -1,6 +1,5 @@ package net.minestom.server.utils.chunk; -import net.minestom.server.ServerFlag; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; @@ -289,6 +288,14 @@ public final class ChunkUtils { return xyz & 0xF; } + public static int toRegionCoordinate(int chunkCoordinate) { + return chunkCoordinate >> 5; + } + + public static int toRegionLocal(int chunkCoordinate) { + return chunkCoordinate & 0x1F; + } + public static int floorSection(int coordinate) { return coordinate - (coordinate & 0xF); } diff --git a/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java b/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java index f7b8fb29a..21b2ea82a 100644 --- a/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java +++ b/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java @@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.*; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; public interface BinaryTagSerializer { @@ -35,6 +36,27 @@ public interface BinaryTagSerializer { }; } + static @NotNull BinaryTagSerializer lazy(@NotNull Supplier> self) { + return new BinaryTagSerializer<>() { + private BinaryTagSerializer serializer = null; + + @Override + public @NotNull BinaryTag write(@NotNull T value) { + return serializer().write(value); + } + + @Override + public @NotNull T read(@NotNull BinaryTag tag) { + return serializer().read(tag); + } + + private BinaryTagSerializer serializer() { + if (serializer == null) serializer = self.get(); + return serializer; + } + }; + } + static @NotNull BinaryTagSerializer coerced(@NotNull BinaryTagType type) { return new BinaryTagSerializer<>() { @Override diff --git a/src/test/java/net/minestom/server/instance/AnvilLoaderIntegrationTest.java b/src/test/java/net/minestom/server/instance/AnvilLoaderIntegrationTest.java index e66729c4e..c58c9c81c 100644 --- a/src/test/java/net/minestom/server/instance/AnvilLoaderIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/AnvilLoaderIntegrationTest.java @@ -1,5 +1,6 @@ package net.minestom.server.instance; +import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.instance.block.Block; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.NamespaceID; diff --git a/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java b/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java index c9741e2fc..da61ba340 100644 --- a/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java @@ -1,7 +1,11 @@ package net.minestom.server.instance.light; import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.*; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.LightingChunk; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.palette.Palette; import net.minestom.testing.Env; diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/level.dat b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/level.dat new file mode 100644 index 000000000..1ab0d3113 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/level.dat differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.-1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.-1.mca new file mode 100644 index 000000000..59d7f272b Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.-1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.0.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.0.mca new file mode 100644 index 000000000..a1f345fc0 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.0.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.1.mca new file mode 100644 index 000000000..0ab617d08 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.-1.1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.-1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.-1.mca new file mode 100644 index 000000000..56b19b12a Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.-1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.0.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.0.mca new file mode 100644 index 000000000..e577cc7a8 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.0.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.1.mca new file mode 100644 index 000000000..542637cdb Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.0.1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.-1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.-1.mca new file mode 100644 index 000000000..dd156a244 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.-1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.0.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.0.mca new file mode 100644 index 000000000..329a892f4 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.0.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.1.mca new file mode 100644 index 000000000..9166a684f Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.1.1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.-1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.-1.mca new file mode 100644 index 000000000..1d34d1856 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.-1.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.0.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.0.mca new file mode 100644 index 000000000..2aa4e7517 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.0.mca differ diff --git a/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.1.mca b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.1.mca new file mode 100644 index 000000000..c8315e8a7 Binary files /dev/null and b/src/test/resources/net/minestom/server/instance/anvil_vanilla_sample/region/r.2.1.mca differ