diff --git a/build.gradle.kts b/build.gradle.kts index f6bc1a01f..784371194 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { group = "net.dmulloy2" description = "Provides access to the Minecraft protocol" -val mcVersion = "26.1" +val mcVersion = "26.2" val isSnapshot = version.toString().endsWith("-SNAPSHOT") val isJitPack = System.getenv("JITPACK")?.equals("true", ignoreCase = true) ?: false val commitHash = System.getenv("COMMIT_SHA") ?: "" diff --git a/src/main/java/com/comphenix/protocol/PacketType.java b/src/main/java/com/comphenix/protocol/PacketType.java index 713491638..6b7ab5fa3 100644 --- a/src/main/java/com/comphenix/protocol/PacketType.java +++ b/src/main/java/com/comphenix/protocol/PacketType.java @@ -506,7 +506,7 @@ public static class Client extends PacketTypeEnum { public static final PacketType STRUCT = new PacketType(PROTOCOL, SENDER, 0x3B, "SetStructureBlock", "Struct"); public static final PacketType SET_TEST_BLOCK = new PacketType(PROTOCOL, SENDER, 0x3C, "SetTestBlock"); public static final PacketType UPDATE_SIGN = new PacketType(PROTOCOL, SENDER, 0x3D, "SignUpdate", "UpdateSign", "CPacketUpdateSign"); - public static final PacketType SPECTATE_ENTITY = new PacketType(PROTOCOL, SENDER, 0x3E, "SpectateEntity"); + public static final PacketType SPECTATE_ENTITY = new PacketType(PROTOCOL, SENDER, 0x3E, "SpectatorAction", "SpectateEntity"); public static final PacketType ARM_ANIMATION = new PacketType(PROTOCOL, SENDER, 0x3F, "Swing", "ArmAnimation", "CPacketAnimation"); public static final PacketType SPECTATE = new PacketType(PROTOCOL, SENDER, 0x40, "TeleportToEntity", "Spectate", "CPacketSpectate"); public static final PacketType TEST_INSTANCE_BLOCK_ACTION = new PacketType(PROTOCOL, SENDER, 0x41, "TestInstanceBlockAction"); diff --git a/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 662199634..650068702 100644 --- a/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -35,12 +35,12 @@ public class ProtocolLibrary { /** * The maximum version ProtocolLib has been tested with. */ - public static final String MAXIMUM_MINECRAFT_VERSION = "26.1"; + public static final String MAXIMUM_MINECRAFT_VERSION = "26.2"; /** - * The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (1.21.11) was released. + * The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (26.2) was released. */ - public static final String MINECRAFT_LAST_RELEASE_DATE = "2026-03-24"; + public static final String MINECRAFT_LAST_RELEASE_DATE = "2026-06-16"; private static Plugin plugin; private static ProtocolConfig config; diff --git a/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java b/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java index 2ac8b870d..9eade005f 100644 --- a/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java +++ b/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java @@ -412,8 +412,8 @@ public static Set getClientPacketTypes() { private static Class searchForPacket(List classNames) { for (String name : classNames) { try { - Class clazz = MinecraftReflection.getMinecraftClass(name); - if (MinecraftReflection.getPacketClass().isAssignableFrom(clazz) + Class clazz = resolvePacketClassName(name); + if (clazz != null && MinecraftReflection.getPacketClass().isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers())) { return clazz; } @@ -424,6 +424,22 @@ private static Class searchForPacket(List classNames) { return null; } + /** + * Resolve a single packet class name. Fully-qualified names (those already containing their + * package, e.g. {@code net.minecraft.network.protocol.game.XPacket}) are loaded directly, since + * {@link MinecraftReflection#getMinecraftClass(String)} would otherwise prepend the Minecraft + * package and fail to find them. Relative names continue to be resolved against the Minecraft + * package. + */ + private static Class resolvePacketClassName(String name) { + Optional> direct = MinecraftReflection.getOptionalClass(name); + if (direct.isPresent()) { + return direct.get(); + } + + return MinecraftReflection.getMinecraftClass(name); + } + /** * Retrieves the correct packet class from a given type. * diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftProtocolVersion.java b/src/main/java/com/comphenix/protocol/utility/MinecraftProtocolVersion.java index 46cce1b7a..a784db7d7 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftProtocolVersion.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftProtocolVersion.java @@ -100,6 +100,7 @@ private static NavigableMap createLookup() { map.put(new MinecraftVersion(1, 21, 11), 774); map.put(new MinecraftVersion(26, 1, 0), 775); + map.put(new MinecraftVersion(26, 2, 0), 776); return map; } diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 3486265fe..9c33fd2b3 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -1118,6 +1118,15 @@ public static Optional> getStyledFormatClass() { return getOptionalNMS("network.chat.numbers.StyledFormat"); } + /** + * Retrieve the NMS TeamColor class, added in 26.2. + * + * @return The TeamColor class. + */ + public static Optional> getTeamColorClass() { + return getOptionalNMS("world.scores.TeamColor"); + } + /** * Retrieve the Gson class used by Minecraft. * @@ -1480,6 +1489,18 @@ private static Class getClass(String className) { .orElseThrow(() -> new RuntimeException("Cannot find class " + className)); } + /** + * Attempt to load a class by its fully-qualified canonical name, without prepending the + * Minecraft package. Unlike {@link #getMinecraftClass(String)}, this can resolve names that + * already include their package (e.g. {@code net.minecraft.network.protocol.game.XPacket}). + * + * @param className - the fully-qualified class name. + * @return Optional that may contain the class. + */ + public static Optional> getOptionalClass(String className) { + return getClassSource().loadClass(className); + } + /** * Retrieve the class object of a specific CraftBukkit class. * @@ -1684,7 +1705,7 @@ public static Class getResourceKey() { } public static Class getEntityTypes() { - return getMinecraftClass("world.entity.EntityTypes", "world.entity.EntityType", "EntityTypes"); + return getMinecraftClass("world.entity.EntityType", "world.entity.EntityTypes", "EntityTypes"); } public static Class getParticleParam() { diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java b/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java index 7eb73cd0a..19e505f84 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftVersion.java @@ -36,6 +36,11 @@ * @author Kristian */ public final class MinecraftVersion implements Comparable, Serializable { + /** + * Version 26.2 - chaos cubed + */ + public static final MinecraftVersion v26_2 = new MinecraftVersion("26.2"); + /** * Version 26.1 - tiny takeover */ @@ -189,7 +194,7 @@ public final class MinecraftVersion implements Comparable, Ser /** * The latest release version of minecraft. */ - public static final MinecraftVersion LATEST = v26_1; + public static final MinecraftVersion LATEST = v26_2; // used when serializing private static final long serialVersionUID = -8695133558996459770L; diff --git a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index c0dfb42dd..9159e88d9 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -70,6 +70,7 @@ import com.google.common.collect.Lists; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.WorldType; @@ -873,6 +874,13 @@ public static EquivalentConverter getEntityTypeConverter() { return ignoreNull(new EquivalentConverter() { @Override public Object getGeneric(EntityType specific) { + if (MinecraftVersion.v26_2.atOrAbove()) { + // EntityType.byString was removed in 26.2, resolve through the registry instead + WrappedRegistry registry = WrappedRegistry.getRegistry(MinecraftReflection.getEntityTypes()); + NamespacedKey key = specific.getKey(); + return registry.get(new MinecraftKey(key.getNamespace(), key.getKey())); + } + if (entityTypeFromName == null) { Class entityTypesClass = MinecraftReflection.getEntityTypes(); entityTypeFromName = Accessors.getMethodAccessor( diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java index 2eb45bab4..58a4ad81d 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java @@ -3,6 +3,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Optional; + import com.comphenix.protocol.injector.StructureCache; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.utility.MinecraftReflection; @@ -90,12 +92,26 @@ public TeamCollisionRule getCollisionRule() { @NotNull public EnumWrappers.ChatFormatting getColor() { + if (MinecraftVersion.v26_2.atOrAbove()) { + Optional optional = modifier.>withType(Optional.class).read(0); + Object teamColor = optional != null ? optional.orElse(null) : null; + if (teamColor == null) { + return EnumWrappers.ChatFormatting.RESET; + } + + return EnumWrappers.ChatFormatting.valueOf(((Enum) teamColor).name()); + } + return modifier .withType(EnumWrappers.getChatFormattingClass(), EnumWrappers.getChatFormattingConverter()) .read(0); } public int getOptions() { + if (MinecraftVersion.v26_2.atOrAbove()) { + return (byte) modifier.withType(byte.class).read(0); + } + return (int) modifier.withType(int.class).read(0); } @@ -210,9 +226,27 @@ public WrappedTeamParameters build() { wrapped.modifier.withType(String.class).writeSafely(1, collisionRule.toString()); } - wrapped.modifier.withType(EnumWrappers.getChatFormattingClass()).write(0, EnumWrappers.getChatFormattingConverter().getGeneric(color)); - wrapped.modifier.withType(int.class).write(0, options); + if (MinecraftVersion.v26_2.atOrAbove()) { + wrapped.modifier.withType(Optional.class).write(0, Optional.ofNullable(toNmsTeamColor(color))); + wrapped.modifier.withType(byte.class).write(0, (byte) options); + } else { + wrapped.modifier.withType(EnumWrappers.getChatFormattingClass()).write(0, EnumWrappers.getChatFormattingConverter().getGeneric(color)); + wrapped.modifier.withType(int.class).write(0, options); + } return wrapped; } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Object toNmsTeamColor(EnumWrappers.ChatFormatting color) { + Class teamColorClass = MinecraftReflection.getTeamColorClass() + .orElseThrow(() -> new IllegalStateException("TeamColor class doesn't exist on this server version")); + + try { + return Enum.valueOf((Class) teamColorClass, color.name()); + } catch (IllegalArgumentException ex) { + // formatting-only values (e.g. RESET) don't map to a team color + return null; + } + } } } diff --git a/src/test/java/com/comphenix/protocol/BukkitInitialization.java b/src/test/java/com/comphenix/protocol/BukkitInitialization.java index 03aa9359f..981e82f28 100644 --- a/src/test/java/com/comphenix/protocol/BukkitInitialization.java +++ b/src/test/java/com/comphenix/protocol/BukkitInitialization.java @@ -79,6 +79,15 @@ public class BukkitInitialization { private boolean initialized; private boolean packaged; + private static ServerLevel mockServerLevel; + + /** + * @return the mocked NMS server level set up during initialization, usable as a Level for constructing test entities + */ + public static ServerLevel getMockServerLevel() { + return mockServerLevel; + } + private BukkitInitialization() { System.out.println("Created new BukkitInitialization on " + Thread.currentThread().getName()); } @@ -246,6 +255,7 @@ private void initialize() { CraftWorld world = mock(CraftWorld.class); when(world.getHandle()).thenReturn(nmsWorld); + mockServerLevel = nmsWorld; List worlds = Collections.singletonList(world); when(mockedServer.getWorlds()).thenReturn(worlds); diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedDataWatcherTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedDataWatcherTest.java index 5e2933054..8507e0870 100644 --- a/src/test/java/com/comphenix/protocol/wrappers/WrappedDataWatcherTest.java +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedDataWatcherTest.java @@ -46,7 +46,7 @@ public class WrappedDataWatcherTest { public static void prepare() { BukkitInitialization.initializeAll(); - ThrownEgg nmsEgg = new ThrownEgg(null, 0, 0, 0, net.minecraft.world.item.ItemStack.EMPTY); + ThrownEgg nmsEgg = new ThrownEgg(BukkitInitialization.getMockServerLevel(), 0, 0, 0, net.minecraft.world.item.ItemStack.EMPTY); mockEntity = new CraftEgg(null, nmsEgg); } diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java index 89e37d88e..1792ec042 100644 --- a/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java @@ -5,14 +5,16 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Optional; + import com.comphenix.protocol.BukkitInitialization; import com.comphenix.protocol.wrappers.EnumWrappers.TeamCollisionRule; import com.comphenix.protocol.wrappers.EnumWrappers.TeamVisibility; -import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; import net.minecraft.world.scores.Team; +import net.minecraft.world.scores.TeamColor; public class WrappedTeamParametersTest { @BeforeAll @@ -47,12 +49,12 @@ public void testTeamParameters() { assertEquals(1, wrapped.getOptions()); ClientboundSetPlayerTeamPacket.Parameters handle = (ClientboundSetPlayerTeamPacket.Parameters) wrapped.getHandle(); - assertEquals(handle.getDisplayName(), displayName); - assertEquals(handle.getPlayerPrefix(), prefix); - assertEquals(handle.getPlayerSuffix(), suffix); - assertEquals(handle.getNametagVisibility(), Team.Visibility.ALWAYS); - assertEquals(handle.getCollisionRule(), Team.CollisionRule.NEVER); - assertEquals(handle.getColor(), ChatFormatting.RED); - assertEquals(handle.getOptions(), 1); + assertEquals(handle.displayName(), displayName); + assertEquals(handle.playerPrefix(), prefix); + assertEquals(handle.playerSuffix(), suffix); + assertEquals(handle.nameTagVisibility(), Team.Visibility.ALWAYS); + assertEquals(handle.collisionRule(), Team.CollisionRule.NEVER); + assertEquals(handle.color(), Optional.of(TeamColor.RED)); + assertEquals(handle.options(), 1); } }