ProtocolLib

3M Downloads

Modifications of cloned packets sometimes appear for other clients

bo3tools opened this issue ยท 4 comments

commented

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

  1. Compile the ExamplePlugin from the example below
  2. Run a 1.16 server with ProtocolLib build 476 and the compiled ExamplePlugin
  3. Set online-mode to false in server.properties, start the server
  4. Install Node.JS, clone bo3tools/test-mc-client
  5. Install test-mc-client dependencies by running npm install . in the cloned folder

Test the issue

  1. Join the server with your main account
  2. Join the server with bot Apple: node . localhost:25565 Apple
  3. Join the server with bot Gold: node . localhost:25565 Gold
  4. Teleport bots to you
  5. Drop 1 clock on the ground, notice bot messages in chat
  6. Drop another clock at the same spot, combining them into a stack
  7. 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;
    }
}
commented

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!

commented

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

commented

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

commented

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();