Modifications of cloned packets sometimes appear for other clients
bo3tools opened this issue ยท 4 comments
This issue is a follow-up on my attempt to make items appear differently to different clients, described in this comment in issue #925.
I'm using the latest build from commit b871eb3. Unfortunately, fixing cloning the WrappedDataWatcher did not help with the issue where cloned packets sometimes appear on clients they are not intended for.
To quickly test how things appear for different players I've made a small client in Node.JS which you can get here.
Setup
- Compile the ExamplePlugin from the example below
- Run a 1.16 server with ProtocolLib build 476 and the compiled ExamplePlugin
- Set
online-mode
tofalse
in server.properties, start the server - Install Node.JS, clone bo3tools/test-mc-client
- Install test-mc-client dependencies by running
npm install .
in the cloned folder
Test the issue
- Join the server with your main account
- Join the server with bot Apple:
node . localhost:25565 Apple
- Join the server with bot Gold:
node . localhost:25565 Gold
- Teleport bots to you
- Drop 1 clock on the ground, notice bot messages in chat
- Drop another clock at the same spot, combining them into a stack
- Notice bot messages in chat and how the item appears to you
Result
The dropped item (and sometimes bots held item, try giving the clocks to them) appears incorrectly depending on the connection order (players' entity ids?).
Here's a minimal example plugin that hooks into ENTITY_METADATA and ENTITY_EQUIPMENT packets and changes the items depending on what username a player has:
import com.comphenix.protocol.*;
import com.comphenix.protocol.events.*;
import com.comphenix.protocol.wrappers.*;
import org.bukkit.*;
import org.bukkit.entity.*;
import org.bukkit.event.*;
import org.bukkit.inventory.*;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.List;
import java.util.stream.Collectors;
public class ExamplePlugin extends JavaPlugin implements Listener {
private static final Material DISGUISE_WHICH_ITEM = Material.CLOCK;
private static final String WHO_SEES_AS_APPLES = "Apple";
private static final String WHO_SEES_AS_GOLD = "Gold";
@Override
public void onEnable() {
ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
protocolManager.addPacketListener(new PacketAdapter(this, PacketType.Play.Server.ENTITY_METADATA, PacketType.Play.Server.ENTITY_EQUIPMENT) {
@Override
public void onPacketSending(PacketEvent event) {
PacketContainer packet = event.getPacket();
if (event.getPacketType() == PacketType.Play.Server.ENTITY_EQUIPMENT) {
LivingEntity visibleEntity = (LivingEntity) packet.getEntityModifier(event).read(0);
List<Pair<EnumWrappers.ItemSlot, ItemStack>> entityEquipment = packet.getSlotStackPairLists().read(0);
List<Pair<EnumWrappers.ItemSlot, ItemStack>> filteredEquipment = entityEquipment.stream()
.map(pair -> {
EnumWrappers.ItemSlot equippedSlot = pair.getFirst();
ItemStack currentItem = getCurrentEquipment(equippedSlot, visibleEntity);
ItemStack newItem = modifyItem(event, currentItem);
return new Pair<>(equippedSlot, newItem);
}).collect(Collectors.toList());
packet.getSlotStackPairLists().write(0, filteredEquipment);
}
else if (event.getPacketType() == PacketType.Play.Server.ENTITY_METADATA) {
Entity entity = packet.getEntityModifier(event).read(0);
if (entity.getType() == EntityType.DROPPED_ITEM) {
WrappedDataWatcher dataWatcher = new WrappedDataWatcher(packet.deepClone().getWatchableCollectionModifier().read(0));
ItemStack droppedItem = dataWatcher.getItemStack(7);
if (droppedItem != null) {
ItemStack newItem = modifyItem(event, droppedItem);
dataWatcher = dataWatcher.deepClone();
dataWatcher.setObject(7, newItem);
packet.getWatchableCollectionModifier().write(0, dataWatcher.getWatchableObjects());
}
}
}
event.setPacket(packet);
}
});
}
private ItemStack getCurrentEquipment(EnumWrappers.ItemSlot itemSlot, LivingEntity entity) {
EntityEquipment entityEquipment = entity.getEquipment();
assert entityEquipment != null;
switch (itemSlot) {
case MAINHAND: return entityEquipment.getItemInMainHand();
case OFFHAND: return entityEquipment.getItemInOffHand();
case HEAD: return entityEquipment.getHelmet();
case CHEST: return entityEquipment.getChestplate();
case LEGS: return entityEquipment.getLeggings();
case FEET: return entityEquipment.getBoots();
}
return null;
}
private ItemStack modifyItem(PacketEvent event, ItemStack itemStack) {
if (itemStack == null || itemStack.getType() != DISGUISE_WHICH_ITEM) {
return itemStack;
}
Player player = event.getPlayer();
ItemStack newItem = itemStack.clone();
if (player.getName().equals(WHO_SEES_AS_APPLES)) {
newItem.setType(Material.APPLE);
}
else if (player.getName().equals(WHO_SEES_AS_GOLD)) {
newItem.setType(Material.GOLDEN_APPLE);
}
return newItem;
}
}
Cloning the packet in both EntityEquipment and EntityMetadata blocks, getting an error about temporary players:
[ProtocolPlugin] Unhandled exception number 32 occured in onPacketSending(PacketEvent) for ProtocolPlugin
java.lang.UnsupportedOperationException: The method getWorld is not supported for temporary players.
at com.comphenix.protocol.injector.server.TemporaryPlayerFactory$1.intercept(TemporaryPlayerFactory.java:144) ~[?:?]
at com.comphenix.protocol.injector.server.TemporaryPlayer$$EnhancerByCGLIB$$d69760e3.getWorld(<generated>) ~[?:?]
at com.comphenix.protocol.events.PacketContainer.getEntityModifier(PacketContainer.java:409) ~[?:?]
at dev.weary.ProtocolPlugin$1.onPacketSending(ProtocolPlugin.java:31) ~[?:?]
[ProtocolPlugin] Unhandled exception occured in onPacketSending(PacketEvent) for ProtocolPlugin
java.lang.UnsupportedOperationException: The method getWorld is not supported for temporary players.
at com.comphenix.protocol.injector.server.TemporaryPlayerFactory$1.intercept(TemporaryPlayerFactory.java:144) ~[?:?]
at com.comphenix.protocol.injector.server.TemporaryPlayer$$EnhancerByCGLIB$$d69760e3.getWorld(<generated>) ~[?:?]
at com.comphenix.protocol.events.PacketContainer.getEntityModifier(PacketContainer.java:409) ~[?:?]
at dev.weary.ProtocolPlugin$1.onPacketSending(ProtocolPlugin.java:46) ~[?:?]
Line 31:
LivingEntity visibleEntity = (LivingEntity) packet.getEntityModifier(event).read(0);
Line 46:
Entity entity = packet.getEntityModifier(event).read(0);
Changing packet.getEntityModifier
on both lines to event.getPacket().getEntityModifier()
fixes the exception and it appears that the cloning is working correctly!
That's interesting. I wonder why the cloned packet is using temporary players. I'll look into that.
At least the cloning seems to be (mostly) working
Update: couldn't reproduce that temporary player error; accessing the entity modifier of the cloned packets seems to work fine
Try cloning the packet from the get-go and see if that helps:
if packet is entity equipment:
packet = packet.deepClone()
// do modifications
event.setPacket(packet)
Seems likely that the server is sending the same packet instance to everyone
That's interesting. I wonder why the cloned packet is using temporary players. I'll look into that.
At least the cloning seems to be (mostly) working
Update: couldn't reproduce that temporary player error; accessing the entity modifier of the cloned packets seems to work fine
Try the example code from the first comment, then after every if (event.getPacketType() == PacketType.Play.Server.PACKET_TYPE)
check put a packet = packet.deepClone();