Geyser Networking API
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 SessionDefineNetworkChannelsEvent and registering the channel for the connection. Firstly, we will go over how to send a custom message.
Registering the channel
Example:
public class NetworkExtension implements Extension {
private final NetworkChannel myChannel = NetworkChannel.of(this, "my_channel", MyMessage.class);
@Subscribe
public void onDefineChannels(SessionDefineNetworkChannelsEvent event) {
event.define(this.myChannel, MyMessage::new).register();
}
}
In the define method, you also need to define the creator of the message that will be sent. This effectively turns the content from a MessageBuffer into your type. Then calling register will actually register it into the network manager.
As a recommended principle, this should be a record with two constructors: one creating the object just using its values (an all-args constructor), then one for the MessageBuffer. It should also extend Message.Simple.
Example:
public record MyMessage(String name, int entityId) implements Message.Simple {
public MyMessage(MessageBuffer buffer) {
this(buffer.read(DataType.STRING), buffer.read(DataType.INT));
}
@Override
public void encode(MessageBuffer buffer) {
buffer.write(DataType.STRING, this.name);
buffer.write(DataType.INT, this.entityId);
}
}
A MessageBuffer supports both reading and writing, with built-in DataTypes for common values (int, string, long, varint, etc.). Custom DataTypes can easily be added with DataType#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 NetworkManager from a GeyserConnection, and running the #send method.
Example:
@Subscribe
public void onSessionJoin(SessionJoinEvent event) {
GeyserConnection connection = event.connection();
connection.networkManager().send(this.myChannel, new MyMessage(connection.name(), connection.entities().playerEntity().javaId()), MessageDirection.SERVERBOUND);
}
Note: When sending a message to the server, ensure the direction is SERVERBOUND as that will specify that the server should receive the message. If you were to use CLIENTBOUND for example, that would send it to the client.
Now on your server, you can listen for this message! Here is an example using Bukkit:
this.getServer().getMessenger().registerIncomingPluginChannel(this, "network_extension:my_channel", new PluginMessageListener() {
@Override
public void onPluginMessageReceived(@NotNull String s, @NotNull Player player, @NotNull byte[] bytes) {
System.out.println("Received over channel " + s);
System.out.println("Content: " + new String(bytes, StandardCharsets.UTF_8));
}
});
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:
@Subscribe
public void onDefineChannels(SessionDefineNetworkChannelsEvent event) {
event.define(this.myChannel, MyMessage::new)
.clientbound(message -> {
String name = message.name()
// ...
return MessageHandler.State.HANDLED; // Indicates this handler handled the message
})
.register();
}
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 needs to be used: NetworkChannel#packet. This creates a NetworkChannel that is capable of listening for packets.
Example:
private static final NetworkChannel ANIMATE_CHANNEL = NetworkChannel.packet("animate", 44, AnimateMessage.class);
The first parameter is the name of the packet - this is not particularly important and at this point in time can be anything. The following two are very important though: the packet ID and the actual message. These should correspond to real packets in Minecraft: Bedrock Edition. Like above, this should be registered in the SessionDefineNetworkChannelsEvent in the same way.
The AnimateMessage on the other hand from the example, is the actual implementation of the animate packet in Bedrock. Here is how that looks:
public record AnimateMessage(int type, long entityId, float rowingTime) implements Message.Packet {
@Override
public void encode(@NotNull MessageBuffer buffer) {
buffer.write(DataType.VAR_INT, this.type);
buffer.write(DataType.UNSIGNED_VAR_LONG, this.entityId);
buffer.write(DataType.FLOAT, this.rowingTime);
}
public static AnimateMessage decode(@NonNull MessageBuffer buffer) {
int type = buffer.read(DataType.VAR_INT);
long entityId = buffer.read(DataType.UNSIGNED_VAR_LONG);
float rowingTime = (type == 128 || type == 129) ? buffer.read(DataType.FLOAT) : 0.0f;
return new AnimateMessage(type, entityId, rowingTime);
}
}
Now that the AnimateMessage has been created, we can now send it:
@Subscribe
public void onSessionJoin(SessionJoinEvent event) {
event.connection().networkManager().send(ANIMATE_CHANNEL, new AnimateMessage(1, -1, 0f), MessageDirection.SERVERBOUND); // Swing main hand
}
Or if you wanted to listen for other players swinging their arms:
@Subscribe
public void onDefineChannels(SessionDefineNetworkChannelsEvent event) {
event.define(ANIMATE, AnimateMessage::decode)
.clientbound(message -> {
if (message.type() == 1) { // Swing arm
System.out.println("Entity " + message.entityId() + " swung their arm!");
}
return MessageHandler.State.UNHANDLED;
})
.register();
}
It is also worth noting that the MessageHandler.State controls the behavior of the packet once intercepted, meaning if you return UNHANDLED, the message will still make it back to the client. Additionally, returning HANDLED will cause the client to never receive the packet.
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:
private static final NetworkChannel ANIMATE_CHANNEL = NetworkChannel.packet("animate", 44, AnimatePacket.class);
When registering the channel though, it's a tad different. This can be done like so:
event.define(ANIMATE_CHANNEL, Message.Packet.of(AnimatePacket::new)).register();
And sending can be done like so:
AnimatePacket packet = new AnimatePacket();
packet.setAction(AnimatePacket.Action.SWING_ARM);
packet.setRowingTime(0.0f);
event.connection().networkManager().send(ANIMATE_CHANNEL, Message.Packet.of(packet), MessageDirection.CLIENTBOUND);
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:
@Subscribe
public void onDefineChannels(SessionDefineNetworkChannelsEvent event) {
event.define(ANIMATE, Message.Packet.of(AnimatePacket::new))
.clientbound(message -> {
AnimatePacket packet = message.packet();
if (packet.getAction() == AnimatePacket.Action.SWING_ARM) {
System.out.println("Entity " + packet.getRuntimeEntityId() + " swung their arm!");
}
return MessageHandler.State.HANDLED;
})
.register();
}
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
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?
😢😢😢😢
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
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?
At what point during the config stage? During login or switching into the state while previously in the game state?
At the state where fabric/neoforge mods are configured