JuByteNexus Bridge-Protokoll – Vollständige technische Referenz (v1)


1. Überblick

Das JuByteNexus Bridge-Protokoll verbindet Minecraft-Proxy- und Backend-Server mit dem zentralen Nexus-Core über WebSocket (JSON). Die Implementierung besteht aus:

  • Proxy-Seite: NexusVelocityPlugin (Velocity, role: PROXY)
  • Backend-Seite: NexusBridgePlugin (Paper, role: BACKEND)
  • Core-Seite: BridgeWsHandler + BridgeManager (Spring WebSocket-Handler)
  • Gemeinsame Lib: NexusBridgeClient (Reconnect, Heartbeat, Ban-Cache, Outbox)

2. Velocity-Plugin – Proxy-Spezifisches Verhalten

2.1 Lifecycle & Initialisierung

PhaseAktionCode
Plugin-LoadConfig laden (nexus-bridge.properties); NexusBridgeClient instanziieren mit ProxyBridgeListeneronInit(ProxyInitializeEvent)
Config-DefaultingFalls nicht vorhanden: core-url, server-name, token, server-key initialisierenloadConfig()
Startclient.start() nur wenn Token ODER Server-Key vorhandenstart() in Listener
Shutdownclient.stop() (sauberes WebSocket-Close)onShutdown(ProxyShutdownEvent)

2.2 Was Velocity UNTERSCHIEDLICH macht (vs. Paper)

AspektPaperVelocity
RoleBACKENDPROXY
Ban-EnforcementJoin + Live (Kick bei aktivem Ban)Nur Join (Pre-Login), reicht bei Proxy
CapabilitiesKICK, BROADCAST, CHAT_READ, EXECUTEKICK, BROADCAST, SUBSERVER_AWARE
Player-TrackingPro-Backend; TPS/Entity-MetrikenNur Proxy-weite Online-Count
Vault/RanksVollständiger Vault-Support + FarbenKeine Vault-Integration (Ranks kommen vom Backend)
ConsoleKann Console-Output streamenNicht unterstützt
Heartbeat-Fieldstps, cpuLoad, memUsedMb, memMaxMb, entities, chunks, maxPlayers, uptimeSecondsNur playersOnline, maxPlayers

2.3 Weitergeleitete Events (Bridge → Core)

EventVelocity-HandlerPayloadTiming
handshakeonInit(){token, serverName, bridgeType: "VELOCITY", role: "PROXY", version, gameVersion, capabilities}Nach Core-Connect
player.joinonPostLogin(){game: "minecraft", gameId: UUID, name, ip?}Sofort nach erfolg. Login
players.synconConnected(){players: [{gameId, name, ip?}, ...]}Nach jedem Reconnect
player.quitonDisconnect(){game: "minecraft", gameId: UUID}Bei Disconnect

2.4 Kommandos (Core → Velocity)

KommandoHandlerEffektAck
command.kickonKick()player.disconnect(Component.text("Gekickt: " + reason))command.ack
command.broadcastonBroadcast()proxy.sendMessage(Component.text("[Nexus] " + message))Sync (Fire-and-Forget)
command.messageonMessage()player.sendMessage(Component.text(message))Sync
punishment.request(Ban-Cache nur)Ban-Cache lokal aktualisiert; Live-Kicks über onPunishment()Über ban.created
command.executeNicht unterstützt
server.powerNicht unterstützt

2.5 Ban-Cache-Enforcement

Velocity:
  1. Handshake → Core sendet "ban.sync" mit allen aktiven BAN/TEMPBAN
  2. ProxyBridgeListener registriert BanCache.activeBan() im Client
  3. Bei Login: ban check PRÜFT lokal (< 1 ms), keine Netzwerk-Latenz
  4. Live: bei "ban.created" → client kickt sofort Spieler (if online)

Besonderheit Proxy: Ban-Evasion-Schutz über IP ist möglich, kann aber nur vom Paper-Backend konfiguriert werden. Velocity hält die IP-Ban-Liste aus dem Core-Sync, kann aber nicht selbst IP-Bans erzwingen (kümmert sich um Auth nicht).


3. Core WebSocket Handler – Server-Seite

3.1 Endpoint & Connection-Lifecycle

AspektDetails
Endpointws://<host>:8080/bridge/ws (oder über Nginx reverse proxy)
TransportSpring WebSocket (TextWebSocketHandler)
Session-TrackingMap<sessionId, ConnState> mit serverId + Dedup-Set (seenIds)
AuthentifizierungNur Handshake; danach sessionId-basiert
Max ConnectionsTheoretisch unbegrenzt (1 Pro Server)

3.2 Handshake (Bridge → Core)

Request (erste Nachricht, type: handshake):

{
  "type": "handshake",
  "id": "<uuid>",
  "data": {
    "token": "<einmal-token ODER server-key>",
    "serverName": "lobby-1",
    "bridgeType": "PAPER" | "VELOCITY" | "BUNGEE",
    "role": "BACKEND" | "PROXY",
    "version": "0.1.0",
    "gameVersion": "1.21.4",
    "capabilities": ["KICK", "BROADCAST", "CHAT_READ", "EXECUTE", "SUBSERVER_AWARE"]
  }
}

Success-Response (type: handshake.ok):

{
  "type": "handshake.ok",
  "id": "<gen>",
  "data": {
    "serverId": "<uuid>",
    "serverKey": "nxk_<base64-48-bytes>" | null,
    "coreVersion": "0.1.0"
  }
}

Error-Response (type: handshake.error):

{
  "type": "handshake.error",
  "id": "<gen>",
  "data": {
    "code": "NX-3001",
    "message": "Bridge token invalid"
  }
}

Handshake-Logik (BridgeWsHandler.handshake):

  1. Hash des token berechnen: SHA-256(credential)
  2. Nachschlag in GameServer.serverKeyHash → Existiert?
    • Ja: Server bekannt, re-connect, kein neuer Key.
    • Nein: ServerToken mit diesem Hash suchen, used=false?
  3. Bei neuer Server aus Token:
    • Neuer Server erzeugen/update GameServer (name, type, role, version, capabilities)
    • Zufälliger 48-Byte-Key generieren → nxk_<base64> → Hash speichern
    • Token als used=true markieren
  4. ConnState(serverId, BoundedMap<2000 seenIds>) erstellen und registrieren
  5. Antwort mit handshake.ok senden
  6. Ban-Cache-Sync: BridgeManager.sendBanSync(session) (alle aktiven BAN/TEMPBAN/MUTE + Metadaten)
  7. Message-Sync: BridgeManager.sendMessagesSync(session) (Panel-editierte Texte)
  8. Template-Sync: TemplateSyncBridge.sendTo(session) (Ban- & Report-Templates)
  9. Event publish: server.online

4. Kompletter Nachrichtenkatalog (Beide Richtungen)

4.1 Bridge → Core (Inbound in BridgeWsHandler)

Player Events

TypWannFelderBeschreibung
player.joinNach erfolgreichem Logingame: "minecraft", gameId: "<uuid>", name: "<string>", ip: "<ip>"?, rank: "<group>"?, skin: "<url>"?Spieler betritt Server; Optional Vault-Rank + Skin-URL (online-mode).
players.syncNach jedem Reconnect (Handshake+)players: [{game, gameId, name, ip?, rank?}, ...]Roster aller derzeit online Spieler; verhindert verwaiste Offline-Einträge bei Reconnect.
player.quitBei Disconnectgame: "minecraft", gameId: "<uuid>"Spieler verlässt Server.
player.chatChat-Event (nur wenn Bridge Capability CHAT_READ)gameId: "<uuid>", message: "<string>"Nur wenn Modul abonniert hat.
player.report/report-Kommando (Ingame)reporterGameId: "<uuid>", reporterName: "<string>", targetName: "<string>", reason: "<string>", `category: "GENERAL"...`
player.deviceAlle 60s (Telemetry)gameId: "<uuid>", name: "<string>", hardwareId: "<sha256>", os: "<string>"?, mcVersion: "<string>"?, client: "<string>"?Client-Fingerprint vom Plugin-Channel nexus:hwid oder Launcher.
player.inventoryAlle 60s (Telemetry)gameId: "<uuid>", name: "<string>", `kind: "MAIN""ARMOR"

Server Events

TypWannFelderBeschreibung
vault.groupsNach Vault-Init, dann bei Gruppen-Updategroups: {<name>: <hexColor>?} oder groups: [<name>, ...]Alle Vault-Gruppen mit auto-erkannter Farbe. Wird union'ed in VaultGroupRegistry für ACP-Dropdown.
heartbeatAlle 10splayersOnline: <int>, maxPlayers: <int>?, tps: <float>?, cpuLoad: <float>?, memUsedMb: <int>?, memMaxMb: <int>?, entities: <int>?, chunks: <int>?, uptimeSeconds: <long>?Alle Felder optional (rückwärtskompatibel). Backend füllt Metriken; Proxy nur playersOnline.
console.lineStream läuft (nur wenn console.subscribe aktiv)line: "<string>"Eine Zeile Console-Output; landet in ConsoleBuffer (Ring: 400 lines/Server).

Link & Punishment

TypWannFelderBeschreibung
link.request/nexus link (Ingame)gameId: "<uuid>", name: "<string>"Spieler verknüpft Konto; Core antwortet mit link.code.
punishment.request/ban, /mute, … (Ingame-Kommando oder Admin-Modul)targetGameId: "<uuid>"?, targetName: "<string>"?, `type: "BAN""TEMPBAN"
punishment.revokeAdmin hebt Strafe auftargetGameId: "<uuid>"?, targetName: "<string>"?, `type: "BAN"..., actorName: "<string>"`
chatlog.request/chatlog <player> (Ingame, nur wenn Chat-Buffer vorhanden)targetGameId: "<uuid>"?, targetName: "<string>"?, actorName: "<string>"Event chatlog.requested publish'ed für BanSystem-Snapshot.

Scheduler & Acknowledge

TypWannFelderBeschreibung
command.ackNach Kommando-Ausführung (Bridge → Core)`ok: truefalse, error: "<string>"?`

4.2 Core → Bridge (Outbound aus BridgeManager & BridgeWsHandler)

Ban-Cache-Synchronisation

TypWannFelderBeschreibung
ban.syncNach Handshake.ok, einmalig`bans: [{gameId, type, reason, expiresAt: "<ISO-8601>"null, banCode: <int>?, caseNumber: "<string>"?, ip: "<ip>"?}, ...]`
ban.createdBei neuer StrafegameId: "<uuid>", `type: "BAN"..., reason: "<string>", expiresAt: "<ISO>"?, banCode: <int>?, caseNumber: "<string>"?, ip: "<ip>"?`
ban.revokedBei Strafe-AufhebunggameId: "<uuid>", `type: "BAN"...`

Player Commands

TypWannFelderBeschreibungAck?
command.kickAdmin/Modul befiehlt KickgameId: "<uuid>", reason: "<string>"Bridge kickt Spieler mit Grund.Ja
command.messagePM vom Core an SpielergameId: "<uuid>", message: "<string>"Bridge sendet Message an Spieler.Ja
command.broadcastBroadcast an alle Spielermessage: "<string>"Bridge broadcastet.Ja
command.teleportAdmin teleportiert Spieler (z.B. bei Report-Claim)whoGameId: "<uuid>", toGameId: "<uuid>"Bridge teleportiert who zu to.Ja
command.setrankACP setzt Vault-RanggameId: "<uuid>", rank: "<group>", `exclusive: truefalse`Bridge setzt Gruppe in Vault (add oder replace).
command.executeShop-Lieferung oder Script (nur mit Capability EXECUTE)command: "<string>"Konsolen-Kommando auf Main-Thread; Bridge ack't Erfolg/Fehler.Ja

Server Lifecycle

TypWannFelderBeschreibungAck?
server.powerACP befiehlt Stop/Restart`action: "stop""restart"`stop: Bukkit.shutdown(). restart: Restart-Routine.

Console Streaming

TypWannFelderBeschreibungAck?
console.subscribeACP öffnet Console-TabBridge startet Console-Streaming.Nein
console.unsubscribeACP schließt Console-TabBridge stoppt Streaming.Nein

Link & Messages

TypWannFelderBeschreibungAck?
link.codeAntwort auf link.requestgameId: "<uuid>", code: "ABC-123", expiresInSeconds: 300Bridge zeigt Code im Chat.Nein
messages.syncNach Handshake, bei Hot-Reloadmessages: {<key>: "<legacy-§-code-text>", ...}Panel-editierte spielersichtbare Texte; Bridge nutzt für Replacements (z.B. Kick-Grund).Nein

Templates

TypWannFelderBeschreibungAck?
templates.syncBei Template-UpdatebanTemplates: ["template1", ...], reportTemplates: [...]Namen für Tab-Completion & Validierung.Nein

5. Verbindung & Resilienz

5.1 Heartbeat-Protokoll

Client-Seite (Bridge):
  - executor.scheduleAtFixedRate(heartbeat, 10, 10, TimeUnit.SECONDS)
  - Nur gesendet wenn ready=true (nach handshake.ok)
  - Payload: playersOnline + alle optional Metriken (paper) oder leer (velocity)

Core-Seite (BridgeWsHandler):
  case "heartbeat":
    - ServerMetrics.applyHeartbeat(server, data) → setzt alle Felder (optional-safe)
    - server.setLastSeenAt(Instant.now())
    - servers.save()
    - alertService.check() → Prüft auf Offline-Anomalien

Heartbeat-Timeout: Nicht implementiert; Core nutzt lastSeenAt für Admin-UI (keine auto-Disconnect bei fehlenden Heartbeats).

5.2 Reconnect & Exponential Backoff

Fehlerfall:
  - socket = null, ready = false
  - backoffSeconds *= 2, max 30s
  - LOG.warn("Core unreachable, retrying in {delay}s")
  - Outbox.add(message) für alle outgoing Events (max 10.000 queued)

Nach reconnect (handshake.ok):
  - backoffSeconds = 1 (reset)
  - Outbox.drain() → alle ausstehend Events flushen (FIFO)
  - Ban-Cache neu synced

Duplikat-Deduplizierung: Jede Nachricht hat eindeutige id. Core-Seite speichert seenIds pro Connection in BoundedMap<2000> (LRU). Bei Replay (z.B. retried message) wird duplikat ignoriert.

5.3 Offline-Verhalten (Cache)

SituationVerhalten
Core down, Spieler versucht LoginProxy/Backend liest aus lokalem Ban-Cache → kein Netzwerk-Hit.
Core down, /ban wird eingegebenEvent in Outbox queued. Bei reconnect gesendet.
Core down, /kick wird eingegebenKein Kommando vom Core erhalten → Spieler bleibt online.
Vault-Gruppen veraltetLetzte Sync-Gruppe gelten; bei reconnect neue Sync.
Message-Bundle altFallback-Texte in Bridge hardcoded (z.B. Kick-Grund Default).

6. Authentifizierung & Sicherheit

6.1 Token vs. Server-Key

AspektTokenServer-Key
FormatBeliebig, ACP-generiertnxk_ + 48 Bytes Base64
GültigkeitEinmal; wird nach Handshake ungültigPersistent, in Bridge-Config gespeichert
VerwendungErstes Handshake einer neuen BridgeAlle folgenden Handshakes
StorageACP (Server-Token-Tabelle)Bridge Config (plain-text)
RotationManuell über ACP (Token-Revoke)Automatisch bei Token-Handshake

6.2 Hash & Verifikation

handshake(data):
  credential = data.token
  credentialHash = SHA-256(credential)
  
  if (findByServerKeyHash(credentialHash)) → known server
  else if (findByTokenHashAndUsedFalse(credentialHash)) → new server
    token.used = true
    newServerKey = "nxk_" + Base64(random 48 bytes)
    sendResponse(handshake.ok, serverKey)
  else → credentials invalid
    sendResponse(handshake.error, NX-3001)
    close()

Kanal-Sicherheit: WebSocket über wss:// (TLS) empfohlen; credentials im data Feld unverschlüsselt (kein HTTP-Body-Encryption notwendig bei TLS).


7. Nachrichtenformat & Frame-Struktur

7.1 Allgemeine Nachricht

{
  "type": "<string>",
  "id": "<uuid oder custom-id>",
  "data": {
    ...
  }
}
  • type: Nachrichten-Typ (z.B. player.join, command.kick, handshake)
  • id: Eindeutige Frame-ID für Dedup. Bei Commands: echoed in command.ack. Optional für fire-and-forget Events.
  • data: Payload als Objekt/Array/String, varies by type.

7.2 Message-ID Semantik

QuelleID-MusterBehavior
Bridge → CoreUUID.randomUUID().toString()Core speichert in seenIds; duplikat ignoriert
Core → BridgeUUID.randomUUID().toString() (fire-and-forget)Kein Ack erwartet
Core → Bridge (Scheduler)"schedrun:<runId>"Bridge ack't mit dieser ID → Core matched Result

8. Fehlerbehandlung & Edge Cases

8.1 Fehler-Szenarien

FehlerHandling
Ungültiger Tokenhandshake.error NX-3001, Close
Malformed JSONLOG.warn, Frame ignoriert (forward-compatible)
Unbekannter Message-TypeForward-compatible: ignoriert, kein Error
Server nicht gefunden (ConnState=null)Close mit SERVER_ERROR
Session ClosedafterConnectionClosed()BridgeManager.unregister(), Players offline markiert
Send Failed (IOException)LOG.warn, Outbox-Flush bei nächstem Connect

8.2 Duplikat-Handling

Scenario: Bridge sendet player.join, dann Verbindung bricht und Reconnect spielt Outbox ab.

Client (Bridge):
  1. Send "player.join" id=abc123
  2. Disconnect
  3. Reconnect + handshake.ok
  4. Outbox.drain() → resend "player.join" id=abc123

Server (Core):
  1. Receive "player.join" id=abc123 → seenIds.add(abc123) ✓
  2. Disconnect
  3. Reconnect, new session, new seenIds = empty
  4. Receive "player.join" id=abc123 → seenIds.add(abc123) ✓
    (Pro Session, also nicht global duplikat-safe über Sessions hinweg)

Implication: Idempotenz auf Core-Seite nur pro Session. Wenn Backend über 2 Sessions versucht, selbe Event zweimal zu senden (nach brutaler Neubindung), wird es twice processed. Aktuell akzeptiert (spieler 2x join, dann quit).


9. Capability-System

Bridges berichten ihre Fähigkeiten im Handshake; Core prüft vor Kommando-Dispatch.

CapabilityMeaningCommands
KICKCan disconnect playerscommand.kick, ban.created
BROADCASTCan send to all playerscommand.broadcast
CHAT_READCan read & forward chat eventsplayer.chat inbound
EXECUTECan run console commandscommand.execute
SUBSERVER_AWAREProxy: weiß von Backend-Servern, kann routingUsed by Velocity for info only

Velocity-Defaults: ["KICK", "BROADCAST", "SUBSERVER_AWARE"]
Paper-Defaults: ["KICK", "BROADCAST", "CHAT_READ", "EXECUTE"]


10. Performance & Skalierung

10.1 Latenz-Profile

OperationLatenzBottleneck
Ban-Check (Join)< 1 msMap Lookup (Local)
Player.join → Core Process50–200 msNetzwerk + Message-Queue
Message-Delivery (Kick)50–200 msNetzwerk
Heartbeat-Cycle10 sScheduler Interval

10.2 Memory-Footprint

ComponentSizeNotes
Ban-Cache (1000 active bans)~500 KBConcurrentHashMap
Message-Bundle (200 keys)~50 KBStrings in HashMap
Console-Buffer (400 lines, 512 bytes avg)~200 KBRing-Buffer pro Server
Outbox (10.000 messages)~5 MBWorst case (connected bridge = leerer Outbox)
Connections-Map~1 KB per BridgeUUID → Session

11. Konfiguration (Bridge-Seite)

11.1 Velocity: plugins/jubyte-nexus-bridge/nexus-bridge.properties

core-url=ws://localhost:8080/bridge/ws
server-name=proxy-1
token=<generated-from-acp>
server-key=nxk_<generated-on-first-handshake>

11.2 Paper: plugins/JuByteNexusBridge/config.yml

core-url: ws://localhost:8080/bridge/ws
server-name: server-1
token: ""
server-key: ""

Flow nach Token-Handshake:

  1. ACP generiert Token, Admin gibt Token in Bridge ein
  2. Bridge: token=<token>
  3. Erstes Handshake → Core antwortet mit serverKey
  4. BridgeListener.onServerKeyIssued(key) speichert persistent
  5. Nächstes Handshake: token=<leer>, nutzt server-key stattdessen

12. Integrationen & Dependencies

12.1 Core-Seitig

KomponenteFunktionQuelle
BridgeManagerSession-Tracking, Command-Dispatch, Ban-BroadcastSpring Component
VaultGroupRegistryGruppen-Union aus allen Bridges für ACP-UIIn-Memory (pro Restart rebuild)
ConsoleBufferRing-Buffer console.line Frames (400 lines/Server)In-Memory (pro Restart reset)
BanCacheProvider (SPI)Abstraktion über Punishment-Module (Ban-System)Plugin-Pattern
EventBusPublish server.online, player.chat, chatlog.requested, etc.Spring Application Events
PlayerServicePlayer join/quit/sync/rank/skin ORMDomain Service
LinkServiceGeneriert Link-Codes (UCP Account-Linking)Domain Service
PunishmentCreator (SPI)Instanziiert Punishment aus RequestPlugin-Pattern

12.2 Bridge-Seitig (Paper)

KomponenteFunktion
PlayerEventsBukkit-Events → Bridge (join, quit, chat, move, …)
BanModule/ban, /mute, /report Commands + Chat-Filter
VaultBridgeVault-Gruppe Erkennung + Farben
HwidChannelPlugin-Channel nexus:hwid für Hardware-Fingerprints
TelemetryAlle 60s: Device + Inventory Snapshot
PluginBridgeListenerImplementiert BridgeListener, dispatch lokale Commands

13. Testing & Debugging

13.1 Logging

Bridge-Seite:

nexus.bridge Logger (System.Logger)
  - WARN: "Core unreachable (…), retrying in 30s"
  - ERROR: "Core rejected handshake: NX-3001 — check the bridge token!"
  - INFO: "Ban cache synced: 42 entries"

Core-Seite:

org.springframework.web.socket → TRACE
  - Inbound frame (type=handshake, id=abc)
  - Outbound frame (type=ban.created, id=xyz)

13.2 Mock-Tests

NexusBridgeClient-Test:

BridgeConfig config = new BridgeConfig("ws://…", "test", "PAPER", "BACKEND", "0.1.0", "1.21", 
    List.of("KICK", "BROADCAST"), "token", "");
NexusBridgeClient client = new NexusBridgeClient(() -> config, listener, () -> 5);
// Manuell BridgeMessage.of("handshake.ok", ...) auf listener → ready.get() = true

14. Zusammenfassung: Differenzen Velocity vs. Paper

EigenschaftVelocity (Proxy)Paper (Backend)
RolleNetzwerk-Edge, Ban-Prä-CheckGame-Server, Spieler-Management
Ban-EnforcementPre-Login nur (kann nicht fein-granular)Join + Live (mit Mute-Chat-Filter)
CapabilitiesKICK, BROADCAST, SUBSERVER_AWAREKICK, BROADCAST, CHAT_READ, EXECUTE
MetrikenNur playersOnline, maxPlayersVoll: TPS, CPU, Entities, Chunks, RAM
Vault-SupportKeine Gruppen-ErkennungVollständig mit Farb-Sync
ConsoleNicht möglichStreaming (400-line Buffer)
HeartbeatMinimalUmfangreich
TelemetryDevice + Inventory (je 60s)
Config-Ortplugins/jubyte-nexus-bridge/nexus-bridge.propertiesplugins/JuByteNexusBridge/config.yml

15. Referenz: Alle Message-Type Strings (Canonical List)

Inbound (Bridge → Core)

handshake
player.join
player.quit
player.chat
player.report
player.device
player.inventory
players.sync
vault.groups
heartbeat
link.request
punishment.request
punishment.revoke
chatlog.request
command.ack
console.line

Outbound (Core → Bridge)

handshake.ok
handshake.error
ban.sync
ban.created
ban.revoked
command.kick
command.teleport
command.setrank
command.message
command.broadcast
command.execute
server.power
console.subscribe
console.unsubscribe
link.code
messages.sync
templates.sync