diff --git a/src/main/java/xyz/nucleoid/map_templates/MapChunk.java b/src/main/java/xyz/nucleoid/map_templates/MapChunk.java index 3c6cf71..d467913 100644 --- a/src/main/java/xyz/nucleoid/map_templates/MapChunk.java +++ b/src/main/java/xyz/nucleoid/map_templates/MapChunk.java @@ -1,6 +1,11 @@ package xyz.nucleoid.map_templates; import com.mojang.serialization.Codec; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; @@ -9,8 +14,11 @@ import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.util.math.Vec3d; import net.minecraft.world.chunk.PalettedContainer; +import org.apache.commons.codec.digest.Blake3; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public final class MapChunk { @@ -18,6 +26,17 @@ public final class MapChunk { private final ChunkSectionPos pos; + /** + * BLAKE3 hash of the block states in this chunk section + */ + private final byte[] blockStatesHash = new byte[32]; + + /** + * Whether the above hash represents currently-stored block data. + */ + private boolean blockStatesHashDirty = true; + + private PalettedContainer container = new PalettedContainer<>(Block.STATE_IDS, Blocks.AIR.getDefaultState(), PalettedContainer.PaletteProvider.BLOCK_STATE); private final List entities = new ArrayList<>(); @@ -26,6 +45,7 @@ public final class MapChunk { } public void set(int x, int y, int z, BlockState state) { + this.blockStatesHashDirty = true; this.container.set(x, y, z, state); } @@ -66,7 +86,8 @@ public List getEntities() { } public void serialize(NbtCompound nbt) { - nbt.put("block_states", BLOCK_CODEC.encodeStart(NbtOps.INSTANCE, this.container).getOrThrow(false, (e) -> {})); + var blockStatesNbt = BLOCK_CODEC.encodeStart(NbtOps.INSTANCE, this.container).getOrThrow(false, (e) -> {}); + nbt.put("block_states", blockStatesNbt); if (!this.entities.isEmpty()) { var entitiesNbt = new NbtList(); @@ -79,12 +100,11 @@ public void serialize(NbtCompound nbt) { public static MapChunk deserialize(ChunkSectionPos pos, NbtCompound nbt) { var chunk = new MapChunk(pos); + var blockStatesNbt = nbt.getCompound("block_states"); var container = BLOCK_CODEC.parse(NbtOps.INSTANCE, nbt.getCompound("block_states")) .promotePartial(errorMessage -> {}).get().left(); - if (container.isPresent()) { - chunk.container = container.get(); - } + container.ifPresent(blockStatePalettedContainer -> chunk.container = blockStatePalettedContainer); if (nbt.contains("entities", NbtElement.LIST_TYPE)) { var entitiesNbt = nbt.getList("entities", NbtElement.COMPOUND_TYPE); @@ -95,4 +115,65 @@ public static MapChunk deserialize(ChunkSectionPos pos, NbtCompound nbt) { return chunk; } + + private NbtElement serializeBlockStates() { + return BLOCK_CODEC.encodeStart(NbtOps.INSTANCE, this.container).getOrThrow(false, (e) -> {}); + } + + /** + * Recompute the hash if it is dirty. + */ + private void recomputeHash() { + if (this.blockStatesHashDirty) { + try { + var md = Blake3.initHash(); + + this.serializeBlockStates().write(new DataOutputStream(new OutputStream() { + private ByteBuffer tmpBuffer; + @Override + public void write(int i) { + if (tmpBuffer == null) { + tmpBuffer = ByteBuffer.allocate(4); + } else { + tmpBuffer.clear(); + } + + tmpBuffer.putInt(i); + md.update(tmpBuffer.array()); + } + + @Override + public void write(byte @NotNull [] bytes, int off, int len) { + md.update(bytes, off, len); + } + })); + + md.doFinalize(this.blockStatesHash, 0, 32); + } catch (IOException e) { + // Unreachable + throw new RuntimeException(e); + } + + this.blockStatesHashDirty = false; + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof MapChunk)) { + return false; + } + + MapChunk other = (MapChunk) obj; + + if (!this.pos.equals(other.pos)) { + return false; + } + + this.recomputeHash(); + other.recomputeHash(); + return Arrays.equals(this.blockStatesHash, other.blockStatesHash); + } } diff --git a/src/main/java/xyz/nucleoid/map_templates/MapTemplate.java b/src/main/java/xyz/nucleoid/map_templates/MapTemplate.java index e3476e5..965a093 100644 --- a/src/main/java/xyz/nucleoid/map_templates/MapTemplate.java +++ b/src/main/java/xyz/nucleoid/map_templates/MapTemplate.java @@ -1,8 +1,6 @@ package xyz.nucleoid.map_templates; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.*; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.block.entity.BlockEntity; @@ -13,10 +11,7 @@ import net.minecraft.util.BlockMirror; import net.minecraft.util.BlockRotation; import net.minecraft.util.Identifier; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkSectionPos; -import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.*; import net.minecraft.world.Heightmap; import net.minecraft.world.biome.Biome; import net.minecraft.world.biome.BiomeKeys; @@ -366,6 +361,45 @@ public static MapTemplate merged(MapTemplate primary, MapTemplate secondary) { return result; } + /** + * All the aspects in which two map templates differ. + * + * @param dataChanged whether the metadata has changed + * @param chunksChanged whether the chunks and blocks of the template has changed + * @param biomeChanged whether the biome of the template has changed + * @param blockEntitiesChanged whether the block entities in the template have changed + */ + public record Diff(boolean dataChanged, boolean chunksChanged, boolean biomeChanged, boolean blockEntitiesChanged) { + public boolean needsRegeneration() { + return this.chunksChanged || this.biomeChanged || this.blockEntitiesChanged; + } + } + + /** + * Calculate the difference in chunks between the two semplates. + * + * @param a the first template + * @param b the second template + */ + public static Diff diff(MapTemplate a, MapTemplate b) { + if (a == b) { + return new Diff(false, false, false, false); + } + + boolean chunksChanged = !(a.chunks.equals(b.chunks)); + boolean dataChanged = !a.getMetadata().equals(b.getMetadata()); + boolean biomeChanged = a.biome != b.biome; + boolean blockEntitiesChanged = !a.blockEntities.equals(b.blockEntities); + return new Diff(dataChanged, chunksChanged, biomeChanged, blockEntitiesChanged); + } + + /** + * Calculate the difference in chunks between this and another template. + */ + public Diff diff(MapTemplate other) { + return MapTemplate.diff(this, other); + } + public void mergeFrom(MapTemplate other) { other.mergeInto(this); } diff --git a/src/main/java/xyz/nucleoid/map_templates/MapTemplateMetadata.java b/src/main/java/xyz/nucleoid/map_templates/MapTemplateMetadata.java index 4786cf6..869230a 100644 --- a/src/main/java/xyz/nucleoid/map_templates/MapTemplateMetadata.java +++ b/src/main/java/xyz/nucleoid/map_templates/MapTemplateMetadata.java @@ -116,4 +116,12 @@ public void setData(NbtCompound data) { public NbtCompound getData() { return this.data; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapTemplateMetadata)) return false; + + return this.data.equals(((MapTemplateMetadata) o).data) && this.regions.equals(((MapTemplateMetadata) o).regions); + } } diff --git a/src/main/java/xyz/nucleoid/map_templates/TemplateRegion.java b/src/main/java/xyz/nucleoid/map_templates/TemplateRegion.java index 1eac4c6..440b30e 100644 --- a/src/main/java/xyz/nucleoid/map_templates/TemplateRegion.java +++ b/src/main/java/xyz/nucleoid/map_templates/TemplateRegion.java @@ -55,4 +55,11 @@ public static TemplateRegion deserialize(NbtCompound nbt) { public TemplateRegion copy() { return new TemplateRegion(this.marker, this.bounds, this.data != null ? this.data.copy() : null); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TemplateRegion other)) return false; + return this.data.equals(other.data) && this.marker.equals(other.marker) && this.bounds.equals(other.bounds); + } }