diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/Protocol1_19To1_18_2.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/Protocol1_19To1_18_2.java
index 048f4f3e..ed01f5e1 100644
--- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/Protocol1_19To1_18_2.java
+++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/Protocol1_19To1_18_2.java
@@ -27,6 +27,7 @@
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.rewriter.BlockItemPacketRewriter1_19;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.rewriter.CommandRewriter1_19;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.rewriter.EntityPacketRewriter1_19;
+import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.BlockAckStorage;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.DimensionRegistryStorage;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.EntityTracker1_19;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.NonceStorage;
@@ -343,6 +344,7 @@ public void register() {
@Override
public void init(final UserConnection user) {
+ user.put(new BlockAckStorage());
user.put(new DimensionRegistryStorage());
addEntityTracker(user, new EntityTracker1_19(user));
}
diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/BlockItemPacketRewriter1_19.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/BlockItemPacketRewriter1_19.java
index f4182440..e375a406 100644
--- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/BlockItemPacketRewriter1_19.java
+++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/BlockItemPacketRewriter1_19.java
@@ -21,10 +21,13 @@
import com.viaversion.viabackwards.api.rewriters.BackwardsItemRewriter;
import com.viaversion.viabackwards.api.rewriters.EnchantmentRewriter;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.Protocol1_19To1_18_2;
+import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.BlockAckStorage;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.LastDeathPosition;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.data.ParticleMappings;
import com.viaversion.viaversion.api.data.entity.EntityTracker;
+import com.viaversion.viaversion.api.minecraft.BlockChangeRecord;
+import com.viaversion.viaversion.api.minecraft.BlockPosition;
import com.viaversion.viaversion.api.minecraft.GlobalBlockPosition;
import com.viaversion.viaversion.api.minecraft.chunks.Chunk;
import com.viaversion.viaversion.api.minecraft.chunks.ChunkSection;
@@ -36,6 +39,7 @@
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.api.type.types.chunk.ChunkType1_18;
import com.viaversion.viaversion.protocols.v1_16_4to1_17.packet.ServerboundPackets1_17;
+import com.viaversion.viaversion.protocols.v1_17_1to1_18.packet.ClientboundPackets1_18;
import com.viaversion.viaversion.protocols.v1_18_2to1_19.packet.ClientboundPackets1_19;
import com.viaversion.viaversion.rewriter.RecipeRewriter;
import com.viaversion.viaversion.util.MathUtil;
@@ -84,14 +88,88 @@ public void register() {
}
});
- protocol.registerClientbound(ClientboundPackets1_19.BLOCK_CHANGED_ACK, null, new PacketHandlers() {
+ protocol.registerClientbound(ClientboundPackets1_19.BLOCK_CHANGED_ACK, ClientboundPackets1_18.BLOCK_UPDATE, new PacketHandlers() {
@Override
public void register() {
- read(Types.VAR_INT); // Sequence
- handler(PacketWrapper::cancel); // This is fine:tm:
+ handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ final int sequence = wrapper.read(Types.VAR_INT);
+ final BlockPosition pos = blockAckStorage.remove(sequence);
+ if (pos == null) {
+ wrapper.cancel();
+ return;
+ }
+
+ final int minSectionY = protocol.getEntityRewriter().tracker(wrapper.user()).currentMinY() >> 4;
+ final int blockState = blockAckStorage.getBlockStateAt(pos, minSectionY);
+ if (blockState < 0) {
+ wrapper.cancel();
+ return;
+ }
+
+ wrapper.write(Types.BLOCK_POSITION1_14, pos);
+ wrapper.write(Types.VAR_INT, blockState);
+ });
+ }
+ });
+
+ protocol.replaceClientbound(ClientboundPackets1_19.BLOCK_UPDATE, new PacketHandlers() {
+ @Override
+ public void register() {
+ handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ final BlockPosition pos = wrapper.passthrough(Types.BLOCK_POSITION1_14);
+ final int blockId = wrapper.read(Types.VAR_INT);
+ final int mappedId = protocol.getMappingData().getNewBlockStateId(blockId);
+ wrapper.write(Types.VAR_INT, mappedId);
+ final int minSectionY = protocol.getEntityRewriter().tracker(wrapper.user()).currentMinY() >> 4;
+ blockAckStorage.updateBlockState(pos.x(), pos.y(), pos.z(), minSectionY, mappedId);
+ });
+ }
+ });
+
+ protocol.replaceClientbound(ClientboundPackets1_19.SECTION_BLOCKS_UPDATE, new PacketHandlers() {
+ @Override
+ public void register() {
+ handler(wrapper -> {
+ final long sectionPos = wrapper.passthrough(Types.LONG);
+ wrapper.passthrough(Types.BOOLEAN); // Suppress light updates
+
+ final int sectionY = (int) ((sectionPos << 44) >> 44);
+ final int chunkZ = (int) ((sectionPos << 22) >> 42);
+ final int chunkX = (int) (sectionPos >> 42);
+
+ final int minSectionY = protocol.getEntityRewriter().tracker(wrapper.user()).currentMinY() >> 4;
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ for (final BlockChangeRecord record : wrapper.passthrough(Types.VAR_LONG_BLOCK_CHANGE_ARRAY)) {
+ final int mappedId = protocol.getMappingData().getNewBlockStateId(record.getBlockId());
+ record.setBlockId(mappedId);
+ blockAckStorage.updateBlockState(
+ (chunkX << 4) | record.getSectionX(),
+ (sectionY << 4) + record.getSectionY(),
+ (chunkZ << 4) | record.getSectionZ(),
+ minSectionY,
+ mappedId
+ );
+ }
+ });
}
});
+ protocol.replaceClientbound(ClientboundPackets1_19.LEVEL_CHUNK_WITH_LIGHT, wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ final Chunk chunk = protocol.getBlockRewriter().handleChunk1_18(wrapper);
+ protocol.getBlockRewriter().handleBlockEntities(chunk, wrapper.user());
+ blockAckStorage.cacheChunk(chunk.getX(), chunk.getZ(), chunk.getSections());
+ });
+
+ protocol.registerClientbound(ClientboundPackets1_19.FORGET_LEVEL_CHUNK, wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ final int chunkX = wrapper.passthrough(Types.INT);
+ final int chunkZ = wrapper.passthrough(Types.INT);
+ blockAckStorage.forgetChunk(chunkX, chunkZ);
+ });
+
protocol.replaceClientbound(ClientboundPackets1_19.LEVEL_PARTICLES, new PacketHandlers() {
@Override
public void register() {
@@ -126,34 +204,46 @@ public void register() {
}
});
- // The server does nothing but track the sequence, so we can just set it as 0
protocol.registerServerbound(ServerboundPackets1_17.PLAYER_ACTION, new PacketHandlers() {
@Override
public void register() {
- map(Types.VAR_INT); // Action
- map(Types.BLOCK_POSITION1_14); // Block position
- map(Types.UNSIGNED_BYTE); // Direction
- create(Types.VAR_INT, 0); // Sequence
+ handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ final int action = wrapper.passthrough(Types.VAR_INT);
+ final BlockPosition pos = wrapper.passthrough(Types.BLOCK_POSITION1_14);
+ wrapper.passthrough(Types.UNSIGNED_BYTE); // Direction
+ final int sequence = blockAckStorage.nextSequence();
+ wrapper.write(Types.VAR_INT, sequence); // Sequence
+ if (action < 3) {
+ blockAckStorage.add(sequence, pos);
+ }
+ });
}
});
protocol.registerServerbound(ServerboundPackets1_17.USE_ITEM_ON, new PacketHandlers() {
@Override
public void register() {
- map(Types.VAR_INT); // Hand
- map(Types.BLOCK_POSITION1_14); // Block position
- map(Types.VAR_INT); // Direction
- map(Types.FLOAT); // X
- map(Types.FLOAT); // Y
- map(Types.FLOAT); // Z
- map(Types.BOOLEAN); // Inside
- create(Types.VAR_INT, 0); // Sequence
+ handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ wrapper.passthrough(Types.VAR_INT); // Hand
+ wrapper.passthrough(Types.BLOCK_POSITION1_14); // Block position
+ wrapper.passthrough(Types.VAR_INT); // Direction
+ wrapper.passthrough(Types.FLOAT); // X
+ wrapper.passthrough(Types.FLOAT); // Y
+ wrapper.passthrough(Types.FLOAT); // Z
+ wrapper.passthrough(Types.BOOLEAN); // Inside
+ wrapper.write(Types.VAR_INT, blockAckStorage.nextSequence()); // Sequence
+ });
}
});
protocol.registerServerbound(ServerboundPackets1_17.USE_ITEM, new PacketHandlers() {
@Override
public void register() {
- map(Types.VAR_INT); // Hand
- create(Types.VAR_INT, 0); // Sequence
+ handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ wrapper.passthrough(Types.VAR_INT); // Hand
+ wrapper.write(Types.VAR_INT, blockAckStorage.nextSequence()); // Sequence
+ });
}
});
diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/EntityPacketRewriter1_19.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/EntityPacketRewriter1_19.java
index f37a9197..8b7f2b13 100644
--- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/EntityPacketRewriter1_19.java
+++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/rewriter/EntityPacketRewriter1_19.java
@@ -23,6 +23,7 @@
import com.viaversion.viabackwards.ViaBackwards;
import com.viaversion.viabackwards.api.rewriters.EntityRewriter;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.Protocol1_19To1_18_2;
+import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.BlockAckStorage;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.DimensionRegistryStorage;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.EntityTracker1_19;
import com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage.LastDeathPosition;
@@ -206,6 +207,8 @@ public void register() {
map(Types.STRING_ARRAY); // Worlds
map(Types.NAMED_COMPOUND_TAG); // Dimension registry
handler(wrapper -> {
+ final BlockAckStorage blockAckStorage = wrapper.user().get(BlockAckStorage.class);
+ blockAckStorage.clear();
final DimensionRegistryStorage dimensionRegistryStorage = wrapper.user().get(DimensionRegistryStorage.class);
dimensionRegistryStorage.clear();
@@ -269,6 +272,7 @@ public void register() {
@Override
public void register() {
handler(wrapper -> {
+ wrapper.user().get(BlockAckStorage.class).clear();
final String dimensionKey = wrapper.read(Types.STRING);
final CompoundTag dimension = wrapper.user().get(DimensionRegistryStorage.class).dimension(dimensionKey);
if (dimension == null) {
diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/storage/BlockAckStorage.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/storage/BlockAckStorage.java
new file mode 100644
index 00000000..89d91be2
--- /dev/null
+++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_19to1_18_2/storage/BlockAckStorage.java
@@ -0,0 +1,123 @@
+/*
+ * This file is part of ViaBackwards - https://github.com/ViaVersion/ViaBackwards
+ * Copyright (C) 2016-2026 ViaVersion and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.viaversion.viabackwards.protocol.v1_19to1_18_2.storage;
+
+import com.viaversion.viaversion.api.connection.StorableObject;
+import com.viaversion.viaversion.api.minecraft.BlockPosition;
+import com.viaversion.viaversion.api.minecraft.ChunkPosition;
+import com.viaversion.viaversion.api.minecraft.chunks.ChunkSection;
+import com.viaversion.viaversion.api.minecraft.chunks.DataPalette;
+import com.viaversion.viaversion.api.minecraft.chunks.DataPaletteImpl;
+import com.viaversion.viaversion.api.minecraft.chunks.PaletteType;
+import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectLinkedOpenHashMap;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public final class BlockAckStorage implements StorableObject {
+
+ private final Int2ObjectLinkedOpenHashMap blockPosition = new Int2ObjectLinkedOpenHashMap<>();
+ private final Map cachedChunks = new HashMap<>();
+ private int sequenceId;
+
+ public int nextSequence() {
+ return sequenceId++;
+ }
+
+ public void add(final int sequence, final BlockPosition position) {
+ blockPosition.put(sequence, position);
+
+ // Some actions may be left unacknowledged by modded servers
+ if (blockPosition.size() > 100) {
+ blockPosition.removeFirst();
+ }
+ }
+
+ public @Nullable BlockPosition remove(final int sequence) {
+ return blockPosition.remove(sequence);
+ }
+
+ public void cacheChunk(final int chunkX, final int chunkZ, final ChunkSection[] sections) {
+ final DataPalette[] blockPalettes = new DataPalette[sections.length];
+ for (int i = 0; i < sections.length; i++) {
+ final ChunkSection section = sections[i];
+ if (section == null) {
+ continue;
+ }
+
+ final DataPalette palette = section.palette(PaletteType.BLOCKS);
+ if (palette != null) {
+ blockPalettes[i] = copyPalette(palette);
+ }
+ }
+ cachedChunks.put(ChunkPosition.chunkKey(chunkX, chunkZ), blockPalettes);
+ }
+
+ public void forgetChunk(final int chunkX, final int chunkZ) {
+ cachedChunks.remove(ChunkPosition.chunkKey(chunkX, chunkZ));
+ }
+
+ public void clear() {
+ blockPosition.clear();
+ cachedChunks.clear();
+ sequenceId = 0;
+ }
+
+ public void updateBlockState(final int blockX, final int blockY, final int blockZ, final int minSectionY, final int state) {
+ final DataPalette[] sections = cachedChunks.get(ChunkPosition.chunkKeyForBlock(blockX, blockZ));
+ if (sections == null) {
+ return;
+ }
+ final int sectionIdx = (blockY >> 4) - minSectionY;
+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
+ return;
+ }
+ final DataPalette palette = sections[sectionIdx];
+ if (palette == null) {
+ return;
+ }
+ palette.setIdAt(blockX & 15, blockY & 15, blockZ & 15, state);
+ }
+
+ public int getBlockStateAt(final BlockPosition pos, final int minSectionY) {
+ final DataPalette[] sections = cachedChunks.get(ChunkPosition.chunkKeyForBlock(pos.x(), pos.z()));
+ if (sections == null) {
+ return -1;
+ }
+ final int sectionIdx = (pos.y() >> 4) - minSectionY;
+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
+ return -1;
+ }
+ final DataPalette palette = sections[sectionIdx];
+ if (palette == null) {
+ return -1;
+ }
+ return palette.idAt(pos.x() & 15, pos.y() & 15, pos.z() & 15);
+ }
+
+ private static DataPalette copyPalette(final DataPalette palette) {
+ final DataPaletteImpl copy = new DataPaletteImpl(ChunkSection.SIZE, palette.size());
+ for (int i = 0; i < palette.size(); i++) {
+ copy.addId(palette.idByIndex(i));
+ }
+ for (int i = 0; i < ChunkSection.SIZE; i++) {
+ copy.setPaletteIndexAt(i, palette.paletteIndexAt(i));
+ }
+ return copy;
+ }
+}