- Overview
- Installation
- Configuration
- API Usage
- Common Operations
- Advanced Features
- REST API
- Best Practices
- Examples
- Troubleshooting
NetworkDataAPI is an enterprise-grade MongoDB connection layer for large Minecraft networks (similar to Hypixel or CubeCraft). It provides a shared MongoDB connection pool that all your plugins can use, eliminating the need for each plugin to create its own database connections.
NetworkDataAPI is a DATABASE CONNECTION LAYER, not a player data manager. It provides:
✅ Shared MongoDB Connection Pool - One connection pool for all plugins
✅ High-Level Database API - Easy-to-use methods for common operations
✅ Automatic Connection Management - Reconnection, retries, health checks
✅ Built-in Caching - Reduces database load by 80%+
✅ Thread-Safe Async Operations - Non-blocking database access
✅ REST API (Optional) - External integrations
❌ NOT an automatic player data tracker
❌ NOT a statistics/economy/levels system
❌ NOT a pre-configured player database
Your custom plugins decide WHAT data to store and WHEN to store it!
- Universal Compatibility: Works on both Paper/Spigot and BungeeCord with the same API
- High Performance: Built-in caching with Caffeine reduces database load by 80%+
- Thread-Safe: All operations are thread-safe with async support
- Connection Pooling: Configurable MongoDB connection pools for optimal performance
- Auto-Recovery: Automatic reconnection and retry logic
- REST API: Optional HTTP endpoints for external integrations
- Production-Ready: Follows SOLID principles with comprehensive error handling
┌─────────────────────────────────────────────────────────┐
│ Your Plugins │
├─────────────────────────────────────────────────────────┤
│ NetworkDataAPI Public API │
├──────────────────┬──────────────────┬──────────────────┤
│ Paper Module │ Core Module │ Bungee Module │
├──────────────────┴──────────────────┴──────────────────┤
│ MongoDB Database │
└─────────────────────────────────────────────────────────┘
- Java 17 or higher
- MongoDB 4.0 or higher
- Paper/Spigot 1.20+ or BungeeCord
Download the appropriate JAR file:
- For Paper/Spigot:
NetworkDataAPI-Paper-1.0-SNAPSHOT.jar - For BungeeCord:
NetworkDataAPI-Bungee-1.0-SNAPSHOT.jar
Place the JAR file in your plugins/ folder:
Paper/Spigot:
server/
└── plugins/
└── NetworkDataAPI-Paper-1.0-SNAPSHOT.jar
BungeeCord:
proxy/
└── plugins/
└── NetworkDataAPI-Bungee-1.0-SNAPSHOT.jar
Start your server/proxy. The plugin will create a default configuration file:
plugins/NetworkDataAPI/config.yml
Edit config.yml and set your MongoDB connection details:
mongodb:
uri: "mongodb://localhost:27017"
database: "minecraft_network"
username: ""
password: ""Restart your server/proxy to apply the configuration.
# MongoDB Connection Settings
mongodb:
uri: "mongodb://localhost:27017"
database: "minecraft_network"
username: ""
password: ""
# Connection pool settings
max-pool-size: 100 # Maximum connections
min-pool-size: 10 # Minimum connections
connection-timeout-ms: 10000
socket-timeout-ms: 5000
server-selection-timeout-ms: 5000
max-connection-idle-time-ms: 60000
max-connection-life-time-ms: 600000
# Cache Settings
cache:
enabled: true
max-size: 10000 # Maximum cached entries
expire-after-write-minutes: 5 # Expire after write
expire-after-access-minutes: 10 # Expire after last access
# REST API Settings (optional)
rest-api:
enabled: false
port: 8080
api-key: "" # Leave empty to disable auth
allowed-ips:
- "127.0.0.1"
# Async Executor Settings
async:
core-pool-size: 4
max-pool-size: 16
keep-alive-seconds: 60
# Logging Settings
logging:
level: "INFO" # TRACE, DEBUG, INFO, WARN, ERROR
debug: false
# Environment Detection
environment:
type: "AUTO" # AUTO, PAPER, BUNGEECORDimport com.cynive.networkdataapi.core.api.APIRegistry;
import com.cynive.networkdataapi.core.api.NetworkDataAPIProvider;
public class YourPlugin extends JavaPlugin {
private NetworkDataAPIProvider api;
@Override
public void onEnable() {
// Check if NetworkDataAPI is available
if (!APIRegistry.isAvailable()) {
getLogger().severe("NetworkDataAPI not found! Disabling plugin.");
getServer().getPluginManager().disablePlugin(this);
return;
}
// Get API instance
api = APIRegistry.getAPI();
// Verify connection health
if (!api.isHealthy()) {
getLogger().warning("NetworkDataAPI database is not healthy!");
}
getLogger().info("Successfully hooked into NetworkDataAPI v" + api.getVersion());
}
}Most common use case: Each plugin creates its own database/collections.
Perfect for: Cosmetics, Economy, Guilds, Stats, Punishments, etc.
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
import com.mongodb.client.model.Filters;
public class CosmeticsPlugin extends JavaPlugin {
private MongoDatabase database;
private MongoCollection<Document> playerCosmetics;
@Override
public void onEnable() {
NetworkDataAPIProvider api = APIRegistry.getAPI();
// Get your own database - completely isolated!
database = api.getDatabase("cosmetics");
// Create your collections
playerCosmetics = database.getCollection("player_cosmetics");
// Create indexes for performance
playerCosmetics.createIndex(
com.mongodb.client.model.Indexes.ascending("uuid")
);
}
// Example: Store cosmetic when player claims it
public void claimCosmetic(Player player, String cosmeticId) {
UUID uuid = player.getUniqueId();
// Check if player has data
Document data = playerCosmetics.find(
Filters.eq("uuid", uuid.toString())
).first();
if (data == null) {
// First cosmetic - create document
data = new Document()
.append("uuid", uuid.toString())
.append("claimed", List.of(cosmeticId))
.append("equipped", cosmeticId);
playerCosmetics.insertOne(data);
} else {
// Add to existing cosmetics
playerCosmetics.updateOne(
Filters.eq("uuid", uuid.toString()),
com.mongodb.client.model.Updates.addToSet("claimed", cosmeticId)
);
}
}
// Example: Get player's cosmetics
public List<String> getCosmetics(UUID uuid) {
Document data = playerCosmetics.find(
Filters.eq("uuid", uuid.toString())
).first();
if (data == null) {
return new ArrayList<>();
}
return data.getList("claimed", String.class, new ArrayList<>());
}
}Example: Economy Plugin
public class EconomyPlugin extends JavaPlugin {
private MongoDatabase database;
private MongoCollection<Document> balances;
@Override
public void onEnable() {
NetworkDataAPIProvider api = APIRegistry.getAPI();
// Own database for economy
database = api.getDatabase("economy");
balances = database.getCollection("player_balances");
}
public void addCoins(UUID uuid, int amount) {
Document data = balances.find(Filters.eq("uuid", uuid.toString())).first();
if (data == null) {
// First coins - create document
balances.insertOne(new Document()
.append("uuid", uuid.toString())
.append("balance", amount)
);
} else {
// Add to existing balance
balances.updateOne(
Filters.eq("uuid", uuid.toString()),
com.mongodb.client.model.Updates.inc("balance", amount)
);
}
}
}Why this is better:
- ✅ Complete isolation - no conflicts with other plugins
- ✅ Uses shared connection pool - no extra connections needed
- ✅ You control the data structure - create fields when needed
- ✅ No automatic data creation - data only exists when relevant
- ✅ Efficient - all plugins share NetworkDataAPI's connection pool
// Use default database from config, but own collections
MongoDatabase sharedDB = api.getDatabase(); // Default from config
MongoCollection<Document> cosmetics = sharedDB.getCollection("cosmetics");
MongoCollection<Document> economy = sharedDB.getCollection("economy");
MongoCollection<Document> stats = sharedDB.getCollection("stats");When to use:
- ✅ Smaller plugins
- ✅ Need cross-plugin queries
- ✅ Simpler setup
Document doc = new Document("uuid", uuid.toString())
.append("coins", 1000)
.append("level", 5)
.append("created", System.currentTimeMillis());
myCollection.insertOne(doc);import com.mongodb.client.model.Filters;
Document data = myCollection.find(
Filters.eq("uuid", uuid.toString())
).first();
if (data != null) {
int coins = data.getInteger("coins", 0);
}import com.mongodb.client.model.Updates;
myCollection.updateOne(
Filters.eq("uuid", uuid.toString()),
Updates.set("coins", 2000)
);
// Multiple fields
myCollection.updateOne(
Filters.eq("uuid", uuid.toString()),
Updates.combine(
Updates.set("coins", 2000),
Updates.set("level", 10)
)
);// Add 100 coins
myCollection.updateOne(
Filters.eq("uuid", uuid.toString()),
Updates.inc("coins", 100)
);myCollection.deleteOne(
Filters.eq("uuid", uuid.toString())
);// Find all with > 1000 coins
myCollection.find(Filters.gt("coins", 1000)).forEach(doc -> {
System.out.println(doc.getString("uuid"));
});
// Top 10 richest players
import com.mongodb.client.model.Sorts;
myCollection.find()
.sort(Sorts.descending("coins"))
.limit(10)
.forEach(doc -> {
// Process
});NetworkDataAPI also provides a PlayerDataService for shared player data between plugins. This is optional - most plugins should use their own databases as shown above!
Use PlayerDataService when:
- You need to share basic player info between plugins
- You want a simple key-value store for player data
- You don't need complete database isolation
import service.com.cynive.networkdataapi.core.PlayerDataService;
import org.bson.Document;
PlayerDataService playerData = api.getPlayerDataService();
// Get player data asynchronously
playerData.getPlayerDataAsync(player.getUniqueId()).thenAccept(data -> {
// This runs asynchronously - safe for database operations
int coins = data.getInteger("coins", 0);
int level = data.getInteger("level", 1);
// To update UI, switch back to main thread:
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage("You have " + coins + " coins!");
player.sendMessage("Your level is: " + level);
});
}).exceptionally(throwable -> {
getLogger().error("Failed to load player data", throwable);
return null;
});// Only use this if absolutely necessary (blocks the thread!)
Document data = playerData.getPlayerData(player.getUniqueId());
int coins = data.getInteger("coins", 0);// Build the data document
Document playerDocument = new Document()
.append("coins", 1000)
.append("level", 5)
.append("experience", 2500)
.append("rank", "VIP")
.append("friends", Arrays.asList("uuid1", "uuid2"))
.append("settings", new Document()
.append("chat", true)
.append("particles", false)
);
// Save asynchronously (recommended)
playerData.savePlayerDataAsync(player.getUniqueId(), playerDocument)
.thenRun(() -> {
player.sendMessage("Data saved successfully!");
})
.exceptionally(throwable -> {
getLogger().error("Failed to save player data", throwable);
return null;
});// Update a single field without loading the entire document
playerData.updateFieldAsync(player.getUniqueId(), "coins", 1500)
.thenRun(() -> {
player.sendMessage("Coins updated!");
});
// Update last seen timestamp
playerData.updateFieldAsync(player.getUniqueId(), "lastSeen", System.currentTimeMillis());Map<String, Object> updates = new HashMap<>();
updates.put("coins", 2000);
updates.put("level", 6);
updates.put("experience", 3000);
playerData.updateFieldsAsync(player.getUniqueId(), updates)
.thenRun(() -> {
player.sendMessage("Profile updated!");
});// Add 100 coins
playerData.incrementFieldAsync(player.getUniqueId(), "coins", 100);
// Remove 50 coins (negative increment)
playerData.incrementFieldAsync(player.getUniqueId(), "coins", -50);
// Increment kills
playerData.incrementFieldAsync(player.getUniqueId(), "kills", 1);playerData.existsAsync(playerUUID).thenAccept(exists -> {
if (exists) {
// Player has data
} else {
// New player
}
});playerData.deletePlayerDataAsync(player.getUniqueId())
.thenAccept(deleted -> {
if (deleted) {
player.sendMessage("Your data has been deleted.");
} else {
player.sendMessage("No data found to delete.");
}
});NetworkDataAPI does NOT automatically manage player data! You must implement your own event listeners in your custom plugins.
NetworkDataAPI is a database connection layer, not a player data management system. This design allows:
- ✅ Multiple plugins to coexist without conflicts
- ✅ Each plugin to control its own data structure
- ✅ No unwanted default fields in your database
- ✅ Complete flexibility in what data to track
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class MyPlayerListener implements Listener {
private final PlayerDataService playerData;
public MyPlayerListener(PlayerDataService playerData) {
this.playerData = playerData;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
var player = event.getPlayer();
UUID uuid = player.getUniqueId();
// Load player data asynchronously
playerData.getPlayerDataAsync(uuid).thenAccept(data -> {
// Initialize default data if this is a new player
if (!data.containsKey("myPlugin")) {
data.put("myPlugin", new Document()
.append("coins", 0)
.append("level", 1)
.append("firstJoin", System.currentTimeMillis())
);
playerData.savePlayerDataAsync(uuid, data);
}
// Update login timestamp
playerData.updateFieldAsync(uuid, "lastLogin", System.currentTimeMillis());
playerData.updateFieldAsync(uuid, "lastKnownName", player.getName());
}).exceptionally(throwable -> {
getLogger().error("Failed to load data for player: " + player.getName(), throwable);
return null;
});
}
}import org.bukkit.event.player.PlayerQuitEvent;
public class MyPlayerListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
var player = event.getPlayer();
// Update logout timestamp
playerData.updateFieldAsync(
player.getUniqueId(),
"lastLogout",
System.currentTimeMillis()
).thenRun(() -> {
getLogger().debug("Updated logout time for: " + player.getName());
});
}
}public class YourPlugin extends JavaPlugin {
@Override
public void onEnable() {
NetworkDataAPIProvider api = APIRegistry.getAPI();
PlayerDataService playerData = api.getPlayerDataService();
// Register your listener
getServer().getPluginManager().registerEvents(
new MyPlayerListener(playerData),
this
);
}
}Check out PlayerConnectionListener.java in the NetworkDataAPI source code for a complete reference implementation that you can copy and adapt for your needs!
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Sorts;
import org.bson.conversions.Bson;
// Find all players with more than 1000 coins
Bson filter = Filters.gt("coins", 1000);
playerData.queryAsync(filter, 10).thenAccept(results -> {
results.forEach(doc -> {
String name = doc.getString("lastKnownName");
int coins = doc.getInteger("coins", 0);
System.out.println(name + " has " + coins + " coins");
});
});
// Complex query: VIP players who logged in recently
Bson complexFilter = Filters.and(
Filters.eq("rank", "VIP"),
Filters.gte("lastLogin", System.currentTimeMillis() - 86400000) // Last 24h
);
playerData.queryAsync(complexFilter, 50).thenAccept(results -> {
// Process results
});While PlayerDataService handles player data, you can easily create your own collections for ANY data type (cosmetics, guilds, punishments, etc.) using the shared database connection:
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
// Get direct access to the shared database connection
MongoDatabase database = api.getDatabase();
// Create your own collection (for example for cosmetics)
MongoCollection<Document> cosmetics = database.getCollection("cosmetics");
// Perform operations on your custom collection
Document cosmetic = new Document("_id", "party_hat")
.append("name", "Party Hat")
.append("type", "HAT")
.append("rarity", "RARE")
.append("price", 1000);
cosmetics.insertOne(cosmetic);
// Create another collection for player cosmetics
MongoCollection<Document> playerCosmetics = database.getCollection("player_cosmetics");
Document playerCosmeticData = new Document("_id", playerUUID.toString())
.append("owned", Arrays.asList("party_hat", "crown", "santa_hat"))
.append("equipped", new Document()
.append("hat", "party_hat")
.append("trail", "hearts")
);
playerCosmetics.insertOne(playerCosmeticData);Benefits of this approach:
- ✅ No separate database connection needed in your plugin
- ✅ Uses the shared connection pool (max 100 connections)
- ✅ Automatic reconnection and error handling
- ✅ Less resource usage - all plugins share the same pool
- ✅ Simple setup - one line of code:
api.getDatabase() - ✅ Full MongoDB API - all operations available
New in v1.0! Each plugin can now have its own MongoDB database for complete isolation:
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
// Each plugin gets its own database
MongoDatabase cosmeticsDB = api.getDatabase("cosmetics_plugin");
MongoDatabase guildsDB = api.getDatabase("guilds_plugin");
MongoDatabase punishmentsDB = api.getDatabase("punishments_plugin");
// Work with your own database - 100% isolated!
MongoCollection<Document> items = cosmeticsDB.getCollection("items");
MongoCollection<Document> purchases = cosmeticsDB.getCollection("purchases");
// No conflicts with other plugins possible!
items.insertOne(new Document("name", "Crown").append("price", 5000));When to use a separate database?
- ✅ Large plugins with lots of data (1M+ documents)
- ✅ Complete isolation from other plugins
- ✅ Own backup schema per plugin
- ✅ Different replication settings
- ✅ Easier data management and migrations
- ✅ Separate monitoring per plugin
When to use the same database with separate collections?
- ✅ Small to medium-sized plugins
- ✅ Cross-plugin queries needed
- ✅ Simpler setup
Complete example: Cosmetics Plugin with its own database
import api.com.cynive.networkdataapi.core.APIRegistry;
import api.com.cynive.networkdataapi.core.NetworkDataAPIProvider;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.Updates;
import org.bson.Document;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class CosmeticsPlugin extends JavaPlugin {
private NetworkDataAPIProvider api;
private MongoDatabase database;
private MongoCollection<Document> itemsCollection;
private MongoCollection<Document> playerCosmeticsCollection;
@Override
public void onEnable() {
// Hook into NetworkDataAPI
api = APIRegistry.getAPI();
// Get your own dedicated database
database = api.getDatabase("cosmetics_plugin");
// Initialize collections
itemsCollection = database.getCollection("items");
playerCosmeticsCollection = database.getCollection("player_cosmetics");
// Create indexes for better performance
createIndexes();
// Load cosmetic items from database
loadCosmeticItems();
getLogger().info("Cosmetics Plugin using dedicated database: cosmetics_plugin");
}
private void createIndexes() {
// Index on item type for faster queries
itemsCollection.createIndex(new Document("type", 1));
// Index on rarity for faster filtering
itemsCollection.createIndex(new Document("rarity", 1));
// Compound index for player queries
playerCosmeticsCollection.createIndex(
new Document("uuid", 1).append("equipped", 1)
);
}
private void loadCosmeticItems() {
// Count items
long itemCount = itemsCollection.countDocuments();
if (itemCount == 0) {
getLogger().info("No cosmetic items found. Creating defaults...");
createDefaultItems();
} else {
getLogger().info("Loaded " + itemCount + " cosmetic items");
}
}
private void createDefaultItems() {
// Create default cosmetic items
List<Document> defaultItems = Arrays.asList(
new Document("_id", "party_hat")
.append("name", "Party Hat")
.append("type", "HAT")
.append("rarity", "RARE")
.append("price", 1000),
new Document("_id", "crown")
.append("name", "Royal Crown")
.append("type", "HAT")
.append("rarity", "LEGENDARY")
.append("price", 5000),
new Document("_id", "hearts_trail")
.append("name", "Hearts Trail")
.append("type", "TRAIL")
.append("rarity", "UNCOMMON")
.append("price", 500)
);
itemsCollection.insertMany(defaultItems);
getLogger().info("Created " + defaultItems.size() + " default items");
}
// API Methods for your plugin
public CompletableFuture<List<Document>> getPlayerCosmetics(UUID playerUUID) {
return CompletableFuture.supplyAsync(() -> {
Document playerData = playerCosmeticsCollection.find(
Filters.eq("_id", playerUUID.toString())
).first();
if (playerData == null) {
return Collections.emptyList();
}
return playerData.getList("owned", String.class).stream()
.map(itemId -> itemsCollection.find(Filters.eq("_id", itemId)).first())
.filter(Objects::nonNull)
.collect(Collectors.toList());
});
}
public CompletableFuture<Boolean> purchaseCosmetic(UUID playerUUID, String itemId) {
return CompletableFuture.supplyAsync(() -> {
// Get item
Document item = itemsCollection.find(Filters.eq("_id", itemId)).first();
if (item == null) return false;
int price = item.getInteger("price", 0);
// Check if player can afford it (using NetworkDataAPI player data)
Document playerData = api.getPlayerDataService().getPlayerData(playerUUID);
int coins = playerData.getInteger("coins", 0);
if (coins < price) return false;
// Deduct coins
api.getPlayerDataService().incrementField(playerUUID, "coins", -price);
// Add cosmetic to player
playerCosmeticsCollection.updateOne(
Filters.eq("_id", playerUUID.toString()),
Updates.addToSet("owned", itemId),
new UpdateOptions().upsert(true)
);
return true;
});
}
public CompletableFuture<Void> equipCosmetic(UUID playerUUID, String itemId) {
return CompletableFuture.runAsync(() -> {
// Get item type
Document item = itemsCollection.find(Filters.eq("_id", itemId)).first();
if (item == null) return;
String type = item.getString("type");
// Equip the cosmetic
playerCosmeticsCollection.updateOne(
Filters.eq("_id", playerUUID.toString()),
Updates.set("equipped." + type.toLowerCase(), itemId),
new UpdateOptions().upsert(true)
);
});
}
}Benefits of own database per plugin:
- ✅ Complete isolation - no conflicts possible
- ✅ Own backup strategy per plugin
- ✅ Better organization for large datasets
- ✅ Separate monitoring and performance tuning
- ✅ Uses the same connection pool - efficient!
- ✅ No extra configuration - works out-of-the-box
Database Management Best Practices:
// ❌ WRONG: Creating your own MongoClient
MongoClient myOwnClient = MongoClients.create("mongodb://localhost:27017");
// This wastes resources and connections!
// ✅ CORRECT: Use NetworkDataAPI's connection
MongoDatabase myDB = api.getDatabase("my_plugin");
// Uses the shared, configured connection pool!Example: Guild Plugin with its own database
import api.com.cynive.networkdataapi.core.APIRegistry;
import api.com.cynive.networkdataapi.core.NetworkDataAPIProvider;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.UUID;
public class GuildsPlugin extends JavaPlugin {
private MongoDatabase guildsDatabase;
private MongoCollection<Document> guildsCollection;
private MongoCollection<Document> guildMembersCollection;
@Override
public void onEnable() {
NetworkDataAPIProvider api = APIRegistry.getAPI();
// Dedicated database for guilds
guildsDatabase = api.getDatabase("guilds_plugin");
guildsCollection = guildsDatabase.getCollection("guilds");
guildMembersCollection = guildsDatabase.getCollection("guild_members");
// Create indexes
guildsCollection.createIndex(new Document("name", 1));
guildsCollection.createIndex(new Document("level", -1));
guildMembersCollection.createIndex(new Document("guildId", 1));
}
public void createGuild(String guildName, UUID ownerUUID) {
Document guild = new Document()
.append("name", guildName)
.append("owner", ownerUUID.toString())
.append("level", 1)
.append("members", 1)
.append("createdAt", System.currentTimeMillis())
.append("bankBalance", 0);
guildsCollection.insertOne(guild);
// Add owner as member
Document member = new Document()
.append("uuid", ownerUUID.toString())
.append("guildId", guild.getObjectId("_id"))
.append("rank", "OWNER")
.append("joinedAt", System.currentTimeMillis());
guildMembersCollection.insertOne(member);
}
}Complete example for a Cosmetics Plugin:
See COSMETICS_PLUGIN_EXAMPLE.java in the repository for a fully working example!
// Save nested structure
Document settings = new Document()
.append("notifications", new Document()
.append("chat", true)
.append("friend_requests", false)
)
.append("privacy", new Document()
.append("show_online", true)
.append("allow_messages", true)
);
playerData.updateFieldAsync(playerUUID, "settings", settings);
// Read nested value
playerData.getPlayerDataAsync(playerUUID).thenAccept(data -> {
Document settingsDoc = (Document) data.get("settings");
if (settingsDoc != null) {
Document notifs = (Document) settingsDoc.get("notifications");
boolean chatNotifs = notifs.getBoolean("chat", true);
}
});
// Update nested field using dot notation
playerData.updateFieldAsync(playerUUID, "settings.notifications.chat", false);In config.yml:
rest-api:
enabled: true
port: 8080
api-key: "your-secret-key-here"
allowed-ips:
- "127.0.0.1"
- "10.0.0.0/24"GET /api/health
Response:
{
"status": "healthy",
"timestamp": 1699123456789
}GET /api/player/{uuid}
Headers:
X-API-Key: your-secret-key-here
Response:
{
"_id": "uuid-here",
"coins": 1000,
"level": 5,
"lastLogin": 1699123456789
}POST /api/player/{uuid}
Headers:
X-API-Key: your-secret-key-here
Content-Type: application/json
Body:
{
"coins": 1500,
"level": 6
}
Response:
{
"message": "Player data updated successfully"
}DELETE /api/player/{uuid}
Headers:
X-API-Key: your-secret-key-here
Response:
{
"message": "Player data deleted successfully"
}GET /api/stats
Headers:
X-API-Key: your-secret-key-here
Response:
{
"running": true,
"timestamp": 1699123456789
}import requests
API_URL = "http://localhost:8080/api"
API_KEY = "your-secret-key-here"
def get_player_data(uuid):
headers = {"X-API-Key": API_KEY}
response = requests.get(f"{API_URL}/player/{uuid}", headers=headers)
return response.json()
def update_player_coins(uuid, coins):
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
data = {"coins": coins}
response = requests.post(f"{API_URL}/player/{uuid}", headers=headers, json=data)
return response.json()
# Usage
player_uuid = "123e4567-e89b-12d3-a456-426614174000"
data = get_player_data(player_uuid)
print(f"Player has {data['coins']} coins")
update_player_coins(player_uuid, 2000)// ❌ BAD: Blocks main thread
Document data = playerData.getPlayerData(uuid);
// ✅ GOOD: Non-blocking
playerData.getPlayerDataAsync(uuid).thenAccept(data -> {
// Process data
});playerData.getPlayerDataAsync(uuid)
.thenAccept(data -> {
// Success handler
})
.exceptionally(throwable -> {
// Error handler
getLogger().error("Failed to load data", throwable);
player.sendMessage("Failed to load your data. Please try again.");
return null;
});// ❌ BAD: Loads entire document just to update one field
Document data = playerData.getPlayerData(uuid);
data.put("coins", 1000);
playerData.savePlayerData(uuid, data);
// ✅ GOOD: Updates only the field
playerData.updateFieldAsync(uuid, "coins", 1000);The plugin automatically caches player data, but you can also implement your own caching:
private final Map<UUID, PlayerProfile> profileCache = new ConcurrentHashMap<>();
public CompletableFuture<PlayerProfile> getProfile(UUID uuid) {
// Check local cache first
if (profileCache.containsKey(uuid)) {
return CompletableFuture.completedFuture(profileCache.get(uuid));
}
// Load from API (which uses its own cache)
return playerData.getPlayerDataAsync(uuid).thenApply(data -> {
PlayerProfile profile = new PlayerProfile(data);
profileCache.put(uuid, profile);
return profile;
});
}// Update multiple players efficiently
List<UUID> players = Arrays.asList(uuid1, uuid2, uuid3);
List<CompletableFuture<Void>> futures = players.stream()
.map(uuid -> playerData.updateFieldAsync(uuid, "event_participated", true))
.collect(Collectors.toList());
// Wait for all updates to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> {
getLogger().info("All players updated!");
});@Override
public void onDisable() {
// Save any pending data
onlinePlayers.forEach(player -> {
Document data = buildPlayerData(player);
// Use synchronous save on shutdown
playerData.savePlayerData(player.getUniqueId(), data);
});
}public class EconomyManager {
private final PlayerDataService playerData;
public EconomyManager(NetworkDataAPIProvider api) {
this.playerData = api.getPlayerDataService();
}
public CompletableFuture<Integer> getBalance(UUID uuid) {
return playerData.getPlayerDataAsync(uuid)
.thenApply(data -> data.getInteger("coins", 0));
}
public CompletableFuture<Boolean> addCoins(UUID uuid, int amount) {
return playerData.incrementFieldAsync(uuid, "coins", amount)
.thenApply(v -> true)
.exceptionally(throwable -> false);
}
public CompletableFuture<Boolean> removeCoins(UUID uuid, int amount) {
return getBalance(uuid).thenCompose(balance -> {
if (balance < amount) {
return CompletableFuture.completedFuture(false);
}
return playerData.incrementFieldAsync(uuid, "coins", -amount)
.thenApply(v -> true);
});
}
public CompletableFuture<Boolean> setBalance(UUID uuid, int amount) {
return playerData.updateFieldAsync(uuid, "coins", amount)
.thenApply(v -> true)
.exceptionally(throwable -> false);
}
}public class StatsManager {
private final PlayerDataService playerData;
public void recordKill(UUID killer, UUID victim) {
// Increment killer's kills
playerData.incrementFieldAsync(killer, "stats.kills", 1);
// Increment victim's deaths
playerData.incrementFieldAsync(victim, "stats.deaths", 1);
// Update kill/death ratio
playerData.getPlayerDataAsync(killer).thenAccept(data -> {
Document stats = (Document) data.get("stats");
int kills = stats.getInteger("kills", 0);
int deaths = stats.getInteger("deaths", 0);
double kd = deaths > 0 ? (double) kills / deaths : kills;
playerData.updateFieldAsync(killer, "stats.kd_ratio", kd);
});
}
public CompletableFuture<Document> getStats(UUID uuid) {
return playerData.getPlayerDataAsync(uuid)
.thenApply(data -> (Document) data.get("stats"));
}
}public class CrossServerMessaging {
// On Server A: Player sends message
public void sendMessage(UUID from, UUID to, String message) {
Document messageDoc = new Document()
.append("from", from.toString())
.append("message", message)
.append("timestamp", System.currentTimeMillis())
.append("read", false);
playerData.getPlayerDataAsync(to).thenAccept(data -> {
List<Document> messages = (List<Document>) data.get("messages");
if (messages == null) {
messages = new ArrayList<>();
}
messages.add(messageDoc);
playerData.updateFieldAsync(to, "messages", messages);
});
}
// On Server B: Player receives messages
public void checkMessages(UUID player) {
playerData.getPlayerDataAsync(player).thenAccept(data -> {
List<Document> messages = (List<Document>) data.get("messages");
if (messages != null) {
messages.stream()
.filter(msg -> !msg.getBoolean("read", false))
.forEach(msg -> {
// Display message to player
String from = msg.getString("from");
String text = msg.getString("message");
// Send to player...
});
}
});
}
}Problem: Your plugin can't find NetworkDataAPI.
Solution:
- Ensure NetworkDataAPI is installed in the plugins folder
- Check that you've added
dependorsoftdependin plugin.yml - Verify NetworkDataAPI loads before your plugin (check startup logs)
Problem: Can't connect to MongoDB.
Solutions:
- Check MongoDB is running:
mongo --eval "db.adminCommand('ping')" - Verify connection string in config.yml
- Check firewall settings
- Test connectivity:
telnet mongodb-host 27017
Problem: Operations taking too long.
Solutions:
- Increase timeout values in config.yml
- Check MongoDB performance
- Review query indexes
- Monitor network latency
Problem: Plugin using too much RAM.
Solutions:
- Reduce
cache.max-sizein config.yml - Decrease
cache.expire-after-write-minutes - Check for memory leaks in your code
Problem: Cache not effective.
Solutions:
- Increase
cache.max-size - Increase
cache.expire-after-access-minutes - Review data access patterns
Enable debug logging in config.yml:
logging:
level: "DEBUG"
debug: trueThen check logs for detailed information:
- Paper/Spigot:
logs/latest.log - BungeeCord:
proxy.log.0
Use the admin command to monitor performance:
/networkdataapi status
/networkdataapi cache stats
- Check logs for error messages
- Enable debug mode
- Review this documentation
- Open an issue on GitHub with:
- Full error logs
- Configuration file
- Steps to reproduce
- Initial release
- Multi-platform support (Paper + BungeeCord)
- MongoDB integration with connection pooling
- Caffeine caching
- REST API
- Full async support
This project is licensed under the MIT License.
Author: Stijn Jakobs Architecture: Enterprise-grade, production-ready Supported Platforms: Paper, Spigot, BungeeCord
For questions or support, please open an issue on GitHub.