mirror of https://github.com/Minestom/Minestom.git
feat: anvil reading, other minor fixes
This commit is contained in:
parent
a50014ad8c
commit
1246fa57d7
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue