11 Commits

Author SHA1 Message Date
e937924317 build: 更新版本号
All checks were successful
Publish Project / build (push) Successful in 2m43s
2025-07-10 20:47:03 +08:00
de10bd7feb feat: 允许单独配置 redis-url
All checks were successful
Publish Project / build (push) Successful in 4m23s
2025-07-10 20:40:36 +08:00
1e0417c814 docs: 修正文档
All checks were successful
Publish Project / build (push) Successful in 2m17s
2025-07-03 02:38:54 +08:00
c3b2248c60 ci: 添加自动构建发布配置 2025-07-03 02:37:36 +08:00
25828267f6 feat: 使用 lettuce-core 连接 redis 提升稳定性 2025-07-03 02:36:20 +08:00
95c0a23857 build: 优化构建脚本 2025-03-23 00:46:54 +08:00
00dbcba628 fix(ball-common): 提前对服务器 id 和 name 进行空判断 2025-01-26 15:01:15 +08:00
2a4a3e9065 perf: 优化消息缓存表格 2024-11-09 22:30:51 +08:00
69432dbbc8 style: 修改错误日志 2024-08-09 04:29:36 +08:00
20f395fa45 build: 更新版本号 2024-08-08 04:47:05 +08:00
4d3d93887d feat: 添加管理员指令 2024-08-08 04:46:07 +08:00
27 changed files with 228 additions and 147 deletions

View File

@@ -0,0 +1,29 @@
name: Publish Project
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: 21
distribution: temurin
cache: gradle
cache-dependency-path: gradle/wrapper/gradle-wrapper.properties
- name: Build Project
env:
ORG_GRADLE_PROJECT_MAVEN_AIRGAME_USERNAME: ${{ secrets.MAVEN_AIRGAME_USERNAME }}
ORG_GRADLE_PROJECT_MAVEN_AIRGAME_PASSWORD: ${{ secrets.MAVEN_AIRGAME_PASSWORD }}
run: chmod +x gradlew && ./gradlew build publish --no-daemon
- name: Publish to Release
uses: softprops/action-gh-release@v2
with:
files: build/*.jar

View File

@@ -11,8 +11,6 @@
3. 命令行窗口中执行`./gradlew clean build`
4. 构建成品在 `build` 文件夹
也可访问我的[Jenkins网站](https://jenkins.airgame.net/job/opensource/job/hamster-ball/)获取最新版
# 安装步骤
1. 关闭服务器
@@ -49,9 +47,9 @@ repositories {
dependencies {
// 对于 Bukkit 插件
compileOnly("cn.hamster3.mc.plugin:ball-bukkit:1.6.4")
compileOnly("cn.hamster3.mc.plugin:ball-bukkit:1.8.1")
// 对于 BungeeCord 插件
compileOnly("cn.hamster3.mc.plugin:ball-bungee:1.6.4")
compileOnly("cn.hamster3.mc.plugin:ball-bungee:1.8.1")
}
```
@@ -77,13 +75,13 @@ dependencies {
<dependency>
<groupId>cn.hamster3.mc.plugin</groupId>
<artifactId>ball-bukkit</artifactId>
<version>1.6.4</version>
<version>1.8.1</version>
</dependency>
<!--对于 BungeeCord 插件-->
<dependency>
<groupId>cn.hamster3.mc.plugin</groupId>
<artifactId>ball-bungee</artifactId>
<version>1.6.4</version>
<version>1.8.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -20,6 +20,6 @@ tasks {
archiveBaseName = "HamsterBall-Bukkit"
}
shadowJar {
destinationDirectory = rootProject.buildDir
destinationDirectory = rootProject.layout.buildDirectory
}
}

View File

@@ -3,7 +3,6 @@ package cn.hamster3.mc.plugin.ball.bukkit.listener;
import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.bukkit.data.BukkitLocation;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.common.event.operate.*;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.common.data.DisplayMessage;
@@ -95,9 +94,6 @@ public class BallBukkitListener implements Listener {
@Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return;
}
@@ -106,9 +102,6 @@ public class BallBukkitListener implements Listener {
@Subscribe
public void onDispatchPlayerCommand(DispatchPlayerCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
if (event.getUuid() != null) {
Player player = Bukkit.getPlayer(event.getUuid());
if (player == null) {

View File

@@ -42,6 +42,11 @@ server-info:
# 不填则自动获取 server.properties 文件中的设置
# port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接
@@ -53,9 +58,6 @@ server-info:
# # MySQL数据库链接填写格式:
# # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数
# url: "jdbc:mysql://localhost:3306/Test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# # 如果你不需要做多端跨服,那么请使用 sqlite 作本地数据库 ↓
# # driver: "org.sqlite.JDBC"
# # url: "jdbc:sqlite:./plugins/HamsterCore/database.db"
# # 用户名
# username: "root"
# # 密码

View File

@@ -12,7 +12,7 @@ UPDATE_CHECKER:
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball
DOWNLOAD_URL: https://jenkins.airgame.net/job/opensource/job/hamster-ball/
load: STARTUP

View File

@@ -18,6 +18,6 @@ tasks {
archiveBaseName = "HamsterBall-BungeeCord"
}
shadowJar {
destinationDirectory = rootProject.buildDir
destinationDirectory = rootProject.layout.buildDirectory
}
}

View File

@@ -12,7 +12,7 @@ public class BungeeBallCommand extends Command {
public static final BungeeBallCommand INSTANCE = new BungeeBallCommand();
public BungeeBallCommand() {
super("hamster-ball", "hamster.ball.admin", "ball");
super("hamster-bungee-ball", "hamster.ball.admin", "bungee-ball", "bball");
}
@Override

View File

@@ -2,7 +2,6 @@ package cn.hamster3.mc.plugin.ball.bungee.listener;
import cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.common.event.operate.*;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import com.google.common.eventbus.Subscribe;
@@ -23,9 +22,6 @@ public class BallBungeeListener {
@Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.PROXY) {
return;
}
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return;
}
@@ -35,9 +31,6 @@ public class BallBungeeListener {
@Subscribe
public void onDispatchPlayerCommandEvent(DispatchPlayerCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
ProxyServer server = ProxyServer.getInstance();
if (event.getUuid() != null) {
ProxiedPlayer player = server.getPlayer(event.getUuid());

View File

@@ -11,7 +11,6 @@ UPDATE_CHECKER:
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball
DOWNLOAD_URL: https://jenkins.airgame.net/job/opensource/job/hamster-ball/
depend:
- HamsterCore

View File

@@ -33,6 +33,11 @@ server-info:
# 不填则自动设置为 25577
port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接

View File

@@ -20,11 +20,14 @@ import cn.hamster3.mc.plugin.core.common.data.DisplayMessage;
import cn.hamster3.mc.plugin.core.common.util.CoreUtils;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redis.clients.jedis.Jedis;
import javax.sql.DataSource;
import java.sql.*;
@@ -58,6 +61,8 @@ public abstract class BallAPI {
@NotNull
private final DataSource datasource;
@NotNull
private final RedisClient redisClient;
@NotNull
private final BallServerInfo localServerInfo;
@NotNull
@@ -69,7 +74,7 @@ public abstract class BallAPI {
private final Map<UUID, BallPlayerInfo> allPlayerInfo;
@NotNull
private final Jedis redisSub;
private final StatefulRedisPubSubConnection<String, String> redisPubSub;
@Nullable
private ScheduledFuture<?> lockUpdater;
@@ -79,6 +84,11 @@ public abstract class BallAPI {
throw new IllegalArgumentException("配置文件中未找到 server-info 节点");
}
localServerInfo = new BallServerInfo(serverInfoConfig, type);
if (config.hasKey("redis-url")) {
redisClient = RedisClient.create(config.getString("redis-url"));
} else {
redisClient = CoreAPI.getInstance().getRedisClient();
}
ConfigSection section = config.getSection("datasource");
if (section != null) {
getLogger().info("启用仓鼠球自定义数据库连接池");
@@ -92,7 +102,7 @@ public abstract class BallAPI {
eventBus.register(BallCommonListener.INSTANCE);
allServerInfo = new ConcurrentHashMap<>();
allPlayerInfo = new ConcurrentHashMap<>();
redisSub = CoreAPI.getInstance().getJedisPool().getResource();
redisPubSub = getRedisClient().connectPubSub();
getLogger().info("频道前缀: " + ballConfig.getChannelPrefix());
getLogger().info("启用子服更新玩家状态: " + ballConfig.isGameServerUpdatePlayerInfo());
if (ballConfig.isGameServerUpdatePlayerInfo()) {
@@ -102,29 +112,31 @@ public abstract class BallAPI {
getLogger().warning("已启用调试模式");
eventBus.register(BallDebugListener.INSTANCE);
}
CoreAPI.getInstance().getExecutorService().submit(() -> redisSub.subscribe(BallRedisListener.INSTANCE, BALL_CHANNEL));
redisPubSub.addListener(BallRedisListener.INSTANCE);
redisPubSub.sync().subscribe(BALL_CHANNEL);
}
protected void enable() throws SQLException, InterruptedException {
try (Jedis jedis = CoreAPI.getInstance().getJedisPool().getResource()) {
try (StatefulRedisConnection<String, String> connect = getRedisClient().connect()) {
RedisCommands<String, String> redis = connect.sync();
String key = "HamsterBall:ServerInfo:" + localServerInfo.getId();
if (jedis.exists(key) && ballConfig.isSingletonServerID()) {
if (redis.exists(key) > 0 && ballConfig.isSingletonServerID()) {
throw new IllegalStateException("已经有一个服务器占用了该 ID");
}
jedis.hset(key, "id", localServerInfo.getId());
jedis.hset(key, "name", localServerInfo.getName());
jedis.hset(key, "type", localServerInfo.getType().name());
jedis.hset(key, "host", localServerInfo.getHost());
jedis.hset(key, "port", String.valueOf(localServerInfo.getPort()));
jedis.expire(key, 180);
redis.hset(key, "id", localServerInfo.getId());
redis.hset(key, "name", localServerInfo.getName());
redis.hset(key, "type", localServerInfo.getType().name());
redis.hset(key, "host", localServerInfo.getHost());
redis.hset(key, "port", String.valueOf(localServerInfo.getPort()));
redis.expire(key, 180);
lockUpdater = CoreAPI.getInstance().getScheduledService().scheduleAtFixedRate(LockUpdateThread.INSTANCE, 1, 1, TimeUnit.MINUTES);
for (String serverInfoKey : jedis.keys("HamsterBall:ServerInfo:*")) {
for (String serverInfoKey : redis.keys("HamsterBall:ServerInfo:*")) {
BallServerInfo info = new BallServerInfo(
jedis.hget(serverInfoKey, "id"),
jedis.hget(serverInfoKey, "name"),
BallServerType.valueOf(jedis.hget(serverInfoKey, "type")),
jedis.hget(serverInfoKey, "host"),
Integer.parseInt(jedis.hget(serverInfoKey, "port"))
redis.hget(serverInfoKey, "id"),
redis.hget(serverInfoKey, "name"),
BallServerType.valueOf(redis.hget(serverInfoKey, "type")),
redis.hget(serverInfoKey, "host"),
Integer.parseInt(redis.hget(serverInfoKey, "port"))
);
allServerInfo.put(info.getId(), info);
}
@@ -140,7 +152,10 @@ public abstract class BallAPI {
") CHARSET utf8mb4;");
statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_cached_message`(" +
"`uuid` CHAR(36) NOT NULL," +
"`message` TEXT NOT NULL" +
"`message` TEXT NOT NULL," +
"`time` DATETIME NOT NULL DEFAULT NOW()," +
"INDEX `idx_uuid` USING BTREE (`uuid`)," +
"INDEX `idx_time` USING BTREE (`time`)" +
") CHARSET utf8mb4;");
}
if (getBallConfig().isGameServerUpdatePlayerInfo()) {
@@ -189,9 +204,10 @@ public abstract class BallAPI {
if (lockUpdater != null) {
lockUpdater.cancel(true);
lockUpdater = null;
try (Jedis jedis = CoreAPI.getInstance().getJedisPool().getResource()) {
try (StatefulRedisConnection<String, String> connect = getRedisClient().connect()) {
RedisCommands<String, String> redis = connect.sync();
String key = "HamsterBall:ServerInfo:" + localServerInfo.getId();
jedis.del(key);
redis.del(key);
}
}
try (Connection connection = getDatasource().getConnection()) {
@@ -203,7 +219,7 @@ public abstract class BallAPI {
statement.executeUpdate();
}
}
redisSub.close();
redisPubSub.close();
}
/**
@@ -257,11 +273,10 @@ public abstract class BallAPI {
*/
public void dispatchConsoleCommand(@Nullable BallServerType type, @Nullable String serverID, @NotNull String command) {
sendBallMessage(BALL_CHANNEL, new BallMessage(
getLocalServerId(), null, BallServerType.GAME,
getLocalServerId(), null, type,
BallActions.DispatchConsoleCommand.name(),
CoreAPI.getInstance().getGson().toJsonTree(new DispatchConsoleCommandEvent(type, serverID, command))
CoreAPI.getInstance().getGson().toJsonTree(new DispatchConsoleCommandEvent(serverID, command))
), false);
}
/**
@@ -273,9 +288,9 @@ public abstract class BallAPI {
*/
public void dispatchPlayerCommand(@Nullable BallServerType type, @Nullable UUID uuid, @NotNull String command) {
sendBallMessage(BALL_CHANNEL, new BallMessage(
getLocalServerId(), null, BallServerType.GAME,
getLocalServerId(), null, type,
BallActions.DispatchPlayerCommand.name(),
CoreAPI.getInstance().getGson().toJsonTree(new DispatchPlayerCommandEvent(type, uuid, command))
CoreAPI.getInstance().getGson().toJsonTree(new DispatchPlayerCommandEvent(uuid, command))
), false);
}
@@ -330,7 +345,7 @@ public abstract class BallAPI {
}
try (Connection connection = getDatasource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `hamster_ball_cached_message` VALUES(?, ?);"
"INSERT INTO `hamster_ball_cached_message` VALUES(?, ?, DEFAULT);"
)) {
statement.setString(1, receiver.toString());
statement.setString(2, message.toJson().toString());
@@ -486,16 +501,12 @@ public abstract class BallAPI {
channel = ballConfig.getChannelPrefix() + channel;
}
if (block) {
try (Jedis jedis = CoreAPI.getInstance().getJedisPool().getResource()) {
jedis.publish(channel, CoreAPI.getInstance().getGson().toJson(message));
}
redisPubSub.sync().publish(channel, CoreAPI.getInstance().getGson().toJson(message));
eventBus.post(new MessageSentEvent(channel, message));
} else {
@NotNull String finalChannel = channel;
CoreAPI.getInstance().getExecutorService().submit(() -> {
try (Jedis jedis = CoreAPI.getInstance().getJedisPool().getResource()) {
jedis.publish(finalChannel, CoreAPI.getInstance().getGson().toJson(message));
}
CoreAPI.getInstance().getExecutorService().execute(() -> {
redisPubSub.sync().publish(finalChannel, CoreAPI.getInstance().getGson().toJson(message));
eventBus.post(new MessageSentEvent(finalChannel, message));
});
}
@@ -521,14 +532,7 @@ public abstract class BallAPI {
* @param channels 频道名称
*/
public void subscribeRaw(@NotNull String... channels) {
BallRedisListener.INSTANCE.subscribe(channels);
// CoreAPI.getInstance().getExecutorService().submit(() -> {
// try {
// redisSub.subscribe(BallRedisListener.INSTANCE, channels);
// } catch (Exception | Error e) {
// e.printStackTrace();
// }
// });
redisPubSub.sync().subscribe(channels);
}
/**
@@ -537,10 +541,7 @@ public abstract class BallAPI {
* @param patterns 频道名称正则表达式
*/
public void subscribePatterns(@NotNull String patterns) {
BallRedisListener.INSTANCE.psubscribe(patterns);
// CoreAPI.getInstance().getExecutorService().submit(
// () -> redisSub.psubscribe(BallRedisListener.INSTANCE, patterns)
// );
redisPubSub.sync().psubscribe(patterns);
}
/**
@@ -563,7 +564,7 @@ public abstract class BallAPI {
* @param channels 频道名称
*/
public void unsubscribeRaw(@NotNull String... channels) {
BallRedisListener.INSTANCE.unsubscribe(channels);
redisPubSub.sync().unsubscribe(channels);
}
/**
@@ -572,7 +573,7 @@ public abstract class BallAPI {
* @param patterns 频道名称正则表达式
*/
public void unsubscribePatterns(@NotNull String patterns) {
BallRedisListener.INSTANCE.punsubscribe(patterns);
redisPubSub.sync().punsubscribe(patterns);
}
@NotNull

View File

@@ -1,13 +1,16 @@
package cn.hamster3.mc.plugin.ball.common.command;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class SudoAllConsoleCommand extends ChildCommand {
public static final SudoAllConsoleCommand INSTANCE = new SudoAllConsoleCommand();
@@ -22,7 +25,7 @@ public class SudoAllConsoleCommand extends ChildCommand {
@Override
public @NotNull String getUsage() {
return "sudo-all-console <服务器ID> <命令内容>";
return "sudo-all-console <服务器类型> <命令内容>";
}
@Override
@@ -37,22 +40,36 @@ public class SudoAllConsoleCommand extends ChildCommand {
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 1) {
if (args.length < 2) {
sender.sendMessage(BallCommand.INSTANCE.getUsage() + " " + getUsage());
return true;
}
StringBuilder builder = new StringBuilder(args[0]);
for (int i = 1; i < args.length; i++) {
BallServerType serverType;
try {
serverType = BallServerType.valueOf(args[0].toUpperCase());
} catch (IllegalArgumentException e) {
sender.sendMessage("§c未知的服务器类型: " + args[0]);
return true;
}
StringBuilder builder = new StringBuilder(args[1]);
for (int i = 2; i < args.length; i++) {
builder.append(" ").append(args[i]);
}
String command = builder.toString();
BallAPI.getInstance().dispatchConsoleCommand(null, null, command);
BallAPI.getInstance().dispatchConsoleCommand(serverType, null, command);
sender.sendMessage("§a已强制所有服务器控制台执行命令: §e/" + command);
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length == 1) {
return Arrays.stream(BallServerType.values())
.map(Enum::name)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
}

View File

@@ -3,11 +3,14 @@ package cn.hamster3.mc.plugin.ball.common.command;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class SudoAllPlayerCommand extends ChildCommand {
public static final SudoAllPlayerCommand INSTANCE = new SudoAllPlayerCommand();
@@ -22,7 +25,7 @@ public class SudoAllPlayerCommand extends ChildCommand {
@Override
public @NotNull String getUsage() {
return "sudo-all-player <命令内容>";
return "sudo-all-player <服务器类型> <命令内容>";
}
@Override
@@ -37,22 +40,36 @@ public class SudoAllPlayerCommand extends ChildCommand {
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 1) {
if (args.length < 2) {
sender.sendMessage(BallCommand.INSTANCE.getUsage() + " " + getUsage());
return true;
}
StringBuilder builder = new StringBuilder(args[0]);
for (int i = 1; i < args.length; i++) {
BallServerType serverType;
try {
serverType = BallServerType.valueOf(args[0].toUpperCase());
} catch (IllegalArgumentException e) {
sender.sendMessage("§c未知的服务器类型: " + args[0]);
return true;
}
StringBuilder builder = new StringBuilder(args[1]);
for (int i = 2; i < args.length; i++) {
builder.append(" ").append(args[i]);
}
String command = builder.toString();
BallAPI.getInstance().dispatchPlayerCommand(null, null, command);
BallAPI.getInstance().dispatchPlayerCommand(serverType, null, command);
sender.sendMessage("§a已强制所有玩家执行命令: §e/" + command);
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length == 1) {
return Arrays.stream(BallServerType.values())
.map(Enum::name)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
}

View File

@@ -1,8 +1,8 @@
package cn.hamster3.mc.plugin.ball.common.command;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

View File

@@ -1,12 +1,14 @@
package cn.hamster3.mc.plugin.ball.common.command;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ChildCommand;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -25,7 +27,7 @@ public class SudoPlayerCommand extends ChildCommand {
@Override
public @NotNull String getUsage() {
return "sudo-player <玩家名|UUID> <命令内容>";
return "sudo-player <服务器类型> <玩家名|UUID> <命令内容>";
}
@Override
@@ -40,43 +42,59 @@ public class SudoPlayerCommand extends ChildCommand {
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 2) {
if (args.length < 3) {
sender.sendMessage(BallCommand.INSTANCE.getUsage() + " " + getUsage());
return true;
}
BallServerType serverType;
try {
serverType = BallServerType.valueOf(args[0].toUpperCase());
} catch (IllegalArgumentException e) {
sender.sendMessage("§c未知的服务器类型: " + args[0]);
return true;
}
BallPlayerInfo info;
try {
UUID uuid = UUID.fromString(args[0]);
UUID uuid = UUID.fromString(args[1]);
info = BallAPI.getInstance().getPlayerInfo(uuid);
} catch (Exception e) {
info = BallAPI.getInstance().getPlayerInfo(args[0]);
info = BallAPI.getInstance().getPlayerInfo(args[1]);
}
if (info == null) {
sender.sendMessage("§c未找到玩家 " + args[0]);
sender.sendMessage("§c未找到玩家 " + args[1]);
return true;
}
if (!info.isOnline()) {
sender.sendMessage("§c玩家 " + args[0] + " 不在线");
sender.sendMessage("§c玩家 " + args[1] + " 不在线");
return true;
}
StringBuilder builder = new StringBuilder(args[1]);
for (int i = 2; i < args.length; i++) {
StringBuilder builder = new StringBuilder(args[2]);
for (int i = 3; i < args.length; i++) {
builder.append(" ").append(args[i]);
}
String command = builder.toString();
BallAPI.getInstance().dispatchPlayerCommand(null, info.getUuid(), command);
BallAPI.getInstance().dispatchPlayerCommand(serverType, info.getUuid(), command);
sender.sendMessage("§a已强制玩家 " + info.getName() + " 执行命令: §e/" + command);
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length == 1) {
return BallAPI.getInstance().getAllPlayerInfo().values().stream()
.map(BallPlayerInfo::getName)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
switch (args.length) {
case 1: {
return Arrays.stream(BallServerType.values())
.map(Enum::name)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
}
case 2: {
return BallAPI.getInstance().getAllPlayerInfo().values().stream()
.map(BallPlayerInfo::getName)
.filter(o -> o.toLowerCase().startsWith(args[1].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
}
}
return Collections.emptyList();
}

View File

@@ -46,7 +46,13 @@ public class BallServerInfo {
public BallServerInfo(@NotNull ConfigSection config, @NotNull BallServerType type) {
Map<String, String> env = System.getenv();
id = env.getOrDefault("BALL_SERVER_ID", config.getString("id"));
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("id 不能为空");
}
name = env.getOrDefault("BALL_SERVER_NAME", config.getString("name"));
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name 不能为空");
}
this.type = type;
host = "0.0.0.0";
port = 0;

View File

@@ -1,6 +1,5 @@
package cn.hamster3.mc.plugin.ball.common.event.operate;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
@@ -9,8 +8,6 @@ import org.jetbrains.annotations.Nullable;
@Data
@AllArgsConstructor
public class DispatchConsoleCommandEvent {
@Nullable
private final BallServerType type;
@Nullable
private final String serverID;
@NotNull

View File

@@ -1,6 +1,5 @@
package cn.hamster3.mc.plugin.ball.common.event.operate;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
@@ -11,8 +10,6 @@ import java.util.UUID;
@Data
@AllArgsConstructor
public class DispatchPlayerCommandEvent {
@Nullable
private final BallServerType type;
@Nullable
private final UUID uuid;
@NotNull

View File

@@ -5,16 +5,17 @@ import cn.hamster3.mc.plugin.ball.common.data.BallMessage;
import cn.hamster3.mc.plugin.ball.common.event.message.MessageReceivedEvent;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import com.google.common.eventbus.EventBus;
import redis.clients.jedis.JedisPubSub;
import io.lettuce.core.pubsub.RedisPubSubListener;
public class BallRedisListener extends JedisPubSub {
import java.util.logging.Level;
public class BallRedisListener implements RedisPubSubListener<String, String> {
public static final BallRedisListener INSTANCE = new BallRedisListener();
private BallRedisListener() {
}
@Override
public void onMessage(String channel, String message) {
public void handle(String channel, String message) {
CoreAPI.getInstance().getExecutorService().submit(() -> {
try {
String finalChannel = channel;
@@ -32,33 +33,38 @@ public class BallRedisListener extends JedisPubSub {
}
eventBus.post(new MessageReceivedEvent(finalChannel, ballMessage));
} catch (Exception | Error e) {
e.printStackTrace();
BallAPI.getInstance().getLogger().log(Level.SEVERE, "解析来自频道 " + channel + " 的数据出错: " + message, e);
}
});
}
@Override
public void onPMessage(String pattern, String channel, String message) {
onMessage(channel, message);
public void message(String channel, String message) {
handle(channel, message);
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
public void message(String pattern, String channel, String message) {
handle(channel, message);
}
@Override
public void subscribed(String channel, long count) {
BallAPI.getInstance().getLogger().info("已订阅 redis 频道 " + channel);
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
BallAPI.getInstance().getLogger().info("已取消订阅 redis 频道 " + channel);
}
@Override
public void onPSubscribe(String pattern, int subscribedChannels) {
public void psubscribed(String pattern, long count) {
BallAPI.getInstance().getLogger().info("已订阅 redis 频道(正则) " + pattern);
}
@Override
public void onPUnsubscribe(String pattern, int subscribedChannels) {
public void unsubscribed(String channel, long count) {
BallAPI.getInstance().getLogger().info("已取消订阅 redis 频道 " + channel);
}
@Override
public void punsubscribed(String pattern, long count) {
BallAPI.getInstance().getLogger().info("已取消订阅 redis 频道(正则) " + pattern);
}
}

View File

@@ -1,8 +1,8 @@
package cn.hamster3.mc.plugin.ball.common.thread;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import redis.clients.jedis.Jedis;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class LockUpdateThread implements Runnable {
public static final LockUpdateThread INSTANCE = new LockUpdateThread();
@@ -13,8 +13,9 @@ public class LockUpdateThread implements Runnable {
@Override
public void run() {
String key = "HamsterBall:ServerInfo:" + BallAPI.getInstance().getLocalServerInfo().getId();
try (Jedis jedis = CoreAPI.getInstance().getJedisPool().getResource()) {
jedis.expire(key, 180);
try (StatefulRedisConnection<String, String> connect = BallAPI.getInstance().getRedisClient().connect()) {
RedisCommands<String, String> redis = connect.sync();
redis.expire(key, 180);
}
}
}

View File

@@ -100,8 +100,8 @@ public class HamsterBallPlugin {
return;
}
CommandMeta commandMeta = proxyServer.getCommandManager()
.metaBuilder("hamster-ball")
.aliases("ball")
.metaBuilder("hamster-velocity-ball")
.aliases("velocity-ball", "vball")
.plugin(this)
.build();
proxyServer.getCommandManager().register(commandMeta, VelocityBallCommand.INSTANCE);

View File

@@ -1,7 +1,6 @@
package cn.hamster3.mc.plugin.ball.velocity.listener;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.common.event.operate.*;
import cn.hamster3.mc.plugin.ball.velocity.HamsterBallPlugin;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
@@ -23,9 +22,6 @@ public class BallVelocityListener {
@Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.PROXY) {
return;
}
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return;
}
@@ -35,9 +31,6 @@ public class BallVelocityListener {
@Subscribe
public void onDispatchPlayerCommand(DispatchPlayerCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
if (event.getUuid() != null) {
Player player = server.getPlayer(event.getUuid()).orElse(null);

View File

@@ -33,6 +33,11 @@ server-info:
# 不填则自动设置为 25577
port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接

View File

@@ -2,4 +2,3 @@ VERSION: ${version}
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball
DOWNLOAD_URL: https://jenkins.airgame.net/job/opensource/job/hamster-ball/

View File

@@ -1,18 +1,24 @@
buildscript {
repositories {
maven("https://maven.airgame.net/maven-public/")
}
}
plugins {
id("java-library")
id("maven-publish")
id("com.github.johnrengelman.shadow") version "8+"
id("com.gradleup.shadow") version "8.3.6"
}
group = "cn.hamster3.mc.plugin"
version = "1.6.5-SNAPSHOT"
version = "1.8.1"
description = "基于 Redis 的 Minecraft 服务端通用消息中间件"
subprojects {
apply {
plugin("java-library")
plugin("maven-publish")
plugin("com.github.johnrengelman.shadow")
plugin("com.gradleup.shadow")
}
group = rootProject.group
@@ -59,10 +65,9 @@ subprojects {
repositories {
maven {
url = uri("https://maven.airgame.net/public")
credentials {
username = rootProject.properties.getOrDefault("maven_username", "").toString()
password = rootProject.properties.getOrDefault("maven_password", "").toString()
username = findProperty("MAVEN_AIRGAME_USERNAME")?.toString() ?: ""
password = findProperty("MAVEN_AIRGAME_PASSWORD")?.toString() ?: ""
}
}
}

View File

@@ -1,6 +1,6 @@
#Sun Aug 20 16:53:32 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists