Flectone LogoFlectonePulse

Proxy and multiserver

Passing data between the servers of a network through ProxySender, ProxyRegistry, the ProxyMessageEvent event and payload reading

When a server sits in a network under BungeeCord, Velocity or Redis, FlectonePulse syncs messages and caches between the servers over its own channel

The sending server writes a message with a module tag and arbitrary bytes of data

The Proxy transport delivers the bytes to the other servers of the network

On each server ProxyMessageEvent fires, listeners read the data and handle it

The handler sets processed to true so that the message does not count as lost

The channel tag is built from the module name. A ModuleName.COMMAND_COIN.toProxyTag() call gives FlectonePulse:COMMAND_COIN


Checking the network

Package net.flectone.pulse.platform.registry
import net.flectone.pulse.platform.registry.ProxyRegistry;

ProxyRegistry proxyRegistry = flectonePulse.get(ProxyRegistry.class);

if (!proxyRegistry.hasEnabledProxy()) {
    // the network is not set up, there is nothing to sync
    return;
}
MethodWhat it does
hasEnabledProxy()Checks whether at least one transport works
hasEnabledProxy(Predicate<Proxy>)Checks whether a working transport matches a condition
getProxies()Gives every registered transport
registry(Proxy)Adds your own transport
reload()Reopens the transports for the current configuration

The Proxy interface itself describes one transport through the methods isEnable(), onEnable(), onDisable() and sendMessage(FEntity, ModuleName, byte[])


Sending

Package net.flectone.pulse.platform.sender
import net.flectone.pulse.constant.ModuleName;
import net.flectone.pulse.platform.sender.ProxySender;

ProxySender proxySender = flectonePulse.get(ProxySender.class);

// a simple notice with no data
proxySender.send(fPlayer, ModuleName.COMMAND_COIN);

// with extra data
proxySender.send(fPlayer, ModuleName.COMMAND_COIN, output -> {
    output.writeInt(percent);
    output.writeUTF("extra text");
});
MethodWhat it does
send(FEntity, ModuleName)Sends a message with no data
send(FEntity, ModuleName, ProxyDataConsumer<DataOutputStream>)Sends a message with data
send(FEntity, ModuleName, ProxyDataConsumer<DataOutputStream>, UUID)The same, but with a set message identifier
send(ModuleName, EventMetadata, MessageContext)Sends a whole module message

Every method gives true when the message left through at least one transport

Information

Most of the time you do not need the proxy directly. When you send a message through MessageDispatcher, a .proxy() or .proxy(output -> ...) call in the EventMetadata builder is enough, the pipeline does the rest

messageDispatcher.dispatch(ModuleName.COMMAND_COIN, EventMetadata.builder()
        .range(Range.Type.PROXY)
        .messageContext(receiver -> /* ... */)
        .proxy(output -> {
            output.writeInt(percent);
            output.writeInt(tps);
        }) // the data travels with the message
        .build()
);

Receiving

Data is read in a ProxyMessageEvent handler through ProxyPayload, strictly in the order it was written

import net.flectone.pulse.annotation.Pulse;
import net.flectone.pulse.constant.ModuleName;
import net.flectone.pulse.listener.PulseListener;
import net.flectone.pulse.model.event.Event;
import net.flectone.pulse.model.event.message.ProxyMessageEvent;
import net.flectone.pulse.util.payload.ProxyPayload;

import java.io.IOException;

public class MyProxyListener implements PulseListener {

    @Pulse
    public Event onProxyMessage(ProxyMessageEvent event) throws IOException {
        // somebody handled the message already
        if (event.processed()) return event;

        // only one module interests us
        if (event.name() != ModuleName.COMMAND_COIN) return event;

        try (ProxyPayload payload = event.openPayload()) {
            int percent = payload.readInt();
            int tps = payload.readInt();

            getLogger().info("From server " + event.server() + " came " + percent + " and " + tps);
        }

        return event.withProcessed(true);
    }

}

ProxyPayload methods

MethodWhat it reads
readString()String
readInt()int
readLong()long
readBoolean()boolean
readUUID()UUID
readAllBytes()The remaining bytes

ProxyPayload implements Closeable, so open it in a try-with-resources block

Attention

The reading order has to match the writing order. When the sender wrote writeInt and then writeUTF, you have to read readInt() and then readString(). Any mismatch gives you an error or garbage

Warning

When no listener marks a message as handled, FlectonePulse writes a warning to the console. Return event.withProcessed(true) even where no handling is needed


The usual handler shape

@Pulse
public Event onProxyMessage(ProxyMessageEvent event) throws IOException {
    if (event.processed()) return event;
    if (event.name() != ModuleName.COMMAND_COIN) return event;

    // the module is off, no handling needed, but the message counts as read
    if (!moduleController.isEnable(coinModule)) return event.withProcessed(true);

    // the module is not set up for the network
    if (!coinModule.config().range().is(Range.Type.PROXY)) return event.withProcessed(true);

    try (ProxyPayload payload = event.openPayload()) {
        int percent = payload.readInt();

        // pass the message on to the local players
        messageDispatcher.dispatch(coinModule, EventMetadata.builder()
                .range(Range.get(Range.Type.SERVER))
                .messageContext(receiver -> MessageContext.builder()
                        .uuid(event.uuid())   // keep the shared identifier
                        .sender(event.sender())
                        .receiver(receiver)
                        .message(coinModule.replaceResult(receiver, percent))
                        .build()
                )
                .build()
        );
    }

    return event.withProcessed(true);
}
Attention

Set Range.Type.SERVER when you pass a message on, not PROXY, otherwise the message travels in circles


Registering the handler

Proxy listeners are worth registering only when the network is really set up

if (proxyRegistry.hasEnabledProxy()) {
    listenerRegistry.registerPermanent(new MyProxyListener());
}
Note

The internal machinery uses this same channel, it drops the caches of punishments, settings, colors and skins through it. The data travels in binary form, so keep the size small. How to set the network up is written on the proxy page

Last update August 11, 2026
Edit on Github

On this page