Skip to content
PDF

Custom Messages

Whilst we can send raw messages using GeyserLink it is easier to define a Message and Response class that will serialize and unserialize the responses.

Custom Message and Response

Let's create a PlayerQueryMessage and PlayerQueryResponse that returns the number of players on the server. It is up to you as to which server will respond to this but for this example we will assume the GeyserLink plugin is on a Spigot server and on a Geyser proxy and that either side may send the message to get a response from the other side.

PlayerQueryMessage

Example

@Getter
@ToString
public class PlayerQueryMessage extends WrappedMessage {
    private final String channel = "myPlugin:command";
    private final String subChannel = "player-query";

    public PlayerQueryMessage(String data) {
        super();
    }

    public PlayerQueryMessage(JsonNode node) {
        super(node);
    }

    @Override
    protected ObjectNode serialize() {
        return super.serialize();
    }
}

Here we define the channel and subchannel that this message will use. The rest is mainly boilerplate dealing with serializing or deserializing the object.

Info

Messages make use of JSON to contain their structure in a packet. This means that when serializing an object it will call the serialize method that will add to an ObjectNode any data relevant for the object.

To deserialize a constructor taking a JsonNode is used which then pulls from that any relevant fields for the object.

PlayerQueryResponse

Example

@Getter
@ToString
public class PlayerQueryResponse extends WrappedResponse {
    private int count;

    public PlayerQueryResponse(int count) {
        super();

        this.count = count;
    }

    public PlayerQueryResponse(JsonNode node) {
        super(node);
        this.count = node.get("count").asInt();
    }

    @Override
    protected ObjectNode serialize() {
        return super.serialize()
                .put("count", count);
    }
}

This one is a bit more interesting as it has a data field count. We have to deal with how to deserialize from a JsonNode and how to serialize to an ObjectNode.

Note

The Response does not need to define a channel or subchannel.

Putting it together

Now you can send a PlayerQueryMessage by doing something like this:

Example

GeyserLink.getInstance().sendMessage(player, new PlayerQueryMessage())
    .onResponse(PlayerQueryResponse.class, (result, signed, response) -> {
        getLogger(String.format("The server has %d players on it", response.getCount()));
    });

Note

When sending a raw message you only have the fields result and signed. When using a wrapped message you also get a field for the message itself. In the above case response will be a PlayerQueryResponse object and will be deserialized from the signed object but we still get the signed object as it has data on it that could be useful.

Message Event

Whichever server is responding to the message will need to register an event listener for the message. The following is a simple example for a Spigot server.

Example

@EventHandler
public void onGeyserLinkMessage(GeyserLinkMessageEvent event) {
    if (!event.getSignedMessage().getMessage().getChannel().equals("myPlugin:command")) {
        return;
    }

    switch(event.getSignedMessage().getMessage().getSubChannel()) {
        case "player-query":
            GeyserLink.getInstance().sendResponse(event.getPlayer(), event.getSignedMessage().getMessage(),
                    new PlayerQueryResponse(plugin.getServer().getOnlinePlayers().size()));
            break;
    }
}

For completion sake the following is for the GeyserMC server, note how similar it is.

Example

@Event
public void onGeyserLinkMessage(GeyserLinkMessageEvent event) {
    if (!event.getSignedMessage().getMessage().getChannel().equals("myPlugin:command")) {
        return;
    }

    switch(event.getSignedMessage().getMessage().getSubChannel()) {
        case "player-query":
            GeyserLink.getInstance().sendResponse(event.getSession(), event.getSignedMessage().getMessage(),
                    new PlayerQueryResponse(plugin.getConnector().getPlayers().values().size()));
            break;
    }
}

Security

You may have noticed an issue with the previous example. Anyone could connect to a server and either spoof GeyserLink messages or run their own GeyserLink plugin on a proxy and thus the player count must come from a trusted partner. We also don't want a random person being able to query the player count.

Note

If possible try support untrusted clients as well. If the player count in this example is not confidential then there may be no reason not to still allow queries. Also if the message is coming from a client side mod then it will be untrusted by default.

To solve this we need to check on both sides. On the client side the following would only accept trusted responses:

Example

GeyserLink.getInstance().sendMessage(player, new PlayerQueryMessage())
    .onResponse(PlayerQueryResponse.class, (result, signed, response) -> {
        if (signed.isTrusted()) {
            getLogger(String.format("The server has %d players on it", response.getCount()));
        }
    });

The server could be updated as follows:

Example

@EventHandler
public void onGeyserLinkMessage(GeyserLinkMessageEvent event) {
    if (!event.getSignedMessage().getMessage().getChannel().equals("myPlugin:command")) {
        return;
    }

    switch(event.getSignedMessage().getMessage().getSubChannel()) {
        case "player-query":
            if (event.getSignedMessage().isTrusted()) {
                GeyserLink.getInstance().sendResponse(event.getPlayer(), event.getSignedMessage().getMessage(),
                        new PlayerQueryResponse(plugin.getServer().getOnlinePlayers().size()));
            }
            break;
    }
}

Last update: