Conversation
onebeastchris
left a comment
There was a problem hiding this comment.
This looks very promising! Left some notes. A few more things that don't fit anywhere:
- it might be worth to separate listening / receiving packet listeners? This could be used to avoid extra processing for packets that are sent both ways (where one is only interested in one direction)
- Currently; some bedrock packet (de)serializers are modified by Geyser. This would result in incorrect values for some packets... do we want to note that / have a mechanism to disable that?
api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/event/java/ServerReceiveNetworkMessageEvent.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/ExtensionNetworkChannel.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/message/MessageFactory.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/message/Message.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/network/GeyserNetworkManager.java
Outdated
Show resolved
Hide resolved
|
Another thing that came up recently is whether we'd allow forwarding packets to a backend server - even without an extension present. This could be combined with this PR, assuming we'd want to do that.. thoughts? |
|
😢😢😢😢 |
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a comprehensive networking API for Geyser that enables extensions to send and listen for plugin messages, as well as intercept and send packets. The API provides a flexible event-driven architecture with support for message priorities, pipeline tags, and both custom and Cloudburst protocol-based packet handling.
Key Changes
- Adds networking API interfaces and implementations for plugin messages and packet handling
- Introduces event system for registering network channels with configurable handlers and priorities
- Provides message encoding/decoding infrastructure with built-in and custom data types
Reviewed Changes
Copilot reviewed 44 out of 44 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java | Defines event for registering network channels with builder pattern for handlers |
| api/src/main/java/org/geysermc/geyser/api/network/*.java | Core networking API interfaces (NetworkChannel, NetworkManager, MessageDirection) |
| api/src/main/java/org/geysermc/geyser/api/network/message/*.java | Message handling infrastructure (Message, MessageBuffer, MessageCodec, DataType, handlers) |
| core/src/main/java/org/geysermc/geyser/network/*.java | Implementation of network channels and manager with packet/message handling |
| core/src/main/java/org/geysermc/geyser/network/message/*.java | ByteBuf-based message buffer and codec implementations |
| core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java | Integration of network manager into packet sending flow |
| core/src/main/java/org/geysermc/geyser/translator/protocol/java/*.java | Plugin message registration and handling for custom payloads |
| core/src/test/java/org/geysermc/geyser/network/MessageRegistrationOrderTest.java | Tests for message handler priority and pipeline ordering |
| core/src/test/java/org/geysermc/geyser/util/*.java | Refactored test utilities to common util package |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/network/message/ByteBufMessageBuffer.java
Show resolved
Hide resolved
.../src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/network/message/ByteBufCodecLE.java
Show resolved
Hide resolved
| try { | ||
| serializer.deserialize(buffer.buffer(), session.getUpstream().getCodecHelper(), this.packet); | ||
| } catch (Exception e) { | ||
| throw new PacketSerializeException("Error whilst deserializing " + this.packet, e); |
There was a problem hiding this comment.
Might be neat to have our own exception type in the API?
There was a problem hiding this comment.
What would the usecase be?
There was a problem hiding this comment.
Being able to catch specific exceptions to e.g. display a warning :p
We have custom exception types for other api things too; like the ResourcePackRegistrationException
| this.session.sendUpstreamPacket(packetMessage.packet()); | ||
| } else if (packetBase instanceof JavaPacketMessage<?> packetMessage) { | ||
| this.session.sendDownstreamPacket(packetMessage.packet()); | ||
| } else if (packetBase instanceof Message.Packet packet) { |
There was a problem hiding this comment.
Continuing from #5685 (comment) - we could split Message.Packet into Message.BedrockPacket (and therefore leave a spot open for Message.JavaPacket if we desire to add it in the future?)
There was a problem hiding this comment.
I'm not a huge fan of that from the API side. Message.BedrockPacket.of(AnimatePacket::new)) for instance just doesn't seem great. If we are to introduce a Java packet system, it's probably best to define in the NetworkChannel#packet method.
There was a problem hiding this comment.
IMO introducing a Java packet system that isn't a Message.Packet would be more confusing :p
Can we add a field / enum or something to Message.Packet to indicate whether it is Bedrock or Java?
|
I haven't really looked into what's going on here yet, but for emotecraft it's very important to get byte[] from the packet (or at least ByteBuffer) And, of course, sending packets during the configuration state |
At what point during the config stage? During login or switching into the state while previously in the game state? |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
api/src/main/java/org/geysermc/geyser/api/network/message/MessagePriority.java
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/network/NetworkChannel.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
Outdated
Show resolved
Hide resolved
.../src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
At the state where fabric/neoforge mods are configured |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public <T extends MessageBuffer> void send(@NonNull NetworkChannel channel, @NonNull Message<T> message, @NonNull MessageDirection direction) { | ||
| if (channel.isPacket() && message instanceof Message.PacketBase<T> packetBase) { | ||
| if (packetBase instanceof BedrockPacketMessage<?> packetMessage) { | ||
| this.session.sendUpstreamPacket(packetMessage.packet()); | ||
| } else if (packetBase instanceof JavaPacketMessage<?> packetMessage) { | ||
| this.session.sendDownstreamPacket(packetMessage.packet()); | ||
| } else if (packetBase instanceof Message.Packet packet) { | ||
| PacketChannel packetChannel = (PacketChannel) channel; | ||
| int packetId = packetChannel.packetId(); | ||
|
|
||
| ByteBufMessageBuffer buffer = ByteBufCodec.INSTANCE_LE.createBuffer(); | ||
| packet.encode(buffer); | ||
|
|
||
| BedrockCodec codec = this.session.getUpstream().getSession().getCodec(); | ||
| BedrockCodecHelper helper = this.session.getUpstream().getCodecHelper(); | ||
|
|
||
| BedrockPacket bedrockPacket = codec.tryDecode(helper, buffer.buffer(), packetId); | ||
| if (bedrockPacket == null) { | ||
| throw new IllegalArgumentException("No Bedrock packet definition found for packet ID: " + packetId); | ||
| } | ||
|
|
||
| // Clientbound packets are sent upstream, serverbound packets are sent downstream | ||
| if (direction == MessageDirection.CLIENTBOUND) { | ||
| this.session.sendUpstreamPacket(bedrockPacket); | ||
| } else { | ||
| this.session.getUpstream().getSession().getPacketHandler().handlePacket(bedrockPacket); | ||
| } | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| MessageDefinition<T, Message<T>> definition = this.findMessageDefinition(channel, message); | ||
|
|
||
| T buffer = definition.codec.createBuffer(); | ||
| message.encode(buffer); | ||
|
|
||
| ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket( | ||
| Key.key(channel.identifier().toString()), | ||
| buffer.serialize() | ||
| ); | ||
|
|
||
| this.session.sendDownstreamPacket(packet); | ||
| } |
There was a problem hiding this comment.
The send() method always sends messages as ServerboundCustomPayloadPacket (line 180-185), regardless of the direction parameter. This means that messages intended to be CLIENTBOUND will also be sent to the server, which is incorrect. The direction parameter should determine whether to send upstream (to client) or downstream (to server).
| a.priority(direction) | ||
| )); | ||
| ordered.addAll(unpinnedBlock); | ||
| unpinnedBlock.clear(); |
There was a problem hiding this comment.
The line unpinnedBlock.clear() after adding all items to ordered is unnecessary since the list is not reused after this point. While not a bug, removing this line would improve code clarity.
| unpinnedBlock.clear(); |
| List<NetworkChannel> identifiedChannels = new ArrayList<>(); | ||
| for (NetworkChannel registeredChannel : channels) { | ||
| if (!registeredChannel.isPacket() && registeredChannel.identifier().toString().equals(channel)) { | ||
| identifiedChannels.add(registeredChannel); | ||
| } | ||
| } | ||
|
|
||
| if (identifiedChannels.isEmpty()) { |
There was a problem hiding this comment.
Inefficient channel lookup: The code fetches all registered channels and then iterates through them to find matching ones. Consider maintaining a map from channel identifier to NetworkChannel in GeyserNetworkManager for O(1) lookup instead of O(n) iteration.
| List<NetworkChannel> identifiedChannels = new ArrayList<>(); | |
| for (NetworkChannel registeredChannel : channels) { | |
| if (!registeredChannel.isPacket() && registeredChannel.identifier().toString().equals(channel)) { | |
| identifiedChannels.add(registeredChannel); | |
| } | |
| } | |
| if (identifiedChannels.isEmpty()) { | |
| // Build a map from identifier string to NetworkChannel for O(1) lookup | |
| java.util.Map<String, List<NetworkChannel>> channelMap = new java.util.HashMap<>(); | |
| for (NetworkChannel registeredChannel : channels) { | |
| if (!registeredChannel.isPacket()) { | |
| String identifier = registeredChannel.identifier().toString(); | |
| channelMap.computeIfAbsent(identifier, k -> new ArrayList<>()).add(registeredChannel); | |
| } | |
| } | |
| List<NetworkChannel> identifiedChannels = channelMap.get(channel); | |
| if (identifiedChannels == null || identifiedChannels.isEmpty()) { |
| String channels = registeredChannels | ||
| .stream() | ||
| .filter(channel -> !channel.isPacket()) | ||
| .map(channel -> channel.identifier().namespace() + ":" + channel.identifier().path()) |
There was a problem hiding this comment.
The channel identifier is concatenated using namespace() + ":" + path() instead of using the toString() method. While this works, it's better to use channel.identifier().toString() for consistency with how it's used elsewhere (e.g., in JavaCustomPayloadTranslator line 165).
| .map(channel -> channel.identifier().namespace() + ":" + channel.identifier().path()) | |
| .map(channel -> channel.identifier().toString()) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 51 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java:1
- Corrected spelling of 'Cloud' to 'Could'.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
api/src/main/java/org/geysermc/geyser/api/network/PacketChannel.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
Outdated
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java
Outdated
Show resolved
Hide resolved
| * However, it should be noted that if the specified tag does not exist at the time of registration, | ||
| * the handler will be added to the end of the pipeline without throwing an error. |
There was a problem hiding this comment.
Ideally this would be configurable... maybe with optionallyBefore (or alternatively e.g. requiredBefore) to forcibly throw if the specified handler isn't found?
Pipeline order can be a tricky thing, detecting breaks would be difficult if it'll fall back silently
There was a problem hiding this comment.
This seems quite niche IMO - what would the usecase here be?
api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java
Show resolved
Hide resolved
api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDefineNetworkChannelsEvent.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/geysermc/geyser/network/NetworkDefinitionBuilder.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 51 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 51 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (Message<T> message : messages) { | ||
| for (MessageDefinition<?, ?> def : ordered) { | ||
| if (!(channel instanceof BaseNetworkChannel base)) { | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
handleMessages only processes channels that are BaseNetworkChannel (otherwise it silently skips handling). Since the public API accepts any NetworkChannel implementation, consider validating this at registration time (and throwing a clear exception) or refactoring so GeyserNetwork doesn’t depend on the concrete channel implementation for type checks.
| for (Message<T> message : messages) { | |
| for (MessageDefinition<?, ?> def : ordered) { | |
| if (!(channel instanceof BaseNetworkChannel base)) { | |
| continue; | |
| } | |
| if (!(channel instanceof BaseNetworkChannel base)) { | |
| throw new IllegalArgumentException("Unsupported NetworkChannel implementation: " | |
| + channel.getClass().getName() + "; expected BaseNetworkChannel"); | |
| } | |
| for (Message<T> message : messages) { | |
| for (MessageDefinition<?, ?> def : ordered) { |
|
|
||
| @NonNull | ||
| public <T extends MessageBuffer> List<Message<T>> createMessages(@NonNull NetworkChannel channel, @NonNull T buffer) { | ||
| return this.createMessages0(channel, def -> buffer); |
There was a problem hiding this comment.
createMessages(channel, buffer) passes the same buffer instance to every registered definition. If more than one definition exists for a channel, the first message factory will advance the reader index and later factories will decode from the wrong position (often producing invalid messages). Consider creating a fresh buffer per definition (e.g., from the original byte[]), or resetting/copying the buffer before each decode.
| return this.createMessages0(channel, def -> buffer); | |
| byte[] data = buffer.toByteArray(); | |
| return (List<Message<T>>) (List<?>) this.createMessages(channel, data); |
| for (MessageDefinition<?, ?> def : definitions) { | ||
| MessageDefinition<T, M> definition = (MessageDefinition<T, M>) def; | ||
| T buffer = creator.apply(definition); | ||
| M message = definition.createMessage(buffer); | ||
| if (message instanceof BedrockPacketMessage<?> packetMessage) { |
There was a problem hiding this comment.
createMessages0 decodes one message per registered definition, but handleMessages then iterates every decoded message over every definition, causing handlers to run multiple times for a single incoming payload (O(n^2) calls) and potentially decoding the same payload repeatedly. Decoding should generally be done once per incoming payload, with all handlers invoked against that single decoded message instance.
| ByteBuf buffer = Unpooled.buffer(); | ||
| packet.serialize(buffer); | ||
| messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE, buffer))); | ||
| } |
There was a problem hiding this comment.
The ByteBuf allocated via Unpooled.buffer() is never released after packet.serialize(buffer). This risks leaking direct buffers. Use try/finally to release() the buffer after message decoding/handling, consistent with other parts of the codebase that explicitly release Unpooled buffers.
| /** | ||
| * Creates a custom priority in the range [-100, 100]. | ||
| * | ||
| * @param value the priority value | ||
| * @return the priority | ||
| */ | ||
| @NonNull | ||
| public static MessagePriority of(int value) { | ||
| if (value >= 75) return FIRST; | ||
| if (value >= 25) return EARLY; | ||
| if (value <= -75) return LAST; | ||
| if (value <= -25) return LATE; | ||
| return NORMAL; |
There was a problem hiding this comment.
The Javadoc for MessagePriority#of(int) says it "creates a custom priority", but the implementation actually buckets the value into one of the predefined enum constants (FIRST/EARLY/NORMAL/LATE/LAST). Update the Javadoc to reflect the bucketing behavior, or change the API if true custom numeric priorities are intended.
| * Reads the message from the provided buffer. | ||
| * | ||
| * @param buffer the buffer to read from |
There was a problem hiding this comment.
The Message#encode Javadoc says it "Reads the message from the provided buffer", but the method name/signature indicate it writes/encodes the message into the buffer. This is likely inverted wording and can confuse API consumers implementing messages.
| * Reads the message from the provided buffer. | |
| * | |
| * @param buffer the buffer to read from | |
| * Encodes this message into the provided buffer. | |
| * | |
| * @param buffer the buffer to write to |
| /** | ||
| * Creates a new packet message from the given packet object and direction. | ||
| * | ||
| * @param packet the packet object to create the message from | ||
| * @return a new packet message | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| @NonNull | ||
| static <T extends MessageBuffer, P> PacketWrapped<T, P> of(@NonNull Object packet) { | ||
| return (PacketWrapped<T, P>) GeyserApi.api().provider(PacketWrapped.class, packet); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new packet message from the given packet object and direction. | ||
| * | ||
| * @param packetSupplier a supplier that provides the packet object to create the message from | ||
| * @return a new packet message | ||
| */ | ||
| @NonNull | ||
| static <T extends MessageBuffer, P> MessageFactory<T, PacketWrapped<T, P>> of(@NonNull Supplier<P> packetSupplier) { | ||
| return buffer -> of(packetSupplier.get()); | ||
| } |
There was a problem hiding this comment.
The Javadoc for Message.Packet.of(...) methods repeatedly mentions "and direction", but none of these overloads take a direction parameter. Consider removing the direction wording to avoid confusion about how direction is determined/applied.
| ServerboundCustomPayloadPacket packet = new ServerboundCustomPayloadPacket( | ||
| Key.key(channel.identifier().toString()), | ||
| buffer.serialize() | ||
| ); | ||
|
|
There was a problem hiding this comment.
Network#send ignores the direction parameter for non-packet (custom payload) channels and always sends a ServerboundCustomPayloadPacket downstream. This contradicts the API contract/docs that imply direction affects where the message is delivered; either enforce direction == SERVERBOUND here (e.g., throw/return) or implement clientbound injection behavior explicitly.
| ByteBuf buffer = Unpooled.buffer(); | ||
| definition.getSerializer().serialize(buffer, this.session.getUpstream().getCodecHelper(), packet); | ||
| messages.addAll(this.createMessages(channel, new ByteBufMessageBuffer(ByteBufCodec.INSTANCE_LE, buffer))); | ||
| } |
There was a problem hiding this comment.
The ByteBuf allocated via Unpooled.buffer() is never released. Since Netty may allocate direct buffers here, this can lead to memory leaks under leak detection / high throughput. Wrap serialization + message creation in a try/finally and release() the buffer once handlers are done (or use a heap buffer allocator explicitly).
Introduces a networking API to Geyser which can be used in extensions. This supports both sending and listening for plugin messages, and allows Geyser to intercept these, as well as intercepting and sending packets.
Plugin Messages
In order to start sending and listening for plugin messages, you need to tell Geyser which channels to listen for. This can be done by listening on the
SessionDefineNetworkChannelsEventand registering the channel for the connection. Firstly, we will go over how to send a custom message.Registering the channel
Example:
In the
definemethod, you also need to define the creator of the message that will be sent. This effectively turns the content from aMessageBufferinto your type. Then callingregisterwill actually register it into the network manager.As a recommended principle, this should be a
recordwith two constructors: one creating the object just using its values (an all-args constructor), then one for theMessageBuffer. It should also extendMessage.Simple.Example:
A
MessageBuffersupports both reading and writing, with built-inDataTypes for common values (int, string, long, varint, etc.). CustomDataTypes can easily be added withDataType#of.Sending the message
Now that your channel and its corresponding message creator is registered, it can now be either listened for, or sent out. This can easily be sent out by fetching the
Networkfrom aGeyserConnection, and running the#sendmethod.Example:
Note: When sending a message to the server, ensure the direction is
SERVERBOUNDas that will specify that the server should receive the message. If you were to useCLIENTBOUNDfor example, that would send it to the client.Now on your server, you can listen for this message! Here is an example using Bukkit:
Listening for messages
In some cases, it may be more desirable to do the opposite of what was shown above - sending information to your Geyser extension from a server. This too is supported! In that case, when registering the channel, you will need to register a clientbound handler inside your channel definition.
As an example:
And for plugin messages, that is about it!
Packets
This API also supports listening for packets. This works alongside the plugin messaging component to it. There are two methods: defining the packet structure using API, or using the Cloudburst API. Both are explained in more detail below.
Packet Structure Using API
When constructing your
NetworkChannel, a special method (and class) needs to be used:PacketChannel#java, orPacketChannel#bedrock. This creates aNetworkChannelthat is capable of handling packets for either of the two platforms.Example:
It is important to understand that these only work in Extensions. The
thisseen in the example represents an Extension instance, assuming the NetworkChannel is constructed in the extension.The following two parameters are also quite important: the packet ID and the actual message. These should correspond to real packets in the corresponding platform. Like above, this should be registered in the
SessionDefineNetworkChannelsEventin the same way.The
AnimateMessageon the other hand from the example, is the actual implementation of the animate packet in Bedrock. Here is how that looks:Now that the
AnimateMessagehas been created, we can now send it:Or if you wanted to listen for other players swinging their arms:
It is also worth noting that the
MessageHandler.Statecontrols the behavior of the packet once intercepted, meaning if you returnUNHANDLED, the message will still make it back to the client. Additionally, returningHANDLEDwill cause the client to never receive the packet.Important Note for Registering Java Packets
When registering packets for Java, it is a hard requirement that the
ProtocolStatealso be specified. This is because in Java Edition, there are multiple protocol states in which packet IDs differ, or don't exist at all (i.e. login packets do not exist in the game state).To specify the protocol state, simply call
protocolStateafter defining the packet inSessionDefineNetworkChannelsEventand set the state it belongs to. An example is provided below:Packet Structure Using Cloudburst Protocol Library
While the first example required a bit more manual work, in some cases that may be more desired for fine-turning the entire process. However, it is also possible to simply just use the raw packet objects themselves as provided by the Cloudburst Protocol Library.
In addition to depending on the Geyser API, depending on Cloudburst Protocol too is all that is required here - no need to rely on any Geyser internals!
Creating the channel is nearly identical as above, except rather than a custom
AnimateMessage, just use the packet directly like so:When registering the channel though, it's a tad different. This can be done like so:
And sending can be done like so:
However, if you want to listen for one of these, the process is very similar as earlier, except you need to obtain the packet from the message (as opposed to it being the message), like so:
Note that this is only an initial draft and subject to change! Testing and feedback are more than welcome.
A gist of what was covered above with slightly more can be found here: https://gist.github.com/Redned235/3cf05b62290fa9eec70d8b4f3fa22f67