diff --git a/docs/optional-modules/mongodb.md b/docs/optional-modules/mongodb.md
index 6d12305..600c4f4 100644
--- a/docs/optional-modules/mongodb.md
+++ b/docs/optional-modules/mongodb.md
@@ -4,28 +4,69 @@ description: MongoDB database integration for document-based storage.
# MongoDB
-## Setup
+The `pluginbase-mongo` module provides a thin wrapper around the MongoDB Java driver (v5.6.1, shaded and relocated) for use in Spigot plugins.
+
+## Dependency
+
+Add `pluginbase-mongo` as a dependency. The MongoDB driver is shaded into the module jar, so no additional runtime dependencies are needed.
+
+```xml
+
+ dev.demeng
+ pluginbase-mongo
+ 1.36.1-SNAPSHOT
+
+```
+
+## Credentials
+
+`MongoCredentials` holds the connection URI and database name.
+
+| Field | Type | Description |
+|------------|----------|---------------------------------------------|
+| `uri` | `String` | MongoDB connection URI (required, non-null) |
+| `database` | `String` | Name of the default database (required, non-null) |
+
+There are two ways to create credentials:
```java
-import dev.demeng.pluginbase.mongo.Mongo;
import dev.demeng.pluginbase.mongo.MongoCredentials;
-// Create credentials with connection URI
+// From explicit values
MongoCredentials credentials = MongoCredentials.of(
"mongodb://username:password@localhost:27017",
"minecraft"
);
-// Or simple localhost connection
+// From a Bukkit ConfigurationSection (keys: "uri", "database")
MongoCredentials credentials = MongoCredentials.of(
- "mongodb://localhost:27017",
- "minecraft"
+ getConfig().getConfigurationSection("mongo")
);
+```
+
+The `ConfigurationSection` overload reads `uri` (default `mongodb://localhost:27017`) and `database` (default `minecraft`).
+
+## Creating a Connection
+
+Pass credentials to the `Mongo` constructor. This immediately opens a connection and selects the default database.
+
+```java
+import dev.demeng.pluginbase.mongo.Mongo;
-// Create Mongo instance
Mongo mongo = new Mongo(credentials);
```
+## API Reference
+
+`Mongo` implements `IMongo`, which extends `Terminable`.
+
+| Method | Return Type | Description |
+|--------------------------------|-----------------|------------------------------------------------|
+| `getClient()` | `MongoClient` | The underlying MongoDB client instance |
+| `getDatabase()` | `MongoDatabase` | The default database specified in credentials |
+| `getDatabase(String name)` | `MongoDatabase` | A database by name from the same client |
+| `close()` | `void` | Closes the client connection |
+
## Basic Operations
### Insert Document
@@ -100,6 +141,41 @@ players.updateOne(
players.deleteOne(Filters.eq("uuid", uuid.toString()));
```
+## Queries
+
+```java
+import com.mongodb.client.model.Sorts;
+
+// Top players by coins
+List top = players
+ .find()
+ .sort(Sorts.descending("coins"))
+ .limit(10)
+ .into(new ArrayList<>());
+
+// Players with level >= 10
+List highLevel = players
+ .find(Filters.gte("level", 10))
+ .into(new ArrayList<>());
+
+// Compound filter
+List results = players
+ .find(Filters.and(
+ Filters.gte("level", 5),
+ Filters.lte("coins", 1000)
+ ))
+ .into(new ArrayList<>());
+```
+
+## Indexes
+
+```java
+import com.mongodb.client.model.Indexes;
+
+players.createIndex(Indexes.ascending("uuid"));
+players.createIndex(Indexes.descending("coins"));
+```
+
## Complete Example
```java
@@ -110,10 +186,9 @@ public class MyPlugin extends BasePlugin {
@Override
protected DependencyContainer configureDependencies() {
- String uri = getConfig().getString("mongo.uri", "mongodb://localhost:27017");
- String database = getConfig().getString("mongo.database", "minecraft");
-
- MongoCredentials credentials = MongoCredentials.of(uri, database);
+ MongoCredentials credentials = MongoCredentials.of(
+ getConfig().getConfigurationSection("mongo")
+ );
this.mongo = new Mongo(credentials);
this.players = mongo.getDatabase().getCollection("players");
@@ -126,12 +201,10 @@ public class MyPlugin extends BasePlugin {
@Override
protected void enable() {
- // Load on join
Events.subscribe(PlayerJoinEvent.class)
.handler(e -> loadPlayer(e.getPlayer()))
.bindWith(this);
- // Save on quit
Events.subscribe(PlayerQuitEvent.class)
.handler(e -> savePlayer(e.getPlayer()))
.bindWith(this);
@@ -144,7 +217,6 @@ public class MyPlugin extends BasePlugin {
return players.find(Filters.eq("uuid", uuid.toString())).first();
}).thenApplySync(doc -> {
if (doc != null) {
- // Apply loaded data
PlayerData data = new PlayerData(
uuid,
doc.getString("name"),
@@ -154,7 +226,6 @@ public class MyPlugin extends BasePlugin {
cache.put(uuid, data);
Text.tell(player, "&aData loaded!");
} else {
- // Create new player
createPlayer(uuid, player.getName());
}
return doc;
@@ -196,42 +267,6 @@ public class MyPlugin extends BasePlugin {
}
```
-## Queries
-
-```java
-import com.mongodb.client.model.Sorts;
-
-// Top players by coins
-List top = players
- .find()
- .sort(Sorts.descending("coins"))
- .limit(10)
- .into(new ArrayList<>());
-
-// Players with level >= 10
-List highLevel = players
- .find(Filters.gte("level", 10))
- .into(new ArrayList<>());
-
-// Complex query
-List results = players
- .find(Filters.and(
- Filters.gte("level", 5),
- Filters.lte("coins", 1000)
- ))
- .into(new ArrayList<>());
-```
-
-## Indexes
-
-```java
-import com.mongodb.client.model.Indexes;
-
-// Create index for faster queries
-players.createIndex(Indexes.ascending("uuid"));
-players.createIndex(Indexes.descending("coins"));
-```
-
## Cleanup
-MongoDB connections are automatically closed when plugin disables (implements AutoCloseable).
+`Mongo` implements `Terminable`. The connection is closed automatically when the plugin disables or when `close()` is called manually.
diff --git a/docs/optional-modules/redis.md b/docs/optional-modules/redis.md
index 4ff0c65..eb871eb 100644
--- a/docs/optional-modules/redis.md
+++ b/docs/optional-modules/redis.md
@@ -4,59 +4,124 @@ description: Redis pub/sub for cross-server messaging.
# Redis
-## Setup
+The `pluginbase-redis` module wraps Jedis (v7.0.0, shaded and relocated) to provide pub/sub messaging between Spigot servers. Messages are wrapped in a `MessageTransferObject` that carries a server ID, a JSON payload, and a timestamp.
+
+## Dependency
+
+Add `pluginbase-redis` as a dependency. Jedis and commons-pool2 are shaded into the module jar.
+
+```xml
+
+ dev.demeng
+ pluginbase-redis
+ 1.36.1-SNAPSHOT
+
+```
+
+## Credentials
+
+`RedisCredentials` holds connection details for a Redis instance.
+
+| Field | Type | Nullable | Description |
+|------------|-----------|----------|------------------------------------------|
+| `host` | `String` | No | Hostname or IP address |
+| `port` | `int` | -- | Port number (typically 6379) |
+| `user` | `String` | Yes | Username for ACL auth (null to skip) |
+| `password` | `String` | Yes | Password (null to skip) |
+| `ssl` | `boolean` | -- | Whether to use SSL |
+
+Empty or blank strings for `user` and `password` are normalized to `null`.
+
+There are two ways to create credentials:
```java
-import dev.demeng.pluginbase.redis.Redis;
import dev.demeng.pluginbase.redis.RedisCredentials;
-// Create credentials
+// From explicit values
RedisCredentials credentials = RedisCredentials.of(
"localhost",
6379,
- null, // username (optional)
- "password", // password (optional)
- false // SSL
+ null, // user
+ "password", // password
+ false // ssl
);
-// Or for localhost without auth
+// From a Bukkit ConfigurationSection (keys: host, port, user, password, ssl)
RedisCredentials credentials = RedisCredentials.of(
- "localhost",
- 6379,
- null,
- null,
- false
+ getConfig().getConfigurationSection("redis")
);
-
-// Create Redis instance with server ID
-Redis redis = new Redis("server-1", credentials);
```
-## Pub/Sub Messaging
+The `ConfigurationSection` overload defaults to `localhost:6379`, no auth, SSL off.
-### Publishing Messages
+## Creating a Connection
+
+The `Redis` constructor takes a server ID (used as the sender ID in messages), credentials, and an optional varargs list of channels to subscribe to immediately.
```java
-// Publish string message
-redis.publishString("chat", "Hello from server 1!");
+import dev.demeng.pluginbase.redis.Redis;
-// Publish object (serialized to JSON)
-PlayerData data = new PlayerData(uuid, name, coins);
-redis.publishObject("player-data", data);
+// Subscribe to channels at construction time
+Redis redis = new Redis("server-1", credentials, "chat", "alerts");
+
+// Or create without initial subscriptions, then subscribe later
+Redis redis = new Redis("server-1", credentials);
+redis.subscribe("chat", "alerts");
```
-### Receiving Messages
+## API Reference
+
+`Redis` implements `IRedis`, which extends `Terminable`.
+
+| Method | Return Type | Description |
+|-------------------------------------------------|-------------|--------------------------------------------------------------------|
+| `getServerId()` | `String` | The server ID passed to the constructor |
+| `getJedisPool()` | `JedisPool` | The underlying Jedis connection pool |
+| `subscribe(String... channels)` | `boolean` | Subscribes to channels not already subscribed. Returns true if at least one new channel was added. |
+| `unsubscribe(String... channels)` | `void` | Unsubscribes from the given channels |
+| `isSubscribed(String channel)` | `boolean` | Whether the given channel is currently subscribed |
+| `publishObject(String channel, Object obj)` | `boolean` | Publishes an object (serialized to JSON). Returns false if closing or serialization fails. |
+| `publishString(String channel, String str)` | `boolean` | Publishes a plain string. Returns false if closing or serialization fails. |
+| `close()` | `void` | Unsubscribes from all channels and destroys the connection pool |
+
+Publishing happens asynchronously on the PluginBase async scheduler.
+
+## Receiving Messages
+
+When a message arrives on a subscribed channel, two Bukkit events are fired:
+
+1. **`AsyncRedisMessageReceiveEvent`** -- fired on the async thread where the message was received.
+2. **`RedisMessageReceiveEvent`** -- fired on the main server thread (scheduled via `Schedulers.sync()`).
+
+Both events implement `IRedisMessageReceiveEvent` and expose the same properties.
-Use Events to receive published messages:
+### Event Properties
+
+| Method | Return Type | Description |
+|-------------------------------------------|--------------|----------------------------------------------------------|
+| `getChannel()` | `String` | The channel the message arrived on |
+| `getSenderId()` | `String` | The server ID of the sender |
+| `getMessage()` | `String` | The raw JSON string payload |
+| `getMessageObject(Class objectClass)` | `Optional`| The payload deserialized to the given type, or empty if parsing fails |
+| `getTimestamp()` | `long` | Epoch millis when the message was published |
+
+Use the async event when you do not need to interact with the Bukkit API (file I/O, logging, forwarding). Use the sync event when you need to modify world state, send packets, or call Bukkit methods that require the main thread.
```java
import dev.demeng.pluginbase.redis.event.AsyncRedisMessageReceiveEvent;
+import dev.demeng.pluginbase.redis.event.RedisMessageReceiveEvent;
-// Subscribe to channel
-redis.subscribe("chat");
-
-// Listen for messages
+// Async: good for logging or forwarding
Events.subscribe(AsyncRedisMessageReceiveEvent.class)
+ .filter(e -> e.getChannel().equals("chat"))
+ .handler(e -> {
+ String message = e.getMessageObject(String.class).orElse("");
+ getLogger().info("[Network] " + message);
+ })
+ .bindWith(this);
+
+// Sync: safe to call Bukkit API
+Events.subscribe(RedisMessageReceiveEvent.class)
.filter(e -> e.getChannel().equals("chat"))
.handler(e -> {
String message = e.getMessageObject(String.class).orElse("");
@@ -65,25 +130,37 @@ Events.subscribe(AsyncRedisMessageReceiveEvent.class)
.bindWith(this);
```
-## Cross-Server Messaging
+## Publishing Messages
+
+```java
+// Publish a plain string
+redis.publishString("chat", "Hello from server 1!");
+
+// Publish an object (serialized to JSON via Gson)
+PlayerData data = new PlayerData(uuid, name, coins);
+redis.publishObject("player-data", data);
+```
+
+Both methods return `boolean`: `true` if the message was queued for publishing, `false` if the connection is closing or serialization failed.
+
+## Complete Example
```java
-public class MyPlugin extends BasePlugin {
+public class CrossServerPlugin extends BasePlugin {
private Redis redis;
- private String serverName;
@Override
protected DependencyContainer configureDependencies() {
RedisCredentials credentials = RedisCredentials.of(
- getConfig().getString("redis.host", "localhost"),
- getConfig().getInt("redis.port", 6379),
- null,
- getConfig().getString("redis.password"),
- false
+ getConfig().getConfigurationSection("redis")
);
- this.redis = new Redis("my-server", credentials);
+ this.redis = new Redis(
+ getConfig().getString("server-id", "server-1"),
+ credentials,
+ "global-chat", "player-join"
+ );
return DependencyInjection.builder()
.register(this)
@@ -93,13 +170,8 @@ public class MyPlugin extends BasePlugin {
@Override
protected void enable() {
- this.serverName = getConfig().getString("server-name", "Server");
-
- // Subscribe to channels
- redis.subscribe("global-chat", "player-join", "player-quit");
-
- // Listen for global chat
- Events.subscribe(AsyncRedisMessageReceiveEvent.class)
+ // Sync event: broadcasts to online players (requires main thread)
+ Events.subscribe(RedisMessageReceiveEvent.class)
.filter(e -> e.getChannel().equals("global-chat"))
.handler(e -> {
String message = e.getMessageObject(String.class).orElse("");
@@ -107,8 +179,7 @@ public class MyPlugin extends BasePlugin {
})
.bindWith(this);
- // Listen for player joins
- Events.subscribe(AsyncRedisMessageReceiveEvent.class)
+ Events.subscribe(RedisMessageReceiveEvent.class)
.filter(e -> e.getChannel().equals("player-join"))
.handler(e -> {
String playerName = e.getMessageObject(String.class).orElse("");
@@ -116,11 +187,11 @@ public class MyPlugin extends BasePlugin {
})
.bindWith(this);
- // Publish on events
+ // Publish local events to the network
Events.subscribe(AsyncPlayerChatEvent.class)
.handler(e -> {
- String message = e.getPlayer().getName() + ": " + e.getMessage();
- redis.publishString("global-chat", message);
+ String msg = e.getPlayer().getName() + ": " + e.getMessage();
+ redis.publishString("global-chat", msg);
})
.bindWith(this);
@@ -133,128 +204,6 @@ public class MyPlugin extends BasePlugin {
}
```
-## Commands Across Servers
-
-```java
-// Send command to all servers
-@Command("globalkick")
-@CommandPermission("admin.globalkick")
-public void globalKick(Player sender, String targetName) {
- // Publish kick command
- redis.publishString("kick-player", targetName);
- Text.tell(sender, "&aKick command sent to all servers!");
-}
-
-// In enable():
-redis.subscribe("kick-player");
-
-Events.subscribe(AsyncRedisMessageReceiveEvent.class)
- .filter(e -> e.getChannel().equals("kick-player"))
- .handler(e -> {
- String playerName = e.getMessageObject(String.class).orElse("");
- Player target = Bukkit.getPlayerExact(playerName);
- if (target != null) {
- target.kickPlayer("§cYou have been kicked from the network!");
- }
- })
- .bindWith(this);
-```
-
-## Complete Example
-
-```java
-public class CrossServerPlugin extends BasePlugin {
-
- private Redis redis;
-
- @Override
- protected DependencyContainer configureDependencies() {
- RedisCredentials credentials = RedisCredentials.of(
- "localhost",
- 6379,
- null,
- null,
- false
- );
-
- this.redis = new Redis("server-1", credentials);
-
- return DependencyInjection.builder()
- .register(this)
- .register(Redis.class, redis)
- .build();
- }
-
- @Override
- protected void enable() {
- // Subscribe to channels
- redis.subscribe("chat", "teleport-request");
-
- // Global chat
- Events.subscribe(AsyncRedisMessageReceiveEvent.class)
- .filter(e -> e.getChannel().equals("chat"))
- .handler(e -> {
- String message = e.getMessageObject(String.class).orElse("");
- Bukkit.broadcastMessage("§7[Network] §f" + message);
- })
- .bindWith(this);
-
- Events.subscribe(AsyncPlayerChatEvent.class)
- .handler(e -> {
- String msg = e.getPlayer().getName() + ": " + e.getMessage();
- redis.publishString("chat", msg);
- e.setCancelled(true); // Handle locally via redis
- })
- .bindWith(this);
-
- // Server-to-server teleport
- Events.subscribe(AsyncRedisMessageReceiveEvent.class)
- .filter(e -> e.getChannel().equals("teleport-request"))
- .handler(e -> {
- String data = e.getMessageObject(String.class).orElse("");
- String[] parts = data.split(":");
- if (parts.length < 2) return;
-
- String playerName = parts[0];
- String serverName = parts[1];
-
- Player player = Bukkit.getPlayerExact(playerName);
- if (player != null) {
- // Connect player to target server (BungeeCord)
- sendToServer(player, serverName);
- }
- })
- .bindWith(this);
-
- // Commands
- Lamp handler = createCommandHandler();
- handler.register(new NetworkCommands(redis));
- }
-}
-
-public class NetworkCommands {
- private final Redis redis;
-
- public NetworkCommands(Redis redis) {
- this.redis = redis;
- }
-
- @Command("alert")
- @CommandPermission("admin.alert")
- public void alert(Player sender, String message) {
- redis.publishString("alert", message);
- Text.tell(sender, "&aAlert sent to all servers!");
- }
-
- @Command("tpserver")
- @CommandPermission("admin.tpserver")
- public void tpServer(Player sender, Player target, String server) {
- redis.publishString("teleport-request", target.getName() + ":" + server);
- Text.tell(sender, "&aTeleport request sent!");
- }
-}
-```
-
## Cleanup
-Redis connections are automatically closed when plugin disables (implements AutoCloseable).
+`Redis` implements `Terminable`. All subscriptions are cancelled and the Jedis pool is destroyed automatically when the plugin disables or when `close()` is called manually.