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.