dmulloy2 / ProtocolLib

Provides read and write access to the Minecraft protocol with Bukkit.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[1.20.4+] Unable to set the value of CommonPlayerSpawnInfo's fields

ineanto opened this issue · comments

  • This issue is not solved in a development build

Describe the bug
Invoking WrappedRegistry#getDimensionRegistry throws a null pointer exception.

To Reproduce

  1. Download build 790
  2. Run the provided code snippet on server

Expected behavior
This should not happen.

Version Info
ProtocolLib dump

Additional context
I was trying to access the Holder<DimensionManager> variable in the CommonPlayerSpawnInfo class. The code looks like this:

final StructureModifier<World> worldHolder = commonPlayerSpawnInfoStructure.getHolders(
                    MinecraftReflection.getDimensionManager(),
                    Converters.holder(BukkitConverters.getDimensionConverter(),
                            WrappedRegistry.getDimensionRegistry())
            );

Doing this results in ProtocolLib throwing out this error: No registry found for class net.minecraft.world.level.dimension.DimensionType.
This is weird considering that there's a method called getDimensionRegistry inside the WrappedRegistry class...
Any clues of why? Thank you!

Edit: clarified some things

Bump, anyone?

@lukalt sorry to tag you but would you have any idea this could happen? I've not had any answer in more than a month and this is starting to bug me out...

The method you are using is not compatible with Minecraft 1.20.6. The Dimension Type now only holds some metadata for the world and there is no 1:1 mapping to an org.bukkit.World. ProtocolLib is designed to support a wide range of Minecraft versions. That's why the method still exists.

ProtocolLib does not provide a proper wrapper for the "new" Dimension Type. So it's currently not possible to access the data in a convenient way.

This hacky way might do the job though. It attempts to obtain the level identifier "minecraft:levelxyz" and check if a world with that tag exists. This is untested though.

private static boolean keysEquals(MinecraftKey wrappedKey, NamespacedKey bukkitKey) {
        // compare bukkit minecraft key and NMS wrapped minecraft key
        return wrappedKey.getPrefix().equals(bukkitKey.getNamespace()) && wrappedKey.getKey().equals(bukkitKey.getKey());
    }
    
    public static Optional<World> getWorld(PacketEvent event) throws Throwable {
        Class<?> commonPlayerInfoClazz = MinecraftReflection.getMinecraftClass("network.protocol.game.CommonPlayerSpawnInfo");
        // access CommonPlayerSpawnInfo, first field of that type in the Respawn / Login packets
        Object commonSpawnData = Accessors.getFieldAccessor(event.getPacket().getClass(), commonPlayerInfoClazz, true).getField().get(event.getPacket());
        // get the key of the level the player is joining. Second field in the object. First of type ResourceKey
        MinecraftKey key = MinecraftKey.fromHandle(Accessors.getFieldAccessor(commonPlayerInfoClazz, MinecraftReflection.getResourceKey(), true).get(commonSpawnData)); // wrap to ProtocolLib handle
        for (World world : Bukkit.getWorlds()) {
            if(keysEquals(key, world.getKey()))
            {
                return Optional.of(world);
            }
        }
        return Optional.empty();
    }

I am planning to update my fork of Packet Wrapper (see https://github.com/lukalt/PacketWrapper/) to the latest Minecraft version, which then would include such wrappers. That's, however, currently postponed due to lack of time :(

The method you are using is not compatible with Minecraft 1.20.6. The Dimension Type now only holds some metadata for the world and there is no 1:1 mapping to an org.bukkit.World. ProtocolLib is designed to support a wide range of Minecraft versions. That's why the method still exists.

ProtocolLib does not provide a proper wrapper for the "new" Dimension Type. So it's currently not possible to access the data in a convenient way.

This hacky way might do the job though. It attempts to obtain the level identifier "minecraft:levelxyz" and check if a world with that tag exists. This is untested though.

private static boolean keysEquals(MinecraftKey wrappedKey, NamespacedKey bukkitKey) {
        // compare bukkit minecraft key and NMS wrapped minecraft key
        return wrappedKey.getPrefix().equals(bukkitKey.getNamespace()) && wrappedKey.getKey().equals(bukkitKey.getKey());
    }
    
    public static Optional<World> getWorld(PacketEvent event) throws Throwable {
        Class<?> commonPlayerInfoClazz = MinecraftReflection.getMinecraftClass("network.protocol.game.CommonPlayerSpawnInfo");
        // access CommonPlayerSpawnInfo, first field of that type in the Respawn / Login packets
        Object commonSpawnData = Accessors.getFieldAccessor(event.getPacket().getClass(), commonPlayerInfoClazz, true).getField().get(event.getPacket());
        // get the key of the level the player is joining. Second field in the object. First of type ResourceKey
        MinecraftKey key = MinecraftKey.fromHandle(Accessors.getFieldAccessor(commonPlayerInfoClazz, MinecraftReflection.getResourceKey(), true).get(commonSpawnData)); // wrap to ProtocolLib handle
        for (World world : Bukkit.getWorlds()) {
            if(keysEquals(key, world.getKey()))
            {
                return Optional.of(world);
            }
        }
        return Optional.empty();
    }

I am planning to update my fork of Packet Wrapper (see https://github.com/lukalt/PacketWrapper/) to the latest Minecraft version, which then would include such wrappers. That's, however, currently postponed due to lack of time :(

I lack words to thank you for your enlightment! How do I keep track of changes to the protocol that are "invisible" ? I mean the variable has not changed since 1.20.2 but the underlying type has? Wth :(
In regards to it's usage with ProtocolLib, what would be the proper way of setting this variable now in the respawn packet?

Keeping track of these changes can be really though, in particular because Mojang started restructuring the contents of the packets. I usually decompile the current version of the server and compare it to the version it worked on before. That can be quite challenging tbh

To modify the dimension (i.e., the tag of the world the player is spawned into), you can try
Accessors.getFieldAccessor(commonPlayerInfoClazz, MinecraftReflection.getResourceKey(), true).set(commonSpawnData, minecraftKey.getHandle()); with minecraftKey being of type MinecraftKey (see https://github.com/dmulloy2/ProtocolLib/blob/master/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java)

Please note that changing this can cause unexpected behavior as the server will still assume that the player is in the previous world and will not provide chunks etc...

Keeping track of these changes can be really though, in particular because Mojang started restructuring the contents of the packets. I usually decompile the current version of the server and compare it to the version it worked on before. That can be quite challenging tbh

To modify the dimension (i.e., the tag of the world the player is spawned into), you can try Accessors.getFieldAccessor(commonPlayerInfoClazz, MinecraftReflection.getResourceKey(), true).set(commonSpawnData, minecraftKey.getHandle()); with minecraftKey being of type MinecraftKey (see https://github.com/dmulloy2/ProtocolLib/blob/master/src/main/java/com/comphenix/protocol/wrappers/MinecraftKey.java)

Please note that changing this can cause unexpected behavior as the server will still assume that the player is in the previous world and will not provide chunks etc...

That's the method I'm currently using though, guess I'm just bad at comparing packet lmao
Thank you for taking the time to explain all of this to me! Take care

Well, I truly tried everything. F this game
(This comment is not an answer to this issue. This is a mere update based on the many hours of researches/trial and error I went through to diagnose this issue that might help anyone coming here trying to diagnose this issue.)

So, I've dug around and found what might have caused the solution @lukalt provided above to fail, even after fixing the code provided (this was untested and needed some tweaks): CommonSpawnPlayerInfo (and ClientboundRespawnPacket), are Records.

Let me explain.

try {
    final Object spawnInfoStructureHandle = spawnInfoStructure.getHandle();
    final RecordComponent[] components = spawnInfoStructureHandle.getClass().getRecordComponents();

    final Field levelKeyField = spawnInfoStructureHandle.getClass().getDeclaredField(components[1].getAccessor().getName());
    levelKeyField.setAccessible(true);
    levelKeyField.set(spawnInfoStructureHandle, BukkitConverters.getWorldKeyConverter().getGeneric(Bukkit.getWorld("world")));
    } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException()
    }
}

This code successfully gets the ResourceKey<Level> field inside the CommonSpawnPlayerInfo record. It then tries to set a new value. Everything should be fine, you might say. Nuh-uh. This code will fail when trying to set the value.
Turns out you can't go and play with class fields as easily as with record components because those are marked final and are not modifiable.

Trying to bypass the final modifier using this code will fail:

Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(levelKeyField, levelKeyField.getModifiers() & ~Modifier.FINAL);

This behavior is by design (see here) which I have nothing against. So... here I'm am, stuck. I guess I'm done for now until I find a way around this limitation. Maybe create a whole new instance of the record by hand using reflection?

@dmulloy2 @lukalt , would you have any idea? Would I have to wait for ProtocolLib to update and provide a fix? Thank you!

Have you tried using an accessor for the field instead? It uses a hack to get around that issue

Have you tried using an accessor for the field instead? It uses a hack to get around that issue

What do you mean by that? Doing something like the following?

Accessors.getFieldAccessor(spawnInfoStructureHandle , MinecraftReflection.getResourceKey(), true)

IIRC this doesn't work and just throws an error :(