From f3488f34f1af5f05c73b53a5951beaa6d298ab8c Mon Sep 17 00:00:00 2001 From: "Lukas Rieger (Blue)" Date: Mon, 5 Feb 2024 15:08:00 +0100 Subject: [PATCH] Implement support for 1.15+ chunks --- .../core/world/mca/chunk/ChunkLoader.java | 2 +- .../core/world/mca/chunk/Chunk_1_15.java | 274 ++++++++++++++++++ 2 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/Chunk_1_15.java diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/ChunkLoader.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/ChunkLoader.java index 55bdb394..e6eb0b80 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/ChunkLoader.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/ChunkLoader.java @@ -19,7 +19,7 @@ public class ChunkLoader { // sorted list of chunk-versions, loaders at the start of the list are preferred over loaders at the end private static final List> CHUNK_VERSION_LOADERS = List.of( //new ChunkVersionLoader<>(Chunk_1_13.Data.class, Chunk_1_13::new, 0), - //new ChunkVersionLoader<>(Chunk_1_15.Data.class, Chunk_1_15::new, 2200), + new ChunkVersionLoader<>(Chunk_1_15.Data.class, Chunk_1_15::new, 2200), new ChunkVersionLoader<>(Chunk_1_16.Data.class, Chunk_1_16::new, 2500), new ChunkVersionLoader<>(Chunk_1_18.Data.class, Chunk_1_18::new, 2844) ); diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/Chunk_1_15.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/Chunk_1_15.java new file mode 100644 index 00000000..a01ed70b --- /dev/null +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/chunk/Chunk_1_15.java @@ -0,0 +1,274 @@ +package de.bluecolored.bluemap.core.world.mca.chunk; + +import de.bluecolored.bluemap.core.logger.Logger; +import de.bluecolored.bluemap.core.util.Key; +import de.bluecolored.bluemap.core.world.Biome; +import de.bluecolored.bluemap.core.world.BlockState; +import de.bluecolored.bluemap.core.world.DimensionType; +import de.bluecolored.bluemap.core.world.LightData; +import de.bluecolored.bluemap.core.world.mca.MCAUtil; +import de.bluecolored.bluemap.core.world.mca.region.MCARegion; +import de.bluecolored.bluenbt.NBTName; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +public class Chunk_1_15 extends MCAChunk { + + private static final Level EMPTY_LEVEL = new Level(); + private static final HeightmapsData EMPTY_HEIGHTMAPS_DATA = new HeightmapsData(); + + private static final Key STATUS_EMPTY = new Key("minecraft", "empty"); + private static final Key STATUS_FULL = new Key("minecraft", "full"); + + private final boolean generated; + private final boolean hasLightData; + private final long inhabitedTime; + + private final int skyLight; + + private final boolean hasWorldSurfaceHeights; + private final long[] worldSurfaceHeights; + private final boolean hasOceanFloorHeights; + private final long[] oceanFloorHeights; + + private final Section[] sections; + private final int sectionMin, sectionMax; + + private final int[] biomes; + + public Chunk_1_15(MCARegion region, Data data) { + super(region, data); + + Level level = data.level; + + this.generated = !STATUS_EMPTY.equals(level.status); + this.hasLightData = STATUS_FULL.equals(level.status); + this.inhabitedTime = level.inhabitedTime; + + DimensionType dimensionType = getRegion().getWorld().getDimensionType(); + this.skyLight = dimensionType.hasSkylight() ? 16 : 0; + + this.worldSurfaceHeights = level.heightmaps.worldSurface; + this.oceanFloorHeights = level.heightmaps.oceanFloor; + + this.hasWorldSurfaceHeights = this.worldSurfaceHeights.length >= 36; + this.hasOceanFloorHeights = this.oceanFloorHeights.length >= 36; + + this.biomes = level.biomes; + + SectionData[] sectionsData = level.sections; + if (sectionsData != null && sectionsData.length > 0) { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + + // find section min/max y + for (SectionData sectionData : sectionsData) { + int y = sectionData.getY(); + if (min > y) min = y; + if (max < y) max = y; + } + + // load sections into ordered array + this.sections = new Section[1 + max - min]; + for (SectionData sectionData : sectionsData) { + Section section = new Section(sectionData); + int y = section.getSectionY(); + + if (min > y) min = y; + if (max < y) max = y; + + this.sections[section.sectionY - min] = section; + } + + this.sectionMin = min; + this.sectionMax = max; + } else { + this.sections = new Section[0]; + this.sectionMin = 0; + this.sectionMax = 0; + } + } + + @Override + public boolean isGenerated() { + return generated; + } + + @Override + public boolean hasLightData() { + return hasLightData; + } + + @Override + public long getInhabitedTime() { + return inhabitedTime; + } + + @Override + public BlockState getBlockState(int x, int y, int z) { + Section section = getSection(y >> 4); + if (section == null) return BlockState.AIR; + + return section.getBlockState(x, y, z); + } + + @Override + public String getBiome(int x, int y, int z) { + if (this.biomes.length < 16) return Biome.DEFAULT.getFormatted(); + + int biomeIntIndex = (y & 0b1100) << 2 | z & 0b1100 | (x & 0b1100) >> 2; + + // shift y up/down if not in range + if (biomeIntIndex >= biomes.length) biomeIntIndex -= (((biomeIntIndex - biomes.length) >> 4) + 1) * 16; + if (biomeIntIndex < 0) biomeIntIndex -= (biomeIntIndex >> 4) * 16; + + return LegacyBiomes.idFor(biomes[biomeIntIndex]); + } + + @Override + public LightData getLightData(int x, int y, int z, LightData target) { + if (!hasLightData) return target.set(skyLight, 0); + + int sectionY = y >> 4; + Section section = getSection(sectionY); + if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(skyLight, 0); + + return section.getLightData(x, y, z, target); + } + + @Override + public int getMinY(int x, int z) { + return sectionMin * 16; + } + + @Override + public int getMaxY(int x, int z) { + return sectionMax * 16 + 15; + } + + @Override + public boolean hasWorldSurfaceHeights() { + return hasWorldSurfaceHeights; + } + + @Override + public int getWorldSurfaceY(int x, int z) { + return (int) MCAUtil.getValueFromLongStream( + worldSurfaceHeights, + (z & 0xF) << 4 | x & 0xF, + 9 + ); + } + + @Override + public boolean hasOceanFloorHeights() { + return hasOceanFloorHeights; + } + + @Override + public int getOceanFloorY(int x, int z) { + return (int) MCAUtil.getValueFromLongStream( + oceanFloorHeights, + (z & 0xF) << 4 | x & 0xF, + 9 + ); + } + + private @Nullable Section getSection(int y) { + y -= sectionMin; + if (y < 0 || y >= this.sections.length) return null; + return this.sections[y]; + } + + protected static class Section { + + private final int sectionY; + private final BlockState[] blockPalette; + private final long[] blocks; + private final byte[] blockLight; + private final byte[] skyLight; + + private final int bitsPerBlock; + + public Section(SectionData sectionData) { + this.sectionY = sectionData.y; + + this.blockPalette = sectionData.palette; + this.blocks = sectionData.blockStates; + + this.blockLight = sectionData.getBlockLight(); + this.skyLight = sectionData.getSkyLight(); + + this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result) + } + + public BlockState getBlockState(int x, int y, int z) { + if (blockPalette.length == 1) return blockPalette[0]; + if (blockPalette.length == 0) return BlockState.AIR; + + int id = (int) MCAUtil.getValueFromLongStream( + blocks, + (y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF, + bitsPerBlock + ); + if (id >= blockPalette.length) { + Logger.global.noFloodWarning("palette-warning", "Got block-palette id " + id + " but palette has size of " + blockPalette.length + "! (Future occasions of this error will not be logged)"); + return BlockState.MISSING; + } + + return blockPalette[id]; + } + + public LightData getLightData(int x, int y, int z, LightData target) { + if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0); + + int blockByteIndex = (y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF; + int blockHalfByteIndex = blockByteIndex >> 1; + boolean largeHalf = (blockByteIndex & 0x1) != 0; + + return target.set( + this.skyLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0, + this.blockLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0 + ); + } + + public int getSectionY() { + return sectionY; + } + + } + + @Getter + @SuppressWarnings("FieldMayBeFinal") + public static class Data extends MCAChunk.Data { + private Level level = EMPTY_LEVEL; + } + + @Getter + @SuppressWarnings("FieldMayBeFinal") + public static class Level { + private Key status = STATUS_EMPTY; + private long inhabitedTime = 0; + private HeightmapsData heightmaps = EMPTY_HEIGHTMAPS_DATA; + private SectionData @Nullable [] sections = null; + private int[] biomes = EMPTY_INT_ARRAY; + } + + @Getter + @SuppressWarnings("FieldMayBeFinal") + protected static class HeightmapsData { + @NBTName("WORLD_SURFACE") private long[] worldSurface = EMPTY_LONG_ARRAY; + @NBTName("OCEAN_FLOOR") private long[] oceanFloor = EMPTY_LONG_ARRAY; + } + + @Getter + @SuppressWarnings("FieldMayBeFinal") + protected static class SectionData { + private int y = 0; + private byte[] blockLight = EMPTY_BYTE_ARRAY; + private byte[] skyLight = EMPTY_BYTE_ARRAY; + private BlockState[] palette = EMPTY_BLOCKSTATE_ARRAY; + private long[] blockStates = EMPTY_LONG_ARRAY; + } + +}