feat: anvil reading, other minor fixes

This commit is contained in:
mworzala 2024-04-23 00:03:52 -04:00
parent a50014ad8c
commit 1246fa57d7
No known key found for this signature in database
GPG Key ID: B148F922E64797C7
28 changed files with 703 additions and 500 deletions

View File

@ -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);

View File

@ -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<String, RegionFile> alreadyLoaded = new ConcurrentHashMap<>();
private final Path path;
private final Path levelPath;
private final Path regionPath;
private static class RegionCache extends ConcurrentHashMap<IntIntImmutablePair, Set<IntIntImmutablePair>> {
}
/**
* 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<Int2ObjectMap<BlockState>> 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<IntIntImmutablePair> 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<String, Biome> 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<NBTCompound> 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<String, String> 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<Void> 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<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);
// 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<NBTCompound> blockEntities = new ArrayList<>();
// chunkWriter.setStatus(ChunkColumn.GenerationStatus.Full);
//
// List<NBTCompound> 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<IntIntImmutablePair> 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<String, RegionFile> alreadyLoaded = new ConcurrentHashMap<>();
private final Path path;
private final Path levelPath;
private final Path regionPath;
private static class RegionCache extends ConcurrentHashMap<IntIntImmutablePair, Set<IntIntImmutablePair>> {
}
/**
* 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<Int2ObjectMap<BlockState>> 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<IntIntImmutablePair> 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<String, String> 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<Void> 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<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);
// 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<NBTCompound> blockEntities = new ArrayList<>();
// chunkWriter.setStatus(ChunkColumn.GenerationStatus.Full);
//
// List<NBTCompound> 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<IntIntImmutablePair> 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;
}
}

View File

@ -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 <a href="https://minecraft.wiki/w/Region_file_format">Region file format</a>
* @see <a href="https://github.com/Minestom/Hephaistos/blob/master/common/src/main/kotlin/org/jglrxavpok/hephaistos/mca/RegionFile.kt">Hephaistos implementation</a>
*/
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);
}
}

View File

@ -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;
}

View File

@ -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<ItemStack> NETWORK_TYPE = ItemStackImpl.NETWORK_TYPE;
@NotNull BinaryTagSerializer<ItemStack> 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<HoverEvent.ShowItem> asHoverEvent(@NotNull UnaryOperator<HoverEvent.ShowItem> 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

View File

@ -47,8 +47,8 @@ import java.util.Collection;
public sealed interface Material extends StaticProtocolObject, Materials permits MaterialImpl {
NetworkBuffer.Type<Material> NETWORK_TYPE = MaterialImpl.NETWORK_TYPE;
BinaryTagSerializer<Material> NBT_TYPE = MaterialImpl.NBT_TYPE;
NetworkBuffer.Type<Material> NETWORK_TYPE = NetworkBuffer.lazy(() -> MaterialImpl.NETWORK_TYPE);
BinaryTagSerializer<Material> NBT_TYPE = BinaryTagSerializer.lazy(() -> MaterialImpl.NBT_TYPE);
/**
* Returns the material registry.

View File

@ -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<PotDecorations> NETWORK_TYPE = new NetworkBuffer.Type<PotDecorations>() {
@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<PotDecorations> NBT_TYPE = new BinaryTagSerializer<PotDecorations>() {
@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<PotDecorations> NETWORK_TYPE = Material.NETWORK_TYPE.list(4).map(PotDecorations::new, PotDecorations::asList);
public static BinaryTagSerializer<PotDecorations> NBT_TYPE = Material.NBT_TYPE.list().map(PotDecorations::new, PotDecorations::asList);
public PotDecorations(@NotNull List<Material> list) {
this(getOrAir(list, 0), getOrAir(list, 1), getOrAir(list, 2), getOrAir(list, 3));

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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<T> {
@ -35,6 +36,27 @@ public interface BinaryTagSerializer<T> {
};
}
static <T> @NotNull BinaryTagSerializer<T> lazy(@NotNull Supplier<BinaryTagSerializer<T>> self) {
return new BinaryTagSerializer<>() {
private BinaryTagSerializer<T> 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<T> serializer() {
if (serializer == null) serializer = self.get();
return serializer;
}
};
}
static <T extends BinaryTag> @NotNull BinaryTagSerializer<T> coerced(@NotNull BinaryTagType<T> type) {
return new BinaryTagSerializer<>() {
@Override

View File

@ -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;

View File

@ -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;