diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 2e0e2ae..0f7e853 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -15,6 +15,7 @@ import io.papermc.paper.network.ChannelInitializeListenerHolder; import io.papermc.paper.network.ConnectionEvent; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.kyori.adventure.key.Key; +import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; @@ -46,6 +47,13 @@ import java.util.Map; public class AxiomPaper extends JavaPlugin implements Listener { + public static final long MIN_POSITION_LONG = BlockPos.asLong(-33554432, -2048, -33554432); + static { + if (MIN_POSITION_LONG != 0b1000000000000000000000000010000000000000000000000000100000000000L) { + throw new Error("BlockPos representation changed!"); + } + } + @Override public void onEnable() { Bukkit.getPluginManager().registerEvents(this, this); diff --git a/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java new file mode 100644 index 0000000..bf14541 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java @@ -0,0 +1,88 @@ +package com.moulberry.axiom.buffer; + +import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; + +public class BiomeBuffer { + + private final Position2ByteMap map; + private final ResourceKey[] palette; + private final Object2ByteMap> paletteReverse; + private int paletteSize = 0; + + public BiomeBuffer() { + this.map = new Position2ByteMap(); + this.palette = new ResourceKey[255]; + this.paletteReverse = new Object2ByteOpenHashMap<>(); + } + + private BiomeBuffer(Position2ByteMap map, ResourceKey[] palette, Object2ByteMap> paletteReverse) { + this.map = map; + this.palette = palette; + this.paletteReverse = paletteReverse; + this.paletteSize = this.paletteReverse.size(); + } + + public void save(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeByte(this.paletteSize); + for (int i = 0; i < this.paletteSize; i++) { + friendlyByteBuf.writeResourceKey(this.palette[i]); + } + this.map.save(friendlyByteBuf); + } + + public static BiomeBuffer load(FriendlyByteBuf friendlyByteBuf) { + int paletteSize = friendlyByteBuf.readByte(); + ResourceKey[] palette = new ResourceKey[255]; + Object2ByteMap> paletteReverse = new Object2ByteOpenHashMap<>(); + for (int i = 0; i < paletteSize; i++) { + ResourceKey key = friendlyByteBuf.readResourceKey(Registries.BIOME); + palette[i] = key; + paletteReverse.put(key, (byte)(i+1)); + } + Position2ByteMap map = Position2ByteMap.load(friendlyByteBuf); + return new BiomeBuffer(map, palette, paletteReverse); + + } + + public void clear() { + this.map.clear(); + } + + public void forEachEntry(PositionConsumer> consumer) { + this.map.forEachEntry((x, y, z, v) -> { + if (v != 0) consumer.accept(x, y, z, this.palette[(v & 0xFF) - 1]); + }); + } + + public ResourceKey get(int quartX, int quartY, int quartZ) { + int index = this.map.get(quartX, quartY, quartZ) & 0xFF; + if (index == 0) return null; + return this.palette[index - 1]; + } + + private int getPaletteIndex(ResourceKey biome) { + int index = this.paletteReverse.getOrDefault(biome, (byte) 0) & 0xFF; + if (index != 0) return index; + + index = this.paletteSize; + if (index >= this.palette.length) { + throw new UnsupportedOperationException("Too many biomes! :("); + } + + this.palette[index] = biome; + this.paletteReverse.put(biome, (byte)(index + 1)); + + this.paletteSize += 1; + return index + 1; + } + + public void set(int quartX, int quartY, int quartZ, ResourceKey biome) { + this.map.put(quartX, quartY, quartZ, (byte) this.getPaletteIndex(biome)); + } + +} diff --git a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java index 6d402f5..cb3f90c 100644 --- a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java +++ b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java @@ -1,13 +1,15 @@ package com.moulberry.axiom.buffer; +import com.moulberry.axiom.AxiomPaper; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectSet; import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.PalettedContainer; public class BlockBuffer { @@ -16,7 +18,7 @@ public class BlockBuffer { private final Long2ObjectMap> values; private PalettedContainer last = null; - private long lastId = Long.MAX_VALUE; + private long lastId = AxiomPaper.MIN_POSITION_LONG; private int count; public BlockBuffer() { @@ -31,9 +33,32 @@ public class BlockBuffer { return this.count; } + public void save(FriendlyByteBuf friendlyByteBuf) { + for (Long2ObjectMap.Entry> entry : this.entrySet()) { + friendlyByteBuf.writeLong(entry.getLongKey()); + entry.getValue().write(friendlyByteBuf); + } + + friendlyByteBuf.writeLong(AxiomPaper.MIN_POSITION_LONG); + } + + public static BlockBuffer load(FriendlyByteBuf friendlyByteBuf) { + BlockBuffer buffer = new BlockBuffer(); + + while (true) { + long index = friendlyByteBuf.readLong(); + if (index == AxiomPaper.MIN_POSITION_LONG) break; + + PalettedContainer palettedContainer = buffer.getOrCreateSection(index); + palettedContainer.read(friendlyByteBuf); + } + + return buffer; + } + public void clear() { this.last = null; - this.lastId = Long.MAX_VALUE; + this.lastId = AxiomPaper.MIN_POSITION_LONG; this.values.clear(); } @@ -112,7 +137,7 @@ public class BlockBuffer { if (this.last == null || id != this.lastId) { this.lastId = id; this.last = this.values.computeIfAbsent(id, k -> new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, - EMPTY_STATE, PalettedContainer.Strategy.SECTION_STATES)); + EMPTY_STATE, PalettedContainer.Strategy.SECTION_STATES)); } return this.last; diff --git a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java new file mode 100644 index 0000000..d4c7d6f --- /dev/null +++ b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java @@ -0,0 +1,179 @@ +package com.moulberry.axiom.buffer; + +import com.moulberry.axiom.AxiomPaper; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; + +import java.util.Arrays; +import java.util.function.LongFunction; + +public class Position2ByteMap { + + @FunctionalInterface + public interface EntryConsumer { + void consume(int x, int y, int z, byte v); + } + + private final byte defaultValue; + private final LongFunction defaultFunction; + private final Long2ObjectMap map = new Long2ObjectOpenHashMap<>(); + + private long lastChunkPos = AxiomPaper.MIN_POSITION_LONG; + private byte[] lastChunk = null; + + public Position2ByteMap() { + this((byte) 0); + } + + public Position2ByteMap(byte defaultValue) { + this.defaultValue = defaultValue; + + if (defaultValue == 0) { + this.defaultFunction = k -> new byte[16*16*16]; + } else { + this.defaultFunction = k -> { + byte[] array = new byte[16*16*16]; + Arrays.fill(array, defaultValue); + return array; + }; + } + } + + public void save(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeByte(this.defaultValue); + for (Long2ObjectMap.Entry entry : this.map.long2ObjectEntrySet()) { + friendlyByteBuf.writeLong(entry.getLongKey()); + friendlyByteBuf.writeBytes(entry.getValue()); + } + friendlyByteBuf.writeLong(AxiomPaper.MIN_POSITION_LONG); + } + + public static Position2ByteMap load(FriendlyByteBuf friendlyByteBuf) { + Position2ByteMap map = new Position2ByteMap(friendlyByteBuf.readByte()); + + while (true) { + long pos = friendlyByteBuf.readLong(); + if (pos == AxiomPaper.MIN_POSITION_LONG) break; + + byte[] bytes = new byte[16*16*16]; + friendlyByteBuf.readBytes(bytes); + map.map.put(pos, bytes); + } + + return map; + } + + public void clear() { + this.map.clear(); + this.lastChunkPos = AxiomPaper.MIN_POSITION_LONG; + this.lastChunk = null; + } + + public byte get(int x, int y, int z) { + int xC = x >> 4; + int yC = y >> 4; + int zC = z >> 4; + + byte[] array = this.getChunk(xC, yC, zC); + if (array == null) return this.defaultValue; + + return array[(x&15) + (y&15)*16 + (z&15)*16*16]; + } + + public void put(int x, int y, int z, byte v) { + int xC = x >> 4; + int yC = y >> 4; + int zC = z >> 4; + + byte[] array = this.getOrCreateChunk(xC, yC, zC); + array[(x&15) + (y&15)*16 + (z&15)*16*16] = v; + } + + public byte add(int x, int y, int z, byte v) { + if (v == 0) return this.get(x, y, z); + + int xC = x >> 4; + int yC = y >> 4; + int zC = z >> 4; + + byte[] array = this.getOrCreateChunk(xC, yC, zC); + return array[(x&15) + (y&15)*16 + (z&15)*16*16] += v; + } + + + public byte binaryAnd(int x, int y, int z, byte v) { + int xC = x >> 4; + int yC = y >> 4; + int zC = z >> 4; + + byte[] array = this.getOrCreateChunk(xC, yC, zC); + return array[(x&15) + (y&15)*16 + (z&15)*16*16] &= v; + } + + public boolean min(int x, int y, int z, byte v) { + int xC = x >> 4; + int yC = y >> 4; + int zC = z >> 4; + + byte[] array = this.getOrCreateChunk(xC, yC, zC); + int index = (x&15) + (y&15)*16 + (z&15)*16*16; + + if (v < array[index]) { + array[index] = v; + return true; + } else { + return false; + } + } + + public void forEachEntry(EntryConsumer consumer) { + for (Long2ObjectMap.Entry entry : this.map.long2ObjectEntrySet()) { + int cx = BlockPos.getX(entry.getLongKey()) * 16; + int cy = BlockPos.getY(entry.getLongKey()) * 16; + int cz = BlockPos.getZ(entry.getLongKey()) * 16; + + int index = 0; + for (int z=0; z<16; z++) { + for (int y=0; y<16; y++) { + for (int x=0; x<16; x++) { + byte v = entry.getValue()[index++]; + if (v != this.defaultValue) { + consumer.consume(cx + x, cy + y, cz + z, v); + } + } + } + } + } + } + + public byte[] getChunk(int xC, int yC, int zC) { + return this.getChunk(BlockPos.asLong(xC, yC, zC)); + } + + public byte[] getChunk(long pos) { + if (this.lastChunkPos != pos) { + byte[] chunk = this.map.get(pos); + this.lastChunkPos = pos; + this.lastChunk = chunk; + } + + return this.lastChunk; + } + + public byte[] getOrCreateChunk(int xC, int yC, int zC) { + return this.getOrCreateChunk(BlockPos.asLong(xC, yC, zC)); + } + + public byte[] getOrCreateChunk(long pos) { + if (this.lastChunk == null || this.lastChunkPos != pos) { + byte[] chunk = this.map.computeIfAbsent(pos, this.defaultFunction); + this.lastChunkPos = pos; + this.lastChunk = chunk; + } + + return this.lastChunk; + } + +} diff --git a/src/main/java/com/moulberry/axiom/buffer/PositionConsumer.java b/src/main/java/com/moulberry/axiom/buffer/PositionConsumer.java new file mode 100644 index 0000000..ee268b2 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/buffer/PositionConsumer.java @@ -0,0 +1,8 @@ +package com.moulberry.axiom.buffer; + +@FunctionalInterface +public interface PositionConsumer { + + void accept(int x, int y, int z, T t); + +} diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java index 064d592..48682bb 100644 --- a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -13,7 +13,7 @@ import java.util.List; public class AxiomBigPayloadHandler extends ByteToMessageDecoder { - private static final ResourceLocation SET_BLOCK_BUFFER = new ResourceLocation("axiom", "set_block_buffer"); + private static final ResourceLocation SET_BUFFER = new ResourceLocation("axiom", "set_buffer"); private final int payloadId; private final Connection connection; private final SetBlockBufferPacketListener listener; @@ -35,7 +35,7 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { if (packetId == payloadId) { ResourceLocation identifier = buf.readResourceLocation(); - if (identifier.equals(SET_BLOCK_BUFFER)) { + if (identifier.equals(SET_BUFFER)) { ServerPlayer player = connection.getPlayer(); if (player != null) { listener.onReceive(player, buf); diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java index 7a24ae6..505c417 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -1,21 +1,29 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.buffer.BiomeBuffer; import com.moulberry.axiom.buffer.BlockBuffer; import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket; import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; @@ -29,7 +37,7 @@ import xyz.jpenilla.reflectionremapper.ReflectionRemapper; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Map; +import java.util.*; public class SetBlockBufferPacketListener { @@ -52,19 +60,26 @@ public class SetBlockBufferPacketListener { } public void onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { + MinecraftServer server = player.getServer(); + if (server == null) return; + ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); - BlockBuffer buffer = new BlockBuffer(); - while (true) { - long index = friendlyByteBuf.readLong(); - if (index == Long.MAX_VALUE) break; - - PalettedContainer palettedContainer = buffer.getOrCreateSection(index); - palettedContainer.read(friendlyByteBuf); + byte type = friendlyByteBuf.readByte(); + if (type == 0) { + BlockBuffer buffer = BlockBuffer.load(friendlyByteBuf); + applyBlockBuffer(server, buffer, worldKey); + } else if (type == 1) { + BiomeBuffer buffer = BiomeBuffer.load(friendlyByteBuf); + applyBiomeBuffer(server, buffer, worldKey); + } else { + throw new RuntimeException("Unknown buffer type: " + type); } + } - player.getServer().execute(() -> { - ServerLevel world = player.getServer().getLevel(worldKey); + private void applyBlockBuffer(MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { + server.execute(() -> { + ServerLevel world = server.getLevel(worldKey); if (world == null) return; BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); @@ -182,4 +197,52 @@ public class SetBlockBufferPacketListener { }); } + + private void applyBiomeBuffer(MinecraftServer server, BiomeBuffer biomeBuffer, ResourceKey worldKey) { + server.execute(() -> { + ServerLevel world = server.getLevel(worldKey); + if (world == null) return; + + Set changedChunks = new HashSet<>(); + + int minSection = world.getMinSection(); + int maxSection = world.getMaxSection(); + + Optional> registryOptional = world.registryAccess().registry(Registries.BIOME); + if (registryOptional.isEmpty()) return; + + Registry registry = registryOptional.get(); + + biomeBuffer.forEachEntry((x, y, z, biome) -> { + int cy = y >> 2; + if (cy < minSection || cy >= maxSection) { + return; + } + + var chunk = (LevelChunk) world.getChunk(x >> 2, z >> 2, ChunkStatus.FULL, false); + if (chunk == null) return; + + var section = chunk.getSection(cy - minSection); + PalettedContainer> container = (PalettedContainer>) section.getBiomes(); + + var holder = registry.getHolder(biome); + if (holder.isPresent()) { + container.set(x & 3, y & 3, z & 3, holder.get()); + changedChunks.add(chunk); + } + }); + + var chunkMap = world.getChunkSource().chunkMap; + HashMap> map = new HashMap<>(); + for (LevelChunk chunk : changedChunks) { + chunk.setUnsaved(true); + ChunkPos chunkPos = chunk.getPos(); + for (ServerPlayer serverPlayer2 : chunkMap.getPlayers(chunkPos, false)) { + map.computeIfAbsent(serverPlayer2, serverPlayer -> new ArrayList<>()).add(chunk); + } + } + map.forEach((serverPlayer, list) -> serverPlayer.connection.send(ClientboundChunksBiomesPacket.forChunks(list))); + }); + } + }