NukkitX Plugin for Synchronized Ban Management & Player Stats Tracking
This plugin REQUIRES a separate BanBridge Backend instance running independently!
The backend is NOT part of the NukkitX server. It must:
- 🖥️ Run on a separate machine or different port (not localhost:25565)
- 🗄️ Have its own MySQL database
- 🌐 Listen on HTTP (typically port 8080)
- 📡 Be accessible from the game server via network
Architecture:
┌──────────────────────┐
│ MySQL Database │
│ (Backend only) │
└──────────┬───────────┘
│
▼
┌──────────────────────────────┐
│ BanBridge Backend │ ← SEPARATE Java process
│ (Host: backend.local:8080) │
│ (or separate port 9000) │
└──────────┬───────────────────┘
│ HTTP REST
▼
┌──────────────────────┐
│ NukkitX Game Server │
│ + BanBridge Plugin │ ← Plugin syncs via HTTP
│ (Host: 0.0.0.0:25565) │
└──────────────────────┘
Setup Order:
- ✅ Start BanBridge Backend first (see Backend
README.md) - ✅ Install BanBridge Client Plugin in plugins folder
- ✅ Configure
config.ymlwith backend URL - ✅ Start NukkitX server
BanBridge is a powerful NukkitX game server plugin that synchronizes player bans and tracks statistics across your Minecraft network. It maintains a local ban cache for offline protection and communicates with a centralized backend server.
🚫 Synchronized Bans - Ban players across multiple servers instantly
📊 Player Stats Tracking - Monitor playtime, kills, deaths
🟢 Presence Tracking - Real-time online/offline status
💾 Local Ban Cache - Offline-safe ban enforcement (works without internet)
🔐 Token-Based Auth - Secure backend communication
⚡ Resilient API - Automatic retry with exponential backoff
🔌 Event-Driven - Efficient NukkitX event hooks
- Java 17+ (OpenJDK or Oracle JDK)
- Maven 3.8+
- NukkitX Server (1.0+)
- BanBridge Backend (running separately with MySQL)
cd /path/to/BanBridgeProjekt/Client
mvn clean packageOutput: target/BanBridge-1.0.0.jar
# Copy to NukkitX plugins directory
cp target/BanBridge-1.0.0.jar /path/to/nukkit/plugins/cd /path/to/nukkit
./nukkit.shThis generates: plugins/BanBridge/config.yml
Edit plugins/BanBridge/config.yml:
# Backend Connection Settings
api:
baseUrl: "http://backend-host:8080" # ← IMPORTANT: Point to Backend Server!
serverKey: "survival-1" # Unique identifier for this server
serverToken: "your-secret-token" # Token from backend DB (servers table)
# Sync Intervals (seconds)
sync:
bansPollSeconds: 10 # Check for new bans every 10 seconds
statsFlushSeconds: 60 # Upload stats every 60 seconds
metricsSeconds: 15 # Report metrics every 15 seconds
presenceSeconds: 10 # Update player presence every 10 seconds
commandsPollSeconds: 3 # Poll for commands every 3 seconds
# HTTP Retry Configuration
httpMaxAttempts: 4 # Retry failed requests 4 times
httpBaseBackoffMillis: 250 # Initial backoff: 250ms
httpMaxBackoffMillis: 5000 # Max backoff: 5 seconds
# Local Cache
cache:
bansFile: "bans-cache.json" # Local ban cache filename
# Debug Web Server (for development only)
web:
enabled: false # Leave disabled in production
bind: "127.0.0.1"
port: 8090Before starting the plugin, verify the backend is running:
# From game server machine
curl http://backend-host:8080/api/server/health
# Expected response:
# {"status":"ok","serverTime":"2026-02-25T...","dbOk":true}./nukkit.shCheck console for:
[BanBridge] Backend health check OK
[BanBridge] Ban cache loaded: 0 entries
[BanBridge] Plugin enabled - syncing with backend
🎉 Done! Plugin is now syncing with backend.
org.banbridge
├── BanBridgePlugin 🎮 Main plugin entry point
├── api/
│ ├── BackendClient 📡 REST client for backend communication
│ ├── BanChangesResponse 📥 Parses ban change responses
│ ├── StatsBatchRequest 📤 Player stats upload payload
│ ├── PresenceRequest 🟢 Player presence updates
│ ├── ServerMetricsRequest 📊 Server metrics reporting
│ ├── CommandAckRequest 💬 Command acknowledgment
│ └── CommandsPollResponse 📋 Polls for backend commands
├── bans/
│ ├── BanCache 💾 Persistent local ban cache
│ └── BanEntry 🚫 Individual ban data
└── stats/
├── StatsAccumulator 📈 Accumulates player stats
├── BandwidthMeter 🌐 Network metrics (abstract)
├── LinuxBandwidthMeter 🐧 Linux-specific metrics
└── OshiBandwidthMeter 💻 Cross-platform metrics
Main plugin class - initializes all components and hooks NukkitX events.
Listens to:
PlayerLoginEvent- Check ban cachePlayerQuitEvent- Record playtimePlayerDeathEvent- Track kills/deaths- Periodic timers - Sync bans, flush stats, update presence
HTTP REST client for all backend communication.
// Health check
HttpResponse<String> health = backendClient.health();
// Fetch new bans
String changes = backendClient.fetchBanChanges(sinceInstant);
// Upload stats
backendClient.postStatsBatch(playerList);
// Update presence (who's online)
backendClient.postPresence(onlinePlayers);
// Report metrics (CPU, memory, bandwidth)
backendClient.postServerMetrics(metrics);
// Poll for commands (future feature)
backendClient.pollCommands();Persistent local ban cache stored as JSON file.
Features:
- Loaded on startup from
bans-cache.json - Updated periodically from backend
- Used at login to kick banned players (works offline!)
- Survives server restarts
// Check if player is banned
if (banCache.isBanned(playerXuid)) {
player.kick("§cYou are banned!");
}
// Add/update ban from backend
banCache.addOrUpdateBan(banEntry);
// Revoke ban
banCache.revokeBan(banId);Tracks player statistics in memory, flushes to backend periodically.
Tracked Metrics:
- Playtime - Incremented every minute per online player
- Kills - On PlayerDeathEvent (killer)
- Deaths - On PlayerDeathEvent (victim)
// Record +60 seconds playtime
statsAccumulator.recordPlaytime(xuid, 60);
// Record kill
statsAccumulator.recordKill(xuid);
// Record death
statsAccumulator.recordDeath(xuid);
// Get accumulated stats
PlayerStats stats = statsAccumulator.getStats(xuid);Monitors network I/O and system resources.
OshiBandwidthMeter- Cross-platform (Windows, Linux, macOS)LinuxBandwidthMeter- Linux-specific optimization
Plugin ──HTTP──> Backend ──SQL──> MySQL Database
All requests include:
X-Server-Key: <serverKey>
X-Server-Token: <serverToken>
GET /api/server/healthResponse:
{
"status": "ok",
"serverTime": "2026-02-25T10:30:00.000Z",
"dbOk": true
}Used by: Initial connection test, periodic health verification
GET /api/server/bans/changes?since=2026-02-25T09:00:00Z
X-Server-Key: survival-1
X-Server-Token: secret123Response:
{
"serverTime": "2026-02-25T10:30:00.000Z",
"changes": [
{
"type": "BAN_UPSERT",
"banId": 123,
"xuid": "2533274790299905",
"reason": "Hacking detected",
"createdAt": "2026-02-25T10:00:00.000Z",
"expiresAt": "2026-02-26T10:00:00.000Z",
"revokedAt": null,
"updatedAt": "2026-02-25T10:15:00.000Z"
}
]
}Ban Status Logic:
- If
revokedAt != null→ UNBAN - If
expiresAtin past → UNBAN (expired) - Otherwise → BAN active
Used by: Ban cache synchronization every bansPollSeconds
POST /api/server/stats/batch
Content-Type: application/json
X-Server-Key: survival-1
X-Server-Token: secret123Request Body:
{
"players": [
{
"xuid": "2533274790299905",
"name": "PlayerName",
"playtimeDeltaSeconds": 3600,
"killsDelta": 15,
"deathsDelta": 3
}
]
}Response: 200 OK (empty)
Used by: Periodic stats flush every statsFlushSeconds
POST /api/server/presence/batch
Content-Type: application/json
X-Server-Key: survival-1
X-Server-Token: secret123Snapshot Mode (Recommended):
{
"snapshot": true,
"players": [
{
"xuid": "2533274790299905",
"name": "PlayerName",
"ip": "192.168.1.100",
"hwid": "device_hash"
}
]
}Effect: All listed players = online, all others = offline
Event Mode:
{
"players": [
{"xuid": "...", "online": true},
{"xuid": "...", "online": false}
]
}Effect: Update only specified players
Response: 200 OK (empty)
Used by: Every presenceSeconds to keep backend in sync
POST /api/server/metrics
Content-Type: application/json
X-Server-Key: survival-1
X-Server-Token: secret123Request Body:
{
"cpuUsagePercent": 45.5,
"memoryUsageMB": 2048,
"memoryMaxMB": 4096,
"playerCount": 25,
"uploadBandwidthKbps": 100.5,
"downloadBandwidthKbps": 200.3
}Response: 200 OK (empty)
Used by: Every metricsSeconds for monitoring
# Backend API Connection
api:
baseUrl: "http://backend-host:8080"
serverKey: "survival-1"
serverToken: "secret_token_abc123"
# Synchronization Intervals (seconds)
sync:
bansPollSeconds: 10 # ← How often to check for new bans
statsFlushSeconds: 60 # ← How often to upload stats
metricsSeconds: 15 # ← How often to report metrics
presenceSeconds: 10 # ← How often to update who's online
commandsPollSeconds: 3 # ← How often to check for commands
# HTTP Retry Logic
httpMaxAttempts: 4 # ← Retry failed requests this many times
httpBaseBackoffMillis: 250 # ← Initial wait before first retry (ms)
httpMaxBackoffMillis: 5000 # ← Never wait longer than this (ms)
# Local Cache
cache:
bansFile: "bans-cache.json" # ← Where to store ban cache
# Debug Web Server (development only)
web:
enabled: false # ← Set to true for local testing
bind: "127.0.0.1"
port: 8090For Production:
sync:
bansPollSeconds: 30 # Don't hammer backend
statsFlushSeconds: 300 # Reduce network traffic
metricsSeconds: 60
presenceSeconds: 30
httpMaxAttempts: 3 # Balance reliability vs speedFor Testing/Development:
sync:
bansPollSeconds: 5 # Faster testing
statsFlushSeconds: 10 # See changes immediately
metricsSeconds: 5
presenceSeconds: 5
httpMaxAttempts: 1 # Fail fast
web:
enabled: true # Debug endpoints┌──────────────────────────────────────────┐
│ Backend Admin UI │
│ (Admin clicks "Ban Player") │
└──────────────────┬───────────────────────┘
│ (Ban stored)
▼
┌─────────────────┐
│ Backend MySQL │
│ bans table │
└────────┬────────┘
│ (Plugin polls every 10s)
▼
┌───────────────────────┐
│ GET /bans/changes │
│ since=... │
└───────────┬───────────┘
│ (New ban in response)
▼
┌──────────────────────┐
│ BanCache.json │
│ (Local cache updated)│
└──────────┬───────────┘
│
┌───────┴──────────┐
▼ ▼
Player Online Player Offline
(Next login) (Already offline)
│ │
▼ ▼
Check cache When they login:
Kick immediately Check cache
Kick immediately
Player Action Recorded By Flushed
─────────────────────────────────────────────────────
Join server → PlayerLoginEvent → Every 60s
Playtime (per min) → Timer tick → Every 60s
Kill other player → PlayerDeathEvent → Every 60s
Die → PlayerDeathEvent → Every 60s
Leave server → PlayerQuitEvent → Immediate
Real-time Events Update Backend
─────────────────────────────────────────
PlayerLoginEvent → Add to online list
PlayerQuitEvent → Remove from online list
Every 10 seconds → POST /presence/batch
→ Backend shows who's online
All API requests require:
api:
serverKey: "survival-1" # Server identifier
serverToken: "abc123xyz..." # Secret tokenImportant:
- Tokens are server-side only (sent in HTTP headers)
- Never expose tokens in public logs
- Never commit tokens to version control
- Regenerate if compromised
Ban cache (bans-cache.json) is:
- ✅ Stored locally on game server
- ✅ Only readable by NukkitX process
- ✅ Used for offline protection (no internet needed to ban)
- ✅ Automatically synced with backend
If a request fails:
Request fails
↓
Wait 250ms (httpBaseBackoffMillis)
↓
Retry (attempt 2/4)
↓
Fails again
↓
Wait 500ms (250 * 2)
↓
Retry (attempt 3/4)
↓
Fails again
↓
Wait 1000ms (250 * 4)
↓
Retry (attempt 4/4)
↓
Fails again
↓
Wait 5000ms (capped at max)
↓
Give up, log error, continue
Plugin continues working even if backend is down:
- ✅ Local ban cache still enforced
- ✅ Stats accumulated in memory
- ✅ Player can join/leave normally
- ✅ When backend recovers → Auto-resync
- ✅ No data lost
[BanBridge] Backend health check OK
[BanBridge] Ban cache updated: 5 new bans, 0 unbans
[BanBridge] Stats flushed: 42 players updated
[BanBridge] Presence updated: 25 players online
[BanBridge] Server metrics: CPU 45%, Mem 2048MB/4096MB
[WARN] [BanBridge] Backend health check FAILED (attempt 1/4)
[WARN] [BanBridge] Retrying in 250ms...
[ERROR] [BanBridge] Ban sync FAILED after 4 attempts
PlayerLoginEvent
├─ Load player stats from accumulator
├─ Check BanCache.isBanned()
└─ If banned: player.kick("§cYou are banned!")
PlayerQuitEvent
├─ Save current playtime
└─ Flush stats if needed
PlayerDeathEvent
├─ Check if killer is known
├─ Record kill for killer (if possible)
└─ Record death for victimTimer Task (every bansPollSeconds)
└─ GET /api/server/bans/changes
└─ Update BanCache
Timer Task (every statsFlushSeconds)
└─ POST /api/server/stats/batch
└─ Upload accumulated stats
Timer Task (every presenceSeconds)
└─ POST /api/server/presence/batch
└─ Sync online player list
Timer Task (every metricsSeconds)
└─ POST /api/server/metrics
└─ Report CPU, memory, bandwidth
Timer Task (every commandsPollSeconds)
└─ GET /api/server/commands
└─ Check for backend commandsmvn clean packageJAR: target/BanBridge-1.0.0.jar
In config.yml:
web:
enabled: true
bind: "127.0.0.1"
port: 8090
api:
baseUrl: "http://127.0.0.1:8090" # Point to debug serverThen visit: http://127.0.0.1:8090/admin/players
# Check if backend is reachable
/banbridge health
# Force ban sync
/banbridge sync-bans
# Force stats flush
/banbridge flush-stats
# Show ban cache info
/banbridge cache-infoFrom game server:
curl -H "X-Server-Key: survival-1" \
-H "X-Server-Token: token123" \
http://backend-host:8080/api/server/healthExpected:
{"status":"ok","serverTime":"...","dbOk":true}[ERROR] Backend health check FAILED: Connection refused
Checklist:
- Is backend running? →
ps aux | grep BackendBridge(on backend host) - Is port 8080 correct? → Check backend config
- Is URL reachable? →
curl http://backend-host:8080/api/server/health - Firewall blocking? →
telnet backend-host 8080
Fix: Start backend, verify connectivity, restart plugin
[WARN] Backend auth failed (401): Check serverToken
Checklist:
- Backend DB has servers table?
- Server entry exists? →
SELECT * FROM servers WHERE server_key='survival-1'; - Token matches? → Copy exact token from DB
Fix: Update token in config.yml, restart plugin
[WARN] Ban changes fetch returned 0 changes
Checklist:
- Are there actual bans in backend? → Check admin UI
bansPollSecondsreasonable? (10-30 is good)- Token correct? → Test health check
Fix: Manually trigger: /banbridge sync-bans
[WARN] Stats batch POST returned empty
Checklist:
- Have active players on server?
statsFlushSecondsconfigured? (60 is default)- Backend receiving requests? → Check backend logs
Fix: Wait for next flush, check backend connectivity
- First run → Ban cache empty, wait for first sync
- Manually trigger →
/banbridge sync-bans - Check that ban is active (not expired/revoked)
[ERROR] Failed to enable BanBridge: NullPointerException
Fix: Check config.yml syntax (YAML format)
Ban cache and stats accumulator might be large on long-running servers.
Solutions:
- Increase JVM memory:
java -Xmx2G -jar nukkit.jar - Reduce sync intervals slightly
- Implement cache cleanup (future enhancement)
┌──────────────────────┐
│ BanBridge Backend │
│ (Single instance) │
└──────────┬───────────┘
│
┌──────┼──────┬────────┬────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Surv. Creat. PvP Hard. Skyw.
(Port (Port (Port (Port (Port
25565) 25566) 25567) 25568) 25569)
Configure each server's plugin:
Server 1 - Survival:
api:
baseUrl: "http://backend-host:8080"
serverKey: "survival"
serverToken: "token_abc123"Server 2 - Creative:
api:
baseUrl: "http://backend-host:8080"
serverKey: "creative"
serverToken: "token_def456"Server 3 - PvP:
api:
baseUrl: "http://backend-host:8080"
serverKey: "pvp"
serverToken: "token_ghi789"Result: Ban one player → All servers enforce immediately! ✅
| Library | Version | Purpose |
|---|---|---|
| Jackson Databind | 2.17.2 | JSON processing |
| NukkitX | 1.0 | Game server API |
| OSHI | 6.6.5 | System metrics |
- Fork repository
- Create feature branch:
git checkout -b feature/name - Commit changes:
git commit -m 'Add feature' - Push:
git push origin feature/name - Open pull request
- Java 17+ features (records, text blocks)
- Jackson for JSON
- Try-with-resources for resources
- Defensive null-checking
Proprietary - All rights reserved.
GitHub Issues: Report bugs here
Setup Guide: See BanBridgePluginSetupGuide.txt
Backend Integration: See Backend README.md
Team: BanBridge Development Team
Made with ❤️ for Minecraft Servers
🎮 Plugin • 🖥️ Backend • 📚 Docs • 🐛 Issues