Skip to content

Commit 2684acd

Browse files
authored
Merge pull request #3 from Josscoder/dev
Dev
2 parents 3d1cc16 + 3704955 commit 2684acd

File tree

22 files changed

+347
-72
lines changed

22 files changed

+347
-72
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,108 @@
44

55
**RedisBridge** is a complete rewrite of my previous plugin [JBridge](https://github.com/JossArchived/JBridge), developed for Nukkit and WaterdogPE. It provides automatic server registration, player management, and seamless communication between backend servers and the proxy.
66

7+
# ⚙️ Available Commands
8+
RedisBridge includes ready-to-use commands for your proxy (WaterdogPE, Velocity, BungeeCord) or backend servers:
9+
10+
- `/lobby`
11+
Teleports the player to an available lobby instance using the `LOWEST_PLAYERS` strategy to avoid overloading a single lobby while keeping activity balanced.
12+
13+
- `/transfer <server>`
14+
Transfers the player to a specific instance if available. Useful for networks with multiple game modes.
15+
16+
- `/whereami`
17+
Displays information to the player showing which instance and group they are currently in, along with the number of players and instance capacity.
18+
19+
# 📡 Instance Management Usage
20+
RedisBridge includes a **distributed instance discovery and selection system** for minigame servers, lobbies, or backend servers using Redis and a low-latency distributed cache.
21+
22+
Each server instance sends **automatic heartbeats** via `InstanceHeartbeatMessage` containing:
23+
24+
- `id` (unique identifier)
25+
26+
- `group` (e.g., `lobby`, `solo_skywars`, `duels`)
27+
28+
- `players` (current online players)
29+
30+
- `maxPlayers` (maximum capacity)
31+
32+
- `host` and `port`
33+
34+
This allows other servers and the proxy to know in real time which instances are available, their capacity, and their status.
35+
36+
The `InstanceManager`:
37+
38+
- Uses a local cache with a 10-second expiration to keep instance state updated efficiently.
39+
40+
- Allows you to:
41+
42+
- Retrieve instances by ID (`getInstanceById`)
43+
44+
- Retrieve all instances in a group (`getGroupInstances`)
45+
46+
- Get total player counts or per group (`getTotalPlayerCount`, `getGroupPlayerCount`)
47+
48+
- Get total maximum player capacity or per group (`getTotalMaxPlayers`, `getGroupMaxPlayers`)
49+
50+
Provides **automatic available instance selection** using different strategies:
51+
52+
- `RANDOM`: Selects a random instance in the group.
53+
54+
- `LOWEST_PLAYERS`: Selects the instance with the fewest players.
55+
56+
- `MOST_PLAYERS_AVAILABLE`: Selects the instance with the most players.
57+
58+
Example:
59+
60+
```java
61+
InstanceInfo instance = InstanceManager.getInstance().selectAvailableInstance("lobby", InstanceManager.SelectionStrategy.LOWEST_PLAYERS);
62+
63+
if (instance != null) {
64+
// Connect player to this instance
65+
}
66+
```
67+
68+
This system enables your network to distribute players dynamically without relying on a heavy centralized matchmaking server.
69+
70+
# 🚀 Communication Usage
71+
RedisBridge simplifies inter-server communication over Redis, enabling you to publish and subscribe to messages seamlessly between your proxy and backend servers.
72+
73+
## How it works?
74+
- Uses Redis Pub/Sub on a single channel (redis-bridge-channel) for all message transmission.
75+
76+
- Messages are serialized in JSON and identified using their type field.
77+
78+
- Each message type can have:
79+
80+
- A registered class (MessageRegistry) for deserialization.
81+
82+
- An optional handler (MessageHandlerRegistry) for automatic processing when received.
83+
84+
- Includes default messages for instance heartbeat and shutdown announcements to enable automatic instance tracking.
85+
86+
## Publishing Messages
87+
To send a message to all connected instances:
88+
```java
89+
YourCustomMessage message = new YourCustomMessage();
90+
// fill your message data here
91+
92+
redisBridge.publish(message, "sender-id");
93+
```
94+
95+
## Handling Incoming Messages
96+
- Register your message type:
97+
```java
98+
MessageRegistry.register("your-message-type", YourCustomMessage.class);
99+
```
100+
- Register your message handler:
101+
```java
102+
MessageHandlerRegistry.register("your-message-type", new MessageHandler<YourCustomMessage>() {
103+
@Override
104+
public void handle(YourCustomMessage message) {
105+
// handle your message here
106+
}
107+
});
108+
```
7109

8110
## License
9111
**RedisBridge** is licensed under the [MIT License](./LICENSE). Feel free to use, modify, and distribute it in your projects.

core/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@
3131
<version>33.4.8-jre</version>
3232
<scope>compile</scope>
3333
</dependency>
34+
<dependency>
35+
<groupId>com.fasterxml.jackson.core</groupId>
36+
<artifactId>jackson-core</artifactId>
37+
<version>2.19.2</version>
38+
<scope>compile</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>com.fasterxml.jackson.core</groupId>
42+
<artifactId>jackson-databind</artifactId>
43+
<version>2.17.2</version>
44+
<scope>compile</scope>
45+
</dependency>
3446
</dependencies>
3547

3648
</project>

core/src/main/java/net/josscoder/redisbridge/core/RedisBridgeCore.java

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package net.josscoder.redisbridge.core;
22

3-
import com.google.gson.Gson;
4-
import com.google.gson.GsonBuilder;
5-
import net.josscoder.redisbridge.core.data.InstanceInfo;
3+
import net.josscoder.redisbridge.core.instance.InstanceInfo;
4+
import net.josscoder.redisbridge.core.instance.InstanceManager;
5+
import net.josscoder.redisbridge.core.message.MessageBase;
6+
import net.josscoder.redisbridge.core.message.MessageHandler;
7+
import net.josscoder.redisbridge.core.message.MessageHandlerRegistry;
8+
import net.josscoder.redisbridge.core.message.MessageRegistry;
69
import net.josscoder.redisbridge.core.logger.ILogger;
7-
import net.josscoder.redisbridge.core.manager.InstanceManager;
10+
import net.josscoder.redisbridge.core.message.defaults.InstanceHeartbeatMessage;
11+
import net.josscoder.redisbridge.core.message.defaults.InstanceShutdownMessage;
12+
import net.josscoder.redisbridge.core.utils.JsonUtils;
813
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
914
import redis.clients.jedis.Jedis;
1015
import redis.clients.jedis.JedisPool;
1116
import redis.clients.jedis.JedisPubSub;
1217

18+
/**
19+
* Part of this code is taken from:
20+
* <a href="https://github.com/theminecoder/DynamicServers/blob/master/dynamicservers-common/src/main/java/me/theminecoder/dynamicservers/DynamicServersCore.java">DynamicServers</a>
21+
*/
1322
public class RedisBridgeCore {
1423

15-
private static final Gson GSON = new GsonBuilder().create();
16-
public static final String INSTANCE_HEARTBEAT_CHANNEL = "instance_heartbeat_channel";
17-
public static final String INSTANCE_REMOVE_CHANNEL = "instance_removed_channel";
24+
public static final String CHANNEL = "redis-bridge-channel";
1825

1926
private JedisPool jedisPool = null;
2027
private Thread listenerThread;
@@ -37,16 +44,30 @@ public void connect(String host, int port, String password, ILogger logger) {
3744
try (Jedis jedis = jedisPool.getResource()) {
3845
jedis.subscribe(new JedisPubSub() {
3946
@Override
40-
public void onMessage(String channel, String message) {
41-
if (channel.equals(INSTANCE_HEARTBEAT_CHANNEL)) {
42-
InstanceInfo data = GSON.fromJson(message, InstanceInfo.class);
43-
InstanceManager.INSTANCE_CACHE.put(data.getId(), data);
44-
} else if (channel.equals(INSTANCE_REMOVE_CHANNEL)) {
45-
InstanceInfo data = GSON.fromJson(message, InstanceInfo.class);
46-
InstanceManager.INSTANCE_CACHE.invalidate(data.getId());
47+
public void onMessage(String channel, String messageJson) {
48+
try {
49+
String type = JsonUtils.extractType(messageJson);
50+
51+
Class<? extends MessageBase> clazz = MessageRegistry.getClass(type);
52+
if (clazz == null) {
53+
logger.debug("Unregistered message type: " + type);
54+
55+
return;
56+
}
57+
58+
MessageBase message = JsonUtils.fromJson(messageJson, clazz);
59+
60+
MessageHandler<MessageBase> handler = MessageHandlerRegistry.getHandler(type);
61+
if (handler != null) {
62+
handler.handle(message);
63+
} else {
64+
logger.debug("No handler found for message type: " + type);
65+
}
66+
} catch (Exception e) {
67+
logger.error("Error handling message", e);
4768
}
4869
}
49-
}, INSTANCE_REMOVE_CHANNEL, INSTANCE_HEARTBEAT_CHANNEL);
70+
}, CHANNEL);
5071
} catch (Exception e) {
5172
logger.error("RedisBridge encountered an error, will retry in 1 second", e);
5273
try {
@@ -62,18 +83,33 @@ public void onMessage(String channel, String message) {
6283
listenerThread.start();
6384
}
6485

65-
public void publish(String message, String channel) {
86+
public void publish(MessageBase message, String sender) {
6687
try (Jedis jedis = jedisPool.getResource()) {
67-
jedis.publish(channel, message);
88+
message.setTimestamp(System.currentTimeMillis());
89+
message.setSender(sender);
90+
91+
String json = JsonUtils.toJson(message);
92+
jedis.publish(CHANNEL, json);
6893
}
6994
}
7095

71-
public void publishInstanceInfo(InstanceInfo info) {
72-
publish(GSON.toJson(info), INSTANCE_HEARTBEAT_CHANNEL);
73-
}
96+
public void registerDefaultMessages() {
97+
MessageRegistry.register(InstanceHeartbeatMessage.TYPE, InstanceHeartbeatMessage.class);
98+
MessageHandlerRegistry.register(InstanceHeartbeatMessage.TYPE, new MessageHandler<InstanceHeartbeatMessage>() {
99+
@Override
100+
public void handle(InstanceHeartbeatMessage message) {
101+
InstanceInfo instance = message.getInstance();
102+
InstanceManager.INSTANCE_CACHE.put(instance.getId(), instance);
103+
}
104+
});
74105

75-
public void publishInstanceRemove(InstanceInfo info) {
76-
publish(GSON.toJson(info), INSTANCE_REMOVE_CHANNEL);
106+
MessageRegistry.register(InstanceShutdownMessage.TYPE, InstanceShutdownMessage.class);
107+
MessageHandlerRegistry.register(InstanceShutdownMessage.TYPE, new MessageHandler<InstanceShutdownMessage>() {
108+
@Override
109+
public void handle(InstanceShutdownMessage message) {
110+
InstanceManager.INSTANCE_CACHE.invalidate(message.getSender());
111+
}
112+
});
77113
}
78114

79115
public void close() {

core/src/main/java/net/josscoder/redisbridge/core/data/InstanceInfo.java

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.josscoder.redisbridge.core.instance;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@NoArgsConstructor
9+
@JsonIgnoreProperties(ignoreUnknown = true)
10+
public class InstanceInfo {
11+
private String id;
12+
private String host;
13+
private int port;
14+
private String group;
15+
private int maxPlayers;
16+
private int players;
17+
18+
public boolean isFull() {
19+
return players >= maxPlayers;
20+
}
21+
}

core/src/main/java/net/josscoder/redisbridge/core/manager/InstanceManager.java renamed to core/src/main/java/net/josscoder/redisbridge/core/instance/InstanceManager.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
package net.josscoder.redisbridge.core.manager;
1+
package net.josscoder.redisbridge.core.instance;
22

33
import com.google.common.cache.Cache;
44
import com.google.common.cache.CacheBuilder;
55
import lombok.Getter;
6-
import net.josscoder.redisbridge.core.data.InstanceInfo;
76

87
import java.util.*;
98
import java.util.concurrent.TimeUnit;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.josscoder.redisbridge.core.message;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public abstract class MessageBase {
7+
8+
private final String type;
9+
private String sender;
10+
private Long timestamp;
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package net.josscoder.redisbridge.core.message;
2+
3+
public abstract class MessageHandler <T extends MessageBase> {
4+
public abstract void handle(T message);
5+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.josscoder.redisbridge.core.message;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class MessageHandlerRegistry {
7+
8+
private static final Map<String, MessageHandler<? extends MessageBase>> handlers = new HashMap<>();
9+
10+
public static void register(String type, MessageHandler<? extends MessageBase> handler) {
11+
handlers.put(type, handler);
12+
}
13+
14+
@SuppressWarnings("unchecked")
15+
public static <T extends MessageBase> MessageHandler<T> getHandler(String type) {
16+
return (MessageHandler<T>) handlers.get(type);
17+
}
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package net.josscoder.redisbridge.core.message;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class MessageRegistry {
7+
8+
private static final Map<String, Class<? extends MessageBase>> messages = new HashMap<>();
9+
10+
public static void register(String type, Class<? extends MessageBase> aClass) {
11+
messages.put(type, aClass);
12+
}
13+
14+
public static Class<? extends MessageBase> getClass(String type) {
15+
return messages.get(type);
16+
}
17+
}

0 commit comments

Comments
 (0)