platform-velocity - Velocity Plugin (Proxy Relay)
platform-velocity is a thin layer whose only job is to relay cross-server global chat.
Its substance is just LunaticChat / BuildInfo / two files under messaging/; it has no command classes. Note that while /lcv is a Velocity-related feature, the command implementation lives on the platform-paper side.
Why the Velocity side is thin
The protocol definition is held by engine, and all chat state (channels, DMs, settings) lives on the Paper side. The only responsibility left to Velocity is "distribute a received global chat message to the other servers", so this layer is intentionally kept thin. Neither platform owns the protocol; both depend on engine as equal peers (see the Design Overview).
Lifecycle
LunaticChat(@Plugin) — receivesProxyServer/Logger/PluginContainervia a Guice@Injectconstructor- The
versionin the@Pluginannotation is fixed at"0.0.0"and is not used at runtime. The real version is obtained fromvelocity-plugin.jsonviaPluginContainer.description.version(startup fails if it is missing) @Subscribe onProxyInitializationcreatesCrossServerChatRelay, then creates andinitialize()sPluginMessageHandlerwith it injected@Subscribe onProxyShutdowncallsmessageHandler.shutdown()
Message reception and dispatch
PluginMessageHandler handles reception on the lunaticchat:main channel. In initialize() it calls channelRegistrar.register(CHANNEL) and subscribes to events.
@Subscribe onPluginMessage processing:
- Ignore if
event.identifier != CHANNEL - Warn and discard if the source is not a
ServerConnection - Branch on the result of
PluginMessageCodec.decode()withwhen Handshake→ check compatibility and reply withHandshakeResponse/StatusRequest→ reply withStatusResponse/GlobalChatMessage→ delegate to the relay / otherwise (a Velocity-originated response type) → warn only
Trust boundary: rejecting client-originated messages
The check for whether the source is a ServerConnection is not just a type guard — it is a trust boundary.
Velocity plugin messages can arrive not only from backend servers but also from clients. By rejecting anything other than a backend connection here, it prevents clients from directly injecting global chat or forged handshakes. Only messages from trusted server connections are relayed.
Handshake handling
On receiving a Handshake, it judges compatibility via the engine's ProtocolVersion.isCompatible(major, minor).
- Compatible — reply with
HandshakeResponsewherecompatible=true - Incompatible — reply with
compatible=falseand an error string carrying both the Paper-side and Velocity-side versions
HandshakeResponse / StatusResponse always carry Velocity's own ProtocolVersion (MAJOR / MINOR / PATCH), so the Paper side can learn the peer's protocol from the response.
Cross-server relay
CrossServerChatRelay.relayGlobalMessage(message, sourceServer) is the heart of the relay.
server.allServers
.filter { it != sourceServer } // exclude the source
.forEach { it.sendPluginMessage(CHANNEL, encoded) }It excludes the source server and broadcasts to all remaining backends (stage one of echo prevention). The relay count is logged.
What gets relayed / what stays local
Keeping the relay scope minimal is a key design point.
- The only thing Velocity relays to other servers is the
GlobalChatMessage Handshake/HandshakeResponse/StatusRequest/StatusResponsecomplete between Velocity and a single Paper, and are not forwarded- DM and channel chat are never sent to Velocity at all (they complete locally within Paper)
Two-stage echo/loop prevention
To keep global chat from being displayed multiple times through relay loops, it is prevented in two places.
- Velocity side — broadcast excluding the source server
- Paper side — a dedup LRU cache keyed by
messageId(TTL 60s). The sender also registers its ownmessageIdright after generation to prevent an echo on its own server
Message flow
- Paper sends a
Handshake(its own protocol version), triggered by a player connecting - Velocity judges with
ProtocolVersion.isCompatibleand replies withHandshakeResponse→ if compatible, the Paper side becomesCONNECTED - A player sends global chat (no active channel, or a
!prefix) → Paper sends aGlobalChatMessage(a newmessageId) to Velocity and displays normal chat on the source - Velocity relays to all backends except the source
- Each Paper receives it → dedups by
messageId→ formats withcrossServerGlobalChatFormatand delivers to all players
Implementation notes
- The
pluginparameter ofPluginMessageHandleris typedAnybecause Velocity'sEventManager.register()takes anObject(the API itself isn't type-safe, so making it generic offers little benefit). - To run cross-server chat, Velocity's
velocity.tomlneedsbungee-plugin-message-channel=true(plugin messaging enabled).
Related
- Design Overview
- engine - Shared Kernel — protocol details
- platform-paper - Paper / Folia Plugin — the Paper-side counterpart