From daaa8b3f4d07ca6e1962c92ea591032ee9ed3559 Mon Sep 17 00:00:00 2001 From: Term4 Date: Thu, 2 Jul 2026 11:32:54 -0500 Subject: [PATCH] Fix splash potions turning into drinkable potions when re-sent by the client The 1.9->1.8 serverbound item conversion only restored the splash potion id (373 -> 438) when no Potion tag was present, but the clientbound conversion keeps that tag on the item. A creative mode client re-sending its inventory (e.g. on inventory close) therefore turned every splash potion into a drinkable one. --- .../rewriter/BlockItemPacketRewriter1_9.java | 3 +- .../rewriter/EntityPacketRewriter1_9.java | 42 +++++++++++++++++++ .../v1_9to1_8/storage/EntityTracker1_9.java | 10 +++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java index 4963eacfe..9d49ce332 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/BlockItemPacketRewriter1_9.java @@ -332,7 +332,8 @@ public Item handleItemToServer(UserConnection connection, Item item) { item.setData((short) 0); } - if (item.identifier() == 373 && (tag == null || !tag.contains("Potion"))) { // Potions + if (item.identifier() == 373) { // Potions + // Restore the splash id even when a Potion tag is present (e.g. items re-sent by a creative mode client) if (item.data() >= 16384) { item.setIdentifier(438); item.setData((short) (item.data() - 8192)); diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/EntityPacketRewriter1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/EntityPacketRewriter1_9.java index 0b610f738..932053aca 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/EntityPacketRewriter1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/rewriter/EntityPacketRewriter1_9.java @@ -138,6 +138,16 @@ public void register() { wrapper.passthrough(Types.SHORT); // Velocity x wrapper.passthrough(Types.SHORT); // Velocity y wrapper.passthrough(Types.SHORT); // Velocity z + if (type.is(EntityTypes1_9.EntityType.POTION)) { + // 1.8 clients read the potion type from the object data, but newer versions only + // send it in the entity data - the next packet. Held until then, see handleEntityData + wrapper.cancel(); + final EntityTracker1_9 entityTracker = tracker(wrapper.user()); + entityTracker.getPendingPotionSpawns().put(entityId, new EntityTracker1_9.PotionSpawn( + wrapper.get(Types.INT, 0), wrapper.get(Types.INT, 1), wrapper.get(Types.INT, 2), + wrapper.get(Types.BYTE, 1), wrapper.get(Types.BYTE, 2), + wrapper.get(Types.SHORT, 0), wrapper.get(Types.SHORT, 1), wrapper.get(Types.SHORT, 2))); + } } else { final short velocityX = wrapper.read(Types.SHORT); final short velocityY = wrapper.read(Types.SHORT); @@ -541,8 +551,40 @@ protected void registerRewrites() { filter().handler(this::handleEntityData); } + private void sendDelayedPotionSpawn(EntityDataHandlerEvent event, EntityTracker1_9 tracker, Item item) { + final EntityTracker1_9.PotionSpawn spawn = tracker.getPendingPotionSpawns().remove(event.entityId()); + if (spawn == null) { + return; + } + final Item converted = protocol.getItemRewriter().handleItemToClient(event.user(), item); + short data = converted == null ? 0 : converted.data(); + if (data <= 0) { + data = 16384; // Unmapped potion, at least display a splash bottle + } + + final PacketWrapper addEntity = PacketWrapper.create(ClientboundPackets1_8.ADD_ENTITY, event.user()); + addEntity.write(Types.VAR_INT, event.entityId()); + addEntity.write(Types.BYTE, (byte) EntityTypes1_9.ObjectType.POTION.getId()); + addEntity.write(Types.INT, spawn.x()); + addEntity.write(Types.INT, spawn.y()); + addEntity.write(Types.INT, spawn.z()); + addEntity.write(Types.BYTE, spawn.pitch()); + addEntity.write(Types.BYTE, spawn.yaw()); + addEntity.write(Types.INT, (int) data); + addEntity.write(Types.SHORT, spawn.velocityX()); + addEntity.write(Types.SHORT, spawn.velocityY()); + addEntity.write(Types.SHORT, spawn.velocityZ()); + addEntity.send(Protocol1_9To1_8.class); // Before the entity data is sent + } + private void handleEntityData(EntityDataHandlerEvent event, EntityData entityData) { final EntityTracker1_9 tracker = tracker(event.user()); + if (event.entityType() == EntityTypes1_9.EntityType.POTION && entityData.value() instanceof Item potionItem) { + // No 1.8 equivalent; supplies the object data of the held-back spawn. Matched by value type as the index is version dependent + sendDelayedPotionSpawn(event, tracker, potionItem); + event.cancel(); + return; + } if (entityData.id() == EntityDataIndex1_9.ENTITY_STATUS.getIndex()) { tracker.getStatus().put(event.entityId(), (Byte) entityData.value()); } diff --git a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/EntityTracker1_9.java b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/EntityTracker1_9.java index 15d7d20ab..24b3795f8 100644 --- a/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/EntityTracker1_9.java +++ b/common/src/main/java/com/viaversion/viarewind/protocol/v1_9to1_8/storage/EntityTracker1_9.java @@ -35,6 +35,7 @@ public class EntityTracker1_9 extends EntityTrackerBase { private final Int2ObjectMap vehicles = new Int2ObjectOpenHashMap<>(); private final Int2ObjectMap offsets = new Int2ObjectOpenHashMap<>(); private final Int2IntMap status = new Int2IntOpenHashMap(); + private final Int2ObjectMap pendingPotionSpawns = new Int2ObjectOpenHashMap<>(); public EntityTracker1_9(UserConnection connection) { super(connection, EntityTypes1_9.EntityType.PLAYER); @@ -45,6 +46,7 @@ public void removeEntity(int id) { vehicles.remove(id); offsets.remove(id); status.remove(id); + pendingPotionSpawns.remove(id); vehicles.forEach((vehicle, passengers) -> passengers.rem(id)); vehicles.int2ObjectEntrySet().removeIf(entry -> entry.getValue().isEmpty()); @@ -88,4 +90,12 @@ public Integer getVehicle(final int passenger) { public Int2IntMap getStatus() { return status; } + + public Int2ObjectMap getPendingPotionSpawns() { + return pendingPotionSpawns; + } + + /** Spawn packet values of a thrown potion, held until its item entity data supplies the 1.8 object data. */ + public record PotionSpawn(int x, int y, int z, byte pitch, byte yaw, short velocityX, short velocityY, short velocityZ) { + } }