48 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
518517a4e0 feat: 添加管理员指令 2024-08-08 03:48:17 +08:00
3570930b1f Merge branch 'refs/heads/master' into dev 2024-08-08 03:42:06 +08:00
5724e5e662 feat: 添加管理员指令 2024-08-08 03:41:50 +08:00
e4425c0b1b Merge branch 'refs/heads/master' into dev 2024-08-08 03:38:47 +08:00
5c913fa2db feat: 添加 singleton-server-id 配置 2024-04-24 14:12:06 +08:00
08a41c1209 refactor: 在初始化中订阅redis连接 2024-04-24 11:44:36 +08:00
d38bd5e41f build: 更新版本号到 1.6.4 2024-03-27 10:37:33 +08:00
52e25a0433 Merge branch 'dev' 2024-03-27 10:36:18 +08:00
cc33304ad4 fix(ball-common): 修复 jedis 线程占用问题 2024-03-27 09:41:56 +08:00
6d2abfa44b build: 更改版本号 2024-03-26 22:25:26 +08:00
bc49510c46 Merge branch 'dev' 2024-03-26 22:23:34 +08:00
74f1a4557d perf: 将 jedis 订阅频道处理代码移动至异步线程处理 2024-03-26 19:00:55 +08:00
84721e31ee fix(ball-common): 修复当事件处理失败时直接会阻塞后续redis消息的bug 2024-03-26 17:25:49 +08:00
4e29246441 build: 优化构建脚本 2024-03-21 23:30:04 +08:00
aa0e94c7b7 docs: 修改描述 2024-03-19 16:22:36 +08:00
aac7248994 docs: 修改版本更新信息 2024-03-19 16:19:00 +08:00
54461173c0 Merge pull request '修改版本号' (#4) from dev into master
Reviewed-on: #4
2024-03-19 10:53:48 +08:00
54f9e5cbc6 build: 修改版本号 2024-03-19 10:53:21 +08:00
8dae4f7667 Merge pull request '添加 auto-register-game-server 功能' (#3) from dev into master
Reviewed-on: #3
2024-03-19 10:51:57 +08:00
47a183f4ca feat: 优化代码,添加 auto-register-game-server 功能 2024-03-19 10:50:19 +08:00
b0e149febb perf: 简化代码 2024-03-18 18:05:57 +08:00
44bd5731de feat: 支持仓鼠核心自动检测更新 2024-03-18 16:15:20 +08:00
3555532df0 feat: 支持仓鼠核心自动检测更新 2024-03-18 16:13:42 +08:00
63ac3f7984 build: 优化构建脚本 2024-03-18 10:13:15 +08:00
37ef53d635 feat(ball-velocity): 添加 velocity 支持 2024-03-17 22:12:34 +08:00
059864424c perf: 优化代码 2024-03-17 22:12:01 +08:00
92dc1d506b feat: 兼容仓鼠核心升级 2024-03-17 20:51:45 +08:00
e48274a08c feat: 在 Redis 锁定服务器 ID 2024-03-17 10:15:47 +08:00
48d4e31dca perf: 简化代码 2024-03-17 02:02:13 +08:00
134454f94f perf: 简化代码 2024-03-17 01:21:08 +08:00
ade9a1094a perf: 简化代码 2024-03-17 01:13:45 +08:00
0d2f5f0468 chore: 将 Jenkins 参数写入文件中 2024-03-16 21:56:27 +08:00
57e8c2e8be style(ball-bungee): 修正日志 2024-03-15 01:50:32 +08:00
d299d6c28a chore: 修改 maven 仓库地址 2024-03-13 22:10:08 +08:00
ea8be49669 perf: 移除无效代码 2024-03-13 21:57:05 +08:00
a36406c3db feat: 适配仓鼠球更新 jedis 版本 2024-03-13 17:48:57 +08:00
98fd32f1ed feat: 适配仓鼠球更新 jedis 版本 2024-03-13 17:37:41 +08:00
61 changed files with 1983 additions and 542 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

@@ -1,6 +1,6 @@
# [HamsterBall](https://git.airgame.net/MiniDay/hamster-ball) # [HamsterBall](https://git.airgame.net/MiniDay/hamster-ball)
仓鼠球:一个基于 Redis 的 Minecraft 服务端通用消息中间件原HamsterService 基于 Redis 的 Minecraft 服务端通用消息中间件
该插件依赖于 [仓鼠核心](https://git.airgame.net/MiniDay/hamster-core) 该插件依赖于 [仓鼠核心](https://git.airgame.net/MiniDay/hamster-core)
@@ -11,8 +11,6 @@
3. 命令行窗口中执行`./gradlew clean build` 3. 命令行窗口中执行`./gradlew clean build`
4. 构建成品在 `build` 文件夹 4. 构建成品在 `build` 文件夹
也可访问我的[Jenkins网站](https://jenkins.airgame.net/job/opensource/job/hamster-ball/)获取最新版
# 安装步骤 # 安装步骤
1. 关闭服务器 1. 关闭服务器
@@ -28,11 +26,9 @@
为了适配 docker 环境,本插件除了从 `config.yml` 中配置服务器信息以外,还支持从环境变量中读取 为了适配 docker 环境,本插件除了从 `config.yml` 中配置服务器信息以外,还支持从环境变量中读取
| 环境变量 | 描述 | 对应 config 值 | | 环境变量 | 描述 | 对应 config 值 |
|:----------------------------|:-------------------|:-----------------| |:-----------------|:-------------------|:-----------------|
| BALL_LOCAL_SERVER_IP | 本服务器 IP | server-info.host | | BALL_SERVER_ID | 本服务器唯一识别码,最长 32 字符 | server-info.id |
| BALL_LOCAL_SERVER_PORT | 本服务器端口 | server-info.port | | BALL_SERVER_NAME | 本服务端名称,用于展示给玩家看 | server-info.name |
| BALL_LOCAL_SERVER_INFO_ID | 本服务器唯一识别码,最长 32 字符 | server-info.id |
| BALL_LOCAL_SERVER_INFO_NAME | 本服务端名称,用于展示给玩家看 | server-info.name |
# 开发 # 开发
@@ -51,9 +47,9 @@ repositories {
dependencies { dependencies {
// 对于 Bukkit 插件 // 对于 Bukkit 插件
compileOnly("cn.hamster3.mc.plugin:ball-bukkit:1.5.7") compileOnly("cn.hamster3.mc.plugin:ball-bukkit:1.8.1")
// 对于 BungeeCord 插件 // 对于 BungeeCord 插件
compileOnly("cn.hamster3.mc.plugin:ball-bungee:1.5.7") compileOnly("cn.hamster3.mc.plugin:ball-bungee:1.8.1")
} }
``` ```
@@ -79,13 +75,13 @@ dependencies {
<dependency> <dependency>
<groupId>cn.hamster3.mc.plugin</groupId> <groupId>cn.hamster3.mc.plugin</groupId>
<artifactId>ball-bukkit</artifactId> <artifactId>ball-bukkit</artifactId>
<version>1.5.7</version> <version>1.8.1</version>
</dependency> </dependency>
<!--对于 BungeeCord 插件--> <!--对于 BungeeCord 插件-->
<dependency> <dependency>
<groupId>cn.hamster3.mc.plugin</groupId> <groupId>cn.hamster3.mc.plugin</groupId>
<artifactId>ball-bungee</artifactId> <artifactId>ball-bungee</artifactId>
<version>1.5.7</version> <version>1.8.1</version>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -3,15 +3,11 @@
evaluationDependsOn(":ball-common") evaluationDependsOn(":ball-common")
dependencies { dependencies {
implementation(project(":ball-common")) { api(project(":ball-common")) { isTransitive = false }
isTransitive = false
}
compileOnly("org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT") compileOnly("org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT")
compileOnly("cn.hamster3.mc.plugin:core-bukkit:+")
compileOnly("cn.hamster3.mc.plugin:core-bukkit:1.2.2") compileOnly("me.clip:placeholderapi:2.11.5") { isTransitive = false }
compileOnly("me.clip:placeholderapi:2.11.5") {
isTransitive = false
}
} }
tasks { tasks {
@@ -24,6 +20,6 @@ tasks {
archiveBaseName = "HamsterBall-Bukkit" archiveBaseName = "HamsterBall-Bukkit"
} }
shadowJar { shadowJar {
destinationDirectory = rootProject.buildDir destinationDirectory = rootProject.layout.buildDirectory
} }
} }

View File

@@ -6,17 +6,30 @@ import cn.hamster3.mc.plugin.ball.bukkit.listener.BallBukkitListener;
import cn.hamster3.mc.plugin.ball.bukkit.listener.UpdatePlayerInfoListener; import cn.hamster3.mc.plugin.ball.bukkit.listener.UpdatePlayerInfoListener;
import cn.hamster3.mc.plugin.ball.bukkit.util.BallBukkitUtils; import cn.hamster3.mc.plugin.ball.bukkit.util.BallBukkitUtils;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.command.BallCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo; import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions; import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent; import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent;
import cn.hamster3.mc.plugin.core.bukkit.api.CoreBukkitAPI;
import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import lombok.Getter; import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@SuppressWarnings("CallToPrintStackTrace")
public class HamsterBallPlugin extends JavaPlugin { public class HamsterBallPlugin extends JavaPlugin {
@Getter @Getter
private static HamsterBallPlugin instance; private static HamsterBallPlugin instance;
@@ -39,10 +52,25 @@ public class HamsterBallPlugin extends JavaPlugin {
reloadConfig(); reloadConfig();
logger.info("已读取配置文件"); logger.info("已读取配置文件");
try { try {
BallBukkitAPI.init(); File dataFolder = getDataFolder();
if (dataFolder.mkdir()) {
logger.info("已生成插件存档文件夹");
}
File configFile = new File(dataFolder, "config.yml");
if (!configFile.exists()) {
Files.copy(
Objects.requireNonNull(getResource("config.yml")),
configFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
}
YamlConfig config = YamlConfig.load(configFile);
BallBukkitAPI.init(config);
logger.info("已初始化 BallAPI"); logger.info("已初始化 BallAPI");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.log(Level.SEVERE, "BallAPI 初始化失败", e);
Bukkit.shutdown();
return;
} }
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
logger.info("仓鼠球初始化完成,总计耗时 " + time + " ms"); logger.info("仓鼠球初始化完成,总计耗时 " + time + " ms");
@@ -56,9 +84,8 @@ public class HamsterBallPlugin extends JavaPlugin {
try { try {
BallBukkitAPI.getInstance().enable(); BallBukkitAPI.getInstance().enable();
} catch (Exception e) { } catch (Exception e) {
getLogger().info("仓鼠球启动失败,原因:" + e.getMessage()); logger.log(Level.SEVERE, "仓鼠球启动失败", e);
e.printStackTrace(); logger.info("由于仓鼠球启动失败,服务器将立即关闭");
getLogger().info("由于仓鼠球启动失败,服务器将立即关闭");
Bukkit.shutdown(); Bukkit.shutdown();
return; return;
} }
@@ -81,7 +108,7 @@ public class HamsterBallPlugin extends JavaPlugin {
BallBukkitUtils.uploadPlayerInfo(playerInfo); BallBukkitUtils.uploadPlayerInfo(playerInfo);
}); });
} else { } else {
BallAPI.getInstance().subscribeIgnorePrefix(BallAPI.PLAYER_INFO_CHANNEL); BallAPI.getInstance().subscribeRaw(BallAPI.PLAYER_INFO_CHANNEL);
} }
sync(() -> { sync(() -> {
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
@@ -105,9 +132,40 @@ public class HamsterBallPlugin extends JavaPlugin {
try { try {
BallBukkitAPI.getInstance().disable(); BallBukkitAPI.getInstance().disable();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.log(Level.SEVERE, "关闭仓鼠球时遇到了一个异常", e);
} }
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
logger.info("仓鼠球已关闭,总计耗时 " + time + " ms"); logger.info("仓鼠球已关闭,总计耗时 " + time + " ms");
} }
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return BallCommand.INSTANCE.onCommand(new AdaptCommandSender() {
@Override
public boolean hasPermission(@NotNull String permission) {
return sender.hasPermission(permission);
}
@Override
public void sendMessage(@NotNull Component message) {
CoreBukkitAPI.getInstance().getAudienceProvider().sender(sender).sendMessage(message);
}
}, args);
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return BallCommand.INSTANCE.onTabComplete(new AdaptCommandSender() {
@Override
public boolean hasPermission(@NotNull String permission) {
return sender.hasPermission(permission);
}
@Override
public void sendMessage(@NotNull Component message) {
CoreBukkitAPI.getInstance().getAudienceProvider().sender(sender).sendMessage(message);
}
}, args);
}
} }

View File

@@ -1,73 +1,36 @@
package cn.hamster3.mc.plugin.ball.bukkit.api; package cn.hamster3.mc.plugin.ball.bukkit.api;
import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin; import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.bukkit.util.BallBukkitUtils;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.config.BallConfig;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType; import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.sql.DataSource;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
public class BallBukkitAPI extends BallAPI { public class BallBukkitAPI extends BallAPI {
public BallBukkitAPI(@NotNull BallConfig ballConfig) { public BallBukkitAPI(@NotNull ConfigSection config) {
super(ballConfig); super(config, BallServerType.GAME);
} }
public static BallBukkitAPI getInstance() { public static BallBukkitAPI getInstance() {
return (BallBukkitAPI) instance; return (BallBukkitAPI) instance;
} }
public static void init() { public static void init(@NotNull YamlConfig config) {
if (instance != null) { if (instance != null) {
return; return;
} }
HamsterBallPlugin plugin = HamsterBallPlugin.getInstance(); instance = new BallBukkitAPI(config);
plugin.saveDefaultConfig();
plugin.reloadConfig();
FileConfiguration config = plugin.getConfig();
Map<String, String> env = System.getenv();
BallServerInfo serverInfo = new BallServerInfo(
env.getOrDefault("BALL_LOCAL_SERVER_INFO_ID", config.getString("server-info.id")),
env.getOrDefault("BALL_LOCAL_SERVER_INFO_NAME", config.getString("server-info.name")),
BallServerType.GAME,
env.getOrDefault("BALL_LOCAL_SERVER_IP", config.getString("server-info.host", Bukkit.getIp())),
Integer.parseInt(
env.getOrDefault("BALL_LOCAL_SERVER_PORT", String.valueOf(config.getInt("server-info.port", Bukkit.getPort())))
)
);
DataSource datasource;
if (config.contains("datasource")) {
plugin.getLogger().info("启用仓鼠球自定义数据库连接池");
datasource = BallBukkitUtils.getDataSource(config.getConfigurationSection("datasource"));
} else {
plugin.getLogger().info("复用 HamsterCore 的数据库连接池");
datasource = CoreAPI.getInstance().getDataSource();
}
BallConfig ballConfig = new BallConfig(
config.getBoolean("debug", false),
config.getString("channel-prefix", "") + ":",
config.getBoolean("game-server-update-player-info", false),
config.getStringList("load-player-info-filter"),
serverInfo,
datasource
);
instance = new BallBukkitAPI(ballConfig);
} }
@Override @Override
public void enable() throws SQLException, InterruptedException { public void enable() throws SQLException, InterruptedException {
instance.getLocalServerInfo().setHost(Bukkit.getIp().isEmpty() ? "127.0.0.1" : Bukkit.getIp());
instance.getLocalServerInfo().setPort(Bukkit.getPort());
super.enable(); super.enable();
} }

View File

@@ -3,15 +3,14 @@ package cn.hamster3.mc.plugin.ball.bukkit.listener;
import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin; import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.bukkit.data.BukkitLocation; 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.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.common.event.operate.*;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.common.data.DisplayMessage; import cn.hamster3.mc.plugin.core.common.data.DisplayMessage;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.audience.Audience;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.text.TextReplacementConfig;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -30,8 +29,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level;
@SuppressWarnings("CallToPrintStackTrace")
public class BallBukkitListener implements Listener { public class BallBukkitListener implements Listener {
public static final BallBukkitListener INSTANCE = new BallBukkitListener(); public static final BallBukkitListener INSTANCE = new BallBukkitListener();
@@ -81,7 +80,7 @@ public class BallBukkitListener implements Listener {
} }
removeCachedPlayerMessage(uuid); removeCachedPlayerMessage(uuid);
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); BallAPI.getInstance().getLogger().log(Level.SEVERE, "发送玩家缓存消息时出现了一个异常", e);
} }
}); });
} }
@@ -95,9 +94,6 @@ public class BallBukkitListener implements Listener {
@Subscribe @Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) { public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) { if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return; return;
} }
@@ -106,9 +102,6 @@ public class BallBukkitListener implements Listener {
@Subscribe @Subscribe
public void onDispatchPlayerCommand(DispatchPlayerCommandEvent event) { public void onDispatchPlayerCommand(DispatchPlayerCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
if (event.getUuid() != null) { if (event.getUuid() != null) {
Player player = Bukkit.getPlayer(event.getUuid()); Player player = Bukkit.getPlayer(event.getUuid());
if (player == null) { if (player == null) {

View File

@@ -5,50 +5,17 @@ import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions; import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerInfoUpdateEvent; import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerInfoUpdateEvent;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.lib.com.zaxxer.hikari.HikariConfig;
import cn.hamster3.mc.plugin.core.lib.com.zaxxer.hikari.HikariDataSource;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.sql.DataSource;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.logging.Level;
@SuppressWarnings("CallToPrintStackTrace")
public final class BallBukkitUtils { public final class BallBukkitUtils {
private BallBukkitUtils() { private BallBukkitUtils() {
} }
@Nullable
public static DataSource getDataSource(@Nullable ConfigurationSection datasourceConfig) {
if (datasourceConfig == null) {
return null;
}
try {
HikariConfig hikariConfig = new HikariConfig();
String driver = datasourceConfig.getString("driver");
hikariConfig.setDriverClassName(driver);
hikariConfig.setJdbcUrl(datasourceConfig.getString("url"));
hikariConfig.setUsername(datasourceConfig.getString("username"));
hikariConfig.setPassword(datasourceConfig.getString("password"));
hikariConfig.setMaximumPoolSize(datasourceConfig.getInt("maximum-pool-size", 3));
hikariConfig.setMinimumIdle(datasourceConfig.getInt("minimum-idle", 1));
long keepAliveTime = datasourceConfig.getLong("keep-alive-time", 0);
if (keepAliveTime > 5000) {
hikariConfig.setKeepaliveTime(keepAliveTime);
}
hikariConfig.setIdleTimeout(datasourceConfig.getLong("idle-timeout", 10 * 60 * 1000));
hikariConfig.setMaxLifetime(datasourceConfig.getLong("max-lifetime", 30 * 60 * 1000));
hikariConfig.setValidationTimeout(datasourceConfig.getLong("validation-timeout", 5000));
hikariConfig.setPoolName("HamsterBall-Pool");
return new HikariDataSource(hikariConfig);
} catch (Exception | Error e) {
return null;
}
}
public static void uploadPlayerInfo(@NotNull BallPlayerInfo playerInfo) { public static void uploadPlayerInfo(@NotNull BallPlayerInfo playerInfo) {
CoreAPI.getInstance().getExecutorService().execute(() -> { CoreAPI.getInstance().getExecutorService().execute(() -> {
try (Connection connection = BallAPI.getInstance().getDatasource().getConnection()) { try (Connection connection = BallAPI.getInstance().getDatasource().getConnection()) {
@@ -63,7 +30,7 @@ public final class BallBukkitUtils {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); BallAPI.getInstance().getLogger().log(Level.SEVERE, "更新玩家数据时遇到了一个异常", e);
} }
BallAPI.getInstance().sendBallMessage( BallAPI.getInstance().sendBallMessage(
BallAPI.PLAYER_INFO_CHANNEL, BallAPI.PLAYER_INFO_CHANNEL,

View File

@@ -1,6 +1,13 @@
# 是否允许在控制台输出调试信息 # 是否允许在控制台输出调试信息
debug: false debug: false
# 是否启用服务器 ID 单例模式
# 启用后,当一个服务器启动后将会占用 服务器唯一识别码
# 其他使用相同 服务器唯一识别码 的服务器将无法启动
# 测试端中可关闭该功能
# 推荐在正式服中开启该功能以防止服务器 ID 重复
singleton-server-id: false
# 频道名前缀 # 频道名前缀
# 使用这个配置选项可以划分子服消息通信分组 # 使用这个配置选项可以划分子服消息通信分组
# 只有在同一个频道名的子服才能互相通信 # 只有在同一个频道名的子服才能互相通信
@@ -11,7 +18,7 @@ channel-prefix: ""
# 如果一个群组服同时拥有多个 BC 入口 # 如果一个群组服同时拥有多个 BC 入口
# 且每个 BC 入口为不同的玩家名称分配不同的 UUID # 且每个 BC 入口为不同的玩家名称分配不同的 UUID
# (例如正版、盗版双入口,或网易多入口接同一个子服) # (例如正版、盗版双入口,或网易多入口接同一个子服)
# 则可以启用该功能以防止 UUID 紊乱的问题 # 则可以启用该功能以防止同一个名称占用多个 UUID 的问题
game-server-update-player-info: false game-server-update-player-info: false
# 该选项仅在 game-server-update-player-info 为 true 时有效 # 该选项仅在 game-server-update-player-info 为 true 时有效
@@ -35,22 +42,26 @@ server-info:
# 不填则自动获取 server.properties 文件中的设置 # 不填则自动获取 server.properties 文件中的设置
# port: 25577 # port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置 # 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置 # 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接 # 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接
# 如果你需要让每个服务器单独存储仓鼠球信息 # 如果你需要让每个服务器单独存储仓鼠球信息
# 这个选项就会很有用 # 这个选项就会很有用
#datasource: #datasource:
# # 数据库链接驱动地址 # # 数据库链接驱动地址旧版服务端低于1.13请使用com.mysql.jdbc.Driver
# driver: "com.mysql.jdbc.Driver" # driver: "com.mysql.cj.jdbc.Driver"
# # 数据库链接填写格式: # # MySQL数据库链接填写格式:
# # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数 # # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数
# # 除非你知道自己在做什么,否则不建议随意更改参数 # url: "jdbc:mysql://localhost:3306/Test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# url: "jdbc:mysql://localhost:3306/Test1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# # 用户名 # # 用户名
# username: "Test" # username: "root"
# # 密码 # # 密码
# password: "Test123.." # password: "Root123.."
# # 最小闲置链接数 # # 最小闲置链接数
# # 推荐值1~3 # # 推荐值1~3
# minimum-idle: 0 # minimum-idle: 0

View File

@@ -4,8 +4,15 @@ version: ${version}
api-version: 1.13 api-version: 1.13
author: MiniDay author: MiniDay
description: ${description}
website: https://git.airgame.net/MiniDay/hamster-ball website: https://git.airgame.net/MiniDay/hamster-ball
description: 仓鼠球:一个基于 Redis 的 Minecraft 服务端通用消息中间件原HamsterService
UPDATE_CHECKER:
VERSION: ${version}
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball
load: STARTUP load: STARTUP
@@ -14,3 +21,11 @@ depend:
softdepend: softdepend:
- PlaceholderAPI - PlaceholderAPI
commands:
hamster-ball:
aliases: [ ball ]
permissions:
hamster.ball.admin:
default: op

View File

@@ -3,12 +3,9 @@
evaluationDependsOn(":ball-common") evaluationDependsOn(":ball-common")
dependencies { dependencies {
implementation(project(":ball-common")) { api(project(":ball-common")) { isTransitive = false }
isTransitive = false
}
compileOnly("net.md-5:bungeecord-api:1.20-R0.1") compileOnly("net.md-5:bungeecord-api:1.20-R0.1")
compileOnly("cn.hamster3.mc.plugin:core-bungee:+")
compileOnly("cn.hamster3.mc.plugin:core-bungee:1.2.2")
} }
tasks { tasks {
@@ -21,6 +18,6 @@ tasks {
archiveBaseName = "HamsterBall-BungeeCord" archiveBaseName = "HamsterBall-BungeeCord"
} }
shadowJar { shadowJar {
destinationDirectory = rootProject.buildDir destinationDirectory = rootProject.layout.buildDirectory
} }
} }

View File

@@ -1,24 +1,32 @@
package cn.hamster3.mc.plugin.ball.bungee; package cn.hamster3.mc.plugin.ball.bungee;
import cn.hamster3.mc.plugin.ball.bungee.api.BallBungeeCordAPI; import cn.hamster3.mc.plugin.ball.bungee.api.BallBungeeCordAPI;
import cn.hamster3.mc.plugin.ball.bungee.command.BungeeBallCommand;
import cn.hamster3.mc.plugin.ball.bungee.listener.BallBungeeListener; import cn.hamster3.mc.plugin.ball.bungee.listener.BallBungeeListener;
import cn.hamster3.mc.plugin.ball.bungee.listener.BallBungeeMainListener; import cn.hamster3.mc.plugin.ball.bungee.listener.BallBungeeMainListener;
import cn.hamster3.mc.plugin.ball.bungee.listener.BungeeServerListener;
import cn.hamster3.mc.plugin.ball.bungee.listener.UpdatePlayerInfoListener; import cn.hamster3.mc.plugin.ball.bungee.listener.UpdatePlayerInfoListener;
import cn.hamster3.mc.plugin.ball.bungee.util.BallBungeeCordUtils; import cn.hamster3.mc.plugin.ball.bungee.util.BallBungeeCordUtils;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo; import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions; import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent; import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent;
import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import lombok.Getter; import lombok.Getter;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@SuppressWarnings("CallToPrintStackTrace")
public class HamsterBallPlugin extends Plugin { public class HamsterBallPlugin extends Plugin {
@Getter @Getter
private static HamsterBallPlugin instance; private static HamsterBallPlugin instance;
@Getter
private YamlConfig config;
@Override @Override
public void onLoad() { public void onLoad() {
@@ -27,11 +35,25 @@ public class HamsterBallPlugin extends Plugin {
logger.info("仓鼠球正在初始化"); logger.info("仓鼠球正在初始化");
instance = this; instance = this;
try { try {
BallBungeeCordAPI.init(); File dataFolder = getDataFolder();
if (dataFolder.mkdir()) {
logger.info("已生成插件存档文件夹");
}
File configFile = new File(dataFolder, "config.yml");
if (!configFile.exists()) {
Files.copy(
getResourceAsStream("config.yml"),
configFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
}
config = YamlConfig.load(configFile);
BallBungeeCordAPI.init(config);
logger.info("已初始化 BallAPI"); logger.info("已初始化 BallAPI");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.log(Level.SEVERE, "BallAPI 初始化失败", e);
ProxyServer.getInstance().stop("由于 HamsterBall 未能成功连接, 服务器将立即关闭"); ProxyServer.getInstance().stop("由于 HamsterBall 初始化失败, 服务器将立即关闭");
return;
} }
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
logger.info("仓鼠球初始化完成,总计耗时 " + time + " ms"); logger.info("仓鼠球初始化完成,总计耗时 " + time + " ms");
@@ -45,19 +67,29 @@ public class HamsterBallPlugin extends Plugin {
try { try {
BallBungeeCordAPI.getInstance().enable(); BallBungeeCordAPI.getInstance().enable();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.log(Level.SEVERE, "仓鼠球启动失败", e);
logger.info("由于仓鼠球启动失败,服务器将立即关闭");
ProxyServer.getInstance().stop("仓鼠球启动失败");
return;
} }
ProxyServer.getInstance().getPluginManager().registerCommand(this, BungeeBallCommand.INSTANCE);
logger.info("已注册命令 BungeeBallCommand");
BallAPI.getInstance().getEventBus().register(BallBungeeListener.INSTANCE); BallAPI.getInstance().getEventBus().register(BallBungeeListener.INSTANCE);
logger.info("已注册监听器 BallBungeeListener"); logger.info("已注册监听器 BallBungeeListener");
ProxyServer.getInstance().getPluginManager().registerListener(this, BallBungeeMainListener.INSTANCE); ProxyServer.getInstance().getPluginManager().registerListener(this, BallBungeeMainListener.INSTANCE);
logger.info("已注册监听器 BallBungeeMainListener"); logger.info("已注册监听器 BallBungeeMainListener");
ProxyServer.getInstance().getPluginManager().registerListener(this, UpdatePlayerInfoListener.INSTANCE); ProxyServer.getInstance().getPluginManager().registerListener(this, UpdatePlayerInfoListener.INSTANCE);
logger.info("已注册监听器 UpdatePlayerInfoListener"); logger.info("已注册监听器 UpdatePlayerInfoListener");
if (config.getBoolean("auto-register-game-server", false)) {
BallAPI.getInstance().getEventBus().register(BungeeServerListener.INSTANCE);
logger.info("已注册监听器 BungeeServerListener");
BungeeServerListener.INSTANCE.onEnable();
}
if (BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) { if (BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) {
BallAPI.getInstance().subscribePatterns("*" + BallAPI.PLAYER_INFO_CHANNEL); BallAPI.getInstance().subscribePatterns("*" + BallAPI.PLAYER_INFO_CHANNEL);
} else { } else {
BallAPI.getInstance().subscribeIgnorePrefix(BallAPI.PLAYER_INFO_CHANNEL); BallAPI.getInstance().subscribeRaw(BallAPI.PLAYER_INFO_CHANNEL);
} }
BallAPI.getInstance().sendRawBallMessage( BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL, BallActions.ServerOnline.name(), BallAPI.BALL_CHANNEL, BallActions.ServerOnline.name(),
@@ -84,9 +116,7 @@ public class HamsterBallPlugin extends Plugin {
try { try {
BallBungeeCordAPI.getInstance().disable(); BallBungeeCordAPI.getInstance().disable();
} catch (Exception e) { } catch (Exception e) {
getLogger().info("仓鼠球启动失败,原因:" + e.getMessage()); logger.log(Level.SEVERE, "关闭仓鼠球时遇到了一个异常", e);
e.printStackTrace();
ProxyServer.getInstance().stop("由于仓鼠球启动失败,服务器将立即关闭");
} }
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
logger.info("仓鼠球已关闭,总计耗时 " + time + " ms"); logger.info("仓鼠球已关闭,总计耗时 " + time + " ms");

View File

@@ -1,70 +1,44 @@
package cn.hamster3.mc.plugin.ball.bungee.api; package cn.hamster3.mc.plugin.ball.bungee.api;
import cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin; import cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.bungee.util.BallBungeeCordUtils;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.config.BallConfig;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType; import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.core.bungee.util.CoreBungeeCordUtils; import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import net.md_5.bungee.config.Configuration; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.sql.DataSource; import java.net.InetSocketAddress;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
public class BallBungeeCordAPI extends BallAPI { public class BallBungeeCordAPI extends BallAPI {
public BallBungeeCordAPI(@NotNull BallConfig ballConfig) { public BallBungeeCordAPI(@NotNull ConfigSection config) {
super(ballConfig); super(config, BallServerType.PROXY);
} }
public static BallBungeeCordAPI getInstance() { public static BallBungeeCordAPI getInstance() {
return (BallBungeeCordAPI) instance; return (BallBungeeCordAPI) instance;
} }
public static void init() { public static void init(@NotNull YamlConfig config) {
if (instance != null) { if (instance != null) {
return; return;
} }
HamsterBallPlugin plugin = HamsterBallPlugin.getInstance(); instance = new BallBungeeCordAPI(config);
Configuration config = CoreBungeeCordUtils.getPluginConfig(plugin);
Map<String, String> env = System.getenv();
BallServerInfo serverInfo = new BallServerInfo(
env.getOrDefault("BALL_LOCAL_SERVER_INFO_ID", config.getString("server-info.id")),
env.getOrDefault("BALL_LOCAL_SERVER_INFO_NAME", config.getString("server-info.name")),
BallServerType.PROXY,
env.getOrDefault("BALL_LOCAL_SERVER_IP", config.getString("server-info.host", "0.0.0.0")),
Integer.parseInt(
env.getOrDefault("BALL_LOCAL_SERVER_PORT", config.getString("server-info.port", "25577"))
)
);
DataSource datasource;
if (config.contains("datasource")) {
plugin.getLogger().info("启用仓鼠球自定义数据库连接池");
datasource = BallBungeeCordUtils.getDataSource(config.getSection("datasource"));
} else {
plugin.getLogger().info("复用 HamsterCore 的数据库连接池");
datasource = CoreAPI.getInstance().getDataSource();
}
BallConfig ballConfig = new BallConfig(
config.getBoolean("debug", false),
config.getString("channel-prefix", "") + ":",
config.getBoolean("game-server-update-player-info", false),
config.getStringList("load-player-info-filter"),
serverInfo,
datasource
);
instance = new BallBungeeCordAPI(ballConfig);
} }
@Override @Override
public void enable() throws SQLException, InterruptedException { public void enable() throws SQLException, InterruptedException {
for (ListenerInfo listenerInfo : ProxyServer.getInstance().getConfig().getListeners()) {
if (!(listenerInfo.getSocketAddress() instanceof InetSocketAddress)) {
continue;
}
InetSocketAddress address = (InetSocketAddress) listenerInfo.getSocketAddress();
instance.getLocalServerInfo().setHost(address.getHostString());
instance.getLocalServerInfo().setPort(address.getPort());
}
super.enable(); super.enable();
} }

View File

@@ -0,0 +1,32 @@
package cn.hamster3.mc.plugin.ball.bungee.command;
import cn.hamster3.mc.plugin.ball.common.command.BallCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import cn.hamster3.mc.plugin.core.bungee.api.CoreBungeeAPI;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.plugin.Command;
import org.jetbrains.annotations.NotNull;
public class BungeeBallCommand extends Command {
public static final BungeeBallCommand INSTANCE = new BungeeBallCommand();
public BungeeBallCommand() {
super("hamster-bungee-ball", "hamster.ball.admin", "bungee-ball", "bball");
}
@Override
public void execute(CommandSender sender, String[] args) {
BallCommand.INSTANCE.onCommand(new AdaptCommandSender() {
@Override
public boolean hasPermission(@NotNull String permission) {
return sender.hasPermission(permission);
}
@Override
public void sendMessage(@NotNull Component message) {
CoreBungeeAPI.getInstance().getAudienceProvider().sender(sender).sendMessage(message);
}
}, args);
}
}

View File

@@ -2,12 +2,11 @@ package cn.hamster3.mc.plugin.ball.bungee.listener;
import cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin; import cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; 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.common.event.operate.*;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.audience.Audience;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
@@ -23,9 +22,6 @@ public class BallBungeeListener {
@Subscribe @Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) { public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.PROXY) {
return;
}
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) { if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return; return;
} }
@@ -35,9 +31,6 @@ public class BallBungeeListener {
@Subscribe @Subscribe
public void onDispatchPlayerCommandEvent(DispatchPlayerCommandEvent event) { public void onDispatchPlayerCommandEvent(DispatchPlayerCommandEvent event) {
if (event.getType() != null && event.getType() != BallServerType.GAME) {
return;
}
ProxyServer server = ProxyServer.getInstance(); ProxyServer server = ProxyServer.getInstance();
if (event.getUuid() != null) { if (event.getUuid() != null) {
ProxiedPlayer player = server.getPlayer(event.getUuid()); ProxiedPlayer player = server.getPlayer(event.getUuid());
@@ -55,6 +48,9 @@ public class BallBungeeListener {
@Subscribe @Subscribe
public void onKickPlayerEvent(KickPlayerEvent event) { public void onKickPlayerEvent(KickPlayerEvent event) {
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(event.getUuid()); ProxiedPlayer player = ProxyServer.getInstance().getPlayer(event.getUuid());
if (player == null) {
return;
}
BaseComponent[] components = BungeeComponentSerializer.get().serialize(event.getReason()); BaseComponent[] components = BungeeComponentSerializer.get().serialize(event.getReason());
player.disconnect(components); player.disconnect(components);
} }
@@ -70,8 +66,8 @@ public class BallBungeeListener {
@Subscribe @Subscribe
public void onSendPlayerToLocationEvent(SendPlayerToLocationEvent event) { public void onSendPlayerToLocationEvent(SendPlayerToLocationEvent event) {
String serverID = event.getLocation().getServerID(); String serverID = event.getLocation().getServerID();
ServerInfo serverInfo = ProxyServer.getInstance().getServerInfo(serverID); ServerInfo toServer = ProxyServer.getInstance().getServerInfo(serverID);
if (serverInfo == null) { if (toServer == null) {
HamsterBallPlugin.getInstance().getLogger().warning("试图传送玩家时失败: 服务器 " + serverID + " 不在线"); HamsterBallPlugin.getInstance().getLogger().warning("试图传送玩家时失败: 服务器 " + serverID + " 不在线");
return; return;
} }
@@ -83,12 +79,12 @@ public class BallBungeeListener {
if (player.getServer().getInfo().getName().equals(serverID)) { if (player.getServer().getInfo().getName().equals(serverID)) {
continue; continue;
} }
player.connect(serverInfo); player.connect(toServer);
} }
} }
@Subscribe @Subscribe
public void onSendPlayerToPlayerEvent(SendPlayerToPlayerEvent event) { public void onSendPlayerToPlayer(SendPlayerToPlayerEvent event) {
UUID toPlayerUUID = event.getToPlayerUUID(); UUID toPlayerUUID = event.getToPlayerUUID();
ProxiedPlayer toPlayer = ProxyServer.getInstance().getPlayer(toPlayerUUID); ProxiedPlayer toPlayer = ProxyServer.getInstance().getPlayer(toPlayerUUID);
if (toPlayer == null) { if (toPlayer == null) {
@@ -98,6 +94,9 @@ public class BallBungeeListener {
ServerInfo toServer = toPlayer.getServer().getInfo(); ServerInfo toServer = toPlayer.getServer().getInfo();
for (UUID uuid : event.getSendPlayerUUID()) { for (UUID uuid : event.getSendPlayerUUID()) {
ProxiedPlayer sendPlayer = ProxyServer.getInstance().getPlayer(uuid); ProxiedPlayer sendPlayer = ProxyServer.getInstance().getPlayer(uuid);
if (sendPlayer == null) {
continue;
}
if (sendPlayer.getServer().getInfo().getName().equals(toServer.getName())) { if (sendPlayer.getServer().getInfo().getName().equals(toServer.getName())) {
continue; continue;
} }

View File

@@ -0,0 +1,59 @@
package cn.hamster3.mc.plugin.ball.bungee.listener;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent;
import com.google.common.eventbus.Subscribe;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import java.net.InetSocketAddress;
import java.util.Map;
public class BungeeServerListener {
public static final BungeeServerListener INSTANCE = new BungeeServerListener();
private BungeeServerListener() {
}
public void onEnable() {
for (BallServerInfo info : BallAPI.getInstance().getAllServerInfo().values()) {
if (info.getType() != BallServerType.GAME) {
continue;
}
ProxyServer.getInstance().getServers().put(info.getId(), getServerInfo(info));
BallAPI.getInstance().getLogger().info("已添加服务器入口: " + info.getId());
}
}
@Subscribe
public void onServerOnline(ServerOnlineEvent event) {
if (event.getType() != BallServerType.GAME) {
return;
}
ProxyServer.getInstance().getServers().put(event.getId(), getServerInfo(event));
BallAPI.getInstance().getLogger().info("已添加服务器入口: " + event.getId());
}
@Subscribe
public void onServerOffline(ServerOfflineEvent event) {
if (event.getType() != BallServerType.GAME) {
return;
}
Map<String, ServerInfo> map = ProxyServer.getInstance().getServers();
if (map.remove(event.getId()) != null) {
BallAPI.getInstance().getLogger().info("已移除服务器入口: " + event.getId());
}
}
private ServerInfo getServerInfo(BallServerInfo serverInfo) {
return ProxyServer.getInstance().constructServerInfo(
serverInfo.getId(),
new InetSocketAddress(serverInfo.getHost(), serverInfo.getPort()),
serverInfo.getName(),
false
);
}
}

View File

@@ -5,20 +5,15 @@ import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions; import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerInfoUpdateEvent; import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerInfoUpdateEvent;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.lib.com.zaxxer.hikari.HikariConfig;
import cn.hamster3.mc.plugin.core.lib.com.zaxxer.hikari.HikariDataSource;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.config.Configuration;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.sql.DataSource;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.logging.Level;
@SuppressWarnings("CallToPrintStackTrace")
public final class BallBungeeCordUtils { public final class BallBungeeCordUtils {
private BallBungeeCordUtils() { private BallBungeeCordUtils() {
} }
@@ -49,7 +44,7 @@ public final class BallBungeeCordUtils {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); BallAPI.getInstance().getLogger().log(Level.SEVERE, "更新玩家数据时遇到了一个异常", e);
} }
if (!BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) { if (!BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) {
BallAPI.getInstance().sendRawBallMessage( BallAPI.getInstance().sendRawBallMessage(
@@ -60,32 +55,4 @@ public final class BallBungeeCordUtils {
} }
}); });
} }
@Nullable
public static DataSource getDataSource(@Nullable Configuration datasourceConfig) {
if (datasourceConfig == null) {
return null;
}
try {
HikariConfig hikariConfig = new HikariConfig();
String driver = datasourceConfig.getString("driver");
hikariConfig.setDriverClassName(driver);
hikariConfig.setJdbcUrl(datasourceConfig.getString("url"));
hikariConfig.setUsername(datasourceConfig.getString("username"));
hikariConfig.setPassword(datasourceConfig.getString("password"));
hikariConfig.setMaximumPoolSize(datasourceConfig.getInt("maximum-pool-size", 3));
hikariConfig.setMinimumIdle(datasourceConfig.getInt("minimum-idle", 1));
long keepAliveTime = datasourceConfig.getLong("keep-alive-time", 0);
if (keepAliveTime > 5000) {
hikariConfig.setKeepaliveTime(keepAliveTime);
}
hikariConfig.setIdleTimeout(datasourceConfig.getLong("idle-timeout", 10 * 60 * 1000));
hikariConfig.setMaxLifetime(datasourceConfig.getLong("max-lifetime", 30 * 60 * 1000));
hikariConfig.setValidationTimeout(datasourceConfig.getLong("validation-timeout", 5000));
hikariConfig.setPoolName("HamsterBall-Pool");
return new HikariDataSource(hikariConfig);
} catch (Exception | Error e) {
return null;
}
}
} }

View File

@@ -3,7 +3,14 @@ main: cn.hamster3.mc.plugin.ball.bungee.HamsterBallPlugin
version: ${version} version: ${version}
author: MiniDay author: MiniDay
description: 仓鼠球:一个基于 Redis 的 Minecraft 服务端通用消息中间件原HamsterService description: ${description}
website: https://git.airgame.net/MiniDay/hamster-ball
UPDATE_CHECKER:
VERSION: ${version}
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball
depend: depend:
- HamsterCore - HamsterCore

View File

@@ -1,6 +1,10 @@
# 是否允许在控制台输出调试信息 # 是否允许在控制台输出调试信息
debug: false debug: false
# 是否启用服务器 ID 单例模式
# 启用后,服务器唯一识别码 相同的服务器将无法启动
singleton-server-id: false
# 频道名前缀 # 频道名前缀
# 使用这个配置选项可以划分子服消息通信分组 # 使用这个配置选项可以划分子服消息通信分组
# 只有在同一个频道名的子服才能互相通信 # 只有在同一个频道名的子服才能互相通信
@@ -8,12 +12,14 @@ channel-prefix: ""
# 是否在子服端更新玩家信息 # 是否在子服端更新玩家信息
# 默认情况下BC 统一管理玩家信息,包括记录 UUID 和玩家名称 # 默认情况下BC 统一管理玩家信息,包括记录 UUID 和玩家名称
# 如果一个群组服同时拥有多个 BC 入口 # 如果一个群组服同时拥有多个 BC 入口,且每个 BC 入口为不同的玩家名称分配不同的 UUID
# 且每个 BC 入口为不同的玩家名称分配不同的 UUID
# (例如正版、盗版双入口,或网易多入口接同一个子服) # (例如正版、盗版双入口,或网易多入口接同一个子服)
# 则可以启用该功能以防止 UUID 紊乱的问题 # 则可以启用该功能以防止同一个名称占用多个 UUID 的问题
game-server-update-player-info: false game-server-update-player-info: false
# 启用后,子服启动时会自动注册该子服的入口配置,关闭时也会自动移除该子服的入口配置
auto-register-game-server: false
# 本服务器信息 # 本服务器信息
server-info: server-info:
# 服务器唯一识别码,最长 32 字符 # 服务器唯一识别码,最长 32 字符
@@ -27,6 +33,11 @@ server-info:
# 不填则自动设置为 25577 # 不填则自动设置为 25577
port: 25577 port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置 # 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置 # 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接 # 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接
@@ -34,21 +45,20 @@ server-info:
# 这个选项就会很有用 # 这个选项就会很有用
#datasource: #datasource:
# # 数据库链接驱动地址 # # 数据库链接驱动地址
# driver: "com.mysql.jdbc.Driver" # driver: "com.mysql.cj.jdbc.Driver"
# # 数据库链接填写格式: # # MySQL数据库链接填写格式:
# # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数 # # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数
# # 除非你知道自己在做什么,否则不建议随意更改参数 # url: "jdbc:mysql://localhost:3306/Test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# url: "jdbc:mysql://localhost:3306/Test1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# # 用户名 # # 用户名
# username: "Test" # username: "root"
# # 密码 # # 密码
# password: "Test123.." # password: "Root123.."
# # 最小闲置链接数 # # 最小闲置链接数
# # 推荐值1~3 # # 推荐值1~3
# minimum-idle: 0 # minimum-idle: 0
# # 最大链接数 # # 最大链接数
# # 推荐值:不低于3 # # 推荐值:不低于5
# maximum-pool-size: 3 # maximum-pool-size: 5
# # 保持连接池可用的间隔 # # 保持连接池可用的间隔
# # 除非你的服务器数据库连接经常断开,否则不建议启用该选项 # # 除非你的服务器数据库连接经常断开,否则不建议启用该选项
# # 单位:毫秒 # # 单位:毫秒

View File

@@ -1,7 +1,7 @@
@file:Suppress("VulnerableLibrariesLocal", "GradlePackageVersionRange", "GradlePackageUpdate") @file:Suppress("VulnerableLibrariesLocal")
dependencies { dependencies {
compileOnly("cn.hamster3.mc.plugin:core-common:1.2.0") compileOnly("cn.hamster3.mc.plugin:core-common:+")
compileOnly("com.google.code.gson:gson:2.8.0") compileOnly("com.google.code.gson:gson:2.8.0")
compileOnly("com.google.guava:guava:31.0-jre") compileOnly("com.google.guava:guava:31.0-jre")

View File

@@ -13,13 +13,19 @@ import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent;
import cn.hamster3.mc.plugin.ball.common.listener.BallCommonListener; import cn.hamster3.mc.plugin.ball.common.listener.BallCommonListener;
import cn.hamster3.mc.plugin.ball.common.listener.BallDebugListener; import cn.hamster3.mc.plugin.ball.common.listener.BallDebugListener;
import cn.hamster3.mc.plugin.ball.common.listener.BallRedisListener; import cn.hamster3.mc.plugin.ball.common.listener.BallRedisListener;
import cn.hamster3.mc.plugin.ball.common.thread.LockUpdateThread;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import cn.hamster3.mc.plugin.core.common.data.DisplayMessage; import cn.hamster3.mc.plugin.core.common.data.DisplayMessage;
import cn.hamster3.mc.plugin.core.lib.io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import cn.hamster3.mc.plugin.core.common.util.CoreUtils;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.text.Component;
import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus; 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 lombok.Getter;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -27,10 +33,13 @@ import javax.sql.DataSource;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@SuppressWarnings("unused")
@Getter @Getter
@SuppressWarnings({"unused", "CallToPrintStackTrace"})
public abstract class BallAPI { public abstract class BallAPI {
/** /**
* API 使用的通信频道 * API 使用的通信频道
@@ -40,32 +49,60 @@ public abstract class BallAPI {
* API 使用的玩家信息更新通信频道 * API 使用的玩家信息更新通信频道
*/ */
public static final String PLAYER_INFO_CHANNEL = "HamsterBall:PlayerInfo"; public static final String PLAYER_INFO_CHANNEL = "HamsterBall:PlayerInfo";
/** /**
* API 实例 * API 实例
*/ */
@Getter @Getter
protected static BallAPI instance; protected static BallAPI instance;
@NotNull @NotNull
private final BallConfig ballConfig; private final BallConfig ballConfig;
@NotNull
private final DataSource datasource;
@NotNull
private final RedisClient redisClient;
@NotNull
private final BallServerInfo localServerInfo;
@NotNull @NotNull
private final EventBus eventBus; private final EventBus eventBus;
@NotNull @NotNull
private final Map<String, BallServerInfo> allServerInfo; private final Map<String, BallServerInfo> allServerInfo;
@NotNull @NotNull
private final Map<UUID, BallPlayerInfo> allPlayerInfo; private final Map<UUID, BallPlayerInfo> allPlayerInfo;
@NotNull
private final StatefulRedisPubSubConnection<String, BallMessage> redisPub;
@NotNull
private final StatefulRedisPubSubConnection<String, BallMessage> redisSub;
public BallAPI(@NotNull BallConfig ballConfig) { @NotNull
this.ballConfig = ballConfig; private final StatefulRedisPubSubConnection<String, String> redisPubSub;
redisPub = CoreAPI.getInstance().getRedisClient().connectPubSub(BallMessage.REDIS_CODEC); @Nullable
redisSub = CoreAPI.getInstance().getRedisClient().connectPubSub(BallMessage.REDIS_CODEC); private ScheduledFuture<?> lockUpdater;
public BallAPI(@NotNull ConfigSection config, @NotNull BallServerType type) {
ConfigSection serverInfoConfig = config.getSection("server-info");
if (serverInfoConfig == null) {
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("启用仓鼠球自定义数据库连接池");
datasource = CoreUtils.getDataSource(section);
} else {
getLogger().info("复用 HamsterCore 的数据库连接池");
datasource = CoreAPI.getInstance().getDataSource();
}
ballConfig = new BallConfig(config);
eventBus = new AsyncEventBus("HamsterBall", CoreAPI.getInstance().getExecutorService());
eventBus.register(BallCommonListener.INSTANCE);
allServerInfo = new ConcurrentHashMap<>(); allServerInfo = new ConcurrentHashMap<>();
allPlayerInfo = new ConcurrentHashMap<>(); allPlayerInfo = new ConcurrentHashMap<>();
eventBus = new AsyncEventBus("HamsterBall - EventBus", CoreAPI.getInstance().getExecutorService()); redisPubSub = getRedisClient().connectPubSub();
eventBus.register(BallCommonListener.INSTANCE);
getLogger().info("频道前缀: " + ballConfig.getChannelPrefix()); getLogger().info("频道前缀: " + ballConfig.getChannelPrefix());
getLogger().info("启用子服更新玩家状态: " + ballConfig.isGameServerUpdatePlayerInfo()); getLogger().info("启用子服更新玩家状态: " + ballConfig.isGameServerUpdatePlayerInfo());
if (ballConfig.isGameServerUpdatePlayerInfo()) { if (ballConfig.isGameServerUpdatePlayerInfo()) {
@@ -75,11 +112,35 @@ public abstract class BallAPI {
getLogger().warning("已启用调试模式"); getLogger().warning("已启用调试模式");
eventBus.register(BallDebugListener.INSTANCE); eventBus.register(BallDebugListener.INSTANCE);
} }
redisPubSub.addListener(BallRedisListener.INSTANCE);
redisPubSub.sync().subscribe(BALL_CHANNEL);
} }
protected void enable() throws SQLException, InterruptedException { protected void enable() throws SQLException, InterruptedException {
BallServerInfo localInfo = getLocalServerInfo(); try (StatefulRedisConnection<String, String> connect = getRedisClient().connect()) {
RedisCommands<String, String> redis = connect.sync();
String key = "HamsterBall:ServerInfo:" + localServerInfo.getId();
if (redis.exists(key) > 0 && ballConfig.isSingletonServerID()) {
throw new IllegalStateException("已经有一个服务器占用了该 ID");
}
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 : redis.keys("HamsterBall:ServerInfo:*")) {
BallServerInfo info = new BallServerInfo(
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);
}
}
try (Connection connection = getDatasource().getConnection()) { try (Connection connection = getDatasource().getConnection()) {
try (Statement statement = connection.createStatement()) { try (Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_player_info`(" + statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_player_info`(" +
@@ -89,44 +150,14 @@ public abstract class BallAPI {
"`proxy_server` VARCHAR(32) NOT NULL," + "`proxy_server` VARCHAR(32) NOT NULL," +
"`online` BOOLEAN NOT NULL" + "`online` BOOLEAN NOT NULL" +
") CHARSET utf8mb4;"); ") CHARSET utf8mb4;");
statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_server_info`(" +
"`id` VARCHAR(32) PRIMARY KEY NOT NULL," +
"`name` VARCHAR(32) NOT NULL," +
"`type` VARCHAR(16) NOT NULL," +
"`host` VARCHAR(32) NOT NULL," +
"`port` INT NOT NULL" +
") CHARSET utf8mb4;");
statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_cached_message`(" + statement.execute("CREATE TABLE IF NOT EXISTS `hamster_ball_cached_message`(" +
"`uuid` CHAR(36) NOT NULL," + "`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;"); ") CHARSET utf8mb4;");
} }
try (PreparedStatement statement = connection.prepareStatement(
"REPLACE INTO `hamster_ball_server_info` VALUES(?, ?, ?, ?, ?);"
)) {
statement.setString(1, localInfo.getId());
statement.setString(2, localInfo.getName());
statement.setString(3, localInfo.getType().name());
statement.setString(4, localInfo.getHost());
statement.setInt(5, localInfo.getPort());
statement.executeUpdate();
}
try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM `hamster_ball_server_info`;"
)) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
String serverID = set.getString("id");
allServerInfo.put(serverID, new BallServerInfo(
serverID,
set.getString("name"),
BallServerType.valueOf(set.getString("type")),
set.getString("host"),
set.getInt("port")
));
}
}
}
if (getBallConfig().isGameServerUpdatePlayerInfo()) { if (getBallConfig().isGameServerUpdatePlayerInfo()) {
try (Statement statement = connection.createStatement()) { try (Statement statement = connection.createStatement()) {
try (ResultSet set = statement.executeQuery(String.format( try (ResultSet set = statement.executeQuery(String.format(
@@ -164,22 +195,22 @@ public abstract class BallAPI {
} }
getLogger().info("从数据库中加载了 " + allServerInfo.size() + " 条服务器信息"); getLogger().info("从数据库中加载了 " + allServerInfo.size() + " 条服务器信息");
getLogger().info("从数据库中加载了 " + allPlayerInfo.size() + " 条玩家信息"); getLogger().info("从数据库中加载了 " + allPlayerInfo.size() + " 条玩家信息");
redisPub.addListener(BallRedisListener.INSTANCE);
subscribeIgnorePrefix(BALL_CHANNEL);
} }
protected void disable() throws SQLException, InterruptedException { protected void disable() throws SQLException, InterruptedException {
sendBallMessage(BallAPI.BALL_CHANNEL, new BallMessage( sendBallMessage(BallAPI.BALL_CHANNEL, new BallMessage(
BallActions.ServerOffline.name(), new ServerOfflineEvent(getLocalServerInfo()) BallActions.ServerOffline.name(), new ServerOfflineEvent(getLocalServerInfo())
), false, true); ), false, true);
if (lockUpdater != null) {
try (Connection connection = getDatasource().getConnection()) { lockUpdater.cancel(true);
try (PreparedStatement statement = connection.prepareStatement( lockUpdater = null;
"DELETE FROM `hamster_ball_server_info` WHERE `id`=?;" try (StatefulRedisConnection<String, String> connect = getRedisClient().connect()) {
)) { RedisCommands<String, String> redis = connect.sync();
statement.setString(1, getLocalServerId()); String key = "HamsterBall:ServerInfo:" + localServerInfo.getId();
statement.executeUpdate(); redis.del(key);
} }
}
try (Connection connection = getDatasource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"UPDATE `hamster_ball_player_info` SET `online`=false WHERE `game_server`=? OR `proxy_server`=?" "UPDATE `hamster_ball_player_info` SET `online`=false WHERE `game_server`=? OR `proxy_server`=?"
)) { )) {
@@ -188,6 +219,7 @@ public abstract class BallAPI {
statement.executeUpdate(); statement.executeUpdate();
} }
} }
redisPubSub.close();
} }
/** /**
@@ -241,25 +273,24 @@ public abstract class BallAPI {
*/ */
public void dispatchConsoleCommand(@Nullable BallServerType type, @Nullable String serverID, @NotNull String command) { public void dispatchConsoleCommand(@Nullable BallServerType type, @Nullable String serverID, @NotNull String command) {
sendBallMessage(BALL_CHANNEL, new BallMessage( sendBallMessage(BALL_CHANNEL, new BallMessage(
getLocalServerId(), null, BallServerType.GAME, getLocalServerId(), null, type,
BallActions.DispatchConsoleCommand.name(), BallActions.DispatchConsoleCommand.name(),
CoreAPI.getInstance().getGson().toJsonTree(new DispatchConsoleCommandEvent(type, serverID, command)) CoreAPI.getInstance().getGson().toJsonTree(new DispatchConsoleCommandEvent(serverID, command))
), false); ), false);
} }
/** /**
* 强制玩家执行命令 * 强制玩家执行命令
* *
* @param type 执行对象的服务端类型 * @param type 执行对象的服务端类型null代表所有类型
* @param uuid 执行对象的 UUID * @param uuid 执行对象的 UUIDnull代表所有玩家
* @param command 命令内容 * @param command 命令内容
*/ */
public void dispatchPlayerCommand(@Nullable BallServerType type, @Nullable UUID uuid, @NotNull String command) { public void dispatchPlayerCommand(@Nullable BallServerType type, @Nullable UUID uuid, @NotNull String command) {
sendBallMessage(BALL_CHANNEL, new BallMessage( sendBallMessage(BALL_CHANNEL, new BallMessage(
getLocalServerId(), null, BallServerType.GAME, getLocalServerId(), null, type,
BallActions.DispatchPlayerCommand.name(), BallActions.DispatchPlayerCommand.name(),
CoreAPI.getInstance().getGson().toJsonTree(new DispatchPlayerCommandEvent(type, uuid, command)) CoreAPI.getInstance().getGson().toJsonTree(new DispatchPlayerCommandEvent(uuid, command))
), false); ), false);
} }
@@ -314,14 +345,14 @@ public abstract class BallAPI {
} }
try (Connection connection = getDatasource().getConnection()) { try (Connection connection = getDatasource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement( 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(1, receiver.toString());
statement.setString(2, message.toJson().toString()); statement.setString(2, message.toJson().toString());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); BallAPI.getInstance().getLogger().log(Level.SEVERE, "更新玩家缓存消息时遇到了一个异常", e);
} }
} }
} }
@@ -470,14 +501,12 @@ public abstract class BallAPI {
channel = ballConfig.getChannelPrefix() + channel; channel = ballConfig.getChannelPrefix() + channel;
} }
if (block) { if (block) {
redisSub.sync().publish(channel, message); redisPubSub.sync().publish(channel, CoreAPI.getInstance().getGson().toJson(message));
eventBus.post(new MessageSentEvent(channel, message)); eventBus.post(new MessageSentEvent(channel, message));
} else { } else {
@NotNull String finalChannel = channel; @NotNull String finalChannel = channel;
redisSub.async().publish(channel, message).whenComplete((aLong, throwable) -> { CoreAPI.getInstance().getExecutorService().execute(() -> {
if (throwable != null) { redisPubSub.sync().publish(finalChannel, CoreAPI.getInstance().getGson().toJson(message));
return;
}
eventBus.post(new MessageSentEvent(finalChannel, message)); eventBus.post(new MessageSentEvent(finalChannel, message));
}); });
} }
@@ -488,22 +517,22 @@ public abstract class BallAPI {
* <p> * <p>
* 会自动加上 config 中设置的频道前缀 * 会自动加上 config 中设置的频道前缀
* *
* @param channel 频道名称 * @param channels 频道名称
*/ */
public void subscribe(@NotNull String... channel) { public void subscribe(@NotNull String... channels) {
for (int i = 0; i < channel.length; i++) { for (int i = 0; i < channels.length; i++) {
channel[i] = ballConfig.getChannelPrefix() + channel[i]; channels[i] = ballConfig.getChannelPrefix() + channels[i];
} }
redisPub.sync().subscribe(channel); subscribeRaw(channels);
} }
/** /**
* 忽略频道前缀配置,订阅 redis 消息频道 * 忽略频道前缀配置,订阅 redis 消息频道
* *
* @param channel 频道名称 * @param channels 频道名称
*/ */
public void subscribeIgnorePrefix(@NotNull String... channel) { public void subscribeRaw(@NotNull String... channels) {
redisPub.sync().subscribe(channel); redisPubSub.sync().subscribe(channels);
} }
/** /**
@@ -512,28 +541,30 @@ public abstract class BallAPI {
* @param patterns 频道名称正则表达式 * @param patterns 频道名称正则表达式
*/ */
public void subscribePatterns(@NotNull String patterns) { public void subscribePatterns(@NotNull String patterns) {
redisPub.sync().psubscribe(patterns); redisPubSub.sync().psubscribe(patterns);
} }
/** /**
* 取消订阅 redis 频道 * 取消订阅 redis 消息频道
* <p>
* 会自动加上 config 中设置的频道前缀
* *
* @param channel 频道名称 * @param channels 频道名称
*/ */
public void unsubscribe(@NotNull String... channel) { public void unsubscribe(@NotNull String... channels) {
for (int i = 0; i < channel.length; i++) { for (int i = 0; i < channels.length; i++) {
channel[i] = ballConfig.getChannelPrefix() + channel[i]; channels[i] = ballConfig.getChannelPrefix() + channels[i];
} }
redisPub.sync().unsubscribe(channel); unsubscribeRaw(channels);
} }
/** /**
* 忽略仓鼠球频道前缀配置,取消订阅 redis 频道 * 忽略频道前缀配置,取消订阅 redis 消息频道
* *
* @param channel 频道名称 * @param channels 频道名称
*/ */
public void unsubscribeIgnorePrefix(@NotNull String... channel) { public void unsubscribeRaw(@NotNull String... channels) {
redisPub.sync().unsubscribe(channel); redisPubSub.sync().unsubscribe(channels);
} }
/** /**
@@ -542,22 +573,12 @@ public abstract class BallAPI {
* @param patterns 频道名称正则表达式 * @param patterns 频道名称正则表达式
*/ */
public void unsubscribePatterns(@NotNull String patterns) { public void unsubscribePatterns(@NotNull String patterns) {
redisPub.sync().punsubscribe(patterns); redisPubSub.sync().punsubscribe(patterns);
}
/**
* 获取本地服务器ID
*
* @return 服务器ID
*/
@NotNull
public BallServerInfo getLocalServerInfo() {
return ballConfig.getServerInfo();
} }
@NotNull @NotNull
public String getLocalServerId() { public String getLocalServerId() {
return ballConfig.getServerInfo().getId(); return localServerInfo.getId();
} }
/** /**
@@ -674,9 +695,4 @@ public abstract class BallAPI {
@NotNull @NotNull
public abstract Logger getLogger(); public abstract Logger getLogger();
@NotNull
public DataSource getDatasource() {
return ballConfig.getDatasource() == null ? CoreAPI.getInstance().getDataSource() : ballConfig.getDatasource();
}
} }

View File

@@ -0,0 +1,21 @@
package cn.hamster3.mc.plugin.ball.common.command;
import cn.hamster3.mc.plugin.ball.common.command.adapt.ParentCommand;
import org.jetbrains.annotations.NotNull;
public class BallCommand extends ParentCommand {
public static final BallCommand INSTANCE = new BallCommand();
private BallCommand() {
addChildCommand(PlayerInfoCommand.INSTANCE);
addChildCommand(SudoPlayerCommand.INSTANCE);
addChildCommand(SudoAllPlayerCommand.INSTANCE);
addChildCommand(SudoConsoleCommand.INSTANCE);
addChildCommand(SudoAllConsoleCommand.INSTANCE);
}
@Override
public @NotNull String getName() {
return "hamster-ball";
}
}

View File

@@ -0,0 +1,77 @@
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.BallPlayerInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class PlayerInfoCommand extends ChildCommand {
public static final PlayerInfoCommand INSTANCE = new PlayerInfoCommand();
private PlayerInfoCommand() {
}
@Override
public @NotNull String getName() {
return "player-info";
}
@Override
public @NotNull String getUsage() {
return "player-info <玩家名|UUID>";
}
@Override
public @NotNull String getDescription() {
return "查看玩家信息";
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return sender.hasPermission("hamster.ball.admin");
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 1) {
sender.sendMessage(BallCommand.INSTANCE.getUsage() + " " + getUsage());
return true;
}
BallPlayerInfo info;
try {
UUID uuid = UUID.fromString(args[0]);
info = BallAPI.getInstance().getPlayerInfo(uuid);
} catch (Exception e) {
info = BallAPI.getInstance().getPlayerInfo(args[0]);
}
if (info == null) {
sender.sendMessage("§c未找到玩家 " + args[0] + " 的信息");
return true;
}
sender.sendMessage("§a玩家名称: " + info.getName());
sender.sendMessage("§a玩家UUID: " + info.getUuid());
sender.sendMessage("§a玩家在线: " + info.isOnline());
sender.sendMessage("§a接入点: " + info.getProxyServer());
sender.sendMessage("§a所在子服: " + info.getGameServer());
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());
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,75 @@
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.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();
private SudoAllConsoleCommand() {
}
@Override
public @NotNull String getName() {
return "sudo-all-console";
}
@Override
public @NotNull String getUsage() {
return "sudo-all-console <服务器类型> <命令内容>";
}
@Override
public @NotNull String getDescription() {
return "强制所有控制台执行指令";
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return sender.hasPermission("hamster.ball.admin");
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 2) {
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;
}
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(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

@@ -0,0 +1,75 @@
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();
private SudoAllPlayerCommand() {
}
@Override
public @NotNull String getName() {
return "sudo-all-player";
}
@Override
public @NotNull String getUsage() {
return "sudo-all-player <服务器类型> <命令内容>";
}
@Override
public @NotNull String getDescription() {
return "强制所有玩家执行指令";
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return sender.hasPermission("hamster.ball.admin");
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 2) {
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;
}
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(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

@@ -0,0 +1,72 @@
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.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;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class SudoConsoleCommand extends ChildCommand {
public static final SudoConsoleCommand INSTANCE = new SudoConsoleCommand();
private SudoConsoleCommand() {
}
@Override
public @NotNull String getName() {
return "sudo-console";
}
@Override
public @NotNull String getUsage() {
return "sudo-console <服务器ID> <命令内容>";
}
@Override
public @NotNull String getDescription() {
return "强制控制台执行指令";
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return sender.hasPermission("hamster.ball.admin");
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length < 2) {
sender.sendMessage(BallCommand.INSTANCE.getUsage() + " " + getUsage());
return true;
}
BallServerInfo info = BallAPI.getInstance().getServerInfo(args[0]);
if (info == null) {
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, info.getId(), 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().getAllServerInfo().values().stream()
.map(BallServerInfo::getId)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.limit(10)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,101 @@
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.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;
import java.util.stream.Collectors;
public class SudoPlayerCommand extends ChildCommand {
public static final SudoPlayerCommand INSTANCE = new SudoPlayerCommand();
private SudoPlayerCommand() {
}
@Override
public @NotNull String getName() {
return "sudo-player";
}
@Override
public @NotNull String getUsage() {
return "sudo-player <服务器类型> <玩家名|UUID> <命令内容>";
}
@Override
public @NotNull String getDescription() {
return "强制玩家执行指令";
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return sender.hasPermission("hamster.ball.admin");
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
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[1]);
info = BallAPI.getInstance().getPlayerInfo(uuid);
} catch (Exception e) {
info = BallAPI.getInstance().getPlayerInfo(args[1]);
}
if (info == null) {
sender.sendMessage("§c未找到玩家 " + args[1]);
return true;
}
if (!info.isOnline()) {
sender.sendMessage("§c玩家 " + args[1] + " 不在线");
return true;
}
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(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) {
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

@@ -0,0 +1,14 @@
package cn.hamster3.mc.plugin.ball.common.command.adapt;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
public interface AdaptCommandSender {
boolean hasPermission(@NotNull String permission);
void sendMessage(@NotNull Component message);
default void sendMessage(@NotNull String message) {
sendMessage(Component.text(message));
}
}

View File

@@ -0,0 +1,24 @@
package cn.hamster3.mc.plugin.ball.common.command.adapt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public abstract class ChildCommand {
@NotNull
public abstract String getName();
@NotNull
public abstract String getUsage();
@NotNull
public abstract String getDescription();
public abstract boolean hasPermission(@NotNull AdaptCommandSender sender);
public abstract boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args);
@Nullable
public abstract List<String> onTabComplete(@NotNull AdaptCommandSender sender, @NotNull String[] args);
}

View File

@@ -0,0 +1,151 @@
package cn.hamster3.mc.plugin.ball.common.command.adapt;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public abstract class ParentCommand extends ChildCommand {
@NotNull
private final Map<String, ChildCommand> childCommands;
public ParentCommand() {
childCommands = new LinkedHashMap<>();
}
@NotNull
@Override
public abstract String getName();
@Nullable
public ParentCommand getParent() {
return null;
}
@NotNull
public Collection<ChildCommand> getChildCommands() {
return childCommands.values();
}
@NotNull
@Override
public String getUsage() {
ParentCommand parent = getParent();
if (parent == null) {
return "/" + getName();
}
return parent.getUsage() + " " + getName();
}
@Override
public boolean hasPermission(@NotNull AdaptCommandSender sender) {
return true;
}
@Override
public @NotNull String getDescription() {
return "";
}
/**
* 获取所有子命令
* <p>
* 如果子命令也是 ParentCommand 类型,则继续递归获取该 ParentCommand 的子命令
*
* @return 所有子命令
*/
@NotNull
public List<ChildCommand> getEndChildCommands() {
ArrayList<ChildCommand> list = new ArrayList<>();
for (ChildCommand command : getChildCommands()) {
if (command instanceof ParentCommand) {
list.addAll(((ParentCommand) command).getEndChildCommands());
} else {
list.add(command);
}
}
return list;
}
public void addChildCommand(@NotNull ChildCommand command) {
childCommands.put(command.getName(), command);
}
@NotNull
public Map<String, String> getCommandHelp(AdaptCommandSender sender) {
Map<String, String> map = new LinkedHashMap<>();
for (ChildCommand child : getChildCommands()) {
if (!child.hasPermission(sender)) {
continue;
}
if (child instanceof ParentCommand) {
Map<String, String> childMap = ((ParentCommand) child).getCommandHelp(sender);
map.putAll(childMap);
continue;
}
map.put(getUsage() + " " + child.getUsage(), child.getDescription());
}
return map;
}
public void sendHelp(@NotNull AdaptCommandSender sender) {
sender.sendMessage("§e==================== [ " + getName() + " 使用帮助] ====================");
Map<String, String> map = getCommandHelp(sender);
int maxLength = map.keySet().stream()
.map(String::length)
.max(Integer::compareTo)
.orElse(-1);
for (Map.Entry<String, String> entry : map.entrySet()) {
sender.sendMessage(String.format("§a%-" + maxLength + "s - %s", entry.getKey(), entry.getValue()));
}
}
@Override
public boolean onCommand(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (!hasPermission(sender)) {
sender.sendMessage(Component.translatable("commands.help.failed").color(NamedTextColor.RED));
return true;
}
if (args.length == 0) {
sendHelp(sender);
return true;
}
for (ChildCommand childCommand : getChildCommands()) {
if (!childCommand.getName().equalsIgnoreCase(args[0])) {
continue;
}
if (!childCommand.hasPermission(sender)) {
sender.sendMessage(Component.translatable("commands.help.failed").color(NamedTextColor.RED));
return true;
}
return childCommand.onCommand(sender, Arrays.copyOfRange(args, 1, args.length));
}
sender.sendMessage(Component.translatable("commands.help.failed").color(NamedTextColor.RED));
return true;
}
@Override
public List<String> onTabComplete(@NotNull AdaptCommandSender sender, @NotNull String[] args) {
if (args.length == 0) {
return getChildCommands().stream()
.filter(o -> o.hasPermission(sender))
.map(ChildCommand::getName)
.collect(Collectors.toList());
}
for (ChildCommand child : getChildCommands()) {
if (args[0].equalsIgnoreCase(child.getName())) {
return child.onTabComplete(sender, Arrays.copyOfRange(args, 1, args.length));
}
}
args[0] = args[0].toLowerCase();
return getChildCommands().stream()
.filter(o -> o.hasPermission(sender))
.map(ChildCommand::getName)
.filter(o -> o.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
}
}

View File

@@ -1,24 +1,29 @@
package cn.hamster3.mc.plugin.ball.common.config; package cn.hamster3.mc.plugin.ball.common.config;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo; import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.sql.DataSource;
import java.util.List; import java.util.List;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class BallConfig { public class BallConfig {
private boolean debug; private boolean debug;
private boolean singletonServerID;
@NotNull @NotNull
private String channelPrefix; private String channelPrefix;
private boolean gameServerUpdatePlayerInfo; private boolean gameServerUpdatePlayerInfo;
private List<String> loadPlayerInfoFilter;
@NotNull @NotNull
private BallServerInfo serverInfo; private List<String> loadPlayerInfoFilter;
@Nullable
private DataSource datasource; public BallConfig(@NotNull ConfigSection config) {
debug = config.getBoolean("debug", false);
singletonServerID = config.getBoolean("singleton-server-id", false);
channelPrefix = config.getString("channel-prefix", "");
channelPrefix = channelPrefix.isEmpty() ? channelPrefix : channelPrefix + ":";
gameServerUpdatePlayerInfo = config.getBoolean("game-server-update-player-info", false);
loadPlayerInfoFilter = config.getStringList("load-player-info-filter");
}
} }

View File

@@ -3,11 +3,10 @@ package cn.hamster3.mc.plugin.ball.common.data;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; 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.entity.BallServerType;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import cn.hamster3.mc.plugin.core.lib.io.lettuce.core.codec.RedisCodec;
import cn.hamster3.mc.plugin.core.lib.io.lettuce.core.codec.StringCodec;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -16,7 +15,6 @@ import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -25,33 +23,9 @@ import java.util.UUID;
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class BallMessage { public class BallMessage {
/**
* lettuce 编解码器
*/
public static final RedisCodec<String, BallMessage> REDIS_CODEC = new RedisCodec<String, BallMessage>() {
@Override
public String decodeKey(ByteBuffer bytes) {
return StringCodec.UTF8.decodeKey(bytes);
}
@Override
public BallMessage decodeValue(ByteBuffer bytes) {
String string = StringCodec.UTF8.decodeValue(bytes);
return CoreAPI.getInstance().getGson().fromJson(string, BallMessage.class);
}
@Override
public ByteBuffer encodeKey(String key) {
return StringCodec.UTF8.encodeKey(key);
}
@Override
public ByteBuffer encodeValue(BallMessage value) {
return StringCodec.UTF8.encodeValue(CoreAPI.getInstance().getGson().toJson(value));
}
};
/** /**
* 消息发送者 * 消息发送者
*/ */
@@ -100,34 +74,6 @@ public class BallMessage {
this.content = CoreAPI.getInstance().getGson().toJsonTree(content); this.content = CoreAPI.getInstance().getGson().toJsonTree(content);
} }
public BallMessage(@NotNull String senderID, @Nullable String receiverID, @Nullable BallServerType receiverType, @NotNull String action, @Nullable JsonElement content) {
this.senderID = senderID;
this.receiverID = receiverID;
this.receiverType = receiverType;
this.action = action;
this.content = content;
}
/**
* 序列化至 Json
*
* @return json对象
*/
@NotNull
public JsonObject toJson() {
JsonObject object = new JsonObject();
object.addProperty("senderID", senderID);
if (receiverID != null) {
object.addProperty("toServer", receiverID);
}
if (receiverType != null) {
object.addProperty("toServer", receiverType.name());
}
object.addProperty("action", action);
object.add("content", content);
return object;
}
/** /**
* 以 Java 对象获取消息内容 * 以 Java 对象获取消息内容
* *
@@ -199,6 +145,6 @@ public class BallMessage {
@Override @Override
public String toString() { public String toString() {
return toJson().toString(); return CoreAPI.getInstance().getGson().toJson(this);
} }
} }

View File

@@ -1,9 +1,12 @@
package cn.hamster3.mc.plugin.ball.common.entity; package cn.hamster3.mc.plugin.ball.common.entity;
import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
/** /**
@@ -40,6 +43,21 @@ public class BallServerInfo {
*/ */
private int port; private int port;
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;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -13,7 +13,6 @@ public enum BallActions {
BallPlayerLogin, BallPlayerLogin,
BallPlayerPostLogin, BallPlayerPostLogin,
BallPlayerPreConnectServer, BallPlayerPreConnectServer,
BallPlayerConnectServer,
BallPlayerPostConnectServer, BallPlayerPostConnectServer,
BallPlayerLogout, BallPlayerLogout,

View File

@@ -1,7 +1,6 @@
package cn.hamster3.mc.plugin.ball.common.event.message; package cn.hamster3.mc.plugin.ball.common.event.message;
import cn.hamster3.mc.plugin.ball.common.data.BallMessage; import cn.hamster3.mc.plugin.ball.common.data.BallMessage;
import com.google.gson.JsonObject;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -23,11 +22,4 @@ public class MessageEvent extends BallMessage {
setAction(message.getAction()); setAction(message.getAction());
setContent(message.getContent()); setContent(message.getContent());
} }
@Override
public @NotNull JsonObject toJson() {
JsonObject object = super.toJson();
object.addProperty("channel", channel);
return object;
}
} }

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
package cn.hamster3.mc.plugin.ball.common.event.operate; package cn.hamster3.mc.plugin.ball.common.event.operate;
import cn.hamster3.mc.plugin.core.lib.net.kyori.adventure.text.Component;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,27 +0,0 @@
package cn.hamster3.mc.plugin.ball.common.event.player;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* 玩家进入子服
* <p>
* 仅在使用 velocity 代理端时才会触发这个事件
*
* @see BallPlayerPreConnectServerEvent 玩家准备进入子服
* @see BallPlayerConnectServerEvent 玩家进入子服
* @see BallPlayerPostConnectServerEvent 玩家已经进入子服
*/
@Data
@AllArgsConstructor
public class BallPlayerConnectServerEvent {
@NotNull
private final BallPlayerInfo playerInfo;
@Nullable
private final String from;
@NotNull
private final String to;
}

View File

@@ -9,7 +9,6 @@ import org.jetbrains.annotations.NotNull;
* 玩家已经进入子服 * 玩家已经进入子服
* *
* @see BallPlayerPreConnectServerEvent 玩家准备进入子服 * @see BallPlayerPreConnectServerEvent 玩家准备进入子服
* @see BallPlayerConnectServerEvent 玩家进入子服
* @see BallPlayerPostConnectServerEvent 玩家已经进入子服 * @see BallPlayerPostConnectServerEvent 玩家已经进入子服
*/ */
@Data @Data

View File

@@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
* 玩家准备进入子服 * 玩家准备进入子服
* *
* @see BallPlayerPreConnectServerEvent 玩家准备进入子服 * @see BallPlayerPreConnectServerEvent 玩家准备进入子服
* @see BallPlayerConnectServerEvent 玩家进入子服
* @see BallPlayerPostConnectServerEvent 玩家已经进入子服 * @see BallPlayerPostConnectServerEvent 玩家已经进入子服
*/ */
@Data @Data

View File

@@ -0,0 +1,18 @@
package cn.hamster3.mc.plugin.ball.common.event.server;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 服务器上线
*/
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class BallServerEvent extends BallServerInfo {
public BallServerEvent(BallServerInfo info) {
super(info.getId(), info.getName(), info.getType(), info.getHost(), info.getPort());
}
}

View File

@@ -3,19 +3,16 @@ package cn.hamster3.mc.plugin.ball.common.event.server;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo; import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.jetbrains.annotations.NotNull; import lombok.EqualsAndHashCode;
/** /**
* 服务器离线 * 服务器离线
*/ */
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public class ServerOfflineEvent { @EqualsAndHashCode(callSuper = true)
@NotNull public class ServerOfflineEvent extends BallServerEvent {
private final BallServerInfo serverInfo; public ServerOfflineEvent(BallServerInfo info) {
super(info);
@NotNull
public String getServerID() {
return serverInfo.getId();
} }
} }

View File

@@ -3,15 +3,16 @@ package cn.hamster3.mc.plugin.ball.common.event.server;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo; import cn.hamster3.mc.plugin.ball.common.entity.BallServerInfo;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.jetbrains.annotations.NotNull; import lombok.EqualsAndHashCode;
/** /**
* 服务器上线 * 服务器上线
*/ */
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public class ServerOnlineEvent { @EqualsAndHashCode(callSuper = true)
@NotNull public class ServerOnlineEvent extends BallServerEvent {
private final BallServerInfo serverInfo; public ServerOnlineEvent(BallServerInfo info) {
super(info);
}
} }

View File

@@ -83,11 +83,6 @@ public class BallCommonListener {
BallAPI.getInstance().getEventBus().post(event); BallAPI.getInstance().getEventBus().post(event);
break; break;
} }
case BallPlayerConnectServer: {
BallPlayerConnectServerEvent event = CoreAPI.getInstance().getGson().fromJson(message.getContent(), BallPlayerConnectServerEvent.class);
BallAPI.getInstance().getEventBus().post(event);
break;
}
case BallPlayerPostConnectServer: { case BallPlayerPostConnectServer: {
BallPlayerPostConnectServerEvent event = CoreAPI.getInstance().getGson().fromJson(message.getContent(), BallPlayerPostConnectServerEvent.class); BallPlayerPostConnectServerEvent event = CoreAPI.getInstance().getGson().fromJson(message.getContent(), BallPlayerPostConnectServerEvent.class);
BallAPI.getInstance().getEventBus().post(event); BallAPI.getInstance().getEventBus().post(event);
@@ -125,12 +120,6 @@ public class BallCommonListener {
BallAPI.getInstance().getEventBus().post(event); BallAPI.getInstance().getEventBus().post(event);
} }
@Subscribe
public void onBallPlayerConnectServer(BallPlayerConnectServerEvent event) {
BallPlayerInfo info = event.getPlayerInfo();
BallAPI.getInstance().getAllPlayerInfo().put(info.getUuid(), info);
}
@Subscribe @Subscribe
public void onBallPlayerInfoUpdate(BallPlayerInfoUpdateEvent event) { public void onBallPlayerInfoUpdate(BallPlayerInfoUpdateEvent event) {
BallPlayerInfo info = event.getPlayerInfo(); BallPlayerInfo info = event.getPlayerInfo();
@@ -139,12 +128,11 @@ public class BallCommonListener {
@Subscribe @Subscribe
public void onServerOnline(ServerOnlineEvent event) { public void onServerOnline(ServerOnlineEvent event) {
BallServerInfo info = event.getServerInfo(); BallAPI.getInstance().getAllServerInfo().put(event.getId(), event);
BallAPI.getInstance().getAllServerInfo().put(info.getId(), info); switch (event.getType()) {
switch (info.getType()) {
case GAME: { case GAME: {
BallAPI.getInstance().getAllPlayerInfo().forEach((uuid, playerInfo) -> { BallAPI.getInstance().getAllPlayerInfo().forEach((uuid, playerInfo) -> {
if (playerInfo.getGameServer().equals(info.getId())) { if (playerInfo.getGameServer().equals(event.getId())) {
playerInfo.setOnline(false); playerInfo.setOnline(false);
} }
}); });
@@ -152,7 +140,7 @@ public class BallCommonListener {
} }
case PROXY: { case PROXY: {
BallAPI.getInstance().getAllPlayerInfo().forEach((uuid, playerInfo) -> { BallAPI.getInstance().getAllPlayerInfo().forEach((uuid, playerInfo) -> {
if (playerInfo.getProxyServer().equals(info.getId())) { if (playerInfo.getProxyServer().equals(event.getId())) {
playerInfo.setOnline(false); playerInfo.setOnline(false);
} }
}); });
@@ -163,7 +151,7 @@ public class BallCommonListener {
@Subscribe @Subscribe
public void onServerOffline(ServerOfflineEvent event) { public void onServerOffline(ServerOfflineEvent event) {
String serverID = event.getServerID(); String serverID = event.getId();
BallServerInfo info = BallAPI.getInstance().getAllServerInfo().remove(serverID); BallServerInfo info = BallAPI.getInstance().getAllServerInfo().remove(serverID);
if (info == null) { if (info == null) {
return; return;

View File

@@ -3,20 +3,26 @@ package cn.hamster3.mc.plugin.ball.common.listener;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI; import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.data.BallMessage; import cn.hamster3.mc.plugin.ball.common.data.BallMessage;
import cn.hamster3.mc.plugin.ball.common.event.message.MessageReceivedEvent; import cn.hamster3.mc.plugin.ball.common.event.message.MessageReceivedEvent;
import cn.hamster3.mc.plugin.core.lib.io.lettuce.core.pubsub.RedisPubSubListener; import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import io.lettuce.core.pubsub.RedisPubSubListener;
public class BallRedisListener implements RedisPubSubListener<String, BallMessage> { import java.util.logging.Level;
public class BallRedisListener implements RedisPubSubListener<String, String> {
public static final BallRedisListener INSTANCE = new BallRedisListener(); public static final BallRedisListener INSTANCE = new BallRedisListener();
private BallRedisListener() { private BallRedisListener() {
} }
@Override public void handle(String channel, String message) {
public void message(String channel, BallMessage ballMessage) { CoreAPI.getInstance().getExecutorService().submit(() -> {
if (channel.startsWith(BallAPI.getInstance().getBallConfig().getChannelPrefix())) { try {
channel = channel.substring(BallAPI.getInstance().getBallConfig().getChannelPrefix().length()); String finalChannel = channel;
if (finalChannel.startsWith(BallAPI.getInstance().getBallConfig().getChannelPrefix())) {
finalChannel = finalChannel.substring(BallAPI.getInstance().getBallConfig().getChannelPrefix().length());
} }
BallMessage ballMessage = CoreAPI.getInstance().getGson().fromJson(message, BallMessage.class);
BallAPI ballAPI = BallAPI.getInstance(); BallAPI ballAPI = BallAPI.getInstance();
EventBus eventBus = ballAPI.getEventBus(); EventBus eventBus = ballAPI.getEventBus();
if (ballMessage.getReceiverType() != null && ballMessage.getReceiverType() != ballAPI.getLocalServerInfo().getType()) { if (ballMessage.getReceiverType() != null && ballMessage.getReceiverType() != ballAPI.getLocalServerInfo().getType()) {
@@ -25,16 +31,21 @@ public class BallRedisListener implements RedisPubSubListener<String, BallMessag
if (ballMessage.getReceiverID() != null && !ballAPI.isLocalServer(ballMessage.getReceiverID())) { if (ballMessage.getReceiverID() != null && !ballAPI.isLocalServer(ballMessage.getReceiverID())) {
return; return;
} }
try { eventBus.post(new MessageReceivedEvent(finalChannel, ballMessage));
eventBus.post(new MessageReceivedEvent(channel, ballMessage)); } catch (Exception | Error e) {
} catch (Exception e) { BallAPI.getInstance().getLogger().log(Level.SEVERE, "解析来自频道 " + channel + " 的数据出错: " + message, e);
e.printStackTrace();
} }
});
} }
@Override @Override
public void message(String pattern, String channel, BallMessage info) { public void message(String channel, String message) {
message(channel, info); handle(channel, message);
}
@Override
public void message(String pattern, String channel, String message) {
handle(channel, message);
} }
@Override @Override
@@ -44,7 +55,7 @@ public class BallRedisListener implements RedisPubSubListener<String, BallMessag
@Override @Override
public void psubscribed(String pattern, long count) { public void psubscribed(String pattern, long count) {
BallAPI.getInstance().getLogger().info("取消订阅 redis 频道(正则) " + pattern); BallAPI.getInstance().getLogger().info("已订阅 redis 频道(正则) " + pattern);
} }
@Override @Override

View File

@@ -0,0 +1,21 @@
package cn.hamster3.mc.plugin.ball.common.thread;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
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();
private LockUpdateThread() {
}
@Override
public void run() {
String key = "HamsterBall:ServerInfo:" + BallAPI.getInstance().getLocalServerInfo().getId();
try (StatefulRedisConnection<String, String> connect = BallAPI.getInstance().getRedisClient().connect()) {
RedisCommands<String, String> redis = connect.sync();
redis.expire(key, 180);
}
}
}

View File

@@ -0,0 +1,41 @@
@file:Suppress("VulnerableLibrariesLocal")
evaluationDependsOn(":ball-common")
dependencies {
api(project(":ball-common")) { isTransitive = false }
compileOnly("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
compileOnly("cn.hamster3.mc.plugin:core-common:+")
}
sourceSets.create("templates") {
java {
srcDir("src/main/templates")
}
}
val templateSource = file("src/main/templates")
val templateDest = layout.buildDirectory.dir("generated/sources/templates")
val generateTemplates = tasks.register<Copy>("generateTemplates") {
from(templateSource)
into(templateDest)
expand(project.properties)
}
sourceSets.main.get().java.srcDir(generateTemplates.map { it.outputs })
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
}
tasks {
withType<Jar> {
archiveBaseName = "HamsterBall-Velocity"
}
shadowJar {
destinationDirectory = rootProject.layout.buildDirectory
}
}

View File

@@ -0,0 +1,154 @@
package cn.hamster3.mc.plugin.ball.velocity;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent;
import cn.hamster3.mc.plugin.ball.velocity.api.CoreVelocityAPI;
import cn.hamster3.mc.plugin.ball.velocity.command.VelocityBallCommand;
import cn.hamster3.mc.plugin.ball.velocity.listener.BallVelocityListener;
import cn.hamster3.mc.plugin.ball.velocity.listener.BallVelocityMainListener;
import cn.hamster3.mc.plugin.ball.velocity.listener.UpdatePlayerInfoListener;
import cn.hamster3.mc.plugin.ball.velocity.listener.VelocityServerListener;
import cn.hamster3.mc.plugin.ball.velocity.util.BallVelocityUtils;
import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Dependency;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.slf4j.Logger;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
@Plugin(
id = "hamster-ball",
name = "HamsterBall",
version = BuildConstants.VERSION,
description = BuildConstants.DESCRIPTION,
authors = {"MiniDay"},
dependencies = @Dependency(id = "hamster-core")
)
public class HamsterBallPlugin {
@Getter
private static HamsterBallPlugin instance;
@Getter
private final java.util.logging.Logger logger;
@Getter
private final Logger slf4jLogger;
@Getter
private final ProxyServer proxyServer;
@Getter
private final File dataFolder;
@Getter
private YamlConfig config;
@Inject
public HamsterBallPlugin(Logger slf4jLogger, ProxyServer proxyServer, @DataDirectory Path dataPath) {
long start = System.currentTimeMillis();
logger = java.util.logging.Logger.getLogger("hamster-ball");
this.slf4jLogger = slf4jLogger;
this.proxyServer = proxyServer;
dataFolder = dataPath.toFile();
slf4jLogger.info("仓鼠球正在初始化");
instance = this;
try {
File dataFolder = getDataFolder();
if (dataFolder.mkdir()) {
slf4jLogger.info("已生成插件存档文件夹");
}
File configFile = new File(dataFolder, "config.yml");
if (!configFile.exists()) {
Files.copy(
Objects.requireNonNull(getClass().getResourceAsStream("/config.yml")),
configFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
}
config = YamlConfig.load(configFile);
CoreVelocityAPI.init(config);
slf4jLogger.info("已初始化 BallAPI");
} catch (Exception e) {
slf4jLogger.error("BallAPI 初始化失败", e);
proxyServer.shutdown(Component.text("由于 HamsterBall 初始化失败, 服务器将立即关闭"));
}
long time = System.currentTimeMillis() - start;
slf4jLogger.info("仓鼠球初始化完成,总计耗时 " + time + " ms");
}
@Subscribe(order = PostOrder.EARLY)
public void onProxyInitialization(ProxyInitializeEvent event) {
long start = System.currentTimeMillis();
slf4jLogger.info("仓鼠球正在启动");
try {
CoreVelocityAPI.getInstance().enable();
} catch (Exception e) {
slf4jLogger.error("仓鼠球启动失败", e);
slf4jLogger.info("由于仓鼠球启动失败,服务器将立即关闭");
proxyServer.shutdown(Component.text("仓鼠球启动失败"));
return;
}
CommandMeta commandMeta = proxyServer.getCommandManager()
.metaBuilder("hamster-velocity-ball")
.aliases("velocity-ball", "vball")
.plugin(this)
.build();
proxyServer.getCommandManager().register(commandMeta, VelocityBallCommand.INSTANCE);
BallAPI.getInstance().getEventBus().register(BallVelocityListener.INSTANCE);
slf4jLogger.info("已注册监听器 BallVelocityListener");
proxyServer.getEventManager().register(this, BallVelocityMainListener.INSTANCE);
slf4jLogger.info("已注册监听器 BallVelocityMainListener");
proxyServer.getEventManager().register(this, UpdatePlayerInfoListener.INSTANCE);
slf4jLogger.info("已注册监听器 UpdatePlayerInfoListener");
if (config.getBoolean("auto-register-game-server", false)) {
BallAPI.getInstance().getEventBus().register(VelocityServerListener.INSTANCE);
slf4jLogger.info("已注册监听器 VelocityServerListener");
VelocityServerListener.INSTANCE.onEnable();
}
if (BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) {
BallAPI.getInstance().subscribePatterns("*" + BallAPI.PLAYER_INFO_CHANNEL);
} else {
BallAPI.getInstance().subscribeRaw(BallAPI.PLAYER_INFO_CHANNEL);
}
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL, BallActions.ServerOnline.name(),
new ServerOnlineEvent(BallAPI.getInstance().getLocalServerInfo())
);
// 移除失效的在线玩家
BallAPI.getInstance().getAllPlayerInfo().values()
.stream()
.filter(BallPlayerInfo::isOnline)
.filter(o -> BallAPI.getInstance().isLocalServer(o.getProxyServer()))
.forEach(playerInfo -> {
playerInfo.setOnline(false);
BallVelocityUtils.uploadPlayerInfo(playerInfo);
});
long time = System.currentTimeMillis() - start;
slf4jLogger.info("仓鼠球启动完成,总计耗时 {} ms", time);
}
@Subscribe(order = PostOrder.LATE)
public void onProxyShutdown(ProxyShutdownEvent event) {
long start = System.currentTimeMillis();
slf4jLogger.info("仓鼠球正在关闭");
try {
CoreVelocityAPI.getInstance().disable();
} catch (Exception e) {
slf4jLogger.error("关闭仓鼠球时遇到了一个异常", e);
}
long time = System.currentTimeMillis() - start;
slf4jLogger.info("仓鼠球已关闭,总计耗时 {} ms", time);
}
}

View File

@@ -0,0 +1,58 @@
package cn.hamster3.mc.plugin.ball.velocity.api;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.velocity.HamsterBallPlugin;
import cn.hamster3.mc.plugin.core.common.config.ConfigSection;
import cn.hamster3.mc.plugin.core.common.config.YamlConfig;
import com.velocitypowered.api.proxy.config.ProxyConfig;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.logging.Logger;
@SuppressWarnings("unused")
public final class CoreVelocityAPI extends BallAPI {
public CoreVelocityAPI(@NotNull ConfigSection config) {
super(config, BallServerType.PROXY);
}
public static CoreVelocityAPI getInstance() {
return (CoreVelocityAPI) instance;
}
public static void init(@NotNull YamlConfig config) {
if (instance != null) {
return;
}
instance = new CoreVelocityAPI(config);
}
@Override
public void enable() throws SQLException, InterruptedException {
ProxyConfig config = HamsterBallPlugin.getInstance().getProxyServer().getConfiguration();
try {
Field field = config.getClass().getDeclaredField("bind");
field.setAccessible(true);
String bind = (String) field.get(config);
int i = bind.lastIndexOf(":");
String substring = bind.substring(i + 1);
instance.getLocalServerInfo().setHost(bind.substring(0, i));
instance.getLocalServerInfo().setPort(Integer.parseInt(substring));
} catch (Exception e) {
HamsterBallPlugin.getInstance().getSlf4jLogger().error("获取 Velocity 监听端口时遇到了一个异常", e);
}
super.enable();
}
@Override
public void disable() throws SQLException, InterruptedException {
super.disable();
}
@Override
public @NotNull Logger getLogger() {
return HamsterBallPlugin.getInstance().getLogger();
}
}

View File

@@ -0,0 +1,46 @@
package cn.hamster3.mc.plugin.ball.velocity.command;
import cn.hamster3.mc.plugin.ball.common.command.BallCommand;
import cn.hamster3.mc.plugin.ball.common.command.adapt.AdaptCommandSender;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.SimpleCommand;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class VelocityBallCommand implements SimpleCommand {
public static final VelocityBallCommand INSTANCE = new VelocityBallCommand();
private VelocityBallCommand() {
}
@Override
public void execute(Invocation invocation) {
BallCommand.INSTANCE.onCommand(adaptCommandSender(invocation.source()), invocation.arguments());
}
@Override
public boolean hasPermission(Invocation invocation) {
return invocation.source().hasPermission("hamster.ball.admin");
}
@Override
public List<String> suggest(Invocation invocation) {
return BallCommand.INSTANCE.onTabComplete(adaptCommandSender(invocation.source()), invocation.arguments());
}
private AdaptCommandSender adaptCommandSender(@NotNull CommandSource source) {
return new AdaptCommandSender() {
@Override
public boolean hasPermission(@NotNull String permission) {
return source.hasPermission(permission);
}
@Override
public void sendMessage(@NotNull Component message) {
source.sendMessage(message);
}
};
}
}

View File

@@ -0,0 +1,114 @@
package cn.hamster3.mc.plugin.ball.velocity.listener;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
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;
import com.google.common.eventbus.Subscribe;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.adventure.audience.Audience;
import java.util.UUID;
public class BallVelocityListener {
public static final BallVelocityListener INSTANCE = new BallVelocityListener();
private BallVelocityListener() {
}
@Subscribe
public void onDispatchConsoleCommand(DispatchConsoleCommandEvent event) {
if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) {
return;
}
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
server.getCommandManager().executeAsync(server.getConsoleCommandSource(), event.getCommand());
}
@Subscribe
public void onDispatchPlayerCommand(DispatchPlayerCommandEvent event) {
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
if (event.getUuid() != null) {
Player player = server.getPlayer(event.getUuid()).orElse(null);
if (player == null) {
return;
}
server.getCommandManager().executeAsync(player, event.getCommand());
return;
}
for (Player player : server.getAllPlayers()) {
server.getCommandManager().executeAsync(player, event.getCommand());
}
}
@Subscribe
public void onKickPlayer(KickPlayerEvent event) {
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
Player player = server.getPlayer(event.getUuid()).orElse(null);
if (player == null) {
return;
}
player.disconnect(event.getReason());
}
@Subscribe
public void onSendMessageToPlayer(SendMessageToPlayerEvent event) {
for (UUID receiver : event.getReceivers()) {
Audience audience = CoreAPI.getInstance().getAudienceProvider().player(receiver);
event.getMessage().show(audience);
}
}
@Subscribe
public void onSendPlayerToLocation(SendPlayerToLocationEvent event) {
ProxyServer proxyServer = HamsterBallPlugin.getInstance().getProxyServer();
String serverID = event.getLocation().getServerID();
RegisteredServer toServer = proxyServer.getServer(serverID).orElse(null);
if (toServer == null) {
HamsterBallPlugin.getInstance().getLogger().warning("试图传送玩家时失败: 服务器 " + serverID + " 不在线");
return;
}
for (UUID uuid : event.getSendPlayerUUID()) {
Player player = proxyServer.getPlayer(uuid).orElse(null);
if (player == null) {
continue;
}
RegisteredServer currentServer = player.getCurrentServer().map(ServerConnection::getServer).orElse(null);
if (currentServer != null && currentServer.getServerInfo().getName().equals(serverID)) {
continue;
}
player.createConnectionRequest(toServer).fireAndForget();
}
}
@Subscribe
public void onSendPlayerToPlayer(SendPlayerToPlayerEvent event) {
ProxyServer proxyServer = HamsterBallPlugin.getInstance().getProxyServer();
UUID toPlayerUUID = event.getToPlayerUUID();
Player toPlayer = proxyServer.getPlayer(toPlayerUUID).orElse(null);
if (toPlayer == null) {
HamsterBallPlugin.getInstance().getLogger().warning("试图传送玩家时失败: 目标玩家 " + toPlayerUUID + " 不在线");
return;
}
RegisteredServer toServer = toPlayer.getCurrentServer().map(ServerConnection::getServer).orElse(null);
if (toServer == null) {
HamsterBallPlugin.getInstance().getLogger().warning("试图传送玩家时失败: 目标玩家 " + toPlayerUUID + " 不在任何服务器中");
return;
}
for (UUID uuid : event.getSendPlayerUUID()) {
Player sendPlayer = proxyServer.getPlayer(uuid).orElse(null);
if (sendPlayer == null) {
continue;
}
ServerInfo currentServer = sendPlayer.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null);
if (currentServer != null && currentServer.getName().equals(toServer.getServerInfo().getName())) {
continue;
}
sendPlayer.createConnectionRequest(toServer).fireAndForget();
}
}
}

View File

@@ -0,0 +1,58 @@
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.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerLoginEvent;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerPostLoginEvent;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerPreLoginEvent;
import cn.hamster3.mc.plugin.ball.velocity.util.BallVelocityUtils;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.proxy.Player;
public final class BallVelocityMainListener {
public static final BallVelocityMainListener INSTANCE = new BallVelocityMainListener();
private BallVelocityMainListener() {
}
@Subscribe(order = PostOrder.LATE)
public void onPreLogin(PreLoginEvent event) {
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerPreLogin.name(),
new BallPlayerPreLoginEvent(event.getUsername())
);
}
@Subscribe(order = PostOrder.LATE)
public void onLogin(LoginEvent event) {
if (!event.getResult().isAllowed()) {
return;
}
Player player = event.getPlayer();
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerLogin.name(),
new BallPlayerLoginEvent(new BallPlayerInfo(
player.getUniqueId(), player.getUsername(), "connecting",
BallAPI.getInstance().getLocalServerId(), true
))
);
}
@Subscribe(order = PostOrder.LATE)
public void onPostLogin(PostLoginEvent event) {
Player player = event.getPlayer();
BallPlayerInfo playerInfo = BallVelocityUtils.getPlayerInfo(player, true);
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerPostLogin.name(),
new BallPlayerPostLoginEvent(playerInfo)
);
}
}

View File

@@ -0,0 +1,67 @@
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.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerLogoutEvent;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerPostConnectServerEvent;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerPreConnectServerEvent;
import cn.hamster3.mc.plugin.ball.velocity.util.BallVelocityUtils;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.Player;
public class UpdatePlayerInfoListener {
public static final UpdatePlayerInfoListener INSTANCE = new UpdatePlayerInfoListener();
private UpdatePlayerInfoListener() {
}
@Subscribe(order = PostOrder.LATE)
public void onServerConnect(ServerPreConnectEvent event) {
if (event.getResult().isAllowed()) {
return;
}
String name = event.getResult().getServer()
.map(o -> o.getServerInfo().getName())
.orElse(event.getOriginalServer().getServerInfo().getName());
Player player = event.getPlayer();
BallPlayerInfo playerInfo = BallVelocityUtils.getPlayerInfo(player, true);
playerInfo.setGameServer(name);
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerPreConnectServer.name(),
new BallPlayerPreConnectServerEvent(playerInfo, playerInfo.getGameServer(), name)
);
BallVelocityUtils.uploadPlayerInfo(playerInfo);
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe(order = PostOrder.LATE)
public void onServerPostConnect(ServerPostConnectEvent event) {
Player player = event.getPlayer();
BallPlayerInfo playerInfo = BallVelocityUtils.getPlayerInfo(player, true);
playerInfo.setGameServer(player.getCurrentServer().map(o -> o.getServerInfo().getName()).orElse(""));
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerPostConnectServer.name(),
new BallPlayerPostConnectServerEvent(playerInfo)
);
BallVelocityUtils.uploadPlayerInfo(playerInfo);
}
@Subscribe(order = PostOrder.LATE)
public void onDisconnect(DisconnectEvent event) {
Player player = event.getPlayer();
BallPlayerInfo playerInfo = BallVelocityUtils.getPlayerInfo(player, false);
BallAPI.getInstance().sendRawBallMessage(
BallAPI.BALL_CHANNEL,
BallActions.BallPlayerLogout.name(),
new BallPlayerLogoutEvent(playerInfo)
);
BallVelocityUtils.uploadPlayerInfo(playerInfo);
}
}

View File

@@ -0,0 +1,65 @@
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.BallServerInfo;
import cn.hamster3.mc.plugin.ball.common.entity.BallServerType;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent;
import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent;
import cn.hamster3.mc.plugin.ball.velocity.HamsterBallPlugin;
import com.google.common.eventbus.Subscribe;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.net.InetSocketAddress;
public class VelocityServerListener {
public static final VelocityServerListener INSTANCE = new VelocityServerListener();
private VelocityServerListener() {
}
public void onEnable() {
for (BallServerInfo info : BallAPI.getInstance().getAllServerInfo().values()) {
if (info.getType() != BallServerType.GAME) {
continue;
}
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
server.getServer(info.getId())
.map(RegisteredServer::getServerInfo)
.ifPresent(server::unregisterServer);
ServerInfo serverInfo = new ServerInfo(info.getId(), new InetSocketAddress(info.getHost(), info.getPort()));
server.registerServer(serverInfo);
BallAPI.getInstance().getLogger().info("已添加服务器入口: " + info.getId());
}
}
@Subscribe
public void onServerOnline(ServerOnlineEvent event) {
if (event.getType() != BallServerType.GAME) {
return;
}
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
server.getServer(event.getId())
.map(RegisteredServer::getServerInfo)
.ifPresent(server::unregisterServer);
ServerInfo serverInfo = new ServerInfo(event.getId(), new InetSocketAddress(event.getHost(), event.getPort()));
server.registerServer(serverInfo);
BallAPI.getInstance().getLogger().info("已添加服务器入口: " + event.getId());
}
@Subscribe
public void onServerOffline(ServerOfflineEvent event) {
if (event.getType() != BallServerType.GAME) {
return;
}
ProxyServer server = HamsterBallPlugin.getInstance().getProxyServer();
ServerInfo serverInfo = server.getServer(event.getId())
.map(RegisteredServer::getServerInfo)
.orElse(null);
if (serverInfo != null) {
server.unregisterServer(serverInfo);
BallAPI.getInstance().getLogger().info("已移除服务器入口: " + event.getId());
}
}
}

View File

@@ -0,0 +1,58 @@
package cn.hamster3.mc.plugin.ball.velocity.util;
import cn.hamster3.mc.plugin.ball.common.api.BallAPI;
import cn.hamster3.mc.plugin.ball.common.entity.BallPlayerInfo;
import cn.hamster3.mc.plugin.ball.common.event.BallActions;
import cn.hamster3.mc.plugin.ball.common.event.player.BallPlayerInfoUpdateEvent;
import cn.hamster3.mc.plugin.core.common.api.CoreAPI;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import org.jetbrains.annotations.NotNull;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Level;
public final class BallVelocityUtils {
private BallVelocityUtils() {
}
@NotNull
public static BallPlayerInfo getPlayerInfo(@NotNull Player player, boolean online) {
ServerConnection server = player.getCurrentServer().orElse(null);
return new BallPlayerInfo(
player.getUniqueId(),
player.getUsername(),
server == null ? "connecting" : server.getServerInfo().getName(),
BallAPI.getInstance().getLocalServerId(),
online
);
}
public static void uploadPlayerInfo(@NotNull BallPlayerInfo playerInfo) {
CoreAPI.getInstance().getExecutorService().execute(() -> {
try (Connection connection = BallAPI.getInstance().getDatasource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"REPLACE INTO `hamster_ball_player_info` VALUES(?, ?, ?, ?, ?);"
)) {
statement.setString(1, playerInfo.getUuid().toString());
statement.setString(2, playerInfo.getName());
statement.setString(3, playerInfo.getGameServer());
statement.setString(4, playerInfo.getProxyServer());
statement.setBoolean(5, playerInfo.isOnline());
statement.executeUpdate();
}
} catch (SQLException e) {
BallAPI.getInstance().getLogger().log(Level.SEVERE, "更新玩家数据时遇到了一个异常", e);
}
if (!BallAPI.getInstance().getBallConfig().isGameServerUpdatePlayerInfo()) {
BallAPI.getInstance().sendRawBallMessage(
BallAPI.PLAYER_INFO_CHANNEL,
BallActions.BallPlayerInfoUpdate.name(),
new BallPlayerInfoUpdateEvent(playerInfo)
);
}
});
}
}

View File

@@ -0,0 +1,76 @@
# 是否允许在控制台输出调试信息
debug: false
# 是否启用服务器 ID 单例模式
# 启用后,服务器唯一识别码 相同的服务器将无法启动
singleton-server-id: false
# 频道名前缀
# 使用这个配置选项可以划分子服消息通信分组
# 只有在同一个频道名的子服才能互相通信
channel-prefix: ""
# 是否在子服端更新玩家信息
# 默认情况下BC 统一管理玩家信息,包括记录 UUID 和玩家名称
# 如果一个群组服同时拥有多个 BC 入口,且每个 BC 入口为不同的玩家名称分配不同的 UUID
# (例如正版、盗版双入口,或网易多入口接同一个子服)
# 则可以启用该功能以防止同一个名称占用多个 UUID 的问题
game-server-update-player-info: false
# 启用后,子服启动时会自动注册该子服的入口配置,关闭时也会自动移除该子服的入口配置
auto-register-game-server: false
# 本服务器信息
server-info:
# 服务器唯一识别码,最长 32 字符
id: "Velocity"
# 服务端名称,常用于展示给玩家看
name: "代理端"
# 当前子服的地址
# 不填则自动设置为 0.0.0.0
host: 0.0.0.0
# 当前子服端口
# 不填则自动设置为 25577
port: 25577
# Redis 配置
# 如果注释该选项则默认使用 HamsterCore 中的连接配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的 Redis 链接
# redis-url: redis://localhost:6379/0?clientName=HamsterBall&timeout=5s
# 数据库连接池配置
# 如果注释该选项则默认使用 HamsterCore 中的连接池配置
# 否则 HamsterBall 将会使用与 HamsterCore 不同的数据库链接
# 如果你需要让每个服务器单独存储仓鼠球信息
# 这个选项就会很有用
#datasource:
# # 数据库链接驱动地址
# driver: "com.mysql.cj.jdbc.Driver"
# # MySQL数据库链接填写格式:
# # jdbc:mysql://{数据库地址}:{数据库端口}/{使用的库名}?参数
# url: "jdbc:mysql://localhost:3306/Test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
# # 用户名
# username: "root"
# # 密码
# password: "Root123.."
# # 最小闲置链接数
# # 推荐值1~3
# minimum-idle: 0
# # 最大链接数
# # 推荐值不低于5
# maximum-pool-size: 5
# # 保持连接池可用的间隔
# # 除非你的服务器数据库连接经常断开,否则不建议启用该选项
# # 单位:毫秒
# # 默认值为0禁用
# keep-alive-time: 0
# # 连接闲置回收时间
# # 单位:毫秒
# # 推荐值60000010分钟
# idle-timeout: 600000
# # 链接最长存活时间
# # 单位:毫秒
# max-lifetime: 1800000
# # 验证连接存活的超时时间
# # 单位:毫秒
# validation-timeout: 5000

View File

@@ -0,0 +1,4 @@
VERSION: ${version}
CHECK_TYPE: GITEA_RELEASES
GIT_BASE_URL: https://git.airgame.net
GIT_REPO: MiniDay/hamster-ball

View File

@@ -0,0 +1,8 @@
package cn.hamster3.mc.plugin.ball.velocity;
// The constants are replaced before compilation
@SuppressWarnings("unused")
public class BuildConstants {
public static final String VERSION = "${version}";
public static final String DESCRIPTION = "${description}";
}

View File

@@ -1,21 +1,29 @@
buildscript {
repositories {
maven("https://maven.airgame.net/maven-public/")
}
}
plugins { plugins {
id("java") id("java-library")
id("maven-publish") id("maven-publish")
id("com.github.johnrengelman.shadow") version "8+" id("com.gradleup.shadow") version "8.3.6"
} }
group = "cn.hamster3.mc.plugin" group = "cn.hamster3.mc.plugin"
version = "1.5.7" version = "1.8.1"
description = "基于 Redis 的 Minecraft 服务端通用消息中间件"
subprojects { subprojects {
apply { apply {
plugin("java-library") plugin("java-library")
plugin("maven-publish") plugin("maven-publish")
plugin("com.github.johnrengelman.shadow") plugin("com.gradleup.shadow")
} }
group = rootProject.group group = rootProject.group
version = rootProject.version version = rootProject.version
description = rootProject.description
repositories { repositories {
maven("https://maven.airgame.net/maven-public/") maven("https://maven.airgame.net/maven-public/")
@@ -43,12 +51,6 @@ subprojects {
from(rootProject.file("LICENSE")) from(rootProject.file("LICENSE"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
jar {
archiveClassifier = "dev"
}
shadowJar {
archiveClassifier = "FIX"
}
build { build {
dependsOn(shadowJar) dependsOn(shadowJar)
} }
@@ -62,11 +64,10 @@ subprojects {
} }
repositories { repositories {
maven { maven {
url = uri("https://maven.airgame.net/maven-airgame") url = uri("https://maven.airgame.net/public")
credentials { credentials {
username = rootProject.properties.getOrDefault("maven_username", "").toString() username = findProperty("MAVEN_AIRGAME_USERNAME")?.toString() ?: ""
password = rootProject.properties.getOrDefault("maven_password", "").toString() password = findProperty("MAVEN_AIRGAME_PASSWORD")?.toString() ?: ""
} }
} }
} }

View File

@@ -1,6 +1,6 @@
#Sun Aug 20 16:53:32 CST 2023 #Sun Aug 20 16:53:32 CST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -9,3 +9,4 @@ rootProject.name = "hamster-ball"
include("ball-common") include("ball-common")
include("ball-bukkit") include("ball-bukkit")
include("ball-bungee") include("ball-bungee")
include("ball-velocity")