From f682616f81c94d9242b6dfe1c83c1856cf3a42f1 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sat, 10 Oct 2026 13:13:03 -0700
Subject: [PATCH] Decode saved section palettes directly
Saved block state and biome sections are decoded from their palette list and packed data without vanilla's container codecs. Each distinct palette entry is decoded once with the element codec and cached. Packed data stored as a long array is used without copying, so snapshots keep the source arrays; other numeric lists that vanilla accepts are read through NbtOps. Like vanilla, entries that fail to decode use their partial result or the default value and are logged once per section, and sections with missing or invalid data fail to read. Snapshots store their own read-only section data instead of PalettedContainers, which avoids Paper's different unpack signature.
---
.../chunksnapshot/ChunkSnapshotFactory.java | 55 ++---
.../chunksnapshot/ChunkSnapshotImpl.java | 8 +-
.../chunksnapshot/EmptySectionHolder.java | 16 +-
.../chunksnapshot/HeightmapSnapshot.java | 7 +-
.../common/chunksnapshot/SectionData.java | 80 +++++++
.../common/chunksnapshot/SectionDecoder.java | 151 +++++++++++++
.../VanillaChunkSnapshotProvider.java | 9 +-
.../VanillaChunkSnapshotProviderFactory.java | 2 +-
.../ChunkSnapshotFactoryTest.java | 6 +-
.../chunksnapshot/SectionDecoderTest.java | 211 ++++++++++++++++++
.../PaperChunkSnapshotProvider.java | 8 +-
.../PaperChunkSnapshotProviderFactory.java | 3 +-
12 files changed, 496 insertions(+), 60 deletions(-)
create mode 100644 common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionData.java
create mode 100644 common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoder.java
create mode 100644 common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoderTest.java
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactory.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactory.java
index df79403c..d9b52852 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactory.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactory.java
@@ -1,6 +1,5 @@
package xyz.jpenilla.squaremap.common.chunksnapshot;
-import com.mojang.serialization.Codec;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
@@ -9,7 +8,6 @@
import net.minecraft.core.Holder;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
-import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.util.Mth;
import net.minecraft.world.level.ChunkPos;
@@ -19,9 +17,6 @@
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
-import net.minecraft.world.level.chunk.PalettedContainer;
-import net.minecraft.world.level.chunk.PalettedContainerFactory;
-import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
@@ -51,8 +46,8 @@ public static ChunkSnapshot snapshotLiveChunk(final Level level, final ChunkAcce
final LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(chunk.getMinY(), chunk.getHeight());
final int sectionCount = heightAccessor.getSectionsCount();
final LevelChunkSection[] sections = chunk.getSections();
- final PalettedContainer[] states = new PalettedContainer[sectionCount];
- final PalettedContainerRO>[] biomes = new PalettedContainerRO[sectionCount];
+ final SectionData[] states = new SectionData[sectionCount];
+ final SectionData>[] biomes = new SectionData[sectionCount];
final boolean[] empty = new boolean[sectionCount];
if (!chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE)) {
@@ -68,10 +63,10 @@ public static ChunkSnapshot snapshotLiveChunk(final Level level, final ChunkAcce
if (sectionEmpty) {
states[i] = EmptySectionHolder.getEmptySectionBlockStates();
} else {
- states[i] = sections[i].getStates().copy();
+ states[i] = SectionData.of(sections[i].getStates().copy());
}
- biomes[i] = sections[i].getBiomes().copy();
+ biomes[i] = SectionData.of(sections[i].getBiomes().copy());
}
return new ChunkSnapshotImpl(
@@ -91,12 +86,12 @@ public static ChunkSnapshot snapshotLiveChunk(final Level level, final ChunkAcce
*
* The caller must supply NBT already upgraded to the current data version and classified
* as {@link ChunkSnapshotEligibility#ELIGIBLE}. The data is only read, so it may be shared
- * with other readers, but must not be mutated while this runs. This method does not check
- * generation status or complete retrogen and may run off-thread.
+ * with other readers, but must not be mutated afterward; the snapshot keeps its data arrays.
+ * This method does not check generation status or complete retrogen and may run off-thread.
*
* @param levelHeight the level's vertical bounds
* @param dimensionType the level's dimension type
- * @param containerFactory the level's palette container factory
+ * @param sectionDecoder the level's section decoder
* @param chunkData the upgraded, snapshot-ready chunk data
* @return the decoded snapshot
* @throws SerializableChunkData.ChunkReadException if a section palette cannot be decoded
@@ -105,23 +100,20 @@ public static ChunkSnapshot snapshotLiveChunk(final Level level, final ChunkAcce
public static ChunkSnapshot snapshotFromChunkData(
final LevelHeightAccessor levelHeight,
final DimensionType dimensionType,
- final PalettedContainerFactory containerFactory,
+ final SectionDecoder sectionDecoder,
final CompoundTag chunkData
) {
final ChunkPos chunkPos = readChunkPos(chunkData);
final ListTag sectionTags = chunkData.getListOrEmpty(SerializableChunkData.SECTIONS_TAG);
final int sectionCount = levelHeight.getSectionsCount();
- final PalettedContainer[] states = new PalettedContainer[sectionCount];
- final PalettedContainerRO>[] biomes = new PalettedContainerRO[sectionCount];
+ final SectionData[] states = new SectionData[sectionCount];
+ final SectionData>[] biomes = new SectionData[sectionCount];
final boolean[] empty = new boolean[sectionCount];
Arrays.fill(states, EmptySectionHolder.getEmptySectionBlockStates());
Arrays.fill(biomes, EmptySectionHolder.getEmptySectionBiomes());
Arrays.fill(empty, true);
- final Codec>> biomesCodec = containerFactory.biomeContainerCodec();
- final Codec> blockStatesCodec = containerFactory.blockStatesContainerCodec();
-
for (int i = 0; i < sectionTags.size(); ++i) {
final Optional maybeSectionTag = sectionTags.getCompound(i);
if (maybeSectionTag.isPresent()) {
@@ -129,12 +121,16 @@ public static ChunkSnapshot snapshotFromChunkData(
final int y = sectionTag.getByteOr(ChunkDataKeys.SECTION_Y, (byte) 0);
final int index = y - levelHeight.getMinSectionY();
if (index >= 0 && index < sectionCount) {
- final Optional> maybeBlocks = sectionTag.getCompound(ChunkDataKeys.BLOCK_STATES)
- .map((container) -> blockStatesCodec.parse(NbtOps.INSTANCE, container).promotePartial((msg) -> Logging.logger().warn("Failed to decode chunk {} section {}: {}", chunkPos, y, msg)).getOrThrow(SerializableChunkData.ChunkReadException::new));
- final PalettedContainer blocks = maybeBlocks.orElse(EmptySectionHolder.getEmptySectionBlockStates());
- final Optional>> maybeBiomes = sectionTag.getCompound(ChunkDataKeys.BIOMES)
- .map((container) -> biomesCodec.parse(NbtOps.INSTANCE, container).promotePartial((msg) -> Logging.logger().warn("Failed to decode chunk {} section {}: {}", chunkPos, y, msg)).getOrThrow(SerializableChunkData.ChunkReadException::new));
- final PalettedContainerRO> sectionBiomes = maybeBiomes.orElse(EmptySectionHolder.getEmptySectionBiomes());
+ final SectionData blocks = sectionTag.getCompound(ChunkDataKeys.BLOCK_STATES)
+ .map(container -> sectionDecoder.blockStates(container)
+ .promotePartial(msg -> logSectionDecodeErrors(chunkPos, y, msg))
+ .getOrThrow(SerializableChunkData.ChunkReadException::new))
+ .orElseGet(EmptySectionHolder::getEmptySectionBlockStates);
+ final SectionData> sectionBiomes = sectionTag.getCompound(ChunkDataKeys.BIOMES)
+ .map(container -> sectionDecoder.biomes(container)
+ .promotePartial(msg -> logSectionDecodeErrors(chunkPos, y, msg))
+ .getOrThrow(SerializableChunkData.ChunkReadException::new))
+ .orElseGet(EmptySectionHolder::getEmptySectionBiomes);
states[index] = blocks;
empty[index] = !blocks.maybeHas(state -> !state.isAir());
biomes[index] = sectionBiomes;
@@ -188,10 +184,15 @@ public static ChunkPos readChunkPos(final CompoundTag chunkData) {
);
}
+ private static void logSectionDecodeErrors(final ChunkPos chunkPos, final int sectionY, final String message) {
+ // Same message as vanilla's SerializableChunkData.logErrors.
+ Logging.logger().error("Recoverable errors when loading section [{}, {}, {}]: {}", chunkPos.x(), sectionY, chunkPos.z(), message);
+ }
+
private static Map readOrComputeHeightmaps(
final LevelHeightAccessor levelHeight,
final CompoundTag chunkData,
- final PalettedContainer[] states,
+ final SectionData[] states,
final boolean[] empty
) {
final int bits = Mth.ceillog2(levelHeight.getHeight() + 1);
@@ -208,10 +209,10 @@ private static Map readOrComputeHeightmaps(
}
@SuppressWarnings({"unchecked", "rawtypes"})
- private static @Nullable Holder[] uniformBiomes(final PalettedContainerRO>[] biomes) {
+ private static @Nullable Holder[] uniformBiomes(final SectionData>[] biomes) {
final @Nullable Holder[] uniform = new Holder[biomes.length];
for (int i = 0; i < biomes.length; i++) {
- final Holder first = biomes[i].get(0, 0, 0);
+ final Holder first = biomes[i].get(0);
// Checks palette entries only, so a palette with unused entries reads as mixed.
if (!biomes[i].maybeHas(biome -> biome != first)) {
uniform[i] = first;
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotImpl.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotImpl.java
index 0e97d9ec..19e46d93 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotImpl.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotImpl.java
@@ -11,8 +11,6 @@
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.chunk.PalettedContainer;
-import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.material.FluidState;
@@ -24,8 +22,8 @@
@DefaultQualifier(NonNull.class)
record ChunkSnapshotImpl(
LevelHeightAccessor heightAccessor,
- PalettedContainer[] states,
- PalettedContainerRO>[] biomes,
+ SectionData[] states,
+ SectionData>[] biomes,
@Nullable Holder[] uniformBiomes,
Map heightmaps,
boolean[] emptySections,
@@ -95,7 +93,7 @@ public Holder getNoiseBiome(final int quartX, final int quartY, final int
if (uniform != null) {
return uniform;
}
- return this.biomes[sectionIndex].get(quartX & 3, clampedQuartY & 3, quartZ & 3);
+ return this.biomes[sectionIndex].get((clampedQuartY & 3) << 4 | (quartZ & 3) << 2 | quartX & 3);
}
@Override
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/EmptySectionHolder.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/EmptySectionHolder.java
index f404344e..a08fe528 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/EmptySectionHolder.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/EmptySectionHolder.java
@@ -4,7 +4,6 @@
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -12,31 +11,28 @@
@DefaultQualifier(NonNull.class)
public final class EmptySectionHolder {
- private static @MonotonicNonNull PalettedContainer EMPTY_SECTION_BLOCK_STATES;
- private static @MonotonicNonNull PalettedContainer> EMPTY_SECTION_BIOMES;
+ private static @MonotonicNonNull SectionData EMPTY_SECTION_BLOCK_STATES;
+ private static @MonotonicNonNull SectionData> EMPTY_SECTION_BIOMES;
private EmptySectionHolder() {
}
public static void init(final PalettedContainerFactory palettedContainerFactory) {
if (EMPTY_SECTION_BLOCK_STATES == null) {
- EMPTY_SECTION_BLOCK_STATES = new PalettedContainer<>(
- Blocks.AIR.defaultBlockState(),
- palettedContainerFactory.blockStatesStrategy()
- );
+ EMPTY_SECTION_BLOCK_STATES = SectionData.Packed.single(Blocks.AIR.defaultBlockState());
}
// Always replace, biomes can change between integrated server runs
- EMPTY_SECTION_BIOMES = palettedContainerFactory.createForBiomes();
+ EMPTY_SECTION_BIOMES = SectionData.Packed.single(palettedContainerFactory.defaultBiome());
}
- static PalettedContainer getEmptySectionBlockStates() {
+ static SectionData getEmptySectionBlockStates() {
if (EMPTY_SECTION_BLOCK_STATES == null) {
throw new IllegalStateException("EmptySectionHolder not initialized");
}
return EMPTY_SECTION_BLOCK_STATES;
}
- static PalettedContainer> getEmptySectionBiomes() {
+ static SectionData> getEmptySectionBiomes() {
if (EMPTY_SECTION_BIOMES == null) {
throw new IllegalStateException("EmptySectionHolder not initialized");
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/HeightmapSnapshot.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/HeightmapSnapshot.java
index 3a69cc0f..61fbcce8 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/HeightmapSnapshot.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/HeightmapSnapshot.java
@@ -7,7 +7,6 @@
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
-import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.levelgen.Heightmap;
final class HeightmapSnapshot {
@@ -36,7 +35,7 @@ final class HeightmapSnapshot {
static HeightmapSnapshot computeWorldSurface(
final LevelHeightAccessor heightAccessor,
- final PalettedContainer[] sections,
+ final SectionData[] sections,
final boolean[] empty
) {
final SimpleBitStorage data = new SimpleBitStorage(Mth.ceillog2(heightAccessor.getHeight() + 1), 256);
@@ -44,7 +43,7 @@ static HeightmapSnapshot computeWorldSurface(
// Rebuild directly from palette data using the heightmap type's predicate,
// avoiding live section construction and block counting.
for (int sectionIndex = sections.length - 1; sectionIndex >= 0; sectionIndex--) {
- final PalettedContainer section = sections[sectionIndex];
+ final SectionData section = sections[sectionIndex];
if (empty[sectionIndex]) {
continue;
}
@@ -56,7 +55,7 @@ static HeightmapSnapshot computeWorldSurface(
continue;
}
for (int y = 15; y >= 0; y--) {
- if (isOpaque.test(section.get(x, y, z))) {
+ if (isOpaque.test(section.get(y << 8 | z << 4 | x))) {
data.set(index, sectionIndex * 16 + y + 1);
break;
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionData.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionData.java
new file mode 100644
index 00000000..15e319f2
--- /dev/null
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionData.java
@@ -0,0 +1,80 @@
+package xyz.jpenilla.squaremap.common.chunksnapshot;
+
+import java.util.function.Predicate;
+import net.minecraft.util.SimpleBitStorage;
+import net.minecraft.world.level.chunk.MissingPaletteEntryException;
+import net.minecraft.world.level.chunk.PalettedContainer;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+/**
+ * Read-only block states or biomes of a chunk section.
+ *
+ * @param the value type
+ */
+@DefaultQualifier(NonNull.class)
+sealed interface SectionData permits SectionData.Container, SectionData.Packed {
+ /**
+ * Gets a value by its index, as in {@link PalettedContainer#get(int)}.
+ *
+ * @param index the index
+ * @return the value
+ */
+ T get(int index);
+
+ /**
+ * Checks the palette, like {@link PalettedContainer#maybeHas(Predicate)}.
+ *
+ * @param predicate the predicate
+ * @return whether any palette entry matches
+ */
+ boolean maybeHas(Predicate predicate);
+
+ static SectionData of(final PalettedContainer container) {
+ return new Container<>(container);
+ }
+
+ record Container(PalettedContainer container) implements SectionData {
+ @Override
+ public T get(final int index) {
+ return this.container.get(index);
+ }
+
+ @Override
+ public boolean maybeHas(final Predicate predicate) {
+ return this.container.maybeHas(predicate);
+ }
+ }
+
+ // Storage is null for a single-value palette.
+ record Packed(T[] palette, @Nullable SimpleBitStorage storage) implements SectionData {
+ @SuppressWarnings("unchecked")
+ static Packed single(final T value) {
+ return new Packed<>((T[]) new Object[]{value}, null);
+ }
+
+ @Override
+ public T get(final int index) {
+ if (this.storage == null) {
+ return this.palette[0];
+ }
+ final int id = this.storage.get(index);
+ if (id >= this.palette.length) {
+ // Like vanilla's linear and hash map palettes, which also fail on read.
+ throw new MissingPaletteEntryException(id);
+ }
+ return this.palette[id];
+ }
+
+ @Override
+ public boolean maybeHas(final Predicate predicate) {
+ for (final T value : this.palette) {
+ if (predicate.test(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoder.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoder.java
new file mode 100644
index 00000000..e39294e8
--- /dev/null
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoder.java
@@ -0,0 +1,151 @@
+package xyz.jpenilla.squaremap.common.chunksnapshot;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.IntFunction;
+import java.util.stream.LongStream;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Registry;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.LongArrayTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.util.SimpleBitStorage;
+import net.minecraft.world.level.biome.Biome;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.PalettedContainerFactory;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+/**
+ * Decodes saved sections with the same results as vanilla's container codecs, decoding each
+ * distinct palette entry once. Thread-safe.
+ */
+@DefaultQualifier(NonNull.class)
+public final class SectionDecoder {
+ private static final String PALETTE = "palette";
+ private static final String DATA = "data";
+
+ private final CachingPaletteDecoder blockStates;
+ private final int blockEntries;
+ private final CachingPaletteDecoder> biomes;
+ private final int biomeEntries;
+
+ public SectionDecoder(final PalettedContainerFactory containerFactory, final Registry biomeRegistry) {
+ this.blockStates = new CachingPaletteDecoder<>(BlockState.CODEC, containerFactory.defaultBlockState());
+ this.blockEntries = containerFactory.blockStatesStrategy().entryCount();
+ this.biomes = new CachingPaletteDecoder<>(biomeRegistry.holderByNameCodec(), containerFactory.defaultBiome());
+ this.biomeEntries = containerFactory.biomeStrategy().entryCount();
+ }
+
+ public static SectionDecoder create(final ServerLevel level) {
+ return new SectionDecoder(level.palettedContainerFactory(), level.registryAccess().lookupOrThrow(Registries.BIOME));
+ }
+
+ /**
+ * Decodes a section's block states.
+ *
+ * @param container the saved {@code block_states} tag
+ * @return the block states
+ */
+ DataResult> blockStates(final CompoundTag container) {
+ return this.blockStates.resolvePalette(container, BlockState[]::new).flatMap(palette -> {
+ // Matches Strategy.createForBlockStates: at least 4 bits once there is more than one entry.
+ final int bits = palette.length == 1 ? 0 : Math.max(4, Mth.ceillog2(palette.length));
+ return packed(palette, container, bits, this.blockEntries);
+ });
+ }
+
+ /**
+ * Decodes a section's biomes.
+ *
+ * @param container the saved {@code biomes} tag
+ * @return the biomes
+ */
+ @SuppressWarnings("unchecked")
+ DataResult>> biomes(final CompoundTag container) {
+ final DataResult[]> palette = this.biomes.resolvePalette(container, Holder[]::new);
+ // Matches Strategy.createForBiomes, which stores the minimum bits for the palette size.
+ return palette.flatMap(entries -> packed(entries, container, Mth.ceillog2(entries.length), this.biomeEntries));
+ }
+
+ private static DataResult> packed(final T[] palette, final CompoundTag container, final int bits, final int entryCount) {
+ if (bits == 0) {
+ // Vanilla ignores data for single-value palettes.
+ return DataResult.success(new SectionData.Packed<>(palette, null));
+ }
+ final long @Nullable [] data = data(container.get(DATA));
+ if (data == null) {
+ return DataResult.error(() -> "Missing values for non-zero storage");
+ }
+ try {
+ return DataResult.success(new SectionData.Packed<>(palette, new SimpleBitStorage(bits, entryCount, data)));
+ } catch (final SimpleBitStorage.InitializationException e) {
+ return DataResult.error(() -> "Failed to read PalettedContainer: " + e.getMessage());
+ }
+ }
+
+ private static long @Nullable [] data(final @Nullable Tag tag) {
+ if (tag instanceof LongArrayTag longArray) {
+ return longArray.getAsLongArray();
+ }
+ if (tag == null) {
+ return null;
+ }
+ // Vanilla also accepts other numeric lists, and its lenient data field treats unreadable data as missing.
+ return NbtOps.INSTANCE.getLongStream(tag).result().map(LongStream::toArray).orElse(null);
+ }
+
+ // Caches entries that decode without errors, keyed by their tags, which compare by value.
+ private record CachingPaletteDecoder(Codec codec, T defaultValue, Map decoded) {
+ CachingPaletteDecoder(final Codec codec, final T defaultValue) {
+ this(codec, defaultValue, new ConcurrentHashMap<>());
+ }
+
+ DataResult resolvePalette(final CompoundTag container, final IntFunction newArray) {
+ if (!(container.get(PALETTE) instanceof ListTag paletteTag) || paletteTag.isEmpty()) {
+ return DataResult.error(() -> "Missing or empty palette");
+ }
+ final T[] palette = newArray.apply(paletteTag.size());
+ @Nullable StringJoiner errors = null;
+ for (int i = 0; i < palette.length; i++) {
+ final Tag entry = paletteTag.get(i);
+ final @Nullable T cached = this.decoded.get(entry);
+ if (cached != null) {
+ palette[i] = cached;
+ continue;
+ }
+ final DataResult result = this.codec.parse(NbtOps.INSTANCE, entry);
+ if (result instanceof DataResult.Error error) {
+ // Like ExtraCodecs.orElsePartial in vanilla's container codecs: the partial result if any, otherwise the default.
+ if (errors == null) {
+ errors = new StringJoiner("; ");
+ }
+ if (error.partialValue().isPresent()) {
+ palette[i] = error.partialValue().get();
+ errors.add(error.message());
+ } else {
+ palette[i] = this.defaultValue;
+ errors.add("(" + error.message() + " -> using default)");
+ }
+ } else {
+ final T value = result.getOrThrow();
+ palette[i] = value;
+ this.decoded.put(entry, value);
+ }
+ }
+ if (errors != null) {
+ return DataResult.error(errors::toString, palette);
+ }
+ return DataResult.success(palette);
+ }
+ }
+}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProvider.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProvider.java
index b8800ee5..72e76c55 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProvider.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProvider.java
@@ -25,7 +25,6 @@
import net.minecraft.world.level.TicketStorage;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
-import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
@@ -35,7 +34,7 @@
import xyz.jpenilla.squaremap.common.util.concurrent.CompletableFutures;
@DefaultQualifier(NonNull.class)
-record VanillaChunkSnapshotProvider(ServerLevel level, boolean moonrise) implements ChunkSnapshotProvider {
+record VanillaChunkSnapshotProvider(ServerLevel level, boolean moonrise, SectionDecoder sectionDecoder) implements ChunkSnapshotProvider {
private Executor mainThreadExecutor() {
return task -> {
if (this.level.getServer().isSameThread()) {
@@ -71,7 +70,6 @@ private Executor mainThreadExecutor() {
// Read in the same main thread task as the holder checks: unload saves are queued to the
// IO worker on main, so this read is ordered after any save of a chunk we didn't see.
final LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(this.level.getMinY(), this.level.getHeight());
- final PalettedContainerFactory palettedContainerFactory = this.level.palettedContainerFactory();
final DimensionType dimensionType = this.level.dimensionType();
return chunkMap.squaremap$readChunk(chunkPos).thenComposeAsync(
maybeTag -> {
@@ -85,7 +83,7 @@ private Executor mainThreadExecutor() {
case INELIGIBLE -> CompletableFuture.completedFuture(null);
case ELIGIBLE -> {
if (ChunkSnapshotFactory.chunkPosMatches(tag, x, z)) {
- yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, palettedContainerFactory, tag));
+ yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, this.sectionDecoder, tag));
} else {
yield this.vanillaChunkSystemSnapshot(x, z);
}
@@ -200,7 +198,6 @@ private CompletableFuture vanillaLoadChunkAsync(
final ChunkTaskScheduler scheduler = ((ChunkSystemServerLevel) this.level).moonrise$getChunkTaskScheduler();
final Executor executor = task -> scheduler.loadExecutor.createTask(task, Priority.NORMAL).queue();
final LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(this.level.getMinY(), this.level.getHeight());
- final PalettedContainerFactory palettedContainerFactory = this.level.palettedContainerFactory();
final DimensionType dimensionType = this.level.dimensionType();
return data.thenComposeAsync(
rawTag -> {
@@ -214,7 +211,7 @@ private CompletableFuture vanillaLoadChunkAsync(
case INELIGIBLE -> CompletableFuture.completedFuture(null);
case ELIGIBLE -> {
if (ChunkSnapshotFactory.chunkPosMatches(tag, x, z)) {
- yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, palettedContainerFactory, tag));
+ yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, this.sectionDecoder, tag));
} else {
yield this.moonriseChunkSystemSnapshot(x, z);
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProviderFactory.java b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProviderFactory.java
index dd0970a9..5bc6a2c0 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProviderFactory.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/chunksnapshot/VanillaChunkSnapshotProviderFactory.java
@@ -19,6 +19,6 @@ private VanillaChunkSnapshotProviderFactory(final SquaremapPlatform platform) {
@Override
public ChunkSnapshotProvider createChunkSnapshotProvider(final ServerLevel level) {
- return new VanillaChunkSnapshotProvider(level, this.platform.hasMod("moonrise"));
+ return new VanillaChunkSnapshotProvider(level, this.platform.hasMod("moonrise"), SectionDecoder.create(level));
}
}
diff --git a/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactoryTest.java b/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactoryTest.java
index 17a3748f..85e43ac8 100644
--- a/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactoryTest.java
+++ b/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/ChunkSnapshotFactoryTest.java
@@ -40,6 +40,7 @@
class ChunkSnapshotFactoryTest {
private static PalettedContainerFactory factory;
+ private static SectionDecoder decoder;
private static DimensionType dimensionType;
@BeforeAll
@@ -51,6 +52,7 @@ static void bootstrap() {
biomes.register(Biomes.PLAINS, registries.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS).value(), RegistrationInfo.BUILT_IN);
biomes.freeze();
factory = PalettedContainerFactory.create(new RegistryAccess.ImmutableRegistryAccess(List.of(biomes)));
+ decoder = new SectionDecoder(factory, biomes);
dimensionType = registries.lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(BuiltinDimensionTypes.OVERWORLD).value();
EmptySectionHolder.init(factory);
}
@@ -69,7 +71,7 @@ void reconstructsWorldSurfaceLikeVanilla(final int minY, final int height, final
saved.remove(SerializableChunkData.HEIGHTMAPS_TAG);
}
- final ChunkSnapshot snapshot = ChunkSnapshotFactory.snapshotFromChunkData(chunk, dimensionType, factory, saved);
+ final ChunkSnapshot snapshot = ChunkSnapshotFactory.snapshotFromChunkData(chunk, dimensionType, decoder, saved);
final int[] expected = new int[256];
final int[] actual = new int[256];
for (int z = 0; z < 16; z++) {
@@ -84,7 +86,7 @@ void reconstructsWorldSurfaceLikeVanilla(final int minY, final int height, final
@Test
void identifiesEmptyDecodedSections() {
final ProtoChunk chunk = terrainFixture(-64, 384);
- final ChunkSnapshot snapshot = ChunkSnapshotFactory.snapshotFromChunkData(chunk, dimensionType, factory, serialize(chunk));
+ final ChunkSnapshot snapshot = ChunkSnapshotFactory.snapshotFromChunkData(chunk, dimensionType, decoder, serialize(chunk));
for (int i = 0; i < chunk.getSectionsCount(); i++) {
assertEquals(chunk.getSection(i).hasOnlyAir(), snapshot.sectionEmpty(i), "Section " + i);
}
diff --git a/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoderTest.java b/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoderTest.java
new file mode 100644
index 00000000..dd2c7aa3
--- /dev/null
+++ b/common/src/test/java/xyz/jpenilla/squaremap/common/chunksnapshot/SectionDecoderTest.java
@@ -0,0 +1,211 @@
+package xyz.jpenilla.squaremap.common.chunksnapshot;
+
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.Lifecycle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import net.minecraft.SharedConstants;
+import net.minecraft.core.Holder;
+import net.minecraft.core.MappedRegistry;
+import net.minecraft.core.RegistrationInfo;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.data.registries.VanillaRegistries;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.LongTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.StringTag;
+import net.minecraft.server.Bootstrap;
+import net.minecraft.world.level.biome.Biome;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.MissingPaletteEntryException;
+import net.minecraft.world.level.chunk.PalettedContainer;
+import net.minecraft.world.level.chunk.PalettedContainerFactory;
+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Compares {@link SectionDecoder} against vanilla's container codecs on the same section data.
+ */
+class SectionDecoderTest {
+ private static PalettedContainerFactory factory;
+ private static SectionDecoder decoder;
+ private static List allStates;
+ private static List> allBiomes;
+
+ @BeforeAll
+ static void bootstrap() {
+ SharedConstants.tryDetectVersion();
+ Bootstrap.bootStrap();
+ final var registries = VanillaRegistries.createWorldLookup();
+ final MappedRegistry biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
+ registries.lookupOrThrow(Registries.BIOME).listElements()
+ .forEach(biome -> biomes.register(biome.key(), biome.value(), RegistrationInfo.BUILT_IN));
+ biomes.freeze();
+ factory = PalettedContainerFactory.create(new RegistryAccess.ImmutableRegistryAccess(List.of(biomes)));
+ decoder = new SectionDecoder(factory, biomes);
+ allStates = new ArrayList<>();
+ Block.BLOCK_STATE_REGISTRY.forEach(allStates::add);
+ allBiomes = new ArrayList<>();
+ factory.biomeStrategy().globalMap().forEach(allBiomes::add);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3, 5, 16, 17, 100, 256, 257, 1000})
+ void decodesBlockStatesLikeVanilla(final int paletteSize) {
+ assertBlockStatesMatch(blockStatesTag(paletteSize, paletteSize));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3, 4, 5, 8, 9, 30})
+ void decodesBiomesLikeVanilla(final int paletteSize) {
+ assertBiomesMatch(biomesTag(paletteSize));
+ }
+
+ @Test
+ void unknownBlockUsesDefault() {
+ final CompoundTag tag = blockStatesTag(5, 5);
+ tag.getListOrEmpty("palette").set(2, StringTag.valueOf("squaremap_test:missing"));
+ assertBlockStatesMatch(tag);
+ }
+
+ @Test
+ void unknownBiomeUsesDefault() {
+ final CompoundTag tag = biomesTag(3);
+ tag.getListOrEmpty("palette").set(1, StringTag.valueOf("squaremap_test:missing"));
+ assertBiomesMatch(tag);
+ }
+
+ @Test
+ void missingDataMatchesVanilla() {
+ final CompoundTag tag = blockStatesTag(5, 5);
+ tag.remove("data");
+ assertThrows(SerializableChunkData.ChunkReadException.class, () -> vanillaBlockStates(tag));
+ assertThrows(SerializableChunkData.ChunkReadException.class, () -> decodeBlockStates(tag));
+ }
+
+ @Test
+ void wrongDataLengthMatchesVanilla() {
+ final CompoundTag tag = blockStatesTag(5, 5);
+ tag.putLongArray("data", new long[10]);
+ assertThrows(SerializableChunkData.ChunkReadException.class, () -> vanillaBlockStates(tag));
+ assertThrows(SerializableChunkData.ChunkReadException.class, () -> decodeBlockStates(tag));
+ }
+
+ @Test
+ void listDataMatchesVanilla() {
+ final CompoundTag tag = blockStatesTag(5, 5);
+ final ListTag data = new ListTag();
+ for (final long value : tag.getLongArray("data").orElseThrow()) {
+ data.add(LongTag.valueOf(value));
+ }
+ tag.put("data", data);
+ assertBlockStatesMatch(tag);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {5, 300})
+ void outOfRangeIndexMatchesVanilla(final int paletteSize) {
+ final CompoundTag tag = blockStatesTag(paletteSize, paletteSize);
+ final long[] data = tag.getLongArray("data").orElseThrow();
+ final int bits = paletteSize <= 16 ? 4 : 9;
+ // Set the first value to the highest index the bits can hold, past the end of the palette.
+ data[0] = data[0] & ~((1L << bits) - 1) | (1L << bits) - 1;
+ tag.putLongArray("data", data);
+ // Vanilla fails on read for small palettes, but repacks global ones on decode and throws there.
+ // Our decoder never repacks, so it fails on read for both, with the same exception.
+ if (paletteSize <= 256) {
+ final SectionData vanilla = vanillaBlockStates(tag);
+ assertThrows(MissingPaletteEntryException.class, () -> vanilla.get(0));
+ } else {
+ assertThrows(MissingPaletteEntryException.class, () -> vanillaBlockStates(tag));
+ }
+ final SectionData decoded = decodeBlockStates(tag);
+ assertThrows(MissingPaletteEntryException.class, () -> decoded.get(0));
+ }
+
+ @Test
+ void emptyPaletteFails() {
+ final CompoundTag tag = new CompoundTag();
+ tag.put("palette", new ListTag());
+ // Vanilla decodes this, but every read from the section throws.
+ assertThrows(SerializableChunkData.ChunkReadException.class, () -> decodeBlockStates(tag));
+ }
+
+ private static CompoundTag blockStatesTag(final int paletteSize, final long seed) {
+ final Random random = new Random(seed);
+ final List states = new ArrayList<>(allStates);
+ Collections.shuffle(states, random);
+ final PalettedContainer container = factory.createForBlockStates();
+ // Use every chosen state at least once, so the written palette has exactly paletteSize entries.
+ for (int i = 0; i < 4096; i++) {
+ final BlockState state = states.get(i < paletteSize ? i : random.nextInt(paletteSize));
+ container.set(i & 15, i >> 8, i >> 4 & 15, state);
+ }
+ final CompoundTag tag = (CompoundTag) factory.blockStatesContainerCodec().encodeStart(NbtOps.INSTANCE, container).getOrThrow();
+ assertEquals(paletteSize, tag.getListOrEmpty("palette").size());
+ return tag;
+ }
+
+ private static CompoundTag biomesTag(final int paletteSize) {
+ final Random random = new Random(paletteSize);
+ final PalettedContainer> container = factory.createForBiomes();
+ for (int i = 0; i < 64; i++) {
+ final Holder biome = allBiomes.get(i < paletteSize ? i : random.nextInt(paletteSize));
+ container.set(i & 3, i >> 4, i >> 2 & 3, biome);
+ }
+ final CompoundTag tag = (CompoundTag) factory.biomeContainerCodec().encodeStart(NbtOps.INSTANCE, container).getOrThrow();
+ assertEquals(paletteSize, tag.getListOrEmpty("palette").size());
+ return tag;
+ }
+
+ // Like ChunkSnapshotFactory, without logging.
+ private static T orThrow(final DataResult result) {
+ return result.promotePartial(msg -> {}).getOrThrow(SerializableChunkData.ChunkReadException::new);
+ }
+
+ private static SectionData decodeBlockStates(final CompoundTag tag) {
+ return orThrow(decoder.blockStates(tag));
+ }
+
+ private static SectionData> decodeBiomes(final CompoundTag tag) {
+ return orThrow(decoder.biomes(tag));
+ }
+
+ private static SectionData vanillaBlockStates(final CompoundTag tag) {
+ return SectionData.of(orThrow(factory.blockStatesContainerCodec().parse(NbtOps.INSTANCE, tag)));
+ }
+
+ private static SectionData> vanillaBiomes(final CompoundTag tag) {
+ return SectionData.of((PalettedContainer>) orThrow(factory.biomeContainerCodec().parse(NbtOps.INSTANCE, tag)));
+ }
+
+ private static void assertBlockStatesMatch(final CompoundTag tag) {
+ assertSectionDataMatch(vanillaBlockStates(tag), decodeBlockStates(tag), 4096);
+ }
+
+ private static void assertBiomesMatch(final CompoundTag tag) {
+ assertSectionDataMatch(vanillaBiomes(tag), decodeBiomes(tag), 64);
+ }
+
+ // For fixtures that decode; any exception fails the test.
+ private static void assertSectionDataMatch(final SectionData expected, final SectionData actual, final int size) {
+ for (int i = 0; i < size; i++) {
+ assertEquals(expected.get(i), actual.get(i), "Index " + i);
+ }
+ assertEquals(
+ expected.maybeHas(value -> !(value instanceof BlockState state) || !state.isAir()),
+ actual.maybeHas(value -> !(value instanceof BlockState state) || !state.isAir())
+ );
+ }
+}
diff --git a/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProvider.java b/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProvider.java
index ffcd1496..16b8b6ff 100644
--- a/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProvider.java
+++ b/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProvider.java
@@ -15,7 +15,6 @@
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
-import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
@@ -30,13 +29,15 @@
import xyz.jpenilla.squaremap.common.chunksnapshot.ChunkSnapshotFactory;
import xyz.jpenilla.squaremap.common.chunksnapshot.ChunkSnapshotProvider;
import xyz.jpenilla.squaremap.common.util.concurrent.CompletableFutures;
+import xyz.jpenilla.squaremap.common.chunksnapshot.SectionDecoder;
import xyz.jpenilla.squaremap.paper.util.Folia;
@DefaultQualifier(NonNull.class)
record PaperChunkSnapshotProvider(
ServerLevel level,
Server server,
- JavaPlugin plugin
+ JavaPlugin plugin,
+ SectionDecoder sectionDecoder
) implements ChunkSnapshotProvider {
@Override
public CompletableFuture<@Nullable ChunkSnapshot> asyncSnapshot(final int x, final int z) {
@@ -73,7 +74,6 @@ record PaperChunkSnapshotProvider(
final ChunkTaskScheduler scheduler = this.level.moonrise$getChunkTaskScheduler();
final Executor executor = task -> scheduler.loadExecutor.createTask(task, Priority.NORMAL).queue();
final LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(this.level.getMinY(), this.level.getHeight());
- final PalettedContainerFactory palettedContainerFactory = this.level.palettedContainerFactory();
final DimensionType dimensionType = this.level.dimensionType();
return data.thenComposeAsync(
rawTag -> {
@@ -87,7 +87,7 @@ record PaperChunkSnapshotProvider(
case INELIGIBLE -> CompletableFuture.completedFuture(null);
case ELIGIBLE -> {
if (ChunkSnapshotFactory.chunkPosMatches(tag, x, z)) {
- yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, palettedContainerFactory, tag));
+ yield CompletableFuture.completedFuture(ChunkSnapshotFactory.snapshotFromChunkData(heightAccessor, dimensionType, this.sectionDecoder, tag));
} else {
yield this.chunkSystemSnapshot(x, z);
}
diff --git a/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProviderFactory.java b/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProviderFactory.java
index 44fbc6c1..52069018 100644
--- a/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProviderFactory.java
+++ b/paper/src/main/java/xyz/jpenilla/squaremap/paper/chunksnapshot/PaperChunkSnapshotProviderFactory.java
@@ -9,6 +9,7 @@
import org.checkerframework.framework.qual.DefaultQualifier;
import xyz.jpenilla.squaremap.common.chunksnapshot.ChunkSnapshotProvider;
import xyz.jpenilla.squaremap.common.chunksnapshot.ChunkSnapshotProviderFactory;
+import xyz.jpenilla.squaremap.common.chunksnapshot.SectionDecoder;
@DefaultQualifier(NonNull.class)
@Singleton
@@ -24,6 +25,6 @@ private PaperChunkSnapshotProviderFactory(final Server server, final JavaPlugin
@Override
public ChunkSnapshotProvider createChunkSnapshotProvider(final ServerLevel level) {
- return new PaperChunkSnapshotProvider(level, this.server, this.plugin);
+ return new PaperChunkSnapshotProvider(level, this.server, this.plugin, SectionDecoder.create(level));
}
}