├── .assets ├── illustration.png └── time.jpg ├── .gitignore ├── LICENSE ├── api ├── build.gradle └── src │ └── main │ └── java │ └── by │ └── milansky │ └── protocol │ └── api │ ├── packet │ ├── Packet.java │ ├── handler │ │ ├── PacketHandleResult.java │ │ └── PacketHandler.java │ └── registry │ │ ├── ProtocolPacketRegistry.java │ │ └── ProtocolStateRegistry.java │ ├── state │ └── ProtocolState.java │ └── version │ ├── ProtocolVersion.java │ └── ProtocolVersionMapping.java ├── base ├── build.gradle └── src │ ├── main │ └── java │ │ └── by │ │ └── milansky │ │ └── protocol │ │ └── base │ │ ├── packet │ │ ├── handler │ │ │ ├── BaseEmptyPacketHandler.java │ │ │ ├── BaseMergedPacketHandler.java │ │ │ ├── BasePacketHandleResult.java │ │ │ └── annotation │ │ │ │ ├── AnnotationBasedHandler.java │ │ │ │ └── PacketProcessor.java │ │ └── registry │ │ │ ├── BasePacketRegistry.java │ │ │ ├── BaseStateRegistry.java │ │ │ └── BaseSuppliedPacketRegistry.java │ │ ├── throwable │ │ ├── UnsupportedMappingException.java │ │ └── UnsupportedPacketException.java │ │ └── version │ │ └── UnmodifiableVersionMapping.java │ └── test │ └── java │ └── by │ └── milansky │ └── protocol │ └── base │ └── version │ └── UnmodifiableVersionMappingTest.java ├── build.gradle ├── bukkit ├── build.gradle └── src │ └── main │ └── java │ └── by │ └── milansky │ └── protocol │ └── bukkit │ ├── ProtocolBukkit.java │ ├── event │ ├── ProtocolEvent.java │ └── ProtocolPlayerCreateEvent.java │ ├── handler │ └── ProtocolPlayerChannelHandler.java │ ├── injector │ ├── Injector.java │ └── standard │ │ └── ReflectionInjector.java │ ├── listener │ └── ProtocolPlayerListener.java │ └── player │ └── ProtocolPlayer.java ├── examples ├── build.gradle ├── bukkit-disguise │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── by │ │ └── milansky │ │ └── protocol │ │ └── example │ │ ├── DisguisePlugin.java │ │ ├── command │ │ └── DisguiseCommand.java │ │ ├── handler │ │ └── DisguisePacketHandler.java │ │ ├── listener │ │ └── DisguiseListener.java │ │ └── player │ │ ├── DisguisePlayer.java │ │ └── DisguisePlayerStorage.java └── bukkit-nametags │ ├── build.gradle │ └── src │ └── main │ └── java │ └── by │ └── milansky │ └── protocol │ └── example │ ├── NametagPlugin.java │ ├── handler │ └── NametagDebugHandler.java │ └── listener │ └── NametagListener.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md ├── settings.gradle └── vanilla-protocol ├── build.gradle └── src ├── main └── java │ └── by │ └── milansky │ └── protocol │ └── vanilla │ ├── channel │ ├── ProxiedInboundChannelHandler.java │ ├── ProxiedOutboundChannelHandler.java │ └── VanillaChannelInitializer.java │ ├── codec │ ├── VanillaInboundPacketHandler.java │ ├── VanillaOutboundPacketHandler.java │ ├── VanillaPacketEncoder.java │ └── package-info.java │ ├── handler │ └── VanillaStateTrackingHandler.java │ ├── property │ └── Property.java │ ├── registry │ └── VanillaStateRegistry.java │ ├── standard │ ├── ClientboundCompression.java │ ├── ClientboundLoginSuccess.java │ ├── ClientboundSpawnEntityLiving.java │ ├── ClientboundTeam.java │ ├── ClientboundUpdateHealth.java │ ├── ClientboundUpsertPlayerInfo.java │ ├── ServerboundHandshake.java │ ├── ServerboundLogin.java │ └── ServerboundTabcomplete.java │ ├── utility │ ├── ChannelUtility.java │ ├── ProtocolUtility.java │ └── TextColorUtility.java │ └── version │ └── VanillaProtocolVersion.java └── test └── java └── by └── milansky └── protocol └── vanilla └── registry └── VanillaStateRegistryTest.java /.assets/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmilansky/minecraft-protocol-java/c52703b7a98b89cdd66dda30cb3086f834d94351/.assets/illustration.png -------------------------------------------------------------------------------- /.assets/time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmilansky/minecraft-protocol-java/c52703b7a98b89cdd66dda30cb3086f834d94351/.assets/time.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 milansky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation libs.fastutil 3 | } -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/packet/Packet.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.packet; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import io.netty.buffer.ByteBuf; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | /** 8 | * @author milansky 9 | */ 10 | public interface Packet { 11 | void encode(@NotNull ByteBuf byteBuf, @NotNull ProtocolVersion version); 12 | 13 | void decode(@NotNull ByteBuf byteBuf, @NotNull ProtocolVersion version); 14 | } 15 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/packet/handler/PacketHandleResult.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.packet.handler; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * @author milansky 8 | */ 9 | public interface PacketHandleResult { 10 | @Nullable Packet replacement(); 11 | 12 | boolean cancelled(); 13 | 14 | void afterWrite(); 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/packet/handler/PacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.packet.handler; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import io.netty.channel.Channel; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | /** 8 | * @author milansky 9 | */ 10 | public interface PacketHandler { 11 | @NotNull PacketHandleResult handle(@NotNull Channel channel, @NotNull Packet packet); 12 | } 13 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/packet/registry/ProtocolPacketRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.packet.registry; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.api.version.ProtocolVersionMapping; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * @author milansky 11 | */ 12 | public interface ProtocolPacketRegistry { 13 | void register(@NotNull Class packetClass, @NotNull ProtocolVersionMapping mapping); 14 | 15 | @Nullable Class getPacketById(@NotNull ProtocolVersion version, int id); 16 | 17 | @Nullable ProtocolVersionMapping getMapping(final @NotNull Class packetClass); 18 | } 19 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/packet/registry/ProtocolStateRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.packet.registry; 2 | 3 | import by.milansky.protocol.api.state.ProtocolState; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * @author milansky 8 | */ 9 | public interface ProtocolStateRegistry { 10 | ProtocolPacketRegistry clientbound(@NotNull ProtocolState state); 11 | 12 | ProtocolPacketRegistry serverbound(@NotNull ProtocolState state); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/state/ProtocolState.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.state; 2 | 3 | /** 4 | * @author milansky 5 | */ 6 | public enum ProtocolState { 7 | HANDSHAKE, STATUS, LOGIN, CONFIGURATION, PLAY 8 | } 9 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/version/ProtocolVersion.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.version; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * @author milansky 8 | */ 9 | public interface ProtocolVersion { 10 | int protocol(); 11 | 12 | @Nullable ProtocolVersion next(); 13 | 14 | default boolean greater(@NotNull ProtocolVersion other) { 15 | return protocol() > other.protocol(); 16 | } 17 | 18 | default boolean greaterEqual(@NotNull ProtocolVersion other) { 19 | return protocol() >= other.protocol(); 20 | } 21 | 22 | default boolean lower(@NotNull ProtocolVersion other) { 23 | return protocol() < other.protocol(); 24 | } 25 | 26 | default boolean lowerEqual(@NotNull ProtocolVersion other) { 27 | return protocol() <= other.protocol(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/main/java/by/milansky/protocol/api/version/ProtocolVersionMapping.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.api.version; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Unmodifiable; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * @author milansky 10 | */ 11 | public interface ProtocolVersionMapping { 12 | int get(@NotNull ProtocolVersion version); 13 | 14 | @NotNull 15 | @Unmodifiable 16 | Map asVersionMap(); 17 | } 18 | -------------------------------------------------------------------------------- /base/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':api') 3 | implementation libs.fastutil 4 | } -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/handler/BaseEmptyPacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.handler; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 5 | import by.milansky.protocol.api.packet.handler.PacketHandler; 6 | import io.netty.channel.Channel; 7 | import lombok.NoArgsConstructor; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | /** 11 | * @author milansky 12 | */ 13 | @NoArgsConstructor(staticName = "create") 14 | public final class BaseEmptyPacketHandler implements PacketHandler { 15 | @Override 16 | public @NotNull PacketHandleResult handle(final @NotNull Channel channel, final @NotNull Packet packet) { 17 | return BasePacketHandleResult.ok(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/handler/BaseMergedPacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.handler; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 5 | import by.milansky.protocol.api.packet.handler.PacketHandler; 6 | import io.netty.channel.Channel; 7 | import lombok.AccessLevel; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.experimental.FieldDefaults; 10 | import lombok.val; 11 | import org.jetbrains.annotations.Contract; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Collection; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | 18 | /** 19 | * @author milansky 20 | */ 21 | @RequiredArgsConstructor(staticName = "create") 22 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 23 | public final class BaseMergedPacketHandler implements PacketHandler { 24 | Collection childHandlers; 25 | 26 | private static Runnable mergedRunnables(final Runnable... runnables) { 27 | return () -> { 28 | for (val runnable : runnables) { 29 | if (runnable == null) continue; 30 | 31 | runnable.run(); 32 | } 33 | }; 34 | } 35 | 36 | @Contract("_ -> new") 37 | public static BaseMergedPacketHandler create(final PacketHandler... childHandlers) { 38 | // TODO: optimize algorithm if one of the handlers is already a merged handler 39 | return create(new HashSet<>()).append(childHandlers); 40 | } 41 | 42 | @Override 43 | public @NotNull PacketHandleResult handle(final @NotNull Channel channel, final @NotNull Packet packet) { 44 | val result = BasePacketHandleResult.createEmpty(); 45 | 46 | for (val childHandler : childHandlers) { 47 | val childResult = childHandler.handle(channel, packet); 48 | 49 | if (childResult.replacement() != null) 50 | result.setReplacement(childResult.replacement()); 51 | 52 | if (childResult.cancelled()) 53 | result.setCancelled(true); 54 | 55 | result.setAfterWrite(mergedRunnables(result.getAfterWrite(), childResult::afterWrite)); 56 | } 57 | 58 | return result; 59 | } 60 | 61 | public @NotNull BaseMergedPacketHandler append(final PacketHandler... packetHandlers) { 62 | childHandlers.addAll(List.of(packetHandlers)); 63 | 64 | return this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/handler/BasePacketHandleResult.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.handler; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 5 | import lombok.*; 6 | import lombok.experimental.FieldDefaults; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * @author milansky 12 | */ 13 | @Setter 14 | @Getter 15 | @FieldDefaults(level = AccessLevel.PRIVATE) 16 | @AllArgsConstructor(staticName = "create") 17 | @NoArgsConstructor(staticName = "createEmpty") 18 | public final class BasePacketHandleResult implements PacketHandleResult { 19 | private static final Runnable EMPTY_NOT_NULL_RUNNABLE = () -> {}; 20 | 21 | Runnable afterWrite; 22 | @Nullable 23 | Packet replacement; 24 | boolean cancelled; 25 | 26 | public static @NotNull PacketHandleResult cancel() { 27 | return create(EMPTY_NOT_NULL_RUNNABLE, null, true); 28 | } 29 | 30 | public static @NotNull PacketHandleResult replace(final @NotNull Packet replacement) { 31 | return create(EMPTY_NOT_NULL_RUNNABLE, replacement, false); 32 | } 33 | 34 | public static @NotNull PacketHandleResult replace(final @NotNull Packet replacement, final @NotNull Runnable afterWrite) { 35 | return create(afterWrite, replacement, false); 36 | } 37 | 38 | public static @NotNull PacketHandleResult ok() { 39 | return create(EMPTY_NOT_NULL_RUNNABLE, null, false); 40 | } 41 | 42 | @Override 43 | public @Nullable Packet replacement() { 44 | return replacement; 45 | } 46 | 47 | @Override 48 | public boolean cancelled() { 49 | return cancelled; 50 | } 51 | 52 | @Override 53 | public void afterWrite() { 54 | afterWrite.run(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/handler/annotation/AnnotationBasedHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.handler.annotation; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 5 | import by.milansky.protocol.api.packet.handler.PacketHandler; 6 | import by.milansky.protocol.base.packet.handler.BaseMergedPacketHandler; 7 | import by.milansky.protocol.base.packet.handler.BasePacketHandleResult; 8 | import io.netty.channel.Channel; 9 | import lombok.AccessLevel; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.experimental.FieldDefaults; 12 | import lombok.extern.log4j.Log4j2; 13 | import lombok.val; 14 | import org.jetbrains.annotations.Contract; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.lang.reflect.InvocationTargetException; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author milansky 23 | */ 24 | @Log4j2 25 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 26 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 27 | public final class AnnotationBasedHandler implements PacketHandler { 28 | Map, PacketHandler> handlers; 29 | 30 | @Contract("_ -> new") 31 | public static PacketHandler create(final Object... handlers) { 32 | val annotationHandlers = new HashMap, PacketHandler>(); 33 | 34 | for (val handler : handlers) { 35 | val clazz = handler.getClass(); 36 | 37 | for (val declaredMethod : clazz.getDeclaredMethods()) { 38 | if (!declaredMethod.isAnnotationPresent(PacketProcessor.class)) continue; 39 | 40 | if (declaredMethod.getParameterCount() != 2) { 41 | throw new IllegalStateException("Method is marked as @PacketProcessor, but has too many parameters"); 42 | } 43 | 44 | val channelArgumentType = declaredMethod.getParameterTypes()[0]; 45 | val packetArgumentType = declaredMethod.getParameterTypes()[1]; 46 | 47 | if (channelArgumentType != Channel.class || 48 | !Packet.class.isAssignableFrom(packetArgumentType)) { 49 | throw new IllegalStateException("Method is marked as @PacketProcessor, but has no packet argument"); 50 | } 51 | 52 | if (!PacketHandleResult.class.isAssignableFrom(declaredMethod.getReturnType())) { 53 | throw new IllegalStateException("Method is marked as @PacketProcessor, but returns not result type"); 54 | } 55 | 56 | val processorHandler = new PacketHandler() { 57 | @Override 58 | public @NotNull PacketHandleResult handle(final @NotNull Channel channel, final @NotNull Packet packet) { 59 | if (packetArgumentType != Packet.class && packet.getClass() != packetArgumentType) 60 | return BasePacketHandleResult.ok(); 61 | 62 | declaredMethod.setAccessible(true); 63 | 64 | try { 65 | return (PacketHandleResult) declaredMethod.invoke(handler, channel, packet); 66 | } catch (final InvocationTargetException | IllegalAccessException throwable) { 67 | log.catching(throwable); 68 | } finally { 69 | declaredMethod.setAccessible(false); 70 | } 71 | 72 | return BasePacketHandleResult.ok(); 73 | } 74 | }; 75 | 76 | if (annotationHandlers.containsKey(clazz)) { 77 | annotationHandlers.put(packetArgumentType, BaseMergedPacketHandler.create(annotationHandlers.get(clazz), processorHandler)); 78 | continue; 79 | } 80 | 81 | annotationHandlers.put(packetArgumentType, processorHandler); 82 | } 83 | } 84 | 85 | return new AnnotationBasedHandler(annotationHandlers); 86 | } 87 | 88 | @Override 89 | public @NotNull PacketHandleResult handle(final @NotNull Channel channel, final @NotNull Packet packet) { 90 | val handler = handlers.get(packet.getClass()); 91 | 92 | if (handler == null) 93 | return BasePacketHandleResult.ok(); 94 | 95 | return handler.handle(channel, packet); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/handler/annotation/PacketProcessor.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.handler.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author milansky 10 | */ 11 | @Target(ElementType.METHOD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface PacketProcessor { 14 | } 15 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/registry/BasePacketRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.registry; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.registry.ProtocolPacketRegistry; 5 | import by.milansky.protocol.api.version.ProtocolVersion; 6 | import by.milansky.protocol.api.version.ProtocolVersionMapping; 7 | import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; 8 | import lombok.AccessLevel; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.FieldDefaults; 11 | import lombok.val; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | /** 19 | * @author milansky 20 | *

21 | * Looks lame, but this is roughly the only way to achieve constant time 22 | * without adding extra libraries (e.g., BidirectionalMap - de-facto the same thing) 23 | */ 24 | @NoArgsConstructor(staticName = "create") 25 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 26 | public final class BasePacketRegistry implements ProtocolPacketRegistry { 27 | Map, ProtocolVersionMapping> packetToMapping = new HashMap<>(); 28 | Map>> idToPacket = new HashMap<>(); 29 | 30 | public void register(final @NotNull Class packetClass, final @NotNull ProtocolVersionMapping mapping) { 31 | packetToMapping.put(packetClass, mapping); 32 | 33 | mapping.asVersionMap().forEach((version, id) -> { 34 | idToPacket.computeIfAbsent(version, v -> new Int2ObjectArrayMap<>()).put(id, packetClass); 35 | }); 36 | } 37 | 38 | public @Nullable ProtocolVersionMapping getMapping(final @NotNull Class packetClass) { 39 | return packetToMapping.get(packetClass); 40 | } 41 | 42 | public @Nullable Class getPacketById(final @NotNull ProtocolVersion version, final int id) { 43 | val versionMap = idToPacket.get(version); 44 | 45 | if (versionMap == null) return null; 46 | 47 | return versionMap.get(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/registry/BaseStateRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.registry; 2 | 3 | import by.milansky.protocol.api.packet.registry.ProtocolPacketRegistry; 4 | import by.milansky.protocol.api.packet.registry.ProtocolStateRegistry; 5 | import by.milansky.protocol.api.state.ProtocolState; 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.FieldDefaults; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.EnumMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * @author milansky 16 | */ 17 | @RequiredArgsConstructor 18 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 19 | public abstract class BaseStateRegistry implements ProtocolStateRegistry { 20 | Map clientbound, serverbound; 21 | 22 | public BaseStateRegistry() { 23 | this(new EnumMap<>(ProtocolState.class), new EnumMap<>(ProtocolState.class)); 24 | } 25 | 26 | @Override 27 | public final ProtocolPacketRegistry clientbound(final @NotNull ProtocolState state) { 28 | return clientbound.get(state); 29 | } 30 | 31 | @Override 32 | public final ProtocolPacketRegistry serverbound(final @NotNull ProtocolState state) { 33 | return serverbound.get(state); 34 | } 35 | 36 | protected final void registerClientboundState( 37 | final @NotNull ProtocolState state, 38 | final @NotNull ProtocolPacketRegistry registry 39 | ) { 40 | clientbound.put(state, registry); 41 | } 42 | 43 | protected final void registerServerboundState( 44 | final @NotNull ProtocolState state, 45 | final @NotNull ProtocolPacketRegistry registry 46 | ) { 47 | serverbound.put(state, registry); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/packet/registry/BaseSuppliedPacketRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.packet.registry; 2 | 3 | import by.milansky.protocol.api.packet.registry.ProtocolPacketRegistry; 4 | import lombok.AccessLevel; 5 | import lombok.experimental.Delegate; 6 | import lombok.experimental.FieldDefaults; 7 | import org.jetbrains.annotations.Contract; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.function.Consumer; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 16 | public final class BaseSuppliedPacketRegistry implements ProtocolPacketRegistry { 17 | @Delegate 18 | ProtocolPacketRegistry parentRegistry; 19 | 20 | private BaseSuppliedPacketRegistry( 21 | final @NotNull ProtocolPacketRegistry parentRegistry, 22 | final @NotNull Consumer registryConsumer 23 | ) { 24 | this.parentRegistry = parentRegistry; 25 | 26 | registryConsumer.accept(parentRegistry); 27 | } 28 | 29 | private BaseSuppliedPacketRegistry(final @NotNull Consumer registryConsumer) { 30 | this(BasePacketRegistry.create(), registryConsumer); 31 | } 32 | 33 | @Contract("_ -> new") 34 | public static ProtocolPacketRegistry create(final @NotNull Consumer registryConsumer) { 35 | return new BaseSuppliedPacketRegistry(BasePacketRegistry.create(), registryConsumer); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/throwable/UnsupportedMappingException.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.throwable; 2 | 3 | import lombok.experimental.StandardException; 4 | 5 | /** 6 | * @author milansky 7 | */ 8 | @StandardException 9 | public final class UnsupportedMappingException extends RuntimeException { 10 | } 11 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/throwable/UnsupportedPacketException.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.throwable; 2 | 3 | import lombok.experimental.StandardException; 4 | 5 | /** 6 | * @author milansky 7 | */ 8 | @StandardException 9 | public final class UnsupportedPacketException extends RuntimeException { 10 | } 11 | -------------------------------------------------------------------------------- /base/src/main/java/by/milansky/protocol/base/version/UnmodifiableVersionMapping.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.version; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import by.milansky.protocol.api.version.ProtocolVersionMapping; 5 | import by.milansky.protocol.base.throwable.UnsupportedMappingException; 6 | import by.milansky.protocol.base.throwable.UnsupportedPacketException; 7 | import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; 8 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 9 | import lombok.AccessLevel; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.experimental.FieldDefaults; 12 | import lombok.val; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | import org.jetbrains.annotations.Unmodifiable; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collections; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author milansky 23 | */ 24 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 25 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 26 | public final class UnmodifiableVersionMapping implements ProtocolVersionMapping { 27 | @Unmodifiable 28 | Map downstreamMap; 29 | @Nullable ProtocolVersion latestVersion; 30 | 31 | public static @NotNull UnmodifiableVersionMapping createMapping(@NotNull Object... any) { 32 | @Nullable ProtocolVersion latestVersion = null; 33 | 34 | if (any.length % 2 != 0) { 35 | latestVersion = (ProtocolVersion) any[any.length - 1]; 36 | 37 | any = Arrays.copyOfRange(any, 0, any.length - 1); 38 | } 39 | 40 | val downstreamMap = new Object2IntArrayMap(); 41 | 42 | populateDownstreamMap(any, downstreamMap); 43 | fillGapsInMapping(latestVersion, downstreamMap); 44 | 45 | return new UnmodifiableVersionMapping(Collections.unmodifiableMap(downstreamMap), latestVersion); 46 | } 47 | 48 | public static @NotNull UnmodifiableVersionMapping fromMap(final @NotNull Map downstreamMap) { 49 | val completeMap = new Object2IntArrayMap<>(downstreamMap); 50 | 51 | fillGapsInMapping(null, completeMap); 52 | 53 | return new UnmodifiableVersionMapping(Collections.unmodifiableMap(completeMap), null); 54 | } 55 | 56 | private static void populateDownstreamMap( 57 | final @NotNull Object[] any, 58 | final @NotNull Object2IntMap downstreamMap 59 | ) { 60 | for (int i = 0; i < any.length; i += 2) { 61 | validateMappingPair(any, i); 62 | 63 | val version = (ProtocolVersion) any[i]; 64 | val identifier = (int) any[i + 1]; 65 | 66 | downstreamMap.put(version, identifier); 67 | 68 | if (i + 2 < any.length) { 69 | val nextVersion = (ProtocolVersion) any[i + 2]; 70 | 71 | for (var protocol = version; protocol != null && protocol.lower(nextVersion); protocol = protocol.next()) { 72 | downstreamMap.put(protocol, identifier); 73 | } 74 | } 75 | } 76 | } 77 | 78 | private static void validateMappingPair(final @NotNull Object[] any, final int index) { 79 | if (!(any[index] instanceof ProtocolVersion)) { 80 | throw new UnsupportedMappingException("any[2n] must be an instance of ProtocolVersion"); 81 | } 82 | 83 | if (!(any[index + 1] instanceof Integer)) { 84 | throw new UnsupportedMappingException("any[2n + 1] must be an instance of Integer"); 85 | } 86 | } 87 | 88 | private static void fillGapsInMapping(ProtocolVersion latestVersion, Object2IntArrayMap downstreamMap) { 89 | ProtocolVersion highestVersion = null; 90 | int highestIdentifier = -1; 91 | 92 | for (val entry : downstreamMap.entrySet()) { 93 | val version = entry.getKey(); 94 | val identifier = (int) entry.getValue(); 95 | 96 | if (highestVersion == null || version.greater(highestVersion)) { 97 | highestVersion = version; 98 | highestIdentifier = identifier; 99 | } 100 | 101 | var nextVersion = version.next(); 102 | while (nextVersion != null && !downstreamMap.containsKey(nextVersion)) { 103 | downstreamMap.put(nextVersion, identifier); 104 | nextVersion = nextVersion.next(); 105 | } 106 | } 107 | 108 | if (latestVersion == null && highestVersion != null) { 109 | var current = highestVersion.next(); 110 | while (current != null) { 111 | downstreamMap.put(current, highestIdentifier); 112 | current = current.next(); 113 | } 114 | } 115 | } 116 | 117 | public int get(final @NotNull ProtocolVersion version) { 118 | if (latestVersion != null && version.greaterEqual(latestVersion)) { 119 | throw new UnsupportedPacketException(); 120 | } 121 | 122 | return downstreamMap.getOrDefault(version, -1); 123 | } 124 | 125 | public @NotNull @Unmodifiable Map asVersionMap() { 126 | return downstreamMap; 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return downstreamMap.toString(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /base/src/test/java/by/milansky/protocol/base/version/UnmodifiableVersionMappingTest.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.base.version; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import by.milansky.protocol.base.throwable.UnsupportedMappingException; 5 | import by.milansky.protocol.base.throwable.UnsupportedPacketException; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.FieldDefaults; 11 | import lombok.val; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import static org.junit.jupiter.api.Assertions.*; 15 | 16 | class UnmodifiableVersionMappingTest { 17 | @Test 18 | void createMapping_shouldCreateValidMapping() { 19 | val mappings = new Object[]{ 20 | TestProtocolVersion.MINECRAFT_1_7_2, 1, 21 | TestProtocolVersion.MINECRAFT_1_8, 2, 22 | TestProtocolVersion.MINECRAFT_1_9, 3, 23 | TestProtocolVersion.MINECRAFT_1_12, 4, 24 | TestProtocolVersion.MINECRAFT_1_20, 5, 25 | TestProtocolVersion.MINECRAFT_1_21_4 26 | }; 27 | 28 | val versionMapping = UnmodifiableVersionMapping.createMapping(mappings); 29 | 30 | assertNotNull(versionMapping); 31 | 32 | assertEquals(1, versionMapping.get(TestProtocolVersion.MINECRAFT_1_7_2)); 33 | assertEquals(2, versionMapping.get(TestProtocolVersion.MINECRAFT_1_8)); 34 | assertEquals(3, versionMapping.get(TestProtocolVersion.MINECRAFT_1_9)); 35 | assertEquals(4, versionMapping.get(TestProtocolVersion.MINECRAFT_1_12)); 36 | assertEquals(5, versionMapping.get(TestProtocolVersion.MINECRAFT_1_20)); 37 | 38 | assertThrows(UnsupportedPacketException.class, () -> versionMapping.get(TestProtocolVersion.MINECRAFT_1_21_4)); 39 | } 40 | 41 | @Test 42 | void createMapping_withIntermediateVersions_shouldFillAll() { 43 | val mappings = new Object[]{ 44 | TestProtocolVersion.MINECRAFT_1_7_2, 1, 45 | TestProtocolVersion.MINECRAFT_1_7_6, 2 46 | }; 47 | 48 | val versionMapping = UnmodifiableVersionMapping.createMapping(mappings); 49 | 50 | assertNotNull(versionMapping); 51 | 52 | for (val version : TestProtocolVersion.values()) { 53 | if (version.lower(TestProtocolVersion.MINECRAFT_1_7_6) && 54 | version.greaterEqual(TestProtocolVersion.MINECRAFT_1_7_2)) { 55 | assertEquals(1, versionMapping.get(version)); 56 | } 57 | } 58 | 59 | assertEquals(2, versionMapping.get(TestProtocolVersion.MINECRAFT_1_7_6)); 60 | } 61 | 62 | @Test 63 | void createMapping_invalidInput_shouldThrowException() { 64 | val invalidMappings = new Object[]{ 65 | "Invalid", 1, 66 | TestProtocolVersion.MINECRAFT_1_8, "Invalid" 67 | }; 68 | 69 | assertThrows(UnsupportedMappingException.class, 70 | () -> UnmodifiableVersionMapping.createMapping(invalidMappings)); 71 | } 72 | 73 | @Test 74 | void createMapping_emptyInput_shouldReturnEmptyMapping() { 75 | val versionMapping = UnmodifiableVersionMapping.createMapping(); 76 | 77 | assertNotNull(versionMapping); 78 | assertTrue(versionMapping.asVersionMap().isEmpty()); 79 | } 80 | 81 | @Test 82 | void asVersionMap_shouldReturnCorrectUnmodifiableMap() { 83 | val mappings = new Object[]{ 84 | TestProtocolVersion.MINECRAFT_1_8, 2 85 | }; 86 | 87 | val versionMapping = UnmodifiableVersionMapping.createMapping(mappings); 88 | val map = versionMapping.asVersionMap(); 89 | 90 | assertNotNull(map); 91 | 92 | assertEquals(2, map.get(TestProtocolVersion.MINECRAFT_1_8)); 93 | 94 | assertThrows(UnsupportedOperationException.class, () -> map.put(TestProtocolVersion.MINECRAFT_1_8, 1)); 95 | } 96 | 97 | @Getter 98 | @RequiredArgsConstructor 99 | @Accessors(chain = true, fluent = true) 100 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 101 | private enum TestProtocolVersion implements ProtocolVersion { 102 | MINECRAFT_1_7_2(5), 103 | MINECRAFT_1_7_6(6), 104 | MINECRAFT_1_8(47), 105 | MINECRAFT_1_9(107), 106 | MINECRAFT_1_12(335), 107 | MINECRAFT_1_20(763), 108 | MINECRAFT_1_21_4(1000); 109 | 110 | int protocol; 111 | 112 | @Override 113 | public ProtocolVersion next() { 114 | int ordinal = this.ordinal(); 115 | return ordinal < TestProtocolVersion.values().length - 1 116 | ? TestProtocolVersion.values()[ordinal + 1] 117 | : null; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | plugins { 4 | id 'com.gradleup.shadow' version '9.0.0-beta4' apply false 5 | } 6 | 7 | allprojects { 8 | group = 'by.milansky.protocol' 9 | version = '1.1.1' 10 | } 11 | 12 | configure(subprojects - project(':examples')) { 13 | apply plugin: 'java' 14 | apply plugin: 'maven-publish' 15 | 16 | repositories { 17 | mavenLocal() 18 | mavenCentral() 19 | 20 | maven { 21 | url = "https://repo.abelix.club/repository/public/" 22 | } 23 | } 24 | 25 | dependencies { 26 | compileOnly libs.lombok 27 | annotationProcessor libs.lombok 28 | 29 | testCompileOnly libs.lombok 30 | testAnnotationProcessor libs.lombok 31 | 32 | compileOnly libs.fastutil 33 | compileOnly libs.netty.all 34 | compileOnly libs.log4j.core 35 | compileOnly libs.jetbrains.annotations 36 | 37 | compileOnly libs.jackson.databind 38 | compileOnly libs.gson 39 | 40 | compileOnly libs.adventure.api 41 | compileOnly libs.adventure.nbt 42 | compileOnly libs.adventure.serializer.gson 43 | compileOnly libs.adventure.serializer.legacy 44 | compileOnly libs.adventure.serializer.gson.legacy 45 | 46 | testImplementation libs.netty.all 47 | testImplementation libs.log4j.core 48 | 49 | testImplementation libs.junit.api 50 | testRuntimeOnly libs.junit.engine 51 | } 52 | 53 | publishing { 54 | repositories { 55 | maven { 56 | name = "milansky-repo" 57 | url = uri("https://maven.milansky.ovh/releases") 58 | 59 | credentials { 60 | username = System.getenv("MILANSKY_REPO_USER") 61 | password = System.getenv("MILANSKY_REPO_TOKEN") 62 | } 63 | } 64 | } 65 | 66 | publications { 67 | gpr(MavenPublication) { 68 | from(components.java) 69 | 70 | pom { 71 | licenses { 72 | license { 73 | name = 'MIT License' 74 | url = 'https://github.com/rmilansky/minecraft-protocol-java/blob/master/LICENSE' 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | tasks { 83 | register('processSource', Copy) { 84 | from(sourceSets.main.java) 85 | into("$buildDir/src") 86 | 87 | inputs.property 'version', version 88 | filter(ReplaceTokens, tokens: [version: version], beginToken: '${', endToken: '}') 89 | } 90 | 91 | compileJava { 92 | sourceCompatibility = JavaVersion.VERSION_17 93 | targetCompatibility = JavaVersion.VERSION_17 94 | 95 | source = processSource.outputs 96 | } 97 | } 98 | 99 | test { 100 | useJUnitPlatform() 101 | } 102 | } -------------------------------------------------------------------------------- /bukkit/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.gradleup.shadow' 3 | } 4 | 5 | dependencies { 6 | compileOnly libs.spigot.api 7 | 8 | implementation project(":api") 9 | implementation project(":base") 10 | implementation project(":vanilla-protocol") 11 | 12 | implementation libs.adventure.api 13 | implementation libs.adventure.nbt 14 | implementation libs.adventure.serializer.gson 15 | implementation libs.adventure.serializer.legacy 16 | implementation libs.adventure.serializer.gson.legacy 17 | 18 | compileOnly libs.spigot.annotations 19 | annotationProcessor libs.spigot.annotations 20 | } 21 | 22 | tasks { 23 | build { 24 | dependsOn(tasks.shadowJar) 25 | } 26 | } -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/ProtocolBukkit.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import by.milansky.protocol.base.packet.handler.annotation.AnnotationBasedHandler; 5 | import by.milansky.protocol.bukkit.handler.ProtocolPlayerChannelHandler; 6 | import by.milansky.protocol.bukkit.injector.Injector; 7 | import by.milansky.protocol.bukkit.injector.standard.ReflectionInjector; 8 | import by.milansky.protocol.bukkit.listener.ProtocolPlayerListener; 9 | import by.milansky.protocol.vanilla.channel.VanillaChannelInitializer; 10 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 11 | import lombok.extern.log4j.Log4j2; 12 | import lombok.val; 13 | import org.bukkit.Bukkit; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | import org.bukkit.plugin.java.annotation.plugin.Plugin; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | /** 19 | * @author milansky 20 | */ 21 | @Log4j2 22 | @Plugin(name = "Protocol", version = "${version}") 23 | public class ProtocolBukkit extends JavaPlugin { 24 | private Injector injector; 25 | 26 | private static @NotNull ProtocolVersion defineServerVersion() { 27 | val bukkitVersion = Bukkit.getBukkitVersion(); 28 | val globalVersion = bukkitVersion.split("-")[0]; 29 | 30 | return VanillaProtocolVersion.versionByName(globalVersion); 31 | } 32 | 33 | @Override 34 | public void onEnable() { 35 | val playerChannelHandler = ProtocolPlayerChannelHandler.create(); 36 | val serverVersion = defineServerVersion(); 37 | 38 | if (serverVersion == VanillaProtocolVersion.UNKNOWN) { 39 | log.info("Failed to define the server version, shutting down!.."); 40 | Bukkit.shutdown(); 41 | return; 42 | } 43 | 44 | injector = ReflectionInjector.create(VanillaChannelInitializer.create(serverVersion, AnnotationBasedHandler.create(playerChannelHandler))); 45 | injector.inject(); 46 | 47 | getServer().getPluginManager().registerEvents(ProtocolPlayerListener.create(playerChannelHandler), this); 48 | } 49 | 50 | @Override 51 | public void onDisable() { 52 | injector.uninject(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/event/ProtocolEvent.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.event; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | 6 | /** 7 | * @author milansky 8 | */ 9 | public abstract class ProtocolEvent extends Event { 10 | private static final HandlerList handlers = new HandlerList(); 11 | 12 | public ProtocolEvent(final boolean async) { 13 | super(async); 14 | } 15 | 16 | public ProtocolEvent() { 17 | super(); 18 | } 19 | 20 | public static HandlerList getHandlerList() { 21 | return handlers; 22 | } 23 | 24 | public HandlerList getHandlers() { 25 | return handlers; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/event/ProtocolPlayerCreateEvent.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.event; 2 | 3 | import by.milansky.protocol.bukkit.player.ProtocolPlayer; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | import lombok.experimental.FieldDefaults; 9 | 10 | /** 11 | * @author milansky 12 | */ 13 | @Getter 14 | @Accessors(fluent = true) 15 | @RequiredArgsConstructor(staticName = "create") 16 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 17 | public final class ProtocolPlayerCreateEvent extends ProtocolEvent { 18 | ProtocolPlayer protocolPlayer; 19 | } 20 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/handler/ProtocolPlayerChannelHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.handler; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 4 | import by.milansky.protocol.base.packet.handler.BasePacketHandleResult; 5 | import by.milansky.protocol.base.packet.handler.annotation.PacketProcessor; 6 | import by.milansky.protocol.vanilla.standard.ClientboundLoginSuccess; 7 | import io.netty.channel.Channel; 8 | import lombok.AccessLevel; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.FieldDefaults; 13 | import lombok.extern.log4j.Log4j2; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Getter 23 | @Log4j2 24 | @Accessors(fluent = true) 25 | @NoArgsConstructor(staticName = "create") 26 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 27 | public final class ProtocolPlayerChannelHandler { 28 | Map receivedChannels = new HashMap<>(); 29 | 30 | @PacketProcessor 31 | public PacketHandleResult handle(final @NotNull Channel channel, final @NotNull ClientboundLoginSuccess login) { 32 | receivedChannels.put(login.name(), channel); 33 | 34 | return BasePacketHandleResult.ok(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/injector/Injector.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.injector; 2 | 3 | /** 4 | * @author milansky 5 | */ 6 | public interface Injector { 7 | void inject(); 8 | 9 | void uninject(); 10 | } 11 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/injector/standard/ReflectionInjector.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.injector.standard; 2 | 3 | import by.milansky.protocol.bukkit.injector.Injector; 4 | import by.milansky.protocol.vanilla.channel.VanillaChannelInitializer; 5 | import io.netty.channel.*; 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.SneakyThrows; 9 | import lombok.experimental.FieldDefaults; 10 | import lombok.extern.log4j.Log4j2; 11 | import lombok.val; 12 | import org.bukkit.Bukkit; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.lang.reflect.Field; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Log4j2 23 | @RequiredArgsConstructor(staticName = "create") 24 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 25 | public final class ReflectionInjector implements Injector { 26 | private static final Class MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, CRAFT_SERVER_CLASS, NETWORK_MANAGER_CLASS; 27 | private static final Field MINECRAFT_SERVER_FIELD, SERVER_CONNECTION_FIELD, NETWORK_MANAGER_CHANNEL_FIELD; 28 | 29 | static { 30 | try { 31 | val obcPrefix = Bukkit.getServer().getClass().getPackage().getName(); 32 | val nmsPrefix = obcPrefix.replace("org.bukkit.craftbukkit", "net.minecraft.server"); 33 | 34 | MINECRAFT_SERVER_CLASS = findClass(nmsPrefix + ".MinecraftServer", "net.minecraft.server.MinecraftServer"); 35 | SERVER_CONNECTION_CLASS = findClass(nmsPrefix + ".ServerConnection", "net.minecraft.server.network.ServerConnection"); 36 | NETWORK_MANAGER_CLASS = findClass(nmsPrefix + ".NetworkManager", "net.minecraft.network.NetworkManager"); 37 | CRAFT_SERVER_CLASS = findClass(obcPrefix + ".CraftServer"); 38 | 39 | MINECRAFT_SERVER_FIELD = hasFieldOfType(CRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS) 40 | ? fieldByType(CRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS) 41 | : fieldByType(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS); 42 | 43 | SERVER_CONNECTION_FIELD = fieldByType(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS); 44 | NETWORK_MANAGER_CHANNEL_FIELD = fieldByType(NETWORK_MANAGER_CLASS, Channel.class); 45 | } catch (final Throwable throwable) { 46 | log.catching(throwable); 47 | 48 | throw new RuntimeException(throwable); 49 | } 50 | } 51 | 52 | VanillaChannelInitializer channelInitializer; 53 | 54 | private static @NotNull Class findClass(final @NotNull String... names) throws ClassNotFoundException { 55 | for (val name : names) { 56 | try { 57 | return Class.forName(name); 58 | } catch (final ClassNotFoundException ignored) { 59 | } 60 | } 61 | 62 | throw new ClassNotFoundException(); 63 | } 64 | 65 | private static @NotNull Field fieldByType(final Class clazz, final Class fieldClass) { 66 | return Arrays.stream(clazz.getDeclaredFields()) 67 | .filter(field -> field.getType() == fieldClass) 68 | .findFirst() 69 | .orElseThrow(() -> new RuntimeException("Could not find field " + fieldClass + " in class " + clazz)); 70 | } 71 | 72 | private static boolean hasFieldOfType(final Class clazz, final Class fieldClass) { 73 | for (val declaredField : clazz.getDeclaredFields()) 74 | if (declaredField.getType() == fieldClass) return true; 75 | 76 | return false; 77 | } 78 | 79 | @Override 80 | @SneakyThrows 81 | public void inject() { 82 | MINECRAFT_SERVER_FIELD.setAccessible(true); 83 | SERVER_CONNECTION_FIELD.setAccessible(true); 84 | 85 | val minecraftServer = MINECRAFT_SERVER_FIELD.get(Bukkit.getServer()); 86 | val serverConnection = SERVER_CONNECTION_FIELD.get(minecraftServer); 87 | 88 | for (val declaredField : serverConnection.getClass().getDeclaredFields()) { 89 | if (declaredField.getType() != List.class) continue; 90 | 91 | declaredField.setAccessible(true); 92 | 93 | val list = (List) declaredField.get(serverConnection); 94 | 95 | for (val object : list) { 96 | val clazz = object.getClass(); 97 | 98 | if (clazz == NETWORK_MANAGER_CLASS) { 99 | val channel = (Channel) NETWORK_MANAGER_CHANNEL_FIELD.get(object); 100 | 101 | channelInitializer.initChannel(channel); 102 | } else if (ChannelFuture.class.isAssignableFrom(clazz)) { 103 | val future = (ChannelFuture) object; 104 | 105 | future.channel().pipeline().addFirst(ServerBootstrapChannelHandler.create(list, channelInitializer)); 106 | } 107 | } 108 | } 109 | } 110 | 111 | @Override 112 | public void uninject() { 113 | Bukkit.shutdown(); 114 | } 115 | 116 | @RequiredArgsConstructor(staticName = "create") 117 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 118 | private static final class ServerBootstrapChannelHandler extends ChannelInboundHandlerAdapter { 119 | List synchronizationList; 120 | VanillaChannelInitializer channelInitializer; 121 | 122 | @Override 123 | public void channelRead( 124 | final @NotNull ChannelHandlerContext ctx, 125 | final @NotNull Object msg 126 | ) { 127 | if (!(msg instanceof Channel clientChannel)) return; 128 | 129 | clientChannel.pipeline().addFirst(new ChannelInitializer<>() { 130 | @Override 131 | protected void initChannel(final @NotNull Channel channel) { 132 | synchronized (synchronizationList) { 133 | channel.eventLoop().execute(() -> channelInitializer.initChannel(clientChannel)); 134 | } 135 | } 136 | }); 137 | 138 | ctx.fireChannelRead(msg); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/listener/ProtocolPlayerListener.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.listener; 2 | 3 | import by.milansky.protocol.bukkit.event.ProtocolPlayerCreateEvent; 4 | import by.milansky.protocol.bukkit.handler.ProtocolPlayerChannelHandler; 5 | import by.milansky.protocol.bukkit.player.ProtocolPlayer; 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.FieldDefaults; 9 | import lombok.extern.log4j.Log4j2; 10 | import lombok.val; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.event.EventHandler; 13 | import org.bukkit.event.EventPriority; 14 | import org.bukkit.event.Listener; 15 | import org.bukkit.event.player.PlayerJoinEvent; 16 | 17 | /** 18 | * @author milansky 19 | */ 20 | @Log4j2 21 | @RequiredArgsConstructor(staticName = "create") 22 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 23 | public final class ProtocolPlayerListener implements Listener { 24 | ProtocolPlayerChannelHandler protocolPlayerChannelHandler; 25 | 26 | @EventHandler(priority = EventPriority.LOWEST) 27 | public void onPlayerJoin(final PlayerJoinEvent event) { 28 | val player = event.getPlayer(); 29 | val protocolPlayer = ProtocolPlayer.create(player, 30 | protocolPlayerChannelHandler.receivedChannels().remove(player.getName())); 31 | 32 | Bukkit.getServer().getPluginManager().callEvent(ProtocolPlayerCreateEvent.create(protocolPlayer)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bukkit/src/main/java/by/milansky/protocol/bukkit/player/ProtocolPlayer.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.bukkit.player; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.handler.PacketHandler; 5 | import by.milansky.protocol.base.packet.handler.BaseMergedPacketHandler; 6 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 7 | import io.netty.channel.Channel; 8 | import lombok.AccessLevel; 9 | import lombok.Getter; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.ExtensionMethod; 13 | import lombok.experimental.FieldDefaults; 14 | import lombok.extern.log4j.Log4j2; 15 | import lombok.val; 16 | import org.bukkit.entity.Player; 17 | import org.jetbrains.annotations.NotNull; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Log4j2 23 | @Getter 24 | @Accessors(fluent = true) 25 | @ExtensionMethod({ChannelUtility.class}) 26 | @RequiredArgsConstructor(staticName = "create") 27 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 28 | public final class ProtocolPlayer { 29 | Player nativePlayer; 30 | Channel channel; 31 | 32 | public void appendPacketHandler(final @NotNull PacketHandler packetHandler) { 33 | val savedPacketHandler = channel.packetHandler(); 34 | 35 | if (savedPacketHandler instanceof BaseMergedPacketHandler mergedHandler) { 36 | mergedHandler.append(packetHandler); 37 | return; 38 | } 39 | 40 | channel.updatePacketHandler(BaseMergedPacketHandler.create(savedPacketHandler, packetHandler)); 41 | } 42 | 43 | public void sendPacket(final @NotNull Packet packet) { 44 | channel.writeAndFlush(packet); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/build.gradle: -------------------------------------------------------------------------------- 1 | subprojects { 2 | dependencies { 3 | compileOnly libs.spigot.api 4 | 5 | compileOnly project(":bukkit") 6 | compileOnly project(":api") 7 | compileOnly project(":base") 8 | compileOnly project(":vanilla-protocol") 9 | 10 | compileOnly libs.spigot.annotations 11 | annotationProcessor libs.spigot.annotations 12 | } 13 | } -------------------------------------------------------------------------------- /examples/bukkit-disguise/build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmilansky/minecraft-protocol-java/c52703b7a98b89cdd66dda30cb3086f834d94351/examples/bukkit-disguise/build.gradle -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/DisguisePlugin.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example; 2 | 3 | import by.milansky.protocol.example.command.DisguiseCommand; 4 | import by.milansky.protocol.example.listener.DisguiseListener; 5 | import by.milansky.protocol.example.player.DisguisePlayerStorage; 6 | import lombok.val; 7 | import org.bukkit.plugin.java.JavaPlugin; 8 | import org.bukkit.plugin.java.annotation.command.Command; 9 | import org.bukkit.plugin.java.annotation.command.Commands; 10 | import org.bukkit.plugin.java.annotation.plugin.Description; 11 | import org.bukkit.plugin.java.annotation.plugin.Plugin; 12 | 13 | /** 14 | * @author milansky 15 | */ 16 | @Commands({ 17 | @Command(name = "disguise") 18 | }) 19 | @Plugin(name = "Disguise", version = "${version}") 20 | @Description("An example plugin that shows how to use minecraft protocol library") 21 | public final class DisguisePlugin extends JavaPlugin { 22 | @Override 23 | public void onEnable() { 24 | val disguisePlayerStorage = DisguisePlayerStorage.create(); 25 | 26 | getServer().getPluginManager().registerEvents(DisguiseListener.create(disguisePlayerStorage), this); 27 | getServer().getPluginCommand("disguise").setExecutor(DisguiseCommand.create(disguisePlayerStorage)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/command/DisguiseCommand.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.command; 2 | 3 | import by.milansky.protocol.example.player.DisguisePlayerStorage; 4 | import lombok.AccessLevel; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.experimental.FieldDefaults; 7 | import lombok.val; 8 | import org.bukkit.command.Command; 9 | import org.bukkit.command.CommandExecutor; 10 | import org.bukkit.command.CommandSender; 11 | import org.bukkit.entity.Player; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | /** 15 | * @author milansky 16 | */ 17 | @RequiredArgsConstructor(staticName = "create") 18 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 19 | public final class DisguiseCommand implements CommandExecutor { 20 | DisguisePlayerStorage storage; 21 | 22 | @Override 23 | public boolean onCommand( 24 | final @NotNull CommandSender commandSender, 25 | final @NotNull Command command, 26 | final @NotNull String label, 27 | final @NotNull String @NotNull [] arguments 28 | ) { 29 | if (!(commandSender instanceof Player player)) return false; 30 | 31 | val disguisePlayer = storage.disguisePlayer(player); 32 | 33 | if (arguments.length != 1) { 34 | player.sendMessage("/disguise "); 35 | return true; 36 | } 37 | 38 | val argument = arguments[0]; 39 | 40 | if (argument.equalsIgnoreCase("reset")) { 41 | disguisePlayer.updateFakeName(null); 42 | return true; 43 | } 44 | 45 | disguisePlayer.updateFakeName(argument); 46 | 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/handler/DisguisePacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.handler; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 4 | import by.milansky.protocol.base.packet.handler.BasePacketHandleResult; 5 | import by.milansky.protocol.base.packet.handler.annotation.PacketProcessor; 6 | import by.milansky.protocol.example.player.DisguisePlayerStorage; 7 | import by.milansky.protocol.vanilla.standard.ClientboundTeam; 8 | import by.milansky.protocol.vanilla.standard.ClientboundUpsertPlayerInfo; 9 | import io.netty.channel.Channel; 10 | import lombok.AccessLevel; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.experimental.FieldDefaults; 13 | import lombok.extern.log4j.Log4j2; 14 | import lombok.val; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | /** 18 | * @author milansky 19 | */ 20 | @Log4j2 21 | @RequiredArgsConstructor(staticName = "create") 22 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 23 | public final class DisguisePacketHandler { 24 | DisguisePlayerStorage storage; 25 | 26 | @PacketProcessor 27 | public @NotNull PacketHandleResult handle(final Channel ignored, final ClientboundUpsertPlayerInfo playerInfo) { 28 | for (val item : playerInfo.items()) { 29 | val player = storage.disguisePlayer(item.username()); 30 | 31 | player.fakeNameOptional().ifPresent(item::username); 32 | } 33 | 34 | return BasePacketHandleResult.replace(playerInfo); 35 | } 36 | 37 | @PacketProcessor 38 | public @NotNull PacketHandleResult handle(final Channel ignored, final ClientboundTeam team) { 39 | val players = team.players(); 40 | 41 | if (players == null) return BasePacketHandleResult.ok(); 42 | 43 | for (int i = 0; i < players.length; i++) { 44 | val teamPlayerName = players[i]; 45 | val disguisePlayer = storage.disguisePlayer(teamPlayerName); 46 | 47 | players[i] = disguisePlayer.fakeNameOptional().orElse(teamPlayerName); 48 | } 49 | 50 | return BasePacketHandleResult.replace(team); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/listener/DisguiseListener.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.listener; 2 | 3 | import by.milansky.protocol.base.packet.handler.annotation.AnnotationBasedHandler; 4 | import by.milansky.protocol.bukkit.event.ProtocolPlayerCreateEvent; 5 | import by.milansky.protocol.example.handler.DisguisePacketHandler; 6 | import by.milansky.protocol.example.player.DisguisePlayerStorage; 7 | import lombok.AccessLevel; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.experimental.FieldDefaults; 10 | import lombok.extern.log4j.Log4j2; 11 | import lombok.val; 12 | import org.bukkit.event.EventHandler; 13 | import org.bukkit.event.EventPriority; 14 | import org.bukkit.event.Listener; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Log4j2 20 | @RequiredArgsConstructor(staticName = "create") 21 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 22 | public final class DisguiseListener implements Listener { 23 | DisguisePlayerStorage storage; 24 | 25 | @EventHandler(priority = EventPriority.LOWEST) 26 | public void onPlayerCreate(final ProtocolPlayerCreateEvent event) { 27 | val protocolPlayer = event.protocolPlayer(); 28 | 29 | protocolPlayer.appendPacketHandler(AnnotationBasedHandler.create(DisguisePacketHandler.create(storage))); 30 | 31 | storage.disguisePlayer(protocolPlayer.nativePlayer()).protocolPlayer(protocolPlayer); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/player/DisguisePlayer.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.player; 2 | 3 | import by.milansky.protocol.bukkit.player.ProtocolPlayer; 4 | import by.milansky.protocol.vanilla.property.Property; 5 | import by.milansky.protocol.vanilla.standard.ClientboundUpsertPlayerInfo; 6 | import lombok.*; 7 | import lombok.experimental.Accessors; 8 | import lombok.experimental.FieldDefaults; 9 | import lombok.experimental.NonFinal; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.entity.Player; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | import java.util.Optional; 16 | 17 | /** 18 | * @author milansky 19 | */ 20 | @Getter 21 | @Accessors(fluent = true) 22 | @RequiredArgsConstructor(staticName = "create") 23 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 24 | public final class DisguisePlayer { 25 | String name; 26 | @NonFinal 27 | String fakeName; 28 | @Setter 29 | @NonFinal 30 | ProtocolPlayer protocolPlayer; 31 | 32 | public @Nullable Player bukkitHandle() { 33 | return Bukkit.getPlayer(name); 34 | } 35 | 36 | public @NotNull Optional fakeNameOptional() { 37 | return Optional.ofNullable(fakeName); 38 | } 39 | 40 | public void updateFakeName(final String fakeName) { 41 | if (this.fakeName != null && this.fakeName.equals(fakeName)) return; 42 | 43 | val oldName = this.fakeName == null ? name : this.fakeName; 44 | val name = fakeName == null ? this.name : fakeName; 45 | 46 | this.fakeName = fakeName; 47 | 48 | protocolPlayer.sendPacket(new ClientboundUpsertPlayerInfo( 49 | ClientboundUpsertPlayerInfo.Action.REMOVE_PLAYER, 50 | new ClientboundUpsertPlayerInfo.Item[]{ 51 | new ClientboundUpsertPlayerInfo.Item( 52 | bukkitHandle().getUniqueId(), 53 | 54 | oldName, null, null, 55 | null, null, null, 56 | null, null 57 | ) 58 | } 59 | )); 60 | 61 | protocolPlayer.sendPacket(new ClientboundUpsertPlayerInfo( 62 | ClientboundUpsertPlayerInfo.Action.ADD_PLAYER, 63 | new ClientboundUpsertPlayerInfo.Item[]{ 64 | new ClientboundUpsertPlayerInfo.Item( 65 | bukkitHandle().getUniqueId(), 66 | 67 | name, new Property[0], null, 68 | 0, 0, null, 69 | null, null 70 | ) 71 | } 72 | )); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/bukkit-disguise/src/main/java/by/milansky/protocol/example/player/DisguisePlayerStorage.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.player; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import org.bukkit.entity.Player; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @RequiredArgsConstructor(staticName = "create") 16 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 17 | public final class DisguisePlayerStorage { 18 | Map disguisePlayers = new HashMap<>(); 19 | 20 | public @NotNull DisguisePlayer disguisePlayer(final @NotNull String originalName) { 21 | return disguisePlayers.computeIfAbsent(originalName, DisguisePlayer::create); 22 | } 23 | 24 | public @NotNull DisguisePlayer disguisePlayer(final @NotNull Player player) { 25 | return disguisePlayer(player.getName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/bukkit-nametags/build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmilansky/minecraft-protocol-java/c52703b7a98b89cdd66dda30cb3086f834d94351/examples/bukkit-nametags/build.gradle -------------------------------------------------------------------------------- /examples/bukkit-nametags/src/main/java/by/milansky/protocol/example/NametagPlugin.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example; 2 | 3 | import by.milansky.protocol.example.listener.NametagListener; 4 | import org.bukkit.plugin.java.JavaPlugin; 5 | import org.bukkit.plugin.java.annotation.plugin.Description; 6 | import org.bukkit.plugin.java.annotation.plugin.Plugin; 7 | 8 | /** 9 | * @author milansky 10 | */ 11 | @Plugin(name = "Nametags", version = "${version}") 12 | @Description("An example plugin that shows how to use minecraft protocol library") 13 | public final class NametagPlugin extends JavaPlugin { 14 | @Override 15 | public void onEnable() { 16 | getServer().getPluginManager().registerEvents(NametagListener.create(), this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/bukkit-nametags/src/main/java/by/milansky/protocol/example/handler/NametagDebugHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.handler; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 4 | import by.milansky.protocol.base.packet.handler.BasePacketHandleResult; 5 | import by.milansky.protocol.base.packet.handler.annotation.PacketProcessor; 6 | import by.milansky.protocol.vanilla.standard.ServerboundTabcomplete; 7 | import io.netty.channel.Channel; 8 | import lombok.NoArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @Log4j2 16 | @NoArgsConstructor(staticName = "create") 17 | public final class NametagDebugHandler { 18 | @PacketProcessor 19 | public @NotNull PacketHandleResult handle(final Channel channel, final ServerboundTabcomplete tabcomplete) { 20 | log.info("Inbound tabcomplete packet: {}", tabcomplete); 21 | 22 | return BasePacketHandleResult.cancel(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/bukkit-nametags/src/main/java/by/milansky/protocol/example/listener/NametagListener.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.example.listener; 2 | 3 | import by.milansky.protocol.base.packet.handler.annotation.AnnotationBasedHandler; 4 | import by.milansky.protocol.bukkit.event.ProtocolPlayerCreateEvent; 5 | import by.milansky.protocol.example.handler.NametagDebugHandler; 6 | import by.milansky.protocol.vanilla.standard.ClientboundTeam; 7 | import lombok.NoArgsConstructor; 8 | import lombok.extern.log4j.Log4j2; 9 | import lombok.val; 10 | import net.kyori.adventure.text.Component; 11 | import net.kyori.adventure.text.format.NamedTextColor; 12 | import net.kyori.adventure.text.format.TextDecoration; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.Listener; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Log4j2 20 | @NoArgsConstructor(staticName = "create") 21 | public final class NametagListener implements Listener { 22 | @EventHandler 23 | public void onPlayerCreate(final ProtocolPlayerCreateEvent event) { 24 | val protocolPlayer = event.protocolPlayer(); 25 | 26 | protocolPlayer.appendPacketHandler(AnnotationBasedHandler.create(NametagDebugHandler.create())); 27 | 28 | protocolPlayer.sendPacket(new ClientboundTeam( 29 | "test_team", 30 | ClientboundTeam.TeamMode.CREATE, 31 | Component.empty(), 32 | Component.text("GOOD ", NamedTextColor.AQUA, TextDecoration.BOLD), 33 | Component.text(" JOB", NamedTextColor.GREEN, TextDecoration.BOLD), 34 | ClientboundTeam.NameTagVisibility.ALWAYS, 35 | ClientboundTeam.CollisionRule.NEVER, 36 | NamedTextColor.WHITE, 37 | (byte) 0, new String[]{protocolPlayer.nativePlayer().getName()} 38 | )); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmilansky/minecraft-protocol-java/c52703b7a98b89cdd66dda30cb3086f834d94351/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Dec 20 18:37:29 MSK 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | # Introduction 10 | 11 | Minecraft Protocol is a project I developed because all existing libraries for packet handling are either outdated or 12 | insufficiently lightweight for use in my projects (also due to a complete rewrite of the [Abelix](https://abelix.team) 13 | codebase). 14 | 15 | The project was written in a single attempt by one person, so there might be bugs or less-than-perfect code. I would 16 | greatly appreciate any pull requests or issues. 17 | 18 | 19 | 20 | # Project Overview 21 | 22 | ## Purpose 23 | 24 | This project is designed for the most convenient and integrable development of systems that require the use of Minecraft 25 | protocol packets (e.g., fake entities, nametags for servers, proxy systems). 26 | 27 | ## Key Objectives 28 | 29 | * Simplify packet handling and monitoring as much as possible. 30 | * Ensure integration with all modern server cores and standalone applications. 31 | 32 | # 33 | 34 | # Usage Guide 35 | 36 | ## Maven / Gradle library adding 37 | 38 | 1. Add a repository: 39 | 40 | Maven: 41 | 42 | ```xml 43 | 44 | 45 | milansky-repo 46 | https://maven.milansky.ovh/releases 47 | 48 | 49 | ``` 50 | 51 | Gradle: 52 | 53 | ```groovy 54 | repositories { 55 | maven { 56 | url = "https://maven.milansky.ovh/releases" 57 | } 58 | } 59 | ``` 60 | 61 | 2. Add dependencies 62 | 63 | Maven: 64 | 65 | ```xml 66 | 67 | 1.1.1 68 | 69 | 70 | 71 | 72 | by.milansky.protocol 73 | api 74 | ${protocol.version} 75 | 76 | 77 | by.milansky.protocol 78 | base 79 | ${protocol.version} 80 | 81 | 82 | by.milansky.protocol 83 | vanilla-protocol 84 | ${protocol.version} 85 | 86 | 87 | 88 | 89 | by.milansky.protocol 90 | bukkit 91 | ${protocol.version} 92 | 93 | 94 | ``` 95 | 96 | Gradle: 97 | 98 | ```groovy 99 | dependencies { 100 | // It's better to use gradle's dependencyResolutionManagement 101 | def protocolVersion = '1.1.1' 102 | 103 | compileOnly "by.milansky.protocol:api:${protocolVersion}" 104 | compileOnly "by.milansky.protocol:base:${protocolVersion}" 105 | compileOnly "by.milansky.protocol:vanilla-protocol:${protocolVersion}" 106 | 107 | // Add bukkit if you need it 108 | compileOnly "by.milansky.protocol:bukkit:${protocolVersion}" 109 | } 110 | ``` 111 | 112 | ## Java API usage 113 | 114 | More detailed usage examples can be found in the [examples](examples) directory. However, to briefly explain, here's how 115 | you can listen to all `ClientboundTeam` packets: 116 | 117 | 1. Create a handler to process packets: 118 | 119 | ```java 120 | @Log4j2 121 | public final class ClientboundTeamHandler { 122 | @PacketProcessor 123 | public @NotNull PacketHandleResult handle(final Channel channel, final ClientboundTeam team) { 124 | // Log that the server is attempting to send a packet 125 | log.info("Outbound team packet: {}", team); 126 | 127 | if (team.containsPlayer("milanskyy")) { 128 | // Prevent the packet from being sent 129 | return BasePacketHandleResult.cancel(); 130 | } 131 | 132 | // Everything is fine; allow the packet to be sent 133 | return BasePacketHandleResult.ok(); 134 | } 135 | } 136 | ``` 137 | 138 | 2. Add the handler on player join: 139 | 140 | ```java 141 | public final class NametagListener implements Listener { 142 | @EventHandler 143 | public void onCreate(final ProtocolPlayerCreateEvent event) { 144 | val protocolPlayer = event.protocolPlayer(); 145 | 146 | // Since this handler is annotation-based, wrap it in an AnnotationBasedHandler 147 | // and add it to the player 148 | protocolPlayer.appendPacketHandler(AnnotationBasedHandler.create(ClientboundTeamHandler.create())); 149 | } 150 | } 151 | ``` 152 | 153 | # Credits 154 | 155 | Special thanks for ideas and inspiration to these projects: 156 | 157 | * [Velocity](https://github.com/PaperMC/Velocity), [BungeeCord](https://github.com/SpigotMC/BungeeCord) - for some ideas 158 | and packet structures. 159 | * [BridgeNet](https://github.com/MikhailSterkhov/bridgenet) - for the idea of this beautiful readme -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'minecraft-protocol' 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | libs { 6 | version("lombok", "1.18.36") 7 | version("netty", "4.1.72.Final") 8 | version("annotations", "23.0.0") 9 | version("fastutil", "8.5.6") 10 | version("junit", "5.8.2") 11 | version("spigot", "1.8.8-R0.1-SNAPSHOT") 12 | version("spigot-annotations", "1.2.3-SNAPSHOT") 13 | version("mockbukkit", "3.133.2") 14 | version("log4j", "2.17.0") 15 | version("jackson", "2.18.2") 16 | version("adventure", "4.18.0") 17 | version("adventure-facet", "4.3.4") 18 | version("gson", "2.10.1") 19 | 20 | library("lombok", "org.projectlombok", "lombok").versionRef("lombok") 21 | 22 | library("netty-all", "io.netty", "netty-all").versionRef("netty") 23 | library("netty-handler", "io.netty", "netty-handler").versionRef("netty") 24 | library("netty-codec", "io.netty", "netty-codec").versionRef("netty") 25 | library("netty-codec-http", "io.netty", "netty-codec-http").versionRef("netty") 26 | library("netty-epoll", "io.netty", "netty-transport-native-epoll").versionRef("netty") 27 | 28 | library("log4j-core", "org.apache.logging.log4j", "log4j-core").versionRef("log4j") 29 | library("log4j-iostreams", "org.apache.logging.log4j", "log4j-iostreams").versionRef("log4j") 30 | 31 | library("jackson-databind", "com.fasterxml.jackson.core", "jackson-databind").versionRef("jackson") 32 | 33 | library("jetbrains-annotations", "org.jetbrains", "annotations").versionRef("annotations") 34 | library("fastutil", "it.unimi.dsi", "fastutil-core").versionRef("fastutil") 35 | 36 | library("spigot-api", "org.spigotmc", "spigot-api").versionRef("spigot") 37 | library("spigot-all", "org.spigotmc", "spigot").versionRef("spigot") 38 | library("spigot-annotations", "org.spigotmc", "plugin-annotations").versionRef("spigot-annotations") 39 | 40 | library("adventure-api", "net.kyori", "adventure-api").versionRef("adventure") 41 | library("adventure-bom", "net.kyori", "adventure-bom").versionRef("adventure") 42 | library("adventure-nbt", "net.kyori", "adventure-nbt").versionRef("adventure") 43 | library("adventure-facet", "net.kyori", "adventure-platform-facet").versionRef("adventure-facet") 44 | library("adventure-serializer-gson", "net.kyori", "adventure-text-serializer-gson").versionRef("adventure") 45 | library("adventure-serializer-gson-legacy", "net.kyori", "adventure-text-serializer-json-legacy-impl").versionRef("adventure") 46 | library("adventure-serializer-legacy", "net.kyori", "adventure-text-serializer-legacy").versionRef("adventure") 47 | 48 | library("gson", "com.google.code.gson", "gson").versionRef("gson") 49 | 50 | library("junit-engine", "org.junit.jupiter", "junit-jupiter-engine").versionRef("junit") 51 | library("junit-api", "org.junit.jupiter", "junit-jupiter-api").versionRef("junit") 52 | 53 | library("mockbukkit", "com.github.seeseemelk", "MockBukkit-v1.21").versionRef("mockbukkit") 54 | } 55 | } 56 | } 57 | 58 | include 'base' 59 | include 'api' 60 | include 'vanilla-protocol' 61 | include 'bukkit' 62 | include 'examples' 63 | include 'examples:bukkit-nametags' 64 | include 'examples:bukkit-disguise' 65 | findProject(':examples:bukkit-nametags')?.name = 'bukkit-nametags' 66 | findProject(':examples:bukkit-disguise')?.name = 'bukkit-disguise' 67 | 68 | -------------------------------------------------------------------------------- /vanilla-protocol/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':api') 3 | implementation project(':base') 4 | } -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/channel/ProxiedInboundChannelHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.channel; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelPromise; 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.FieldDefaults; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @Log4j2 16 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 17 | @RequiredArgsConstructor(staticName = "create", access = AccessLevel.PACKAGE) 18 | final class ProxiedInboundChannelHandler extends ChannelDuplexHandler { 19 | ChannelDuplexHandler parent; 20 | Runnable afterWrite; 21 | 22 | @Override 23 | public void write( 24 | final @NotNull ChannelHandlerContext ctx, 25 | final @NotNull Object msg, 26 | final @NotNull ChannelPromise promise 27 | ) throws Exception { 28 | parent.write(ctx, msg, promise); 29 | afterWrite.run(); 30 | } 31 | 32 | @Override 33 | public void channelRead( 34 | final @NotNull ChannelHandlerContext ctx, 35 | final @NotNull Object msg 36 | ) throws Exception { 37 | parent.channelRead(ctx, msg); 38 | } 39 | } -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/channel/ProxiedOutboundChannelHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.channel; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelOutboundHandlerAdapter; 5 | import io.netty.channel.ChannelPromise; 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.FieldDefaults; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @Log4j2 16 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 17 | @RequiredArgsConstructor(staticName = "create", access = AccessLevel.PACKAGE) 18 | final class ProxiedOutboundChannelHandler extends ChannelOutboundHandlerAdapter { 19 | ChannelOutboundHandlerAdapter parent; 20 | Runnable afterWrite; 21 | 22 | @Override 23 | public void write( 24 | final @NotNull ChannelHandlerContext ctx, 25 | final @NotNull Object msg, 26 | final @NotNull ChannelPromise promise 27 | ) throws Exception { 28 | parent.write(ctx, msg, promise); 29 | 30 | afterWrite.run(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/channel/VanillaChannelInitializer.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.channel; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandler; 4 | import by.milansky.protocol.api.state.ProtocolState; 5 | import by.milansky.protocol.api.version.ProtocolVersion; 6 | import by.milansky.protocol.base.packet.handler.BaseMergedPacketHandler; 7 | import by.milansky.protocol.base.packet.handler.annotation.AnnotationBasedHandler; 8 | import by.milansky.protocol.vanilla.codec.VanillaInboundPacketHandler; 9 | import by.milansky.protocol.vanilla.codec.VanillaOutboundPacketHandler; 10 | import by.milansky.protocol.vanilla.codec.VanillaPacketEncoder; 11 | import by.milansky.protocol.vanilla.handler.VanillaStateTrackingHandler; 12 | import by.milansky.protocol.vanilla.registry.VanillaStateRegistry; 13 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 14 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 15 | import io.netty.channel.Channel; 16 | import io.netty.channel.ChannelDuplexHandler; 17 | import io.netty.channel.ChannelInitializer; 18 | import io.netty.channel.ChannelOutboundHandlerAdapter; 19 | import io.netty.handler.codec.ByteToMessageDecoder; 20 | import io.netty.handler.codec.MessageToByteEncoder; 21 | import lombok.AccessLevel; 22 | import lombok.RequiredArgsConstructor; 23 | import lombok.experimental.ExtensionMethod; 24 | import lombok.experimental.FieldDefaults; 25 | import lombok.extern.log4j.Log4j2; 26 | import lombok.val; 27 | import org.jetbrains.annotations.Contract; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | /** 31 | * @author milansky 32 | */ 33 | @Log4j2 34 | @ExtensionMethod({ChannelUtility.class}) 35 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 36 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 37 | public final class VanillaChannelInitializer extends ChannelInitializer { 38 | private static final VanillaStateRegistry STANDARD_REGISTRY = VanillaStateRegistry.standardRegistry(); 39 | private static final String CUSTOM_ENCODER_PIPELINE_NAME = "milansky-protocol-encoder", 40 | OUTBOUND_CONFIG_NAME = "outbound_config", INBOUND_CONFIG_NAME = "inbound_config", 41 | ENCODER_NAME = "encoder", DECODER_NAME = "decoder"; 42 | 43 | ProtocolVersion version; 44 | PacketHandler[] additionalHandlers; 45 | 46 | @Contract("_, _ -> new") 47 | public static @NotNull VanillaChannelInitializer create( 48 | final @NotNull ProtocolVersion version, 49 | final @NotNull PacketHandler @NotNull ... additionalHandlers 50 | ) { 51 | return new VanillaChannelInitializer(version, additionalHandlers); 52 | } 53 | 54 | @Override 55 | public void initChannel(final @NotNull Channel channel) { 56 | val packetHandler = BaseMergedPacketHandler.create(AnnotationBasedHandler.create(VanillaStateTrackingHandler.create())); 57 | 58 | packetHandler.append(additionalHandlers); 59 | 60 | channel.updateProtocolState(ProtocolState.HANDSHAKE); 61 | channel.updatePacketHandler(packetHandler); 62 | 63 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_21)) { 64 | initChannelModern(channel); 65 | return; 66 | } 67 | 68 | initChannelBase(channel); 69 | } 70 | 71 | private void initChannelBase(final @NotNull Channel channel) { 72 | val pipeline = channel.pipeline(); 73 | 74 | val decoderName = pipeline.names().contains(INBOUND_CONFIG_NAME) ? INBOUND_CONFIG_NAME : DECODER_NAME; 75 | val encoderName = pipeline.names().contains(OUTBOUND_CONFIG_NAME) ? OUTBOUND_CONFIG_NAME : ENCODER_NAME; 76 | 77 | val encoder = (MessageToByteEncoder) channel.pipeline().get(encoderName); 78 | val outboundHandler = VanillaOutboundPacketHandler.create(version, STANDARD_REGISTRY, encoder); 79 | 80 | pipeline.replace(encoder, encoderName, outboundHandler); 81 | 82 | val decoder = (ByteToMessageDecoder) channel.pipeline().get(decoderName); 83 | val inboundHandler = VanillaInboundPacketHandler.create(version, STANDARD_REGISTRY, decoder); 84 | 85 | pipeline.replace(decoder, decoderName, inboundHandler); 86 | pipeline.addAfter(encoderName, CUSTOM_ENCODER_PIPELINE_NAME, VanillaPacketEncoder.create(version, STANDARD_REGISTRY)); 87 | } 88 | 89 | private void initChannelModern(final @NotNull Channel channel) { 90 | val pipeline = channel.pipeline(); 91 | val outboundHandler = (ChannelOutboundHandlerAdapter) pipeline.get(OUTBOUND_CONFIG_NAME); 92 | 93 | pipeline.replace(outboundHandler, OUTBOUND_CONFIG_NAME, ProxiedOutboundChannelHandler.create(outboundHandler, () -> { 94 | val inboundHandler = (ChannelDuplexHandler) pipeline.get(INBOUND_CONFIG_NAME); 95 | 96 | pipeline.replace(inboundHandler, INBOUND_CONFIG_NAME, ProxiedInboundChannelHandler.create(inboundHandler, () -> { 97 | initChannelBase(channel); 98 | 99 | // FIXME: Just a workaround because of missing handshake :( 100 | channel.updateProtocolState(ProtocolState.LOGIN); 101 | })); 102 | })); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/codec/VanillaInboundPacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.codec; 2 | 3 | import by.milansky.protocol.api.packet.registry.ProtocolStateRegistry; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 6 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.Unpooled; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.handler.codec.ByteToMessageDecoder; 11 | import lombok.AccessLevel; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.SneakyThrows; 14 | import lombok.experimental.ExtensionMethod; 15 | import lombok.experimental.FieldDefaults; 16 | import lombok.extern.log4j.Log4j2; 17 | import lombok.val; 18 | import org.jetbrains.annotations.NotNull; 19 | 20 | import java.lang.invoke.MethodHandle; 21 | import java.lang.invoke.MethodHandles; 22 | import java.util.List; 23 | 24 | /** 25 | * @author milansky 26 | */ 27 | @Log4j2 28 | @RequiredArgsConstructor(staticName = "create") 29 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 30 | @ExtensionMethod({ProtocolUtility.class, ChannelUtility.class}) 31 | public final class VanillaInboundPacketHandler extends ByteToMessageDecoder { 32 | private static final MethodHandle DECODE_METHOD_HANDLE; 33 | 34 | static { 35 | try { 36 | val lookup = MethodHandles.lookup(); 37 | 38 | val declaredMethod = ByteToMessageDecoder.class.getDeclaredMethod( 39 | "decode", ChannelHandlerContext.class, ByteBuf.class, List.class 40 | ); 41 | declaredMethod.setAccessible(true); 42 | 43 | DECODE_METHOD_HANDLE = lookup.unreflect(declaredMethod); 44 | } catch (final Throwable throwable) { 45 | log.catching(throwable); 46 | 47 | throw new RuntimeException(throwable); 48 | } 49 | } 50 | 51 | ProtocolVersion version; 52 | ProtocolStateRegistry stateRegistry; 53 | ByteToMessageDecoder downstream; 54 | 55 | @Override 56 | @SneakyThrows 57 | protected void decode( 58 | final @NotNull ChannelHandlerContext ctx, 59 | final @NotNull ByteBuf byteBuf, 60 | final @NotNull List list 61 | ) { 62 | val copiedBuf = Unpooled.copiedBuffer(byteBuf); 63 | boolean cancelled = false; 64 | 65 | try { 66 | val channel = ctx.channel(); 67 | val identifier = copiedBuf.readVarInt(); 68 | 69 | val clientboundRegistry = stateRegistry.serverbound(channel.protocolState()); 70 | if (clientboundRegistry == null) return; 71 | 72 | val packetClass = clientboundRegistry.getPacketById(version, identifier); 73 | if (packetClass == null) return; 74 | 75 | val packet = packetClass.getDeclaredConstructor().newInstance(); 76 | packet.decode(copiedBuf, version); 77 | 78 | val handleResult = channel.packetHandler().handle(channel, packet); 79 | 80 | if (cancelled = handleResult.cancelled()) return; 81 | 82 | val replacement = handleResult.replacement(); 83 | if (replacement == null) return; 84 | 85 | byteBuf.clear(); 86 | 87 | byteBuf.writeVarInt(identifier); 88 | replacement.encode(byteBuf, version); 89 | } catch (final Throwable throwable) { 90 | log.catching(throwable); 91 | } finally { 92 | if (!cancelled) DECODE_METHOD_HANDLE.invoke(downstream, ctx, byteBuf, list); 93 | 94 | copiedBuf.release(); 95 | byteBuf.skipBytes(byteBuf.readableBytes()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/codec/VanillaOutboundPacketHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.codec; 2 | 3 | import by.milansky.protocol.api.packet.registry.ProtocolStateRegistry; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 6 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.Unpooled; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.handler.codec.MessageToByteEncoder; 11 | import lombok.*; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import lombok.experimental.NonFinal; 16 | import lombok.extern.log4j.Log4j2; 17 | import org.jetbrains.annotations.NotNull; 18 | 19 | import java.lang.invoke.MethodHandle; 20 | import java.lang.invoke.MethodHandles; 21 | 22 | /** 23 | * @author milansky 24 | */ 25 | @Log4j2 26 | @Setter 27 | @Accessors(fluent = true) 28 | @AllArgsConstructor(staticName = "create") 29 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 30 | @ExtensionMethod({ProtocolUtility.class, ChannelUtility.class}) 31 | public final class VanillaOutboundPacketHandler extends MessageToByteEncoder { 32 | private static final MethodHandle ENCODE_METHOD_HANDLE; 33 | 34 | static { 35 | try { 36 | val lookup = MethodHandles.lookup(); 37 | 38 | val declaredMethod = MessageToByteEncoder.class.getDeclaredMethod( 39 | "encode", ChannelHandlerContext.class, Object.class, ByteBuf.class 40 | ); 41 | declaredMethod.setAccessible(true); 42 | 43 | ENCODE_METHOD_HANDLE = lookup.unreflect(declaredMethod); 44 | } catch (final Throwable throwable) { 45 | log.catching(throwable); 46 | 47 | throw new RuntimeException(throwable); 48 | } 49 | } 50 | 51 | ProtocolVersion version; 52 | ProtocolStateRegistry stateRegistry; 53 | @NonFinal 54 | MessageToByteEncoder downstream; 55 | 56 | @Override 57 | @SneakyThrows 58 | protected void encode( 59 | final @NotNull ChannelHandlerContext ctx, 60 | final @NotNull Object object, 61 | final @NotNull ByteBuf byteBuf 62 | ) throws Exception { 63 | if (object instanceof ByteBuf otherBuf) { 64 | byteBuf.writeBytes(otherBuf); 65 | return; 66 | } 67 | 68 | ENCODE_METHOD_HANDLE.invokeExact(downstream, ctx, object, byteBuf); 69 | 70 | val copiedBuf = Unpooled.copiedBuffer(byteBuf); 71 | 72 | try { 73 | val channel = ctx.channel(); 74 | val identifier = copiedBuf.readVarInt(); 75 | 76 | val clientboundRegistry = stateRegistry.clientbound(channel.protocolState()); 77 | if (clientboundRegistry == null) return; 78 | 79 | val packetClass = clientboundRegistry.getPacketById(version, identifier); 80 | if (packetClass == null) return; 81 | 82 | val packet = packetClass.getDeclaredConstructor().newInstance(); 83 | packet.decode(copiedBuf, version); 84 | 85 | val handleResult = channel.packetHandler().handle(channel, packet); 86 | 87 | if (handleResult.cancelled()) { 88 | byteBuf.clear(); 89 | return; 90 | } 91 | 92 | val replacement = handleResult.replacement(); 93 | if (replacement == null) return; 94 | 95 | byteBuf.clear(); 96 | 97 | byteBuf.writeVarInt(identifier); 98 | replacement.encode(byteBuf, version); 99 | } catch (final Throwable throwable) { 100 | log.catching(throwable); 101 | } finally { 102 | copiedBuf.release(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/codec/VanillaPacketEncoder.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.codec; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.packet.registry.ProtocolStateRegistry; 5 | import by.milansky.protocol.api.version.ProtocolVersion; 6 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 7 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 8 | import io.netty.buffer.ByteBuf; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.handler.codec.MessageToByteEncoder; 11 | import lombok.AccessLevel; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import lombok.extern.log4j.Log4j2; 16 | import lombok.val; 17 | import org.jetbrains.annotations.NotNull; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Log4j2 23 | @RequiredArgsConstructor(staticName = "create") 24 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 25 | @ExtensionMethod({ProtocolUtility.class, ChannelUtility.class}) 26 | public final class VanillaPacketEncoder extends MessageToByteEncoder { 27 | ProtocolVersion version; 28 | ProtocolStateRegistry stateRegistry; 29 | 30 | @Override 31 | protected void encode( 32 | final @NotNull ChannelHandlerContext ctx, 33 | @NotNull Packet packet, 34 | final @NotNull ByteBuf byteBuf 35 | ) { 36 | val channel = ctx.channel(); 37 | val result = channel.packetHandler().handle(channel, packet); 38 | 39 | if (result.cancelled()) return; 40 | if (result.replacement() != null) packet = result.replacement(); 41 | 42 | val clientboundRegistry = stateRegistry.clientbound(ctx.channel().protocolState()); 43 | 44 | if (clientboundRegistry == null) 45 | throw new IllegalStateException("Cannot find clientbound registry for packet: " + packet); 46 | 47 | val mapping = clientboundRegistry.getMapping(packet.getClass()); 48 | 49 | if (mapping == null) 50 | throw new IllegalStateException("Failed to find mapper for " + packet.getClass()); 51 | 52 | ProtocolUtility.writeVarInt(byteBuf, mapping.get(version)); 53 | packet.encode(byteBuf, version); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/codec/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author milansky 3 | *

4 | * This package contains all the classes related to reading/writing Minecraft packets, 5 | * as implemented by the core developers — primarily proxy classes that allow us 6 | * to insert our logic into their code. 7 | *

8 | * Issues may arise with newer versions. TODO. 9 | */ 10 | package by.milansky.protocol.vanilla.codec; -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/handler/VanillaStateTrackingHandler.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.handler; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandleResult; 4 | import by.milansky.protocol.api.state.ProtocolState; 5 | import by.milansky.protocol.base.packet.handler.BasePacketHandleResult; 6 | import by.milansky.protocol.base.packet.handler.annotation.PacketProcessor; 7 | import by.milansky.protocol.vanilla.standard.ClientboundLoginSuccess; 8 | import by.milansky.protocol.vanilla.standard.ServerboundHandshake; 9 | import by.milansky.protocol.vanilla.utility.ChannelUtility; 10 | import io.netty.channel.Channel; 11 | import lombok.NoArgsConstructor; 12 | import lombok.experimental.ExtensionMethod; 13 | import lombok.extern.log4j.Log4j2; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Log4j2 20 | @ExtensionMethod({ChannelUtility.class}) 21 | @NoArgsConstructor(staticName = "create") 22 | public final class VanillaStateTrackingHandler { 23 | @PacketProcessor 24 | public @NotNull PacketHandleResult handle(final Channel channel, final @NotNull ServerboundHandshake handshake) { 25 | channel.updateProtocolState(switch (handshake.nextState()) { 26 | case 1 -> ProtocolState.STATUS; 27 | case 2 -> ProtocolState.LOGIN; 28 | 29 | default -> throw new IllegalStateException("Unexpected value: " + handshake.nextState()); 30 | }); 31 | 32 | return BasePacketHandleResult.ok(); 33 | } 34 | 35 | @PacketProcessor 36 | public @NotNull PacketHandleResult handle(final Channel channel, final @NotNull ClientboundLoginSuccess success) { 37 | // TODO: Support for configuration stage to avoid i/o errors in configuration stage 38 | channel.updateProtocolState(ProtocolState.PLAY); 39 | 40 | return BasePacketHandleResult.ok(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/property/Property.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.property; 2 | 3 | /** 4 | * @author milansky 5 | */ 6 | public record Property( 7 | String name, String value, String signature 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/registry/VanillaStateRegistry.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.registry; 2 | 3 | import by.milansky.protocol.api.state.ProtocolState; 4 | import by.milansky.protocol.base.packet.registry.BaseStateRegistry; 5 | import by.milansky.protocol.base.packet.registry.BaseSuppliedPacketRegistry; 6 | import by.milansky.protocol.base.version.UnmodifiableVersionMapping; 7 | import by.milansky.protocol.vanilla.standard.*; 8 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 9 | import lombok.extern.log4j.Log4j2; 10 | 11 | /** 12 | * @author milansky 13 | */ 14 | @Log4j2 15 | public final class VanillaStateRegistry extends BaseStateRegistry { 16 | private VanillaStateRegistry() { 17 | registerClientboundState(ProtocolState.LOGIN, BaseSuppliedPacketRegistry.create(registry -> { 18 | registry.register(ClientboundLoginSuccess.class, 19 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_7_2, 0x02)); 20 | 21 | registry.register(ClientboundCompression.class, 22 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_7_2, 0x03)); 23 | })); 24 | 25 | registerServerboundState(ProtocolState.HANDSHAKE, BaseSuppliedPacketRegistry.create(registry -> { 26 | registry.register(ServerboundHandshake.class, 27 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_7_2, 0x00)); 28 | })); 29 | 30 | registerServerboundState(ProtocolState.PLAY, BaseSuppliedPacketRegistry.create(registry -> { 31 | registry.register(ServerboundTabcomplete.class, 32 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_8, 0x14, 33 | VanillaProtocolVersion.MINECRAFT_1_9, 0x01, VanillaProtocolVersion.MINECRAFT_1_12, 0x02, 34 | VanillaProtocolVersion.MINECRAFT_1_12_1, 0x01, VanillaProtocolVersion.MINECRAFT_1_13, 0x05, 35 | VanillaProtocolVersion.MINECRAFT_1_14, 0x06, VanillaProtocolVersion.MINECRAFT_1_19, 0x08, 36 | VanillaProtocolVersion.MINECRAFT_1_19_1, 0x09, VanillaProtocolVersion.MINECRAFT_1_19_3, 0x08, 37 | VanillaProtocolVersion.MINECRAFT_1_19_4, 0x09, VanillaProtocolVersion.MINECRAFT_1_20_2, 0x0A, 38 | VanillaProtocolVersion.MINECRAFT_1_20_5, 0x0B, VanillaProtocolVersion.MINECRAFT_1_21_2, 0x0D 39 | )); 40 | })); 41 | 42 | registerClientboundState(ProtocolState.PLAY, BaseSuppliedPacketRegistry.create(registry -> { 43 | registry.register(ClientboundSpawnEntityLiving.class, 44 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_8, 15, 45 | VanillaProtocolVersion.MINECRAFT_1_9, 3, VanillaProtocolVersion.MINECRAFT_1_16, 2, 46 | VanillaProtocolVersion.MINECRAFT_1_19)); 47 | 48 | registry.register(ClientboundUpsertPlayerInfo.class, 49 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_8, 0x38, 50 | VanillaProtocolVersion.MINECRAFT_1_9, 0x2D, VanillaProtocolVersion.MINECRAFT_1_12_1, 0x2E, 51 | VanillaProtocolVersion.MINECRAFT_1_13, 0x30, VanillaProtocolVersion.MINECRAFT_1_14, 0x33, 52 | VanillaProtocolVersion.MINECRAFT_1_15, 0x34, VanillaProtocolVersion.MINECRAFT_1_16, 0x33, 53 | VanillaProtocolVersion.MINECRAFT_1_16_2, 0x32, VanillaProtocolVersion.MINECRAFT_1_17, 0x36, 54 | VanillaProtocolVersion.MINECRAFT_1_19, 0x34, VanillaProtocolVersion.MINECRAFT_1_19_1, 0x37, 55 | VanillaProtocolVersion.MINECRAFT_1_19_3)); 56 | 57 | registry.register(ClientboundTeam.class, 58 | UnmodifiableVersionMapping.createMapping(VanillaProtocolVersion.MINECRAFT_1_8, 0x3E, 59 | VanillaProtocolVersion.MINECRAFT_1_9, 0x41, VanillaProtocolVersion.MINECRAFT_1_12, 0x43, 60 | VanillaProtocolVersion.MINECRAFT_1_12_1, 0x44, VanillaProtocolVersion.MINECRAFT_1_13, 0x47, 61 | VanillaProtocolVersion.MINECRAFT_1_14, 0x4B, VanillaProtocolVersion.MINECRAFT_1_15, 0x4C, 62 | VanillaProtocolVersion.MINECRAFT_1_17, 0x55, VanillaProtocolVersion.MINECRAFT_1_19_1, 0x58, 63 | VanillaProtocolVersion.MINECRAFT_1_19_3, 0x56, VanillaProtocolVersion.MINECRAFT_1_19_4, 0x5A, 64 | VanillaProtocolVersion.MINECRAFT_1_20_2, 0x5C, VanillaProtocolVersion.MINECRAFT_1_20_3, 0x5E, 65 | VanillaProtocolVersion.MINECRAFT_1_20_5, 0x60, VanillaProtocolVersion.MINECRAFT_1_21_2, 0x67)); 66 | })); 67 | } 68 | 69 | public static VanillaStateRegistry standardRegistry() { 70 | return new VanillaStateRegistry(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundCompression.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import io.netty.buffer.ByteBuf; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.ExtensionMethod; 13 | import lombok.experimental.FieldDefaults; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @Accessors(fluent = true, chain = true) 23 | @ExtensionMethod({ProtocolUtility.class}) 24 | @FieldDefaults(level = AccessLevel.PRIVATE) 25 | public final class ClientboundCompression implements Packet { 26 | int threshold; 27 | 28 | @Override 29 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 30 | byteBuf.writeVarInt(threshold); 31 | } 32 | 33 | @Override 34 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 35 | threshold = byteBuf.readVarInt(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundLoginSuccess.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 7 | import io.netty.buffer.ByteBuf; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.util.UUID; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Data 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | @Accessors(fluent = true, chain = true) 26 | @ExtensionMethod({ProtocolUtility.class}) 27 | @FieldDefaults(level = AccessLevel.PRIVATE) 28 | public final class ClientboundLoginSuccess implements Packet { 29 | UUID uuid; 30 | String name; 31 | 32 | @Override 33 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 34 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_19)) { 35 | byteBuf.writeUuid(uuid); 36 | } else if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_16)) { 37 | byteBuf.writeUuidIntArray(uuid); 38 | } else if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_7_6)) { 39 | byteBuf.writeString(uuid.toString()); 40 | } 41 | 42 | byteBuf.writeString(name); 43 | } 44 | 45 | @Override 46 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 47 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_19)) { 48 | uuid = byteBuf.readUuid(); 49 | } else if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_16)) { 50 | uuid = byteBuf.readUuidIntArray(); 51 | } else if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_7_6)) { 52 | uuid = UUID.fromString(byteBuf.readString(36)); 53 | } 54 | 55 | name = byteBuf.readString(16); 56 | byteBuf.skipBytes(byteBuf.readableBytes()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundSpawnEntityLiving.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 7 | import io.netty.buffer.ByteBuf; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.util.UUID; 18 | 19 | /** 20 | * @author milansky 21 | */ 22 | @Data 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | @Accessors(fluent = true, chain = true) 26 | @ExtensionMethod({ProtocolUtility.class}) 27 | @FieldDefaults(level = AccessLevel.PRIVATE) 28 | public final class ClientboundSpawnEntityLiving implements Packet { 29 | private static final double POSITION_FACTOR = 32.0; 30 | private static final float ROTATION_FACTOR = 256.0F / 360.0F; 31 | private static final double VELOCITY_FACTOR = 8000.0; 32 | 33 | int entityID; 34 | UUID entityUUID; 35 | int entityTypeID; 36 | double positionX, positionY, positionZ; 37 | float yaw, pitch, headPitch; 38 | double velocityX, velocityY, velocityZ; 39 | 40 | @Override 41 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 42 | byteBuf.writeVarInt(entityID); 43 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_9)) { 44 | byteBuf.writeByte(entityTypeID & 255); 45 | byteBuf.writeInt((int) Math.floor(positionX * POSITION_FACTOR)); 46 | byteBuf.writeInt((int) Math.floor(positionY * POSITION_FACTOR)); 47 | byteBuf.writeInt((int) Math.floor(positionZ * POSITION_FACTOR)); 48 | } else { 49 | byteBuf.writeUuid(entityUUID); 50 | 51 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_11)) { 52 | byteBuf.writeVarInt(entityTypeID); 53 | } else { 54 | byteBuf.writeByte(entityTypeID & 255); 55 | } 56 | 57 | byteBuf.writeDouble(positionX); 58 | byteBuf.writeDouble(positionY); 59 | byteBuf.writeDouble(positionZ); 60 | } 61 | 62 | byteBuf.writeByte((int) (yaw * ROTATION_FACTOR)); 63 | byteBuf.writeByte((int) (pitch * ROTATION_FACTOR)); 64 | byteBuf.writeByte((int) (headPitch * ROTATION_FACTOR)); 65 | 66 | byteBuf.writeShort((int) (velocityX * VELOCITY_FACTOR)); 67 | byteBuf.writeShort((int) (velocityY * VELOCITY_FACTOR)); 68 | byteBuf.writeShort((int) (velocityZ * VELOCITY_FACTOR)); 69 | 70 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_15)) 71 | byteBuf.writeEmptyEntityMetadata(version); 72 | } 73 | 74 | @Override 75 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 76 | this.entityID = byteBuf.readVarInt(); 77 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_9)) { 78 | this.entityUUID = new UUID(0L, 0L); 79 | this.entityTypeID = byteBuf.readByte() & 255; 80 | 81 | this.positionX = byteBuf.readInt() / POSITION_FACTOR; 82 | this.positionY = byteBuf.readInt() / POSITION_FACTOR; 83 | this.positionZ = byteBuf.readInt() / POSITION_FACTOR; 84 | } else { 85 | this.entityUUID = byteBuf.readUuid(); 86 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_11)) { 87 | this.entityTypeID = byteBuf.readVarInt(); 88 | } else { 89 | this.entityTypeID = byteBuf.readUnsignedByte(); 90 | } 91 | 92 | this.positionX = byteBuf.readDouble(); 93 | this.positionY = byteBuf.readDouble(); 94 | this.positionZ = byteBuf.readDouble(); 95 | } 96 | this.yaw = byteBuf.readByte() / ROTATION_FACTOR; 97 | this.pitch = byteBuf.readByte() / ROTATION_FACTOR; 98 | this.headPitch = byteBuf.readByte() / ROTATION_FACTOR; 99 | 100 | this.velocityX = byteBuf.readShort() / VELOCITY_FACTOR; 101 | this.velocityY = byteBuf.readShort() / VELOCITY_FACTOR; 102 | this.velocityZ = byteBuf.readShort() / VELOCITY_FACTOR; 103 | 104 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_15)) { 105 | byteBuf.skipBytes(byteBuf.readableBytes()); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundTeam.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import by.milansky.protocol.vanilla.utility.TextColorUtility; 7 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 8 | import io.netty.buffer.ByteBuf; 9 | import lombok.*; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.ExtensionMethod; 12 | import lombok.experimental.FieldDefaults; 13 | import net.kyori.adventure.text.Component; 14 | import net.kyori.adventure.text.format.NamedTextColor; 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author milansky 23 | */ 24 | @Data 25 | @NoArgsConstructor 26 | @AllArgsConstructor 27 | @Accessors(fluent = true, chain = true) 28 | @ExtensionMethod({ProtocolUtility.class}) 29 | @FieldDefaults(level = AccessLevel.PRIVATE) 30 | public final class ClientboundTeam implements Packet { 31 | String name; 32 | TeamMode mode; 33 | Component displayName, prefix, suffix; 34 | NameTagVisibility nameTagVisibility; 35 | CollisionRule collisionRule; 36 | NamedTextColor color; 37 | byte friendlyFire; 38 | String[] players; 39 | 40 | @Override 41 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 42 | val colorId = TextColorUtility.toId(color); 43 | 44 | byteBuf.writeString(name); 45 | byteBuf.writeByte(mode.ordinal()); 46 | if (mode == TeamMode.CREATE || mode == TeamMode.UPDATE) { 47 | if (version.lowerEqual(VanillaProtocolVersion.MINECRAFT_1_12_2)) { 48 | byteBuf.writeComponent(version, displayName, true); 49 | byteBuf.writeComponent(version, prefix, true); 50 | byteBuf.writeComponent(version, suffix, true); 51 | 52 | byteBuf.writeByte(friendlyFire); 53 | 54 | byteBuf.writeString(nameTagVisibility.identifier); 55 | 56 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_9)) 57 | byteBuf.writeString(collisionRule.identifier); 58 | 59 | byteBuf.writeByte(colorId); 60 | } else { 61 | byteBuf.writeComponent(version, displayName, false); 62 | byteBuf.writeByte(friendlyFire); 63 | byteBuf.writeString(nameTagVisibility.identifier); 64 | byteBuf.writeString(collisionRule.identifier); 65 | 66 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_17)) { 67 | byteBuf.writeVarInt(colorId); 68 | } else { 69 | byteBuf.writeByte(colorId); 70 | } 71 | 72 | byteBuf.writeComponent(version, prefix, false); 73 | byteBuf.writeComponent(version, suffix, false); 74 | } 75 | } 76 | 77 | if (mode == TeamMode.CREATE || mode == TeamMode.ADD_PLAYER || mode == TeamMode.REMOVE_PLAYER) 78 | byteBuf.writeStringArray(players); 79 | } 80 | 81 | @Override 82 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 83 | name = byteBuf.readString(); 84 | mode = TeamMode.VALUES[byteBuf.readByte()]; 85 | 86 | if (mode == TeamMode.CREATE || mode == TeamMode.UPDATE) { 87 | displayName = byteBuf.readComponent(version, version.lower(VanillaProtocolVersion.MINECRAFT_1_13)); 88 | 89 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_13)) { 90 | prefix = byteBuf.readComponent(version, true); 91 | suffix = byteBuf.readComponent(version, true); 92 | } 93 | 94 | friendlyFire = byteBuf.readByte(); 95 | nameTagVisibility = NameTagVisibility.visibilityByIdentifier(byteBuf.readString()); 96 | 97 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_9)) 98 | collisionRule = CollisionRule.ruleByIdentifier(byteBuf.readString()); 99 | 100 | color = TextColorUtility.fromId(version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_13) 101 | ? byteBuf.readVarInt() 102 | : byteBuf.readByte()); 103 | 104 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_13)) { 105 | prefix = byteBuf.readComponent(version, false); 106 | suffix = byteBuf.readComponent(version, false); 107 | } 108 | } 109 | 110 | if (mode == TeamMode.CREATE || mode == TeamMode.ADD_PLAYER || mode == TeamMode.REMOVE_PLAYER) 111 | players = byteBuf.readStringArray(); 112 | } 113 | 114 | public boolean containsPlayer(final String targetName) { 115 | for (val playerName : players) 116 | if (playerName.equals(targetName)) 117 | return true; 118 | 119 | return false; 120 | } 121 | 122 | public enum TeamMode { 123 | CREATE, REMOVE, UPDATE, ADD_PLAYER, REMOVE_PLAYER; 124 | 125 | public static final TeamMode[] VALUES = values(); 126 | } 127 | 128 | @Getter 129 | @RequiredArgsConstructor 130 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 131 | public enum NameTagVisibility { 132 | ALWAYS("always"), 133 | HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), 134 | HIDE_FOR_OWN_TEAM("hideForOwnTeam"), 135 | NEVER("never"); 136 | 137 | private static final Map NAME_TAG_VISIBILITIES = new HashMap<>(); 138 | 139 | static { 140 | for (val visibility : NameTagVisibility.values()) 141 | NAME_TAG_VISIBILITIES.put(visibility.identifier(), visibility); 142 | } 143 | 144 | String identifier; 145 | 146 | public static @Nullable NameTagVisibility visibilityByIdentifier(final @NotNull String identifier) { 147 | return NAME_TAG_VISIBILITIES.get(identifier); 148 | } 149 | } 150 | 151 | @Getter 152 | @RequiredArgsConstructor 153 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 154 | public enum CollisionRule { 155 | ALWAYS("always"), 156 | PUSH_OTHER_TEAMS("pushOtherTeams"), 157 | PUSH_OWN_TEAM("pushOwnTeam"), 158 | NEVER("never"); 159 | 160 | private static final Map COLLISION_RULES = new HashMap<>(); 161 | 162 | static { 163 | for (val rule : CollisionRule.values()) 164 | COLLISION_RULES.put(rule.identifier(), rule); 165 | } 166 | 167 | String identifier; 168 | 169 | public static @Nullable CollisionRule ruleByIdentifier(final @NotNull String name) { 170 | return COLLISION_RULES.get(name); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundUpdateHealth.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import io.netty.buffer.ByteBuf; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.ExtensionMethod; 13 | import lombok.experimental.FieldDefaults; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @Accessors(fluent = true, chain = true) 23 | @ExtensionMethod({ProtocolUtility.class}) 24 | @FieldDefaults(level = AccessLevel.PRIVATE) 25 | public final class ClientboundUpdateHealth implements Packet { 26 | float health, foodSaturation; 27 | int food; 28 | 29 | @Override 30 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 31 | byteBuf.writeFloat(health); 32 | byteBuf.writeVarInt(food); 33 | byteBuf.writeFloat(foodSaturation); 34 | } 35 | 36 | @Override 37 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 38 | health = byteBuf.readFloat(); 39 | food = byteBuf.readVarInt(); 40 | foodSaturation = byteBuf.readFloat(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ClientboundUpsertPlayerInfo.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.property.Property; 6 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 7 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 8 | import io.netty.buffer.ByteBuf; 9 | import lombok.*; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.ExtensionMethod; 12 | import lombok.experimental.FieldDefaults; 13 | import net.kyori.adventure.text.Component; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import java.util.UUID; 17 | 18 | /** 19 | * @author milansky 20 | */ 21 | @Data 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Accessors(fluent = true, chain = true) 25 | @ExtensionMethod({ProtocolUtility.class}) 26 | @FieldDefaults(level = AccessLevel.PRIVATE) 27 | public final class ClientboundUpsertPlayerInfo implements Packet { 28 | Action action; 29 | Item[] items; 30 | 31 | @Override 32 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 33 | byteBuf.writeVarInt(action.ordinal()); 34 | byteBuf.writeVarInt(items.length); 35 | 36 | for (val item : items) { 37 | byteBuf.writeUuid(item.uuid()); 38 | 39 | switch (action) { 40 | case ADD_PLAYER: 41 | byteBuf.writeString(item.username); 42 | byteBuf.writeProperties(item.properties); 43 | byteBuf.writeVarInt(item.gamemode); 44 | byteBuf.writeVarInt(item.ping); 45 | 46 | byteBuf.writeBoolean(item.displayName != null); 47 | if (item.displayName != null) byteBuf.writeComponent(version, item.displayName, false); 48 | 49 | // No chat key 50 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_19)) byteBuf.writeBoolean(false); 51 | break; 52 | case UPDATE_GAMEMODE: 53 | byteBuf.writeVarInt(item.gamemode); 54 | break; 55 | case UPDATE_LATENCY: 56 | byteBuf.writeVarInt(item.ping); 57 | break; 58 | case UPDATE_DISPLAY_NAME: 59 | byteBuf.writeBoolean(item.displayName != null); 60 | if (item.displayName != null) byteBuf.writeComponent(version, item.displayName, false); 61 | 62 | break; 63 | case REMOVE_PLAYER: 64 | break; 65 | } 66 | } 67 | } 68 | 69 | @Override 70 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 71 | action = Action.values()[byteBuf.readVarInt()]; 72 | items = new Item[byteBuf.readVarInt()]; 73 | 74 | for (int i = 0; i < items.length; i++) { 75 | val item = items[i] = new Item(); 76 | item.uuid(byteBuf.readUuid()); 77 | 78 | switch (action) { 79 | case ADD_PLAYER: 80 | item.username = byteBuf.readString(); 81 | item.properties = byteBuf.readProperties(); 82 | item.gamemode = byteBuf.readVarInt(); 83 | item.ping = byteBuf.readVarInt(); 84 | 85 | if (byteBuf.readBoolean()) item.displayName = byteBuf.readComponent(version, false); 86 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_19)) { 87 | if (byteBuf.readBoolean()) { 88 | byteBuf.skipBytes(byteBuf.readableBytes()); 89 | throw new IllegalStateException(); 90 | } 91 | } 92 | break; 93 | case UPDATE_GAMEMODE: 94 | item.gamemode = byteBuf.readVarInt(); 95 | break; 96 | case UPDATE_LATENCY: 97 | item.ping = byteBuf.readVarInt(); 98 | break; 99 | case UPDATE_DISPLAY_NAME: 100 | if (byteBuf.readBoolean()) item.displayName = byteBuf.readComponent(version, false); 101 | } 102 | } 103 | } 104 | 105 | public enum Action { 106 | ADD_PLAYER, 107 | UPDATE_GAMEMODE, 108 | UPDATE_LATENCY, 109 | UPDATE_DISPLAY_NAME, 110 | REMOVE_PLAYER; 111 | } 112 | 113 | @Data 114 | @AllArgsConstructor 115 | @NoArgsConstructor 116 | public static class Item { 117 | UUID uuid; 118 | 119 | String username; 120 | Property[] properties; 121 | 122 | Boolean listed; 123 | 124 | Integer gamemode; 125 | Integer ping; 126 | 127 | Component displayName; 128 | 129 | Integer listOrder; 130 | 131 | Boolean showHat; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ServerboundHandshake.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 7 | import io.netty.buffer.ByteBuf; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | /** 18 | * @author milansky 19 | */ 20 | @Data 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(fluent = true, chain = true) 24 | @ExtensionMethod({ProtocolUtility.class}) 25 | @FieldDefaults(level = AccessLevel.PRIVATE) 26 | public final class ServerboundHandshake implements Packet { 27 | ProtocolVersion protocolVersion; 28 | String host; 29 | int port, nextState; 30 | 31 | @Override 32 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 33 | byteBuf.writeVarInt(protocolVersion.protocol()); 34 | byteBuf.writeString(host); 35 | byteBuf.writeShort(port); 36 | byteBuf.writeVarInt(nextState); 37 | } 38 | 39 | @Override 40 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 41 | protocolVersion = VanillaProtocolVersion.versionByProtocol(byteBuf.readVarInt()); 42 | host = byteBuf.readString(); 43 | port = byteBuf.readUnsignedShort(); 44 | nextState = byteBuf.readVarInt(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ServerboundLogin.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import io.netty.buffer.ByteBuf; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.ExtensionMethod; 13 | import lombok.experimental.FieldDefaults; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | /** 17 | * @author milansky 18 | */ 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @Accessors(fluent = true, chain = true) 23 | @ExtensionMethod({ProtocolUtility.class}) 24 | @FieldDefaults(level = AccessLevel.PRIVATE) 25 | public final class ServerboundLogin implements Packet { 26 | String username; 27 | 28 | @Override 29 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 30 | byteBuf.writeString(username); 31 | } 32 | 33 | @Override 34 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 35 | username = byteBuf.readString(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/standard/ServerboundTabcomplete.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.standard; 2 | 3 | import by.milansky.protocol.api.packet.Packet; 4 | import by.milansky.protocol.api.version.ProtocolVersion; 5 | import by.milansky.protocol.vanilla.utility.ProtocolUtility; 6 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 7 | import io.netty.buffer.ByteBuf; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.ExtensionMethod; 14 | import lombok.experimental.FieldDefaults; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | /** 18 | * @author milansky 19 | */ 20 | @Data 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(fluent = true, chain = true) 24 | @ExtensionMethod({ProtocolUtility.class}) 25 | @FieldDefaults(level = AccessLevel.PRIVATE) 26 | public final class ServerboundTabcomplete implements Packet { 27 | int transactionId; 28 | String cursor; 29 | boolean assumeCommand, hasPosition; 30 | long position; 31 | 32 | @Override 33 | public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 34 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_13)) byteBuf.writeVarInt(transactionId); 35 | 36 | byteBuf.writeString(cursor); 37 | 38 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_13)) { 39 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_9)) { 40 | byteBuf.writeBoolean(assumeCommand); 41 | } 42 | 43 | byteBuf.writeBoolean(hasPosition); 44 | 45 | if (hasPosition) byteBuf.writeLong(position); 46 | } 47 | } 48 | 49 | @Override 50 | public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion version) { 51 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_13)) { 52 | transactionId = byteBuf.readVarInt(); 53 | } 54 | cursor = byteBuf.readString(); 55 | 56 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_13)) { 57 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_9)) { 58 | assumeCommand = byteBuf.readBoolean(); 59 | } 60 | 61 | if (hasPosition = byteBuf.readBoolean()) { 62 | position = byteBuf.readLong(); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/utility/ChannelUtility.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.utility; 2 | 3 | import by.milansky.protocol.api.packet.handler.PacketHandler; 4 | import by.milansky.protocol.api.state.ProtocolState; 5 | import io.netty.channel.Channel; 6 | import io.netty.util.AttributeKey; 7 | import lombok.experimental.UtilityClass; 8 | import lombok.extern.log4j.Log4j2; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * @author milansky 13 | */ 14 | @Log4j2 15 | @UtilityClass 16 | public final class ChannelUtility { 17 | private static final AttributeKey PROTOCOL_STATE_KEY = AttributeKey.valueOf("protocolState"); 18 | private static final AttributeKey PACKET_HANDLER_KEY = AttributeKey.valueOf("packetHandler"); 19 | 20 | public @NotNull ProtocolState protocolState(final @NotNull Channel channel) { 21 | return channel.attr(PROTOCOL_STATE_KEY).get(); 22 | } 23 | 24 | public void updateProtocolState(final @NotNull Channel channel, final @NotNull ProtocolState state) { 25 | channel.attr(PROTOCOL_STATE_KEY).set(state); 26 | } 27 | 28 | public @NotNull PacketHandler packetHandler(final @NotNull Channel channel) { 29 | return channel.attr(PACKET_HANDLER_KEY).get(); 30 | } 31 | 32 | public void updatePacketHandler(final @NotNull Channel channel, final @NotNull PacketHandler handler) { 33 | channel.attr(PACKET_HANDLER_KEY).set(handler); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/utility/ProtocolUtility.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.utility; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import by.milansky.protocol.vanilla.property.Property; 5 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 6 | import com.google.gson.JsonArray; 7 | import com.google.gson.JsonElement; 8 | import com.google.gson.JsonObject; 9 | import com.google.gson.JsonPrimitive; 10 | import com.google.gson.internal.LazilyParsedNumber; 11 | import io.netty.buffer.ByteBuf; 12 | import io.netty.buffer.ByteBufInputStream; 13 | import io.netty.buffer.ByteBufOutputStream; 14 | import io.netty.handler.codec.CorruptedFrameException; 15 | import io.netty.handler.codec.DecoderException; 16 | import io.netty.handler.codec.EncoderException; 17 | import lombok.val; 18 | import net.kyori.adventure.nbt.*; 19 | import net.kyori.adventure.text.Component; 20 | import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; 21 | import net.kyori.adventure.text.serializer.json.JSONOptions; 22 | import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer; 23 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 24 | import net.kyori.option.OptionState; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.io.IOException; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.*; 30 | 31 | /** 32 | * Utilities for writing and reading data in the Minecraft protocol. 33 | *

34 | * A lot of code is taken from ... 35 | */ 36 | public class ProtocolUtility { 37 | public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB 38 | 39 | private static final int MAXIMUM_VARINT_SIZE = 5; 40 | private static final int[] VAR_INT_LENGTHS = new int[65]; 41 | 42 | private static final GsonComponentSerializer PRE_1_16_SERIALIZER, PRE_1_20_3_SERIALIZER, MODERN_SERIALIZER; 43 | private static final BinaryTagType[] BINARY_TAG_TYPES = new BinaryTagType[]{ 44 | BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT, 45 | BinaryTagTypes.LONG, BinaryTagTypes.FLOAT, BinaryTagTypes.DOUBLE, 46 | BinaryTagTypes.BYTE_ARRAY, BinaryTagTypes.STRING, BinaryTagTypes.LIST, 47 | BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY}; 48 | 49 | static { 50 | for (int i = 0; i <= 32; ++i) VAR_INT_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d); 51 | 52 | VAR_INT_LENGTHS[32] = 1; 53 | 54 | PRE_1_16_SERIALIZER = GsonComponentSerializer.builder() 55 | .downsampleColors() 56 | .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) 57 | .options( 58 | OptionState.optionState() 59 | .value(JSONOptions.EMIT_RGB, Boolean.FALSE) 60 | .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.LEGACY_ONLY) 61 | .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) 62 | .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) 63 | .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) 64 | .build() 65 | ) 66 | .build(); 67 | 68 | PRE_1_20_3_SERIALIZER = GsonComponentSerializer.builder() 69 | .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) 70 | .options( 71 | OptionState.optionState() 72 | .value(JSONOptions.EMIT_RGB, Boolean.TRUE) 73 | .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) 74 | .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) 75 | .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) 76 | .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) 77 | .build() 78 | ) 79 | .build(); 80 | 81 | MODERN_SERIALIZER = GsonComponentSerializer.builder() 82 | .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) 83 | .options( 84 | OptionState.optionState() 85 | .value(JSONOptions.EMIT_RGB, Boolean.TRUE) 86 | .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) 87 | .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE) 88 | .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE) 89 | .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) 90 | .build() 91 | ) 92 | .build(); 93 | } 94 | 95 | private static DecoderException badVarint() { 96 | return new CorruptedFrameException("Bad VarInt decoded"); 97 | } 98 | 99 | public static int readVarInt(ByteBuf buf) { 100 | int readable = buf.readableBytes(); 101 | if (readable == 0) { 102 | // special case for empty buffer 103 | throw badVarint(); 104 | } 105 | 106 | // we can read at least one byte, and this should be a common case 107 | int k = buf.readByte(); 108 | if ((k & 0x80) != 128) { 109 | return k; 110 | } 111 | 112 | // in case decoding one byte was not enough, use a loop to decode up to the next 4 bytes 113 | int maxRead = Math.min(MAXIMUM_VARINT_SIZE, readable); 114 | int i = k & 0x7F; 115 | for (int j = 1; j < maxRead; j++) { 116 | k = buf.readByte(); 117 | i |= (k & 0x7F) << j * 7; 118 | if ((k & 0x80) != 128) { 119 | return i; 120 | } 121 | } 122 | throw badVarint(); 123 | } 124 | 125 | public static int varIntBytes(int value) { 126 | return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)]; 127 | } 128 | 129 | public static void writeVarInt(ByteBuf buf, int value) { 130 | // Peel the one and two byte count cases explicitly as they are the most common VarInt sizes 131 | // that the proxy will write, to improve inlining. 132 | if ((value & (0xFFFFFFFF << 7)) == 0) { 133 | buf.writeByte(value); 134 | } else if ((value & (0xFFFFFFFF << 14)) == 0) { 135 | int w = (value & 0x7F | 0x80) << 8 | (value >>> 7); 136 | buf.writeShort(w); 137 | } else { 138 | writeVarIntFull(buf, value); 139 | } 140 | } 141 | 142 | private static void writeVarIntFull(ByteBuf buf, int value) { 143 | // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ 144 | 145 | // This essentially is an unrolled version of the "traditional" VarInt encoding. 146 | if ((value & (0xFFFFFFFF << 7)) == 0) { 147 | buf.writeByte(value); 148 | } else if ((value & (0xFFFFFFFF << 14)) == 0) { 149 | int w = (value & 0x7F | 0x80) << 8 | (value >>> 7); 150 | buf.writeShort(w); 151 | } else if ((value & (0xFFFFFFFF << 21)) == 0) { 152 | write21BitVarInt(buf, value); 153 | } else if ((value & (0xFFFFFFFF << 28)) == 0) { 154 | int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16) 155 | | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21); 156 | buf.writeInt(w); 157 | } else { 158 | int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16 159 | | ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80); 160 | buf.writeInt(w); 161 | buf.writeByte(value >>> 28); 162 | } 163 | } 164 | 165 | public static void write21BitVarInt(ByteBuf buf, int value) { 166 | // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ 167 | int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14); 168 | buf.writeMedium(w); 169 | } 170 | 171 | public static String readString(ByteBuf buf) { 172 | return readString(buf, DEFAULT_MAX_STRING_SIZE); 173 | } 174 | 175 | public static String readString(ByteBuf buf, int cap) { 176 | int length = readVarInt(buf); 177 | return readString(buf, cap, length); 178 | } 179 | 180 | private static String readString(ByteBuf buf, int cap, int length) { 181 | val charArray = new byte[length]; 182 | 183 | buf.readBytes(charArray); 184 | 185 | return new String(charArray); 186 | } 187 | 188 | public static void writeString(ByteBuf buf, CharSequence str) { 189 | writeByteArray(buf, str.toString().getBytes(StandardCharsets.UTF_8)); 190 | } 191 | 192 | public static byte[] readByteArray(ByteBuf buf) { 193 | return readByteArray(buf, DEFAULT_MAX_STRING_SIZE); 194 | } 195 | 196 | public static byte[] readByteArray(ByteBuf buf, int cap) { 197 | int length = readVarInt(buf); 198 | 199 | byte[] array = new byte[length]; 200 | buf.readBytes(array); 201 | return array; 202 | } 203 | 204 | public static void writeByteArray(ByteBuf buf, byte[] array) { 205 | writeVarInt(buf, array.length); 206 | buf.writeBytes(array); 207 | } 208 | 209 | public static int[] readIntegerArray(ByteBuf buf) { 210 | int len = readVarInt(buf); 211 | int[] array = new int[len]; 212 | for (int i = 0; i < len; i++) { 213 | array[i] = readVarInt(buf); 214 | } 215 | return array; 216 | } 217 | 218 | public static UUID readUuid(ByteBuf buf) { 219 | long msb = buf.readLong(); 220 | long lsb = buf.readLong(); 221 | return new UUID(msb, lsb); 222 | } 223 | 224 | public static void writeUuid(ByteBuf buf, UUID uuid) { 225 | buf.writeLong(uuid.getMostSignificantBits()); 226 | buf.writeLong(uuid.getLeastSignificantBits()); 227 | } 228 | 229 | public static UUID readUuidIntArray(ByteBuf buf) { 230 | long msbHigh = (long) buf.readInt() << 32; 231 | long msbLow = (long) buf.readInt() & 0xFFFFFFFFL; 232 | long msb = msbHigh | msbLow; 233 | long lsbHigh = (long) buf.readInt() << 32; 234 | long lsbLow = (long) buf.readInt() & 0xFFFFFFFFL; 235 | long lsb = lsbHigh | lsbLow; 236 | return new UUID(msb, lsb); 237 | } 238 | 239 | public static void writeUuidIntArray(ByteBuf buf, UUID uuid) { 240 | buf.writeInt((int) (uuid.getMostSignificantBits() >> 32)); 241 | buf.writeInt((int) uuid.getMostSignificantBits()); 242 | buf.writeInt((int) (uuid.getLeastSignificantBits() >> 32)); 243 | buf.writeInt((int) uuid.getLeastSignificantBits()); 244 | } 245 | 246 | public static String[] readStringArray(ByteBuf buf) { 247 | int length = readVarInt(buf); 248 | String[] ret = new String[length]; 249 | for (int i = 0; i < length; i++) { 250 | ret[i] = readString(buf); 251 | } 252 | return ret; 253 | } 254 | 255 | public static void writeStringArray(ByteBuf buf, String[] stringArray) { 256 | writeVarInt(buf, stringArray.length); 257 | for (String s : stringArray) { 258 | writeString(buf, s); 259 | } 260 | } 261 | 262 | public static int[] readVarIntArray(ByteBuf buf) { 263 | int length = readVarInt(buf); 264 | int[] ret = new int[length]; 265 | for (int i = 0; i < length; i++) { 266 | ret[i] = readVarInt(buf); 267 | } 268 | return ret; 269 | } 270 | 271 | public static void writeVarIntArray(ByteBuf buf, int[] intArray) { 272 | writeVarInt(buf, intArray.length); 273 | for (int i = 0; i < intArray.length; i++) { 274 | writeVarInt(buf, intArray[i]); 275 | } 276 | } 277 | 278 | public static byte[] readByteArray17(ByteBuf buf) { 279 | int len = readExtendedForgeShort(buf); 280 | 281 | byte[] ret = new byte[len]; 282 | buf.readBytes(ret); 283 | return ret; 284 | } 285 | 286 | public static ByteBuf readRetainedByteBufSlice17(ByteBuf buf) { 287 | int len = readExtendedForgeShort(buf); 288 | 289 | return buf.readRetainedSlice(len); 290 | } 291 | 292 | public static void writeByteArray17(byte[] b, ByteBuf buf, boolean allowExtended) { 293 | writeExtendedForgeShort(buf, b.length); 294 | buf.writeBytes(b); 295 | } 296 | 297 | public static void writeByteBuf17(ByteBuf b, ByteBuf buf, boolean allowExtended) { 298 | writeExtendedForgeShort(buf, b.readableBytes()); 299 | buf.writeBytes(b); 300 | } 301 | 302 | public static int readExtendedForgeShort(ByteBuf buf) { 303 | int low = buf.readUnsignedShort(); 304 | int high = 0; 305 | if ((low & 0x8000) != 0) { 306 | low = low & 0x7FFF; 307 | high = buf.readUnsignedByte(); 308 | } 309 | return ((high & 0xFF) << 15) | low; 310 | } 311 | 312 | public static void writeExtendedForgeShort(ByteBuf buf, int toWrite) { 313 | int low = toWrite & 0x7FFF; 314 | int high = (toWrite & 0x7F8000) >> 15; 315 | if (high != 0) { 316 | low = low | 0x8000; 317 | } 318 | buf.writeShort(low); 319 | if (high != 0) { 320 | buf.writeByte(high); 321 | } 322 | } 323 | 324 | public static String readStringWithoutLength(ByteBuf buf) { 325 | return readString(buf, DEFAULT_MAX_STRING_SIZE, buf.readableBytes()); 326 | } 327 | 328 | public static GsonComponentSerializer getJsonChatSerializer(ProtocolVersion version) { 329 | if (version.greater(VanillaProtocolVersion.MINECRAFT_1_20_3)) { 330 | return MODERN_SERIALIZER; 331 | } 332 | 333 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_16)) { 334 | return PRE_1_20_3_SERIALIZER; 335 | } 336 | 337 | return PRE_1_16_SERIALIZER; 338 | } 339 | 340 | public static void writeBinaryTag(ByteBuf buf, ProtocolVersion version, 341 | T tag) { 342 | BinaryTagType type = (BinaryTagType) tag.type(); 343 | buf.writeByte(type.id()); 344 | try { 345 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_20_2)) { 346 | // Empty name 347 | buf.writeShort(0); 348 | } 349 | type.write(tag, new ByteBufOutputStream(buf)); 350 | } catch (IOException e) { 351 | throw new EncoderException("Unable to encode BinaryTag"); 352 | } 353 | } 354 | 355 | public static BinaryTag readBinaryTag(ByteBuf buf, ProtocolVersion version, 356 | BinaryTagIO.Reader reader) { 357 | BinaryTagType type = BINARY_TAG_TYPES[buf.readByte()]; 358 | if (version.lower(VanillaProtocolVersion.MINECRAFT_1_20_2)) { 359 | buf.skipBytes(buf.readUnsignedShort()); 360 | } 361 | try { 362 | return type.read(new ByteBufInputStream(buf)); 363 | } catch (IOException thrown) { 364 | throw new DecoderException("Unable to parse BinaryTag, full error: " + thrown.getMessage()); 365 | } 366 | } 367 | 368 | public static Component readComponent(final @NotNull ByteBuf buf, final ProtocolVersion version, boolean legacy) { 369 | if (legacy) return Component.text(readString(buf)); 370 | 371 | val serializer = getJsonChatSerializer(version); 372 | 373 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_20_3)) { 374 | val binaryTag = readBinaryTag(buf, version, BinaryTagIO.reader()); 375 | 376 | return serializer.deserialize(deserializeBinaryTag(binaryTag).toString()); 377 | } else if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_13)) { 378 | return serializer.deserialize(readString(buf, DEFAULT_MAX_STRING_SIZE)); 379 | } else { 380 | return serializer.deserialize(readString(buf)); 381 | } 382 | } 383 | 384 | public static void writeComponent( 385 | final @NotNull ByteBuf buf, 386 | final @NotNull ProtocolVersion version, 387 | final @NotNull Component component, 388 | final boolean legacy 389 | ) { 390 | if (legacy) { 391 | writeString(buf, LegacyComponentSerializer.legacySection().serialize(component)); 392 | return; 393 | } 394 | 395 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_20_3)) { 396 | writeBinaryTag(buf, version, serializeBinaryTag(GsonComponentSerializer.gson().serializeToTree(component))); 397 | } else { 398 | writeString(buf, getJsonChatSerializer(version).serialize(component)); 399 | } 400 | } 401 | 402 | public static void writeProperties(ByteBuf buf, Property[] properties) { 403 | writeVarInt(buf, properties.length); 404 | 405 | for (val property : properties) { 406 | writeString(buf, property.name()); 407 | writeString(buf, property.value()); 408 | 409 | val signature = property.signature(); 410 | 411 | if (signature != null && !signature.isEmpty()) { 412 | buf.writeBoolean(true); 413 | writeString(buf, signature); 414 | } else { 415 | buf.writeBoolean(false); 416 | } 417 | } 418 | } 419 | 420 | public static Property[] readProperties(ByteBuf buf) { 421 | val properties = new Property[readVarInt(buf)]; 422 | 423 | for (int i = 0; i < properties.length; i++) { 424 | val name = readString(buf); 425 | val value = readString(buf); 426 | 427 | var signature = ""; 428 | if (buf.readBoolean()) signature = readString(buf); 429 | 430 | properties[i] = new Property(name, value, signature); 431 | } 432 | return properties; 433 | } 434 | 435 | public static void writeEmptyEntityMetadata(final @NotNull ByteBuf buf, final @NotNull ProtocolVersion version) { 436 | if (version.greaterEqual(VanillaProtocolVersion.MINECRAFT_1_9)) { 437 | buf.writeByte(255); 438 | } else { 439 | buf.writeByte(127); 440 | } 441 | } 442 | 443 | public static BinaryTag serializeBinaryTag(final JsonElement json) { 444 | if (json instanceof final JsonPrimitive jsonPrimitive) { 445 | if (jsonPrimitive.isNumber()) { 446 | final Number number = json.getAsNumber(); 447 | 448 | if (number instanceof Byte) { 449 | return ByteBinaryTag.byteBinaryTag((Byte) number); 450 | } else if (number instanceof Short) { 451 | return ShortBinaryTag.shortBinaryTag((Short) number); 452 | } else if (number instanceof Integer) { 453 | return IntBinaryTag.intBinaryTag((Integer) number); 454 | } else if (number instanceof Long) { 455 | return LongBinaryTag.longBinaryTag((Long) number); 456 | } else if (number instanceof Float) { 457 | return FloatBinaryTag.floatBinaryTag((Float) number); 458 | } else if (number instanceof Double) { 459 | return DoubleBinaryTag.doubleBinaryTag((Double) number); 460 | } else if (number instanceof LazilyParsedNumber) { 461 | return IntBinaryTag.intBinaryTag(number.intValue()); 462 | } 463 | } else if (jsonPrimitive.isString()) { 464 | return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString()); 465 | } else if (jsonPrimitive.isBoolean()) { 466 | return ByteBinaryTag.byteBinaryTag((byte) (jsonPrimitive.getAsBoolean() ? 1 : 0)); 467 | } else { 468 | throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive); 469 | } 470 | } else if (json instanceof JsonObject) { 471 | final CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder(); 472 | 473 | for (final Map.Entry property : ((JsonObject) json).entrySet()) { 474 | compound.put(property.getKey(), serializeBinaryTag(property.getValue())); 475 | } 476 | 477 | return compound.build(); 478 | } else if (json instanceof JsonArray) { 479 | final JsonArray jsonArray = json.getAsJsonArray(); 480 | 481 | if (jsonArray.isEmpty()) { 482 | return ListBinaryTag.empty(); 483 | } 484 | 485 | final List tagItems = new ArrayList<>(jsonArray.size()); 486 | BinaryTagType listType = null; 487 | 488 | for (final JsonElement jsonEl : jsonArray) { 489 | final BinaryTag tag = serializeBinaryTag(jsonEl); 490 | tagItems.add(tag); 491 | 492 | if (listType == null) { 493 | listType = tag.type(); 494 | } else if (listType != tag.type()) { 495 | listType = BinaryTagTypes.COMPOUND; 496 | } 497 | } 498 | 499 | switch (Objects.requireNonNull(listType).id()) { 500 | case 1: 501 | final byte[] bytes = new byte[jsonArray.size()]; 502 | for (int i = 0; i < bytes.length; i++) { 503 | bytes[i] = jsonArray.get(i).getAsNumber().byteValue(); 504 | } 505 | 506 | return ByteArrayBinaryTag.byteArrayBinaryTag(bytes); 507 | case 3: 508 | final int[] ints = new int[jsonArray.size()]; 509 | for (int i = 0; i < ints.length; i++) { 510 | ints[i] = jsonArray.get(i).getAsNumber().intValue(); 511 | } 512 | 513 | return IntArrayBinaryTag.intArrayBinaryTag(ints); 514 | case 4: 515 | final long[] longs = new long[jsonArray.size()]; 516 | for (int i = 0; i < longs.length; i++) { 517 | longs[i] = jsonArray.get(i).getAsNumber().longValue(); 518 | } 519 | 520 | return LongArrayBinaryTag.longArrayBinaryTag(longs); 521 | case 10: 522 | tagItems.replaceAll(tag -> { 523 | if (tag.type() == BinaryTagTypes.COMPOUND) { 524 | return tag; 525 | } else { 526 | return CompoundBinaryTag.builder().put("", tag).build(); 527 | } 528 | }); 529 | break; 530 | } 531 | 532 | return ListBinaryTag.listBinaryTag(listType, tagItems); 533 | } 534 | 535 | return EndBinaryTag.endBinaryTag(); 536 | } 537 | 538 | public static JsonElement deserializeBinaryTag(BinaryTag tag) { 539 | switch (tag.type().id()) { 540 | case 1://BinaryTagTypes.BYTE: 541 | return new JsonPrimitive(((ByteBinaryTag) tag).value()); 542 | case 2://BinaryTagTypes.SHORT: 543 | return new JsonPrimitive(((ShortBinaryTag) tag).value()); 544 | case 3://BinaryTagTypes.INT: 545 | return new JsonPrimitive(((IntBinaryTag) tag).value()); 546 | case 4://BinaryTagTypes.LONG: 547 | return new JsonPrimitive(((LongBinaryTag) tag).value()); 548 | case 5://BinaryTagTypes.FLOAT: 549 | return new JsonPrimitive(((FloatBinaryTag) tag).value()); 550 | case 6://BinaryTagTypes.DOUBLE: 551 | return new JsonPrimitive(((DoubleBinaryTag) tag).value()); 552 | case 7://BinaryTagTypes.BYTE_ARRAY: 553 | byte[] byteArray = ((ByteArrayBinaryTag) tag).value(); 554 | 555 | JsonArray jsonByteArray = new JsonArray(byteArray.length); 556 | for (byte b : byteArray) { 557 | jsonByteArray.add(new JsonPrimitive(b)); 558 | } 559 | 560 | return jsonByteArray; 561 | case 8://BinaryTagTypes.STRING: 562 | return new JsonPrimitive(((StringBinaryTag) tag).value()); 563 | case 9://BinaryTagTypes.LIST: 564 | ListBinaryTag items = (ListBinaryTag) tag; 565 | JsonArray jsonList = new JsonArray(items.size()); 566 | 567 | for (BinaryTag subTag : items) { 568 | jsonList.add(deserializeBinaryTag(subTag)); 569 | } 570 | 571 | return jsonList; 572 | case 10://BinaryTagTypes.COMPOUND: 573 | CompoundBinaryTag compound = (CompoundBinaryTag) tag; 574 | JsonObject jsonObject = new JsonObject(); 575 | 576 | compound.keySet().forEach(key -> { 577 | // [{"text":"test1"},"test2"] can't be represented as a binary list tag 578 | // it is represented by a list tag with two compound tags 579 | // the second compound tag will have an empty key mapped to "test2" 580 | // without this fix this would lead to an invalid json component: 581 | // [{"text":"test1"},{"":"test2"}] 582 | jsonObject.add(key.isEmpty() ? "text" : key, deserializeBinaryTag(compound.get(key))); 583 | }); 584 | 585 | return jsonObject; 586 | case 11://BinaryTagTypes.INT_ARRAY: 587 | int[] intArray = ((IntArrayBinaryTag) tag).value(); 588 | 589 | JsonArray jsonIntArray = new JsonArray(intArray.length); 590 | for (int i : intArray) { 591 | jsonIntArray.add(new JsonPrimitive(i)); 592 | } 593 | 594 | return jsonIntArray; 595 | case 12://BinaryTagTypes.LONG_ARRAY: 596 | long[] longArray = ((LongArrayBinaryTag) tag).value(); 597 | 598 | JsonArray jsonLongArray = new JsonArray(longArray.length); 599 | for (long l : longArray) { 600 | jsonLongArray.add(new JsonPrimitive(l)); 601 | } 602 | 603 | return jsonLongArray; 604 | default: 605 | throw new IllegalArgumentException("Unknown NBT tag: " + tag); 606 | } 607 | } 608 | } -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/utility/TextColorUtility.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.utility; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 4 | import lombok.experimental.UtilityClass; 5 | import net.kyori.adventure.text.format.NamedTextColor; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author milansky 14 | */ 15 | @UtilityClass 16 | public final class TextColorUtility { 17 | private static final Map COLOR_MAP = new Int2ObjectOpenHashMap<>(); 18 | private static final Map REVERSE_COLOR_MAP = new HashMap<>(); 19 | 20 | static { 21 | COLOR_MAP.put(0, NamedTextColor.BLACK); 22 | COLOR_MAP.put(1, NamedTextColor.DARK_BLUE); 23 | COLOR_MAP.put(2, NamedTextColor.DARK_GREEN); 24 | COLOR_MAP.put(3, NamedTextColor.DARK_AQUA); 25 | COLOR_MAP.put(4, NamedTextColor.DARK_RED); 26 | COLOR_MAP.put(5, NamedTextColor.DARK_PURPLE); 27 | COLOR_MAP.put(6, NamedTextColor.GOLD); 28 | COLOR_MAP.put(7, NamedTextColor.GRAY); 29 | COLOR_MAP.put(8, NamedTextColor.DARK_GRAY); 30 | COLOR_MAP.put(9, NamedTextColor.BLUE); 31 | COLOR_MAP.put(10, NamedTextColor.GREEN); 32 | COLOR_MAP.put(11, NamedTextColor.AQUA); 33 | COLOR_MAP.put(12, NamedTextColor.RED); 34 | COLOR_MAP.put(13, NamedTextColor.LIGHT_PURPLE); 35 | COLOR_MAP.put(14, NamedTextColor.YELLOW); 36 | COLOR_MAP.put(15, NamedTextColor.WHITE); 37 | 38 | COLOR_MAP.forEach((integer, namedTextColor) -> REVERSE_COLOR_MAP.put(namedTextColor, integer)); 39 | } 40 | 41 | public static @Nullable NamedTextColor fromId(int id) { 42 | return COLOR_MAP.get(id); 43 | } 44 | 45 | public static int toId(final @NotNull NamedTextColor color) { 46 | return REVERSE_COLOR_MAP.getOrDefault(color, 15); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vanilla-protocol/src/main/java/by/milansky/protocol/vanilla/version/VanillaProtocolVersion.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.version; 2 | 3 | import by.milansky.protocol.api.version.ProtocolVersion; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.experimental.Accessors; 7 | import lombok.experimental.FieldDefaults; 8 | import lombok.val; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * @author milansky 16 | */ 17 | @Getter 18 | @Accessors(fluent = true) 19 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 20 | public enum VanillaProtocolVersion implements ProtocolVersion { 21 | UNKNOWN(-1, "Unknown"), 22 | MINECRAFT_1_7_2(4, 23 | "1.7.2", "1.7.3", "1.7.4", "1.7.5"), 24 | MINECRAFT_1_7_6(5, 25 | "1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10"), 26 | MINECRAFT_1_8(47, 27 | "1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9"), 28 | MINECRAFT_1_9(107, "1.9"), 29 | MINECRAFT_1_9_1(108, "1.9.1"), 30 | MINECRAFT_1_9_2(109, "1.9.2"), 31 | MINECRAFT_1_9_4(110, "1.9.3", "1.9.4"), 32 | MINECRAFT_1_10(210, "1.10", "1.10.1", "1.10.2"), 33 | MINECRAFT_1_11(315, "1.11"), 34 | MINECRAFT_1_11_1(316, "1.11.1", "1.11.2"), 35 | MINECRAFT_1_12(335, "1.12"), 36 | MINECRAFT_1_12_1(338, "1.12.1"), 37 | MINECRAFT_1_12_2(340, "1.12.2"), 38 | MINECRAFT_1_13(393, "1.13"), 39 | MINECRAFT_1_13_1(401, "1.13.1"), 40 | MINECRAFT_1_13_2(404, "1.13.2"), 41 | MINECRAFT_1_14(477, "1.14"), 42 | MINECRAFT_1_14_1(480, "1.14.1"), 43 | MINECRAFT_1_14_2(485, "1.14.2"), 44 | MINECRAFT_1_14_3(490, "1.14.3"), 45 | MINECRAFT_1_14_4(498, "1.14.4"), 46 | MINECRAFT_1_15(573, "1.15"), 47 | MINECRAFT_1_15_1(575, "1.15.1"), 48 | MINECRAFT_1_15_2(578, "1.15.2"), 49 | MINECRAFT_1_16(735, "1.16"), 50 | MINECRAFT_1_16_1(736, "1.16.1"), 51 | MINECRAFT_1_16_2(751, "1.16.2"), 52 | MINECRAFT_1_16_3(753, "1.16.3"), 53 | MINECRAFT_1_16_4(754, "1.16.4", "1.16.5"), 54 | MINECRAFT_1_17(755, "1.17"), 55 | MINECRAFT_1_17_1(756, "1.17.1"), 56 | MINECRAFT_1_18(757, "1.18", "1.18.1"), 57 | MINECRAFT_1_18_2(758, "1.18.2"), 58 | MINECRAFT_1_19(759, "1.19"), 59 | MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), 60 | MINECRAFT_1_19_3(761, "1.19.3"), 61 | MINECRAFT_1_19_4(762, "1.19.4"), 62 | MINECRAFT_1_20(763, "1.20", "1.20.1"), 63 | MINECRAFT_1_20_2(764, "1.20.2"), 64 | MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), 65 | MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), 66 | MINECRAFT_1_21(767, "1.21", "1.21.1"), 67 | MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"), 68 | MINECRAFT_1_21_4(769, "1.21.4"); 69 | 70 | public static final VanillaProtocolVersion[] VALUES = values(); 71 | 72 | private static final Map PROTOCOL_VERSION_MAP = new HashMap<>(); 73 | private static final Map PROTOCOL_VERSION_NAME_MAP = new HashMap<>(); 74 | 75 | static { 76 | for (val value : VALUES) { 77 | PROTOCOL_VERSION_MAP.put(value.protocol, value); 78 | 79 | for (val name : value.names) PROTOCOL_VERSION_NAME_MAP.put(name, value); 80 | } 81 | } 82 | 83 | int protocol; 84 | String[] names; 85 | 86 | VanillaProtocolVersion(final int protocol, final String... names) { 87 | this.protocol = protocol; 88 | this.names = names; 89 | } 90 | 91 | public static @NotNull VanillaProtocolVersion versionByProtocol(final int protocol) { 92 | return PROTOCOL_VERSION_MAP.getOrDefault(protocol, UNKNOWN); 93 | } 94 | 95 | public static @NotNull VanillaProtocolVersion versionByName(final String name) { 96 | return PROTOCOL_VERSION_NAME_MAP.getOrDefault(name, UNKNOWN); 97 | } 98 | 99 | public @NotNull String firstName() { 100 | if (names.length == 0) 101 | throw new IllegalStateException("Array is empty => cannot get first name"); 102 | 103 | return names[0]; 104 | } 105 | 106 | public VanillaProtocolVersion next() { 107 | return (ordinal() + 1 < VALUES.length) ? VALUES[ordinal() + 1] : null; 108 | } 109 | } -------------------------------------------------------------------------------- /vanilla-protocol/src/test/java/by/milansky/protocol/vanilla/registry/VanillaStateRegistryTest.java: -------------------------------------------------------------------------------- 1 | package by.milansky.protocol.vanilla.registry; 2 | 3 | import by.milansky.protocol.api.state.ProtocolState; 4 | import by.milansky.protocol.vanilla.standard.ClientboundTeam; 5 | import by.milansky.protocol.vanilla.standard.ServerboundHandshake; 6 | import by.milansky.protocol.vanilla.version.VanillaProtocolVersion; 7 | import lombok.val; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | 13 | /** 14 | * @author milansky 15 | */ 16 | class VanillaStateRegistryTest { 17 | @Test 18 | void standardRegistry_shouldCreateValidHandshakeRegistry() { 19 | val stateRegistry = VanillaStateRegistry.standardRegistry(); 20 | val registry = stateRegistry.serverbound(ProtocolState.HANDSHAKE); 21 | 22 | assertNotNull(registry); 23 | 24 | for (val version : VanillaProtocolVersion.VALUES) { 25 | if (version == VanillaProtocolVersion.UNKNOWN) continue; 26 | 27 | val packetClass = registry.getPacketById(version, 0x00); 28 | 29 | assertNotNull(packetClass); 30 | assertEquals(packetClass, ServerboundHandshake.class); 31 | } 32 | } 33 | 34 | @Test 35 | void standardRegistry_shouldCreateValidPlayRegistry() { 36 | val stateRegistry = VanillaStateRegistry.standardRegistry(); 37 | val registry = stateRegistry.clientbound(ProtocolState.PLAY); 38 | 39 | assertNotNull(registry); 40 | 41 | val packetMapping = registry.getMapping(ClientboundTeam.class); 42 | 43 | assertNotNull(packetMapping); 44 | 45 | assertEquals(packetMapping.get(VanillaProtocolVersion.MINECRAFT_1_8), 0x3E); 46 | } 47 | } 48 | 49 | --------------------------------------------------------------------------------