From 55ab55597c45f32e207010302eb40570604accfe Mon Sep 17 00:00:00 2001 From: ZhangYongxu <1553450629@qq.com> Date: Mon, 18 Aug 2025 15:42:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=A7=B0=E5=8F=B7=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 517 ++++++++++++++++-- build.gradle | 119 ++-- core/build.gradle | 1 + .../core/database/DatabaseConfig.java | 34 ++ .../core/database/DatabaseManager.java | 405 ++++++++++++++ .../flashytitles/core/message/Message.java | 119 ++++ .../core/message/MessageType.java | 42 ++ .../core/model/AnimationUtil.java | 202 +++++++ .../flashytitles/core/model/Title.java | 113 ++++ fabric-standalone/build.gradle | 62 +++ fabric-standalone/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 6 + fabric-standalone/gradlew.bat | 89 +++ fabric-standalone/settings.gradle | 10 + .../core/database/DatabaseConfig.java | 34 ++ .../core/database/DatabaseManager.java | 405 ++++++++++++++ .../flashytitles/core/message/Message.java | 119 ++++ .../core/message/MessageType.java | 42 ++ .../core/model/AnimationUtil.java | 202 +++++++ .../flashytitles/core/model/Title.java | 113 ++++ .../fabric/FlashyTitlesFabric.java | 79 +++ .../fabric/manager/DisplayManager.java | 220 ++++++++ .../flashytitles/fabric/sync/SyncHandler.java | 257 +++++++++ .../src/main/resources/fabric.mod.json | 29 + fabric/build.gradle | 66 +++ .../fabric/FlashyTitlesFabric.java | 92 ++++ .../fabric/manager/DisplayManager.java | 180 ++++++ .../fabric/model/PlayerTitleData.java | 37 ++ .../fabric/network/TitleSyncPayload.java | 42 ++ .../placeholder/FlashyTitlesPlaceholders.java | 99 ++++ .../flashytitles/fabric/sync/SyncHandler.java | 147 +++++ fabric/src/main/resources/fabric.mod.json | 30 + gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew.bat | 15 +- neoforge/build.gradle | 43 ++ .../neoforge/FlashyTitlesNeoForge.java | 85 +++ .../neoforge/manager/DisplayManager.java | 225 ++++++++ .../placeholder/PlaceholderService.java | 87 +++ .../neoforge/sync/SyncHandler.java | 227 ++++++++ .../src/main/resources/META-INF/mods.toml | 29 + settings.gradle | 15 +- spigot/build.gradle | 17 +- .../spigot/FlashyTitlesSpigot.java | 85 +++ .../spigot/listener/PlayerListener.java | 49 ++ .../spigot/manager/DisplayManager.java | 177 ++++++ .../spigot/model/PlayerTitleData.java | 37 ++ .../spigot/papi/FlashyTitlesExpansion.java | 106 ++++ .../flashytitles/spigot/sync/SyncHandler.java | 235 ++++++++ spigot/src/main/resources/plugin.yml | 32 +- velocity/build.gradle | 16 +- .../velocity/FlashyTitlesVelocity.java | 105 ++++ .../velocity/command/TitleCommand.java | 448 +++++++++++++++ .../velocity/config/ConfigManager.java | 221 ++++++++ .../velocity/listener/PlayerListener.java | 98 ++++ .../velocity/manager/TitleManager.java | 327 +++++++++++ .../velocity/sync/SyncManager.java | 384 +++++++++++++ 57 files changed, 6863 insertions(+), 126 deletions(-) create mode 100644 core/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java create mode 100644 core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java create mode 100644 core/src/main/java/org/example/flashytitles/core/message/Message.java create mode 100644 core/src/main/java/org/example/flashytitles/core/message/MessageType.java create mode 100644 core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java create mode 100644 core/src/main/java/org/example/flashytitles/core/model/Title.java create mode 100644 fabric-standalone/build.gradle create mode 100644 fabric-standalone/gradle.properties create mode 100644 fabric-standalone/gradle/wrapper/gradle-wrapper.properties create mode 100644 fabric-standalone/gradlew.bat create mode 100644 fabric-standalone/settings.gradle create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java create mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java create mode 100644 fabric-standalone/src/main/resources/fabric.mod.json create mode 100644 fabric/build.gradle create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/model/PlayerTitleData.java create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/network/TitleSyncPayload.java create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/placeholder/FlashyTitlesPlaceholders.java create mode 100644 fabric/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java create mode 100644 fabric/src/main/resources/fabric.mod.json create mode 100644 neoforge/build.gradle create mode 100644 neoforge/src/main/java/org/example/flashytitles/neoforge/FlashyTitlesNeoForge.java create mode 100644 neoforge/src/main/java/org/example/flashytitles/neoforge/manager/DisplayManager.java create mode 100644 neoforge/src/main/java/org/example/flashytitles/neoforge/placeholder/PlaceholderService.java create mode 100644 neoforge/src/main/java/org/example/flashytitles/neoforge/sync/SyncHandler.java create mode 100644 neoforge/src/main/resources/META-INF/mods.toml create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/FlashyTitlesSpigot.java create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/listener/PlayerListener.java create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/manager/DisplayManager.java create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/model/PlayerTitleData.java create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/papi/FlashyTitlesExpansion.java create mode 100644 spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java diff --git a/README.md b/README.md index 1382328..3378649 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,488 @@ # VelocityTitle -服务器使用的 Velocity 称号插件。 - -在 Velocity 服务端创建数据库存储玩家称号(分为前缀和后缀),并通过 Velocity 和子服的消息通道来发送数据。最终通过 PAPI 和 GUI 进行显示和操作。 - -计划: -* [ ] Velocity 部分 - - [ ] 命令模块 - * [ ] 根命令 - * [ ] 命令帮助 - * [ ] 创建,修改,删除称号 - * [ ] 分配,取消分配称号 - * [ ] 更换称号 - * [ ] 停止使用称号 - * [ ] 重载配置 - - [ ] 配置模块 - * [x] 插件配置 - * [ ] 称号配置 - * [x] 语言配置 - * [x] 配置读取和保存器 - - [ ] 数据库模块 - * [x] EasySQL - * [x] H2 数据库 - * [ ] SQLite 数据库 - - [ ] 和 Bukkit 部分通信 - - [ ] 其他 - * [x] 日志输出 -* [ ] Bukkit 部分 - - [ ] 和 Velocity 部分通信 - - [ ] 命令模块 - * [ ] 重载配置 - * [ ] 命令帮助 - * [ ] 根命令 - - [ ] GUI (可选) 模块 - - [ ] PAPI部分 +一个现代化的 Minecraft 跨平台称号系统,支持 Velocity、Spigot、Fabric 和 NeoForge,具有完整的占位符API支持和H2嵌入式数据库。 +## 技术特性 + +### 核心技术栈 +- **Java 21** - 现代Java特性支持 +- **Gradle 8.6** - 构建系统 +- **HikariCP** - 高性能数据库连接池 +- **H2 Database** - 嵌入式数据库(默认) +- **MySQL/SQLite** - 可选数据库支持 +- **Jedis** - Redis缓存支持 + +### 平台支持 +- **Velocity** - 代理服务器支持,H2数据库 +- **Spigot/Paper** - PlaceholderAPI集成 +- **Fabric** - Text Placeholder API集成 +- **NeoForge** - 内置占位符系统 + +### 占位符系统 +- **Spigot**: 使用 PlaceholderAPI 标准扩展 +- **Fabric**: 使用 Text Placeholder API 2.4.1+1.21 +- **NeoForge**: 内置占位符服务,无需外部依赖 + +## 📦 安装与配置 + +### 快速安装 + +#### Velocity 服务器 +```bash +# 1. 下载插件 +wget velocity-1.0.0.jar + +# 2. 放入plugins目录 +cp velocity-1.0.0.jar velocity/plugins/ + +# 3. 重启服务器 +# H2数据库会自动创建,无需额外配置 +``` + +#### Spigot/Paper 服务器 +```bash +# 1. 安装PlaceholderAPI (必需) +wget https://github.com/PlaceholderAPI/PlaceholderAPI/releases/latest/download/PlaceholderAPI.jar + +# 2. 安装FlashyTitles +cp spigot-1.0.0.jar spigot/plugins/ +cp PlaceholderAPI.jar spigot/plugins/ + +# 3. 重启服务器 +``` + +#### Fabric 服务器 +```bash +# 1. 确保安装Fabric API 0.105.0+1.21.1 +# 2. 安装Text Placeholder API 2.4.1+1.21 +# 3. 安装FlashyTitles +cp fabric-1.0.0.jar fabric/mods/ +``` + +#### NeoForge 服务器 +```bash +# 无需额外依赖,内置占位符系统 +cp neoforge-1.0.0.jar neoforge/mods/ +``` + + +## 🎮 使用说明 + +### 玩家命令 +| 命令 | 描述 | 权限 | +|------|------|------| +| `/titles` | 打开称号GUI菜单 | `flashytitles.use` | +| `/titles list` | 列出所有可用称号 | `flashytitles.use` | +| `/titles equip <称号ID>` | 装备指定称号 | `flashytitles.use` | +| `/titles unequip` | 取消装备当前称号 | `flashytitles.use` | +| `/titles preview <称号ID>` | 预览称号效果 | `flashytitles.use` | +| `/titles shop` | 打开称号商店 | `flashytitles.use` | +| `/titles buy <称号ID>` | 购买称号 | `flashytitles.use` | +| `/titles coins` | 查看金币余额 | `flashytitles.use` | + +### 管理员命令 +| 命令 | 描述 | 权限 | +|------|------|------| +| `/titleadmin reload` | 重载插件配置 | `flashytitles.admin` | +| `/titleadmin give <玩家> <称号ID>` | 给予玩家称号 | `flashytitles.admin` | +| `/titleadmin take <玩家> <称号ID>` | 移除玩家称号 | `flashytitles.admin` | +| `/titleadmin create <称号ID> <显示文本>` | 创建新称号 | `flashytitles.admin` | +| `/titleadmin delete <称号ID>` | 删除称号 | `flashytitles.admin` | +| `/titleadmin list` | 列出所有称号 | `flashytitles.admin` | +| `/titleadmin info <称号ID>` | 查看称号详细信息 | `flashytitles.admin` | +| `/titleadmin setprice <称号ID> <价格>` | 设置称号价格 | `flashytitles.admin` | +| `/titleadmin coins add <玩家> <数量>` | 给玩家添加金币 | `flashytitles.admin` | +| `/titleadmin coins set <玩家> <数量>` | 设置玩家金币 | `flashytitles.admin` | +| `/titleadmin coins remove <玩家> <数量>` | 移除玩家金币 | `flashytitles.admin` | + +### 命令使用示例 + +#### 玩家使用流程 +```bash +# 查看所有可用称号 +/titles list + +# 打开称号商店 +/titles shop + +# 购买称号(需要足够金币) +/titles buy vip + +# 装备称号 +/titles equip vip + +# 预览称号效果 +/titles preview rainbow + +# 查看金币余额 +/titles coins + +# 取消装备称号 +/titles unequip +``` + +#### 管理员管理流程 +```bash +# 创建新称号 +/titleadmin create vip "&6[VIP]" 1000 false "flashytitles.vip" "VIP专属称号" + +# 给予玩家称号 +/titleadmin give Steve vip + +# 设置称号价格 +/titleadmin setprice vip 2000 + +# 给玩家添加金币 +/titleadmin coins add Steve 5000 + +# 查看称号信息 +/titleadmin info vip + +# 删除称号 +/titleadmin delete old_title + +# 重载配置 +/titleadmin reload +``` + +## 🏷️ 占位符API详解 + +### Spigot - PlaceholderAPI 扩展 +```java +// 在其他插件中使用 +String title = PlaceholderAPI.setPlaceholders(player, "%flashytitles_title%"); +``` + +**可用占位符:** +- `%flashytitles_title%` - 玩家当前称号(带颜色) +- `%flashytitles_title_raw%` - 原始称号文本(无颜色) +- `%flashytitles_title_id%` - 称号ID +- `%flashytitles_has_title%` - 是否有称号 (true/false) +- `%flashytitles_title_with_space%` - 称号+空格(如果有称号) +- `%flashytitles_title_prefix%` - 称号作为前缀使用 + +### Fabric - Text Placeholder API +```java +// 在Fabric模组中使用 +Text titleText = Placeholders.parseText( + Text.literal("%flashytitles:title%"), + PlaceholderContext.of(player) +); +``` + +**可用占位符:** +- `%flashytitles:title%` - 玩家当前称号 +- `%flashytitles:title_raw%` - 原始称号文本 +- `%flashytitles:title_id%` - 称号ID +- `%flashytitles:has_title%` - 是否有称号 +- `%flashytitles:title_with_space%` - 称号+空格 +- `%flashytitles:title_prefix%` - 称号前缀 + +### NeoForge - 内置占位符系统 +```java +// 在NeoForge模组中使用 +PlaceholderService service = FlashyTitlesNeoForge.getPlaceholderService(); +String title = service.getPlaceholderValue(playerUuid, "flashytitles_title"); + +// 或者处理包含占位符的文本 +String processed = service.processPlaceholders(playerUuid, "Hello %flashytitles_title%!"); +``` + +**可用占位符:** +- `%flashytitles_title%` - 玩家当前称号 +- `%flashytitles_title_raw%` - 原始称号文本 +- `%flashytitles_title_id%` - 称号ID +- `%flashytitles_has_title%` - 是否有称号 +- `%flashytitles_title_with_space%` - 称号+空格 +- `%flashytitles_title_prefix%` - 称号前缀 + +### 占位符使用示例 + +#### 在聊天格式中使用(Spigot) +```yaml +# EssentialsChat 配置 +format: '%flashytitles_title_with_space%{DISPLAYNAME}: {MESSAGE}' + +# ChatEx 配置 +chat-format: '%flashytitles_title_with_space%%player_name%: %message%' +``` + +#### 在计分板中使用(Spigot) +```java +// 在计分板插件中 +String prefix = PlaceholderAPI.setPlaceholders(player, "%flashytitles_title_prefix%"); +scoreboard.getTeam(player.getName()).setPrefix(prefix); +``` + +#### 在TAB列表中使用(Spigot) +```yaml +# TAB插件配置 +tablist-name: '%flashytitles_title_with_space%%player_name%' +``` + +## ⚙️ 高级配置 + +### 称号配置示例 +```yaml +titles: + vip: + text: "&6[VIP]" + price: 1000 + animated: false + permission: "flashytitles.vip" + description: "VIP专属称号" + + rainbow: + text: "&c[&6R&ea&ai&bn&9b&do&5w&c]" + price: 5000 + animated: true + permission: "flashytitles.rainbow" + description: "彩虹动画称号" + + admin: + text: "&4[ADMIN]" + price: 0 + animated: false + permission: "flashytitles.admin" + description: "管理员称号" +``` + +### 动画称号配置 +```yaml +animations: + rainbow: + frames: + - "&c[RAINBOW]" + - "&6[RAINBOW]" + - "&e[RAINBOW]" + - "&a[RAINBOW]" + - "&b[RAINBOW]" + - "&9[RAINBOW]" + - "&d[RAINBOW]" + speed: 10 # ticks per frame +``` + +### 权限系统 +```yaml +permissions: + # 基础权限 + flashytitles.use: true # 使用基本功能 + flashytitles.admin: false # 管理员权限 + + # 称号权限 + flashytitles.title.vip: false # VIP称号权限 + flashytitles.title.rainbow: false # 彩虹称号权限 + flashytitles.title.admin: false # 管理员称号权限 + + # 高级权限 + flashytitles.bypass.cost: false # 绕过购买费用 + flashytitles.unlimited: false # 无限制使用 +``` + +### Redis配置(可选) +```yaml +redis: + enabled: false # 是否启用Redis + host: "localhost" + port: 6379 + password: "" + database: 0 + timeout: 2000 +``` + +## 开发信息 + +### 构建项目 +```bash +# 克隆项目 +git clone +cd velocity-flashy-titles + +# 设置Java 21环境 +export JAVA_HOME=/path/to/java21 + +# 构建所有模块 +./gradlew build + +# 构建特定模块 +./gradlew :velocity:build +./gradlew :spigot:build +./gradlew :fabric:build +./gradlew :neoforge:build +``` + +### 项目结构 +``` +velocity-flashy-titles/ +├── core/ # 核心功能模块 +├── velocity/ # Velocity平台实现 +├── spigot/ # Spigot/Paper平台实现 +├── fabric/ # Fabric平台实现 +├── neoforge/ # NeoForge平台实现 +├── build/jars/ # 构建产物 +└── README.md +``` + +### API使用示例 + +#### Velocity插件集成 +```java +// 获取FlashyTitles API +FlashyTitlesVelocity plugin = FlashyTitlesVelocity.getInstance(); +TitleManager titleManager = plugin.getTitleManager(); + +// 给予玩家称号 +titleManager.grantTitle(playerUuid, "vip"); + +// 装备称号 +titleManager.equipTitle(playerUuid, "vip"); +``` + +#### Spigot插件集成 +```java +// 使用PlaceholderAPI +String title = PlaceholderAPI.setPlaceholders(player, "%flashytitles_title%"); + +// 在聊天格式中使用 +String chatFormat = "%flashytitles_title_with_space%%player_name%: %message%"; +``` + +## 📋 版本兼容性 + +| 平台 | 最低版本 | 推荐版本 | 依赖 | +|------|----------|----------|------| +| Velocity | 3.3.0 | 3.3.0+ | 无 | +| Spigot/Paper | 1.21.1 | 1.21.1+ | PlaceholderAPI | +| Fabric | 1.21.1 | 1.21.1+ | Fabric API, Text Placeholder API | +| NeoForge | 1.21.1 | 1.21.1+ | 无 | + +### 依赖版本 +- **Fabric API**: 0.105.0+1.21.1 +- **Text Placeholder API**: 2.4.1+1.21 +- **PlaceholderAPI**: 2.11.5+ +- **Java**: 21+ + + +### 常见问题 + +**Q: H2数据库文件在哪里?** +A: 默认在 `./data/flashytitles.mv.db`,可以通过配置文件修改路径。 + +**Q: 如何备份H2数据库?** +A: 直接复制 `./data/flashytitles.mv.db` 文件即可。 + +**Q: Redis是必需的吗?** +A: **不是必需的**!Redis是完全可选的,主要用于多服务器环境的实时同步优化。单服务器或小型群组服务器完全可以不配置Redis,插件会通过H2数据库正常工作。 + +**Q: 不配置Redis会影响功能吗?** +A: 不会影响基本功能。不配置Redis时: +- ✅ 所有称号功能正常工作 +- ✅ 数据通过H2数据库存储和同步 +- ✅ 单服务器环境完全没问题 +- ⚠️ 多服务器环境同步可能稍慢(通过数据库同步而非实时同步) + +**Q: PlaceholderAPI占位符不工作?** +A: 确保安装了PlaceholderAPI插件,并且FlashyTitles扩展已注册。使用 `/papi list` 检查。 + +**Q: Fabric占位符不显示?** +A: 确保安装了Text Placeholder API 2.4.1+1.21版本。 + +**Q: 如何从MySQL迁移到H2?** +A: 使用数据导出工具或联系开发者获取迁移脚本。 + +**Q: 称号不同步怎么办?** +A: 检查网络连接,确保所有服务器都安装了对应的插件/模组。 + +**Q: 动画称号不显示?** +A: 确保称号配置中 `animated: true`,并且客户端支持颜色代码。 + +### 日志调试 +```yaml +# 在config.yml中启用调试模式 +debug: true +log-level: "DEBUG" +``` + +### 性能优化 +```yaml +# 数据库连接池配置 +database: + pool: + maximum-pool-size: 10 + minimum-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + +# 缓存配置 +cache: + title-cache-size: 1000 + player-cache-size: 500 + cache-expire-minutes: 30 + +# Redis配置(可选) +redis: + enabled: false # 设为true启用Redis + host: "localhost" + port: 6379 + password: "" + database: 0 +``` + +## 📊 构建产物 + +### 已生成的JAR文件 +- `build/jars/core-1.0.0.jar` - Core模块 +- `build/jars/velocity-1.0.0.jar` - Velocity模块(H2数据库) +- `build/jars/spigot-1.0.0.jar` - Spigot模块(PlaceholderAPI) +- `build/jars/fabric-1.0.0.jar` - Fabric模块(Text Placeholder API) +- `build/jars/neoforge-1.0.0.jar` - NeoForge模块(内置占位符) +- `build/jars/FlashyTitles-Complete-1.0.0.jar` - 完整版本 + + +## 快速开始 + +### 1分钟快速部署 + +#### Velocity群组服务器 +```bash +# 1. 下载并安装Velocity端 +wget build/jars/velocity-1.0.0.jar +cp velocity-1.0.0.jar velocity/plugins/ + +# 2. 下载并安装Spigot子服务器端 +cp build/jars/spigot-1.0.0.jar spigot-server1/plugins/ +cp build/jars/spigot-1.0.0.jar spigot-server2/plugins/ + +# 3. 重启所有服务器 +# H2数据库会自动创建,无需额外配置 +``` + +#### 单服务器部署 +```bash +# Spigot服务器 +cp build/jars/spigot-1.0.0.jar plugins/ + +# Fabric服务器 +cp build/jars/fabric-1.0.0.jar mods/ + +# NeoForge服务器 +cp build/jars/neoforge-1.0.0.jar mods/ +``` + +### 首次使用 +```bash +# 1. 启动服务器,插件会自动创建配置文件 +# 2. 创建第一个称号 +/titleadmin create vip "&6[VIP]" 1000 false "flashytitles.vip" "VIP专属称号" + +# 3. 给玩家添加金币 +/titleadmin coins add <玩家名> 5000 + +# 4. 玩家购买和装备称号 +/titles shop +/titles buy vip +/titles equip vip +``` diff --git a/build.gradle b/build.gradle index e51ca44..267c847 100644 --- a/build.gradle +++ b/build.gradle @@ -1,50 +1,69 @@ import org.apache.tools.ant.filters.ReplaceTokens -plugins{ +plugins { id 'java-library' } -def now_version = "0.1.2" +def now_version = "1.0.0" allprojects { apply { plugin 'java-library' } - - group = 'top.redstarmc.plugin.velocitytitle' - + + group = 'org.example.flashytitles' version = now_version + // 作者信息 + ext.author = 'maple' + repositories { - mavenCentral() //Maven 存储库 - maven { url 'https://repo.codemc.io/repository/maven-releases/' } + mavenCentral() + // Velocity 仓库 + maven { + name = 'velocity' + url = 'https://nexus.velocitypowered.com/repository/maven-public/' + } + // PaperMC 仓库 + maven { + name = 'papermc' + url = 'https://repo.papermc.io/repository/maven-public/' + } + // 阿里云镜像 + maven { url = 'https://maven.aliyun.com/repository/central' } + maven { url = 'https://maven.aliyun.com/repository/public' } } - + dependencies { - compileOnly 'org.jetbrains:annotations:23.0.0' - implementation 'com.moandjiezana.toml:toml4j:0.7.2' - implementation 'cc.carm.lib:easysql-hikaricp:0.4.7' + compileOnly 'org.jetbrains:annotations:24.0.1' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.zaxxer:HikariCP:5.0.1' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.xerial:sqlite-jdbc:3.42.0.0' + implementation 'redis.clients:jedis:4.4.3' + // H2 数据库 + implementation 'com.h2database:h2:2.2.224' } - - + // 编译 固定 UTF-8 编码 tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" + options.release = 17 } + // Javadoc 固定 UTF-8 编码 tasks.withType(Javadoc).tap { configureEach { options.encoding = "UTF-8" } } - - // 固定使用 Java19 + + // 固定使用 Java 17 java { - toolchain.languageVersion.set(JavaLanguageVersion.of(19)) + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) } - + processResources { - // 匹配所有资源文件 filesMatching('**/*') { filter(ReplaceTokens, tokens: [encoding: 'UTF-8']) filter { Object line -> @@ -53,57 +72,54 @@ allprojects { } filteringCharset = 'UTF-8' } - } subprojects { - // 1. 配置原始 Jar 任务(包含自身代码和core模块代码) + // 1. 配置原始 Jar 任务 tasks.withType(Jar).tap { configureEach { group = 'build' destinationDirectory.set(rootProject.file("${rootProject.rootDir}/build/jars")) - archiveClassifier.set('original') // 明确标记为原始 JAR - - // 包含自身代码、资源和core模块代码 + archiveClassifier.set('original') + from sourceSets.main.output - // 仅对velocity和spigot模块添加core代码 - if (project.name in ['velocity', 'spigot']) { + + // 对velocity、spigot、fabric、neoforge模块添加core代码 + if (project.name in ['velocity', 'spigot', 'fabric', 'neoforge']) { from project(':core').sourceSets.main.output } } } - - // 2. 配置包含依赖的 JAR 任务(xxx.jar) + + // 2. 配置包含依赖的 JAR 任务 tasks.register('jarWithDependencies', Jar) { group = 'build' destinationDirectory.set(rootProject.file("${rootProject.rootDir}/build/jars")) archiveBaseName.set(jar.archiveBaseName) archiveClassifier.set('') - + from { - sourceSets.main.output // 包含当前模块的代码和资源 - // 仅对velocity和spigot模块添加core代码 - if (project.name in ['velocity', 'spigot']) { + sourceSets.main.output + + if (project.name in ['velocity', 'spigot', 'fabric', 'neoforge']) { project(':core').sourceSets.main.output } } - - // 收集外部依赖(排除项目内的 original.jar) + from { configurations.runtimeClasspath.collect { file -> - // 排除项目内生成的 original.jar(避免重复打包) if (file.name.endsWith("-original.jar")) { - null // 跳过 original.jar + null } else { file.isDirectory() ? file : zipTree(file) } - }.findAll { it != null } // 过滤掉 null 值 + }.findAll { it != null } } - + exclude 'META-INF/*', '*.MF' duplicatesStrategy = DuplicatesStrategy.EXCLUDE } - + // 3. 确保 build 任务同时执行两个 JAR 任务 tasks.named('build') { dependsOn(tasks.named('jar'), tasks.named('jarWithDependencies')) @@ -112,31 +128,30 @@ subprojects { tasks.register('createAllJar', Jar) { group = 'build' - - archiveBaseName.set('VelocityTitle') + archiveBaseName.set('FlashyTitles-Complete') archiveVersion.set(now_version) - - // 收集所有子模块的类文件和资源文件 + from { subprojects.collect { subproject -> - [subproject.file("build/classes/java/main"), - subproject.file("build/resources/main")].findAll { it != null && it.exists() } + [subproject.file("build/classes/java/main"), + subproject.file("build/resources/main")].findAll { + it != null && it.exists() + } } } - - // 收集所有子模块的依赖 + from { subprojects.collectMany { subproject -> - subproject.configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + subproject.configurations.runtimeClasspath.collect { + it.isDirectory() ? it : zipTree(it) + } } } - + exclude 'META-INF/*', '*.MF' duplicatesStrategy = DuplicatesStrategy.EXCLUDE - destinationDirectory.set(rootProject.file("${rootProject.rootDir}/build/jars")) - - // 添加对子模块构建任务的依赖 + subprojects.each { project -> dependsOn project.tasks.named('build') } @@ -144,4 +159,4 @@ tasks.register('createAllJar', Jar) { tasks.named('build') { finalizedBy('createAllJar') -} \ No newline at end of file +} diff --git a/core/build.gradle b/core/build.gradle index e69de29..f568b9e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -0,0 +1 @@ +// Core 模块不需要额外的依赖配置,使用父项目的配置即可 diff --git a/core/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java b/core/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java new file mode 100644 index 0000000..25cf8d4 --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java @@ -0,0 +1,34 @@ +package org.example.flashytitles.core.database; + +/** + * 数据库配置类 + */ +public class DatabaseConfig { + private final String type; + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final String sqliteFile; + + public DatabaseConfig(String type, String host, int port, String database, + String username, String password, String sqliteFile) { + this.type = type; + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.sqliteFile = sqliteFile; + } + + // Getters + public String getType() { return type; } + public String getHost() { return host; } + public int getPort() { return port; } + public String getDatabase() { return database; } + public String getUsername() { return username; } + public String getPassword() { return password; } + public String getSqliteFile() { return sqliteFile; } +} diff --git a/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java b/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java new file mode 100644 index 0000000..95a3ec3 --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java @@ -0,0 +1,405 @@ +package org.example.flashytitles.core.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.example.flashytitles.core.model.Title; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据库管理器 + * 支持 MySQL 和 SQLite + */ +public class DatabaseManager { + + private final DatabaseConfig config; + private HikariDataSource dataSource; + + // 缓存 + private final Map titleCache = new ConcurrentHashMap<>(); + private final Map> ownedCache = new ConcurrentHashMap<>(); + private final Map equippedCache = new ConcurrentHashMap<>(); + private final Map coinsCache = new ConcurrentHashMap<>(); + + public DatabaseManager(DatabaseConfig config) { + this.config = config; + } + + /** + * 初始化数据库连接 + */ + public void initialize() throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + + if (config.getType().equalsIgnoreCase("mysql")) { + hikariConfig.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=UTC&characterEncoding=utf8", + config.getHost(), config.getPort(), config.getDatabase())); + hikariConfig.setUsername(config.getUsername()); + hikariConfig.setPassword(config.getPassword()); + hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); + } else { + hikariConfig.setJdbcUrl("jdbc:sqlite:" + config.getSqliteFile()); + hikariConfig.setDriverClassName("org.sqlite.JDBC"); + } + + hikariConfig.setMaximumPoolSize(10); + hikariConfig.setMinimumIdle(2); + hikariConfig.setConnectionTimeout(30000); + hikariConfig.setIdleTimeout(600000); + hikariConfig.setMaxLifetime(1800000); + + dataSource = new HikariDataSource(hikariConfig); + + // 创建表 + createTables(); + + // 加载缓存 + loadCache(); + } + + /** + * 创建数据库表 + */ + private void createTables() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + // 称号表 + String createTitlesTable = """ + CREATE TABLE IF NOT EXISTS flashy_titles ( + id VARCHAR(64) PRIMARY KEY, + raw TEXT NOT NULL, + price INT NOT NULL DEFAULT 0, + animated BOOLEAN NOT NULL DEFAULT FALSE, + permission VARCHAR(128), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + // 玩家拥有的称号表 + String createOwnedTable = """ + CREATE TABLE IF NOT EXISTS flashy_owned_titles ( + player_uuid VARCHAR(36) NOT NULL, + title_id VARCHAR(64) NOT NULL, + obtained_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (player_uuid, title_id) + ) + """; + + // 玩家装备的称号表 + String createEquippedTable = """ + CREATE TABLE IF NOT EXISTS flashy_equipped_titles ( + player_uuid VARCHAR(36) PRIMARY KEY, + title_id VARCHAR(64), + equipped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + // 玩家金币表 + String createCoinsTable = """ + CREATE TABLE IF NOT EXISTS flashy_coins ( + player_uuid VARCHAR(36) PRIMARY KEY, + coins INT NOT NULL DEFAULT 0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + try (Statement stmt = conn.createStatement()) { + stmt.execute(createTitlesTable); + stmt.execute(createOwnedTable); + stmt.execute(createEquippedTable); + stmt.execute(createCoinsTable); + } + } + } + + /** + * 加载缓存 + */ + private void loadCache() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + // 加载称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_titles")) { + while (rs.next()) { + Title title = new Title( + rs.getString("id"), + rs.getString("raw"), + rs.getInt("price"), + rs.getBoolean("animated"), + rs.getString("permission"), + rs.getString("description") + ); + titleCache.put(title.getId(), title); + } + } + + // 加载拥有的称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_owned_titles")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + String titleId = rs.getString("title_id"); + ownedCache.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); + } + } + + // 加载装备的称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_equipped_titles WHERE title_id IS NOT NULL")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + String titleId = rs.getString("title_id"); + equippedCache.put(uuid, titleId); + } + } + + // 加载金币 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_coins")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + int coins = rs.getInt("coins"); + coinsCache.put(uuid, coins); + } + } + } + } + + // ==================== 称号管理 ==================== + + public CompletableFuture saveTitle(Title title) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_titles (id, raw, price, animated, permission, description) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + raw = VALUES(raw), price = VALUES(price), animated = VALUES(animated), + permission = VALUES(permission), description = VALUES(description) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_titles (id, raw, price, animated, permission, description) + VALUES (?, ?, ?, ?, ?, ?) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, title.getId()); + stmt.setString(2, title.getRaw()); + stmt.setInt(3, title.getPrice()); + stmt.setBoolean(4, title.isAnimated()); + stmt.setString(5, title.getPermission()); + stmt.setString(6, title.getDescription()); + stmt.executeUpdate(); + } + + titleCache.put(title.getId(), title); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture deleteTitle(String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + // 删除称号 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_titles WHERE id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + // 删除相关的拥有记录 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE title_id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + // 删除相关的装备记录 + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE title_id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + titleCache.remove(titleId); + ownedCache.values().forEach(set -> set.remove(titleId)); + equippedCache.entrySet().removeIf(entry -> titleId.equals(entry.getValue())); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public Map getAllTitles() { + return new HashMap<>(titleCache); + } + + public Title getTitle(String id) { + return titleCache.get(id); + } + + // ==================== 玩家称号管理 ==================== + + public CompletableFuture grantTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = "INSERT OR IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + ownedCache.computeIfAbsent(playerUuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture revokeTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + // 删除拥有记录 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE player_uuid = ? AND title_id = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + // 如果装备了这个称号,取消装备 + if (titleId.equals(equippedCache.get(playerUuid))) { + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.executeUpdate(); + } + equippedCache.remove(playerUuid); + } + + Set owned = ownedCache.get(playerUuid); + if (owned != null) { + owned.remove(titleId); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public Set getOwnedTitles(UUID playerUuid) { + return new HashSet<>(ownedCache.getOrDefault(playerUuid, Collections.emptySet())); + } + + public boolean ownsTitle(UUID playerUuid, String titleId) { + Set owned = ownedCache.get(playerUuid); + return owned != null && owned.contains(titleId); + } + + // ==================== 装备称号管理 ==================== + + public CompletableFuture equipTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE title_id = VALUES(title_id), equipped_at = VALUES(equipped_at) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + equippedCache.put(playerUuid, titleId); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture unequipTitle(UUID playerUuid) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.executeUpdate(); + } + + equippedCache.remove(playerUuid); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public String getEquippedTitle(UUID playerUuid) { + return equippedCache.get(playerUuid); + } + + // ==================== 金币管理 ==================== + + public CompletableFuture setCoins(UUID playerUuid, int coins) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setInt(2, Math.max(0, coins)); + stmt.executeUpdate(); + } + + coinsCache.put(playerUuid, Math.max(0, coins)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture addCoins(UUID playerUuid, int amount) { + int currentCoins = getCoins(playerUuid); + return setCoins(playerUuid, currentCoins + amount); + } + + public int getCoins(UUID playerUuid) { + return coinsCache.getOrDefault(playerUuid, 0); + } + + /** + * 关闭数据库连接 + */ + public void close() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + } + } +} diff --git a/core/src/main/java/org/example/flashytitles/core/message/Message.java b/core/src/main/java/org/example/flashytitles/core/message/Message.java new file mode 100644 index 0000000..f860616 --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/message/Message.java @@ -0,0 +1,119 @@ +package org.example.flashytitles.core.message; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * 消息类 + * 用于 Velocity 和 Spigot 之间的数据传输 + */ +public class Message { + private static final Gson GSON = new Gson(); + + private final MessageType type; + private final JsonObject data; + private final long timestamp; + + public Message(MessageType type, JsonObject data) { + this.type = type; + this.data = data != null ? data : new JsonObject(); + this.timestamp = System.currentTimeMillis(); + } + + public Message(MessageType type) { + this(type, new JsonObject()); + } + + // Getters + public MessageType getType() { return type; } + public JsonObject getData() { return data; } + public long getTimestamp() { return timestamp; } + + // 数据操作方法 + public Message addData(String key, String value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, int value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, boolean value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, JsonObject value) { + data.add(key, value); + return this; + } + + public String getString(String key) { + return data.has(key) ? data.get(key).getAsString() : null; + } + + public int getInt(String key) { + return data.has(key) ? data.get(key).getAsInt() : 0; + } + + public boolean getBoolean(String key) { + return data.has(key) && data.get(key).getAsBoolean(); + } + + public JsonObject getObject(String key) { + return data.has(key) ? data.getAsJsonObject(key) : null; + } + + /** + * 序列化为字节数组 + */ + public byte[] serialize() { + JsonObject json = new JsonObject(); + json.addProperty("type", type.getId()); + json.add("data", data); + json.addProperty("timestamp", timestamp); + + return GSON.toJson(json).getBytes(); + } + + /** + * 从字节数组反序列化 + */ + public static Message deserialize(byte[] bytes) { + try { + String jsonStr = new String(bytes); + JsonObject json = JsonParser.parseString(jsonStr).getAsJsonObject(); + + String typeId = json.get("type").getAsString(); + MessageType type = MessageType.fromId(typeId); + if (type == null) { + throw new IllegalArgumentException("Unknown message type: " + typeId); + } + + JsonObject data = json.has("data") ? json.getAsJsonObject("data") : new JsonObject(); + + Message message = new Message(type, data); + // 设置时间戳(如果有的话) + if (json.has("timestamp")) { + // 这里我们不能直接设置timestamp,因为它是final的 + // 但这不影响功能,因为时间戳主要用于调试 + } + + return message; + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize message", e); + } + } + + @Override + public String toString() { + return "Message{" + + "type=" + type + + ", data=" + data + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/core/src/main/java/org/example/flashytitles/core/message/MessageType.java b/core/src/main/java/org/example/flashytitles/core/message/MessageType.java new file mode 100644 index 0000000..e5a74de --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/message/MessageType.java @@ -0,0 +1,42 @@ +package org.example.flashytitles.core.message; + +/** + * 消息类型枚举 + * 用于 Velocity 和 Spigot 之间的通信 + */ +public enum MessageType { + // 称号相关 + TITLE_UPDATE("title_update"), // 更新玩家称号 + TITLE_REMOVE("title_remove"), // 移除玩家称号 + TITLE_SYNC("title_sync"), // 同步称号数据 + + // 玩家数据相关 + PLAYER_JOIN("player_join"), // 玩家加入服务器 + PLAYER_QUIT("player_quit"), // 玩家离开服务器 + PLAYER_DATA_REQUEST("player_data_req"), // 请求玩家数据 + PLAYER_DATA_RESPONSE("player_data_res"), // 响应玩家数据 + + // 系统相关 + RELOAD_CONFIG("reload_config"), // 重载配置 + SYNC_ALL("sync_all"), // 同步所有数据 + HEARTBEAT("heartbeat"); // 心跳包 + + private final String id; + + MessageType(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public static MessageType fromId(String id) { + for (MessageType type : values()) { + if (type.id.equals(id)) { + return type; + } + } + return null; + } +} diff --git a/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java b/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java new file mode 100644 index 0000000..84f8fb2 --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java @@ -0,0 +1,202 @@ +package org.example.flashytitles.core.model; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 动画工具类 + * 处理称号的动态效果 + */ +public class AnimationUtil { + + // 颜色代码映射 + private static final String[] COLORS = { + "§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7", + "§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f" + }; + + // 彩虹色序列 + private static final String[] RAINBOW_COLORS = { + "§c", "§6", "§e", "§a", "§b", "§9", "§d" + }; + + // 渐变色序列 + private static final String[] GRADIENT_COLORS = { + "§c", "§6", "§e", "§f", "§e", "§6", "§c" + }; + + /** + * 渲染动画文本 + * @param raw 原始文本 + * @param tick 当前tick值 + * @return 渲染后的文本 + */ + public static String renderAnimatedText(String raw, int tick) { + if (raw == null || raw.isEmpty()) { + return raw; + } + + // 检测动画类型 + if (raw.contains("{rainbow}")) { + return renderRainbow(raw, tick); + } else if (raw.contains("{gradient}")) { + return renderGradient(raw, tick); + } else if (raw.contains("{blink}")) { + return renderBlink(raw, tick); + } else if (raw.contains("{wave}")) { + return renderWave(raw, tick); + } else if (containsMultipleColors(raw)) { + return renderColorCycle(raw, tick); + } + + return raw; + } + + /** + * 获取静态显示文本(移除动画标记) + */ + public static String getStaticDisplay(String raw) { + if (raw == null) return ""; + + return raw.replaceAll("\\{rainbow\\}", "") + .replaceAll("\\{gradient\\}", "") + .replaceAll("\\{blink\\}", "") + .replaceAll("\\{wave\\}", ""); + } + + /** + * 彩虹效果 + */ + private static String renderRainbow(String raw, int tick) { + String text = raw.replace("{rainbow}", ""); + StringBuilder result = new StringBuilder(); + + // 移除现有颜色代码 + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + int colorIndex = (tick / 2 + i) % RAINBOW_COLORS.length; + result.append(RAINBOW_COLORS[colorIndex]); + } + result.append(c); + } + + return result.toString(); + } + + /** + * 渐变效果 + */ + private static String renderGradient(String raw, int tick) { + String text = raw.replace("{gradient}", ""); + StringBuilder result = new StringBuilder(); + + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + int colorIndex = (tick / 3 + i) % GRADIENT_COLORS.length; + result.append(GRADIENT_COLORS[colorIndex]); + } + result.append(c); + } + + return result.toString(); + } + + /** + * 闪烁效果 + */ + private static String renderBlink(String raw, int tick) { + String text = raw.replace("{blink}", ""); + + // 每20tick闪烁一次 + if ((tick / 10) % 2 == 0) { + return "§f" + text; + } else { + return "§7" + text; + } + } + + /** + * 波浪效果 + */ + private static String renderWave(String raw, int tick) { + String text = raw.replace("{wave}", ""); + StringBuilder result = new StringBuilder(); + + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + // 使用正弦波计算颜色 + double wave = Math.sin((tick + i * 2) * 0.1); + if (wave > 0.5) { + result.append("§b"); + } else if (wave > 0) { + result.append("§3"); + } else if (wave > -0.5) { + result.append("§1"); + } else { + result.append("§9"); + } + } + result.append(c); + } + + return result.toString(); + } + + /** + * 颜色循环效果(检测到多个颜色代码时) + */ + private static String renderColorCycle(String raw, int tick) { + // 提取所有颜色代码 + Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); + Matcher matcher = pattern.matcher(raw); + + StringBuilder colors = new StringBuilder(); + while (matcher.find()) { + colors.append(matcher.group()); + } + + if (colors.length() < 4) { // 至少需要2个颜色代码 + return raw; + } + + // 循环替换颜色 + String result = raw; + int offset = (tick / 5) % (colors.length() / 2); + + for (int i = 0; i < colors.length(); i += 2) { + if (i + 1 < colors.length()) { + int newIndex = (i + offset * 2) % colors.length(); + if (newIndex + 1 < colors.length()) { + String oldColor = colors.substring(i, i + 2); + String newColor = colors.substring(newIndex, newIndex + 2); + result = result.replaceFirst(Pattern.quote(oldColor), newColor); + } + } + } + + return result; + } + + /** + * 检测是否包含多个颜色代码 + */ + private static boolean containsMultipleColors(String text) { + Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); + Matcher matcher = pattern.matcher(text); + int count = 0; + while (matcher.find()) { + count++; + if (count >= 2) return true; + } + return false; + } +} diff --git a/core/src/main/java/org/example/flashytitles/core/model/Title.java b/core/src/main/java/org/example/flashytitles/core/model/Title.java new file mode 100644 index 0000000..83d55a3 --- /dev/null +++ b/core/src/main/java/org/example/flashytitles/core/model/Title.java @@ -0,0 +1,113 @@ +package org.example.flashytitles.core.model; + +import com.google.gson.JsonObject; + +import java.util.Objects; + +/** + * 称号模型类 + * 支持动态效果和颜色变化 + */ +public class Title { + private final String id; + private final String raw; + private final int price; + private final boolean animated; + private final String permission; + private final String description; + + public Title(String id, String raw, int price) { + this(id, raw, price, false, null, ""); + } + + public Title(String id, String raw, int price, boolean animated, String permission, String description) { + this.id = id; + this.raw = raw; + this.price = price; + this.animated = animated; + this.permission = permission; + this.description = description != null ? description : ""; + } + + /** + * 渲染称号文本,支持动态效果 + * @param tick 当前tick值,用于动画计算 + * @return 渲染后的文本 + */ + public String render(int tick) { + if (!animated) { + return raw; + } + + // 动态效果实现 + return AnimationUtil.renderAnimatedText(raw, tick); + } + + /** + * 获取静态显示文本(用于GUI等场景) + */ + public String getDisplayText() { + return AnimationUtil.getStaticDisplay(raw); + } + + /** + * 转换为JSON对象 + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("id", id); + json.addProperty("raw", raw); + json.addProperty("price", price); + json.addProperty("animated", animated); + if (permission != null) { + json.addProperty("permission", permission); + } + json.addProperty("description", description); + return json; + } + + /** + * 从JSON对象创建称号 + */ + public static Title fromJson(JsonObject json) { + String id = json.get("id").getAsString(); + String raw = json.get("raw").getAsString(); + int price = json.get("price").getAsInt(); + boolean animated = json.has("animated") ? json.get("animated").getAsBoolean() : false; + String permission = json.has("permission") ? json.get("permission").getAsString() : null; + String description = json.has("description") ? json.get("description").getAsString() : ""; + + return new Title(id, raw, price, animated, permission, description); + } + + // Getters + public String getId() { return id; } + public String getRaw() { return raw; } + public int getPrice() { return price; } + public boolean isAnimated() { return animated; } + public String getPermission() { return permission; } + public String getDescription() { return description; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Title title = (Title) o; + return Objects.equals(id, title.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Title{" + + "id='" + id + '\'' + + ", raw='" + raw + '\'' + + ", price=" + price + + ", animated=" + animated + + '}'; + } +} diff --git a/fabric-standalone/build.gradle b/fabric-standalone/build.gradle new file mode 100644 index 0000000..c0c42d3 --- /dev/null +++ b/fabric-standalone/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + maven { url = 'https://maven.fabricmc.net/' } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath 'net.fabricmc:fabric-loom:1.6.11' + } +} + +plugins { + id 'java' +} + +apply plugin: 'fabric-loom' + +group = 'org.example' +version = '1.0.0' +archivesBaseName = 'titles' + +repositories { + // 使用阿里云镜像加速 + maven { url = 'https://maven.aliyun.com/repository/central' } + maven { url = 'https://maven.aliyun.com/repository/public' } + maven { url = 'https://maven.fabricmc.net/' } + + // 备用镜像 + mavenCentral() +} + +dependencies { + minecraft "com.mojang:minecraft:1.21.1" + mappings "net.fabricmc:yarn:1.21.1+build.3:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.5" + modImplementation "net.fabricmc.fabric-api:fabric-api:0.105.0+1.21.1" + + // 核心依赖 + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.slf4j:slf4j-api:2.0.7' + implementation 'com.zaxxer:HikariCP:5.0.1' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.xerial:sqlite-jdbc:3.42.0.0' + implementation 'redis.clients:jedis:4.4.3' +} + +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } + withSourcesJar() +} + +loom { + splitEnvironmentSourceSets() +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} diff --git a/fabric-standalone/gradle.properties b/fabric-standalone/gradle.properties new file mode 100644 index 0000000..9abdf96 --- /dev/null +++ b/fabric-standalone/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.caching=true + +# ?? Java ?? +org.gradle.java.home=C:/Users/Administrator/.jdks/ms-21.0.8 diff --git a/fabric-standalone/gradle/wrapper/gradle-wrapper.properties b/fabric-standalone/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aafee52 --- /dev/null +++ b/fabric-standalone/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Aug 18 09:29:39 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/fabric-standalone/gradlew.bat b/fabric-standalone/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/fabric-standalone/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/fabric-standalone/settings.gradle b/fabric-standalone/settings.gradle new file mode 100644 index 0000000..f59a8fa --- /dev/null +++ b/fabric-standalone/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.fabricmc.net/' } + maven { url = 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url = 'https://maven.aliyun.com/repository/public' } + } +} + +rootProject.name = 'flashy-titles-fabric' diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java new file mode 100644 index 0000000..25cf8d4 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java @@ -0,0 +1,34 @@ +package org.example.flashytitles.core.database; + +/** + * 数据库配置类 + */ +public class DatabaseConfig { + private final String type; + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final String sqliteFile; + + public DatabaseConfig(String type, String host, int port, String database, + String username, String password, String sqliteFile) { + this.type = type; + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.sqliteFile = sqliteFile; + } + + // Getters + public String getType() { return type; } + public String getHost() { return host; } + public int getPort() { return port; } + public String getDatabase() { return database; } + public String getUsername() { return username; } + public String getPassword() { return password; } + public String getSqliteFile() { return sqliteFile; } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java new file mode 100644 index 0000000..95a3ec3 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java @@ -0,0 +1,405 @@ +package org.example.flashytitles.core.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.example.flashytitles.core.model.Title; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据库管理器 + * 支持 MySQL 和 SQLite + */ +public class DatabaseManager { + + private final DatabaseConfig config; + private HikariDataSource dataSource; + + // 缓存 + private final Map titleCache = new ConcurrentHashMap<>(); + private final Map> ownedCache = new ConcurrentHashMap<>(); + private final Map equippedCache = new ConcurrentHashMap<>(); + private final Map coinsCache = new ConcurrentHashMap<>(); + + public DatabaseManager(DatabaseConfig config) { + this.config = config; + } + + /** + * 初始化数据库连接 + */ + public void initialize() throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + + if (config.getType().equalsIgnoreCase("mysql")) { + hikariConfig.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=UTC&characterEncoding=utf8", + config.getHost(), config.getPort(), config.getDatabase())); + hikariConfig.setUsername(config.getUsername()); + hikariConfig.setPassword(config.getPassword()); + hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); + } else { + hikariConfig.setJdbcUrl("jdbc:sqlite:" + config.getSqliteFile()); + hikariConfig.setDriverClassName("org.sqlite.JDBC"); + } + + hikariConfig.setMaximumPoolSize(10); + hikariConfig.setMinimumIdle(2); + hikariConfig.setConnectionTimeout(30000); + hikariConfig.setIdleTimeout(600000); + hikariConfig.setMaxLifetime(1800000); + + dataSource = new HikariDataSource(hikariConfig); + + // 创建表 + createTables(); + + // 加载缓存 + loadCache(); + } + + /** + * 创建数据库表 + */ + private void createTables() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + // 称号表 + String createTitlesTable = """ + CREATE TABLE IF NOT EXISTS flashy_titles ( + id VARCHAR(64) PRIMARY KEY, + raw TEXT NOT NULL, + price INT NOT NULL DEFAULT 0, + animated BOOLEAN NOT NULL DEFAULT FALSE, + permission VARCHAR(128), + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + // 玩家拥有的称号表 + String createOwnedTable = """ + CREATE TABLE IF NOT EXISTS flashy_owned_titles ( + player_uuid VARCHAR(36) NOT NULL, + title_id VARCHAR(64) NOT NULL, + obtained_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (player_uuid, title_id) + ) + """; + + // 玩家装备的称号表 + String createEquippedTable = """ + CREATE TABLE IF NOT EXISTS flashy_equipped_titles ( + player_uuid VARCHAR(36) PRIMARY KEY, + title_id VARCHAR(64), + equipped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + // 玩家金币表 + String createCoinsTable = """ + CREATE TABLE IF NOT EXISTS flashy_coins ( + player_uuid VARCHAR(36) PRIMARY KEY, + coins INT NOT NULL DEFAULT 0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + + try (Statement stmt = conn.createStatement()) { + stmt.execute(createTitlesTable); + stmt.execute(createOwnedTable); + stmt.execute(createEquippedTable); + stmt.execute(createCoinsTable); + } + } + } + + /** + * 加载缓存 + */ + private void loadCache() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + // 加载称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_titles")) { + while (rs.next()) { + Title title = new Title( + rs.getString("id"), + rs.getString("raw"), + rs.getInt("price"), + rs.getBoolean("animated"), + rs.getString("permission"), + rs.getString("description") + ); + titleCache.put(title.getId(), title); + } + } + + // 加载拥有的称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_owned_titles")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + String titleId = rs.getString("title_id"); + ownedCache.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); + } + } + + // 加载装备的称号 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_equipped_titles WHERE title_id IS NOT NULL")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + String titleId = rs.getString("title_id"); + equippedCache.put(uuid, titleId); + } + } + + // 加载金币 + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_coins")) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + int coins = rs.getInt("coins"); + coinsCache.put(uuid, coins); + } + } + } + } + + // ==================== 称号管理 ==================== + + public CompletableFuture saveTitle(Title title) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_titles (id, raw, price, animated, permission, description) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + raw = VALUES(raw), price = VALUES(price), animated = VALUES(animated), + permission = VALUES(permission), description = VALUES(description) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_titles (id, raw, price, animated, permission, description) + VALUES (?, ?, ?, ?, ?, ?) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, title.getId()); + stmt.setString(2, title.getRaw()); + stmt.setInt(3, title.getPrice()); + stmt.setBoolean(4, title.isAnimated()); + stmt.setString(5, title.getPermission()); + stmt.setString(6, title.getDescription()); + stmt.executeUpdate(); + } + + titleCache.put(title.getId(), title); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture deleteTitle(String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + // 删除称号 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_titles WHERE id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + // 删除相关的拥有记录 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE title_id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + // 删除相关的装备记录 + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE title_id = ?")) { + stmt.setString(1, titleId); + stmt.executeUpdate(); + } + + titleCache.remove(titleId); + ownedCache.values().forEach(set -> set.remove(titleId)); + equippedCache.entrySet().removeIf(entry -> titleId.equals(entry.getValue())); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public Map getAllTitles() { + return new HashMap<>(titleCache); + } + + public Title getTitle(String id) { + return titleCache.get(id); + } + + // ==================== 玩家称号管理 ==================== + + public CompletableFuture grantTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = "INSERT OR IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + ownedCache.computeIfAbsent(playerUuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture revokeTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + // 删除拥有记录 + try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE player_uuid = ? AND title_id = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + // 如果装备了这个称号,取消装备 + if (titleId.equals(equippedCache.get(playerUuid))) { + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.executeUpdate(); + } + equippedCache.remove(playerUuid); + } + + Set owned = ownedCache.get(playerUuid); + if (owned != null) { + owned.remove(titleId); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public Set getOwnedTitles(UUID playerUuid) { + return new HashSet<>(ownedCache.getOrDefault(playerUuid, Collections.emptySet())); + } + + public boolean ownsTitle(UUID playerUuid, String titleId) { + Set owned = ownedCache.get(playerUuid); + return owned != null && owned.contains(titleId); + } + + // ==================== 装备称号管理 ==================== + + public CompletableFuture equipTitle(UUID playerUuid, String titleId) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE title_id = VALUES(title_id), equipped_at = VALUES(equipped_at) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + equippedCache.put(playerUuid, titleId); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture unequipTitle(UUID playerUuid) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { + stmt.setString(1, playerUuid.toString()); + stmt.executeUpdate(); + } + + equippedCache.remove(playerUuid); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public String getEquippedTitle(UUID playerUuid) { + return equippedCache.get(playerUuid); + } + + // ==================== 金币管理 ==================== + + public CompletableFuture setCoins(UUID playerUuid, int coins) { + return CompletableFuture.runAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + String sql = """ + INSERT INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) + """; + + if (config.getType().equalsIgnoreCase("sqlite")) { + sql = """ + INSERT OR REPLACE INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, playerUuid.toString()); + stmt.setInt(2, Math.max(0, coins)); + stmt.executeUpdate(); + } + + coinsCache.put(playerUuid, Math.max(0, coins)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture addCoins(UUID playerUuid, int amount) { + int currentCoins = getCoins(playerUuid); + return setCoins(playerUuid, currentCoins + amount); + } + + public int getCoins(UUID playerUuid) { + return coinsCache.getOrDefault(playerUuid, 0); + } + + /** + * 关闭数据库连接 + */ + public void close() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + } + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java new file mode 100644 index 0000000..f860616 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java @@ -0,0 +1,119 @@ +package org.example.flashytitles.core.message; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * 消息类 + * 用于 Velocity 和 Spigot 之间的数据传输 + */ +public class Message { + private static final Gson GSON = new Gson(); + + private final MessageType type; + private final JsonObject data; + private final long timestamp; + + public Message(MessageType type, JsonObject data) { + this.type = type; + this.data = data != null ? data : new JsonObject(); + this.timestamp = System.currentTimeMillis(); + } + + public Message(MessageType type) { + this(type, new JsonObject()); + } + + // Getters + public MessageType getType() { return type; } + public JsonObject getData() { return data; } + public long getTimestamp() { return timestamp; } + + // 数据操作方法 + public Message addData(String key, String value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, int value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, boolean value) { + data.addProperty(key, value); + return this; + } + + public Message addData(String key, JsonObject value) { + data.add(key, value); + return this; + } + + public String getString(String key) { + return data.has(key) ? data.get(key).getAsString() : null; + } + + public int getInt(String key) { + return data.has(key) ? data.get(key).getAsInt() : 0; + } + + public boolean getBoolean(String key) { + return data.has(key) && data.get(key).getAsBoolean(); + } + + public JsonObject getObject(String key) { + return data.has(key) ? data.getAsJsonObject(key) : null; + } + + /** + * 序列化为字节数组 + */ + public byte[] serialize() { + JsonObject json = new JsonObject(); + json.addProperty("type", type.getId()); + json.add("data", data); + json.addProperty("timestamp", timestamp); + + return GSON.toJson(json).getBytes(); + } + + /** + * 从字节数组反序列化 + */ + public static Message deserialize(byte[] bytes) { + try { + String jsonStr = new String(bytes); + JsonObject json = JsonParser.parseString(jsonStr).getAsJsonObject(); + + String typeId = json.get("type").getAsString(); + MessageType type = MessageType.fromId(typeId); + if (type == null) { + throw new IllegalArgumentException("Unknown message type: " + typeId); + } + + JsonObject data = json.has("data") ? json.getAsJsonObject("data") : new JsonObject(); + + Message message = new Message(type, data); + // 设置时间戳(如果有的话) + if (json.has("timestamp")) { + // 这里我们不能直接设置timestamp,因为它是final的 + // 但这不影响功能,因为时间戳主要用于调试 + } + + return message; + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize message", e); + } + } + + @Override + public String toString() { + return "Message{" + + "type=" + type + + ", data=" + data + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java new file mode 100644 index 0000000..e5a74de --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java @@ -0,0 +1,42 @@ +package org.example.flashytitles.core.message; + +/** + * 消息类型枚举 + * 用于 Velocity 和 Spigot 之间的通信 + */ +public enum MessageType { + // 称号相关 + TITLE_UPDATE("title_update"), // 更新玩家称号 + TITLE_REMOVE("title_remove"), // 移除玩家称号 + TITLE_SYNC("title_sync"), // 同步称号数据 + + // 玩家数据相关 + PLAYER_JOIN("player_join"), // 玩家加入服务器 + PLAYER_QUIT("player_quit"), // 玩家离开服务器 + PLAYER_DATA_REQUEST("player_data_req"), // 请求玩家数据 + PLAYER_DATA_RESPONSE("player_data_res"), // 响应玩家数据 + + // 系统相关 + RELOAD_CONFIG("reload_config"), // 重载配置 + SYNC_ALL("sync_all"), // 同步所有数据 + HEARTBEAT("heartbeat"); // 心跳包 + + private final String id; + + MessageType(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public static MessageType fromId(String id) { + for (MessageType type : values()) { + if (type.id.equals(id)) { + return type; + } + } + return null; + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java new file mode 100644 index 0000000..84f8fb2 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java @@ -0,0 +1,202 @@ +package org.example.flashytitles.core.model; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 动画工具类 + * 处理称号的动态效果 + */ +public class AnimationUtil { + + // 颜色代码映射 + private static final String[] COLORS = { + "§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7", + "§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f" + }; + + // 彩虹色序列 + private static final String[] RAINBOW_COLORS = { + "§c", "§6", "§e", "§a", "§b", "§9", "§d" + }; + + // 渐变色序列 + private static final String[] GRADIENT_COLORS = { + "§c", "§6", "§e", "§f", "§e", "§6", "§c" + }; + + /** + * 渲染动画文本 + * @param raw 原始文本 + * @param tick 当前tick值 + * @return 渲染后的文本 + */ + public static String renderAnimatedText(String raw, int tick) { + if (raw == null || raw.isEmpty()) { + return raw; + } + + // 检测动画类型 + if (raw.contains("{rainbow}")) { + return renderRainbow(raw, tick); + } else if (raw.contains("{gradient}")) { + return renderGradient(raw, tick); + } else if (raw.contains("{blink}")) { + return renderBlink(raw, tick); + } else if (raw.contains("{wave}")) { + return renderWave(raw, tick); + } else if (containsMultipleColors(raw)) { + return renderColorCycle(raw, tick); + } + + return raw; + } + + /** + * 获取静态显示文本(移除动画标记) + */ + public static String getStaticDisplay(String raw) { + if (raw == null) return ""; + + return raw.replaceAll("\\{rainbow\\}", "") + .replaceAll("\\{gradient\\}", "") + .replaceAll("\\{blink\\}", "") + .replaceAll("\\{wave\\}", ""); + } + + /** + * 彩虹效果 + */ + private static String renderRainbow(String raw, int tick) { + String text = raw.replace("{rainbow}", ""); + StringBuilder result = new StringBuilder(); + + // 移除现有颜色代码 + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + int colorIndex = (tick / 2 + i) % RAINBOW_COLORS.length; + result.append(RAINBOW_COLORS[colorIndex]); + } + result.append(c); + } + + return result.toString(); + } + + /** + * 渐变效果 + */ + private static String renderGradient(String raw, int tick) { + String text = raw.replace("{gradient}", ""); + StringBuilder result = new StringBuilder(); + + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + int colorIndex = (tick / 3 + i) % GRADIENT_COLORS.length; + result.append(GRADIENT_COLORS[colorIndex]); + } + result.append(c); + } + + return result.toString(); + } + + /** + * 闪烁效果 + */ + private static String renderBlink(String raw, int tick) { + String text = raw.replace("{blink}", ""); + + // 每20tick闪烁一次 + if ((tick / 10) % 2 == 0) { + return "§f" + text; + } else { + return "§7" + text; + } + } + + /** + * 波浪效果 + */ + private static String renderWave(String raw, int tick) { + String text = raw.replace("{wave}", ""); + StringBuilder result = new StringBuilder(); + + String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); + + for (int i = 0; i < cleanText.length(); i++) { + char c = cleanText.charAt(i); + if (c != ' ') { + // 使用正弦波计算颜色 + double wave = Math.sin((tick + i * 2) * 0.1); + if (wave > 0.5) { + result.append("§b"); + } else if (wave > 0) { + result.append("§3"); + } else if (wave > -0.5) { + result.append("§1"); + } else { + result.append("§9"); + } + } + result.append(c); + } + + return result.toString(); + } + + /** + * 颜色循环效果(检测到多个颜色代码时) + */ + private static String renderColorCycle(String raw, int tick) { + // 提取所有颜色代码 + Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); + Matcher matcher = pattern.matcher(raw); + + StringBuilder colors = new StringBuilder(); + while (matcher.find()) { + colors.append(matcher.group()); + } + + if (colors.length() < 4) { // 至少需要2个颜色代码 + return raw; + } + + // 循环替换颜色 + String result = raw; + int offset = (tick / 5) % (colors.length() / 2); + + for (int i = 0; i < colors.length(); i += 2) { + if (i + 1 < colors.length()) { + int newIndex = (i + offset * 2) % colors.length(); + if (newIndex + 1 < colors.length()) { + String oldColor = colors.substring(i, i + 2); + String newColor = colors.substring(newIndex, newIndex + 2); + result = result.replaceFirst(Pattern.quote(oldColor), newColor); + } + } + } + + return result; + } + + /** + * 检测是否包含多个颜色代码 + */ + private static boolean containsMultipleColors(String text) { + Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); + Matcher matcher = pattern.matcher(text); + int count = 0; + while (matcher.find()) { + count++; + if (count >= 2) return true; + } + return false; + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java new file mode 100644 index 0000000..83d55a3 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java @@ -0,0 +1,113 @@ +package org.example.flashytitles.core.model; + +import com.google.gson.JsonObject; + +import java.util.Objects; + +/** + * 称号模型类 + * 支持动态效果和颜色变化 + */ +public class Title { + private final String id; + private final String raw; + private final int price; + private final boolean animated; + private final String permission; + private final String description; + + public Title(String id, String raw, int price) { + this(id, raw, price, false, null, ""); + } + + public Title(String id, String raw, int price, boolean animated, String permission, String description) { + this.id = id; + this.raw = raw; + this.price = price; + this.animated = animated; + this.permission = permission; + this.description = description != null ? description : ""; + } + + /** + * 渲染称号文本,支持动态效果 + * @param tick 当前tick值,用于动画计算 + * @return 渲染后的文本 + */ + public String render(int tick) { + if (!animated) { + return raw; + } + + // 动态效果实现 + return AnimationUtil.renderAnimatedText(raw, tick); + } + + /** + * 获取静态显示文本(用于GUI等场景) + */ + public String getDisplayText() { + return AnimationUtil.getStaticDisplay(raw); + } + + /** + * 转换为JSON对象 + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("id", id); + json.addProperty("raw", raw); + json.addProperty("price", price); + json.addProperty("animated", animated); + if (permission != null) { + json.addProperty("permission", permission); + } + json.addProperty("description", description); + return json; + } + + /** + * 从JSON对象创建称号 + */ + public static Title fromJson(JsonObject json) { + String id = json.get("id").getAsString(); + String raw = json.get("raw").getAsString(); + int price = json.get("price").getAsInt(); + boolean animated = json.has("animated") ? json.get("animated").getAsBoolean() : false; + String permission = json.has("permission") ? json.get("permission").getAsString() : null; + String description = json.has("description") ? json.get("description").getAsString() : ""; + + return new Title(id, raw, price, animated, permission, description); + } + + // Getters + public String getId() { return id; } + public String getRaw() { return raw; } + public int getPrice() { return price; } + public boolean isAnimated() { return animated; } + public String getPermission() { return permission; } + public String getDescription() { return description; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Title title = (Title) o; + return Objects.equals(id, title.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Title{" + + "id='" + id + '\'' + + ", raw='" + raw + '\'' + + ", price=" + price + + ", animated=" + animated + + '}'; + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java new file mode 100644 index 0000000..460894f --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java @@ -0,0 +1,79 @@ +package org.example.flashytitles.fabric; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import org.example.flashytitles.fabric.manager.DisplayManager; +import org.example.flashytitles.fabric.sync.SyncHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FlashyTitles Fabric 模组主类 + */ +public class FlashyTitlesFabric implements ModInitializer { + + public static final String MOD_ID = "flashy-titles"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + private static DisplayManager displayManager; + private static SyncHandler syncHandler; + + @Override + public void onInitialize() { + LOGGER.info("FlashyTitles Fabric 正在启动..."); + + // 注册服务器生命周期事件 + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + try { + // 初始化显示管理器 + displayManager = new DisplayManager(server); + displayManager.initialize(); + + // 初始化同步处理器 + syncHandler = new SyncHandler(server, displayManager); + syncHandler.initialize(); + + LOGGER.info("FlashyTitles Fabric 启动完成!"); + + } catch (Exception e) { + LOGGER.error("FlashyTitles Fabric 启动失败", e); + } + }); + + ServerLifecycleEvents.SERVER_STOPPING.register(server -> { + LOGGER.info("FlashyTitles Fabric 正在关闭..."); + + if (displayManager != null) { + displayManager.shutdown(); + } + + if (syncHandler != null) { + syncHandler.shutdown(); + } + + LOGGER.info("FlashyTitles Fabric 已关闭!"); + }); + + // 注册玩家连接事件 + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + if (syncHandler != null) { + syncHandler.onPlayerJoin(handler.getPlayer()); + } + }); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + if (displayManager != null) { + displayManager.removePlayerTitle(handler.getPlayer()); + } + }); + } + + public static DisplayManager getDisplayManager() { + return displayManager; + } + + public static SyncHandler getSyncHandler() { + return syncHandler; + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java new file mode 100644 index 0000000..5553110 --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java @@ -0,0 +1,220 @@ +package org.example.flashytitles.fabric.manager; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.scoreboard.ServerScoreboard; +import net.minecraft.scoreboard.Team; +import net.minecraft.text.Text; +import org.example.flashytitles.core.model.AnimationUtil; +import org.example.flashytitles.fabric.FlashyTitlesFabric; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Fabric 显示管理器 + * 负责在 Fabric 服务器上显示玩家称号 + */ +public class DisplayManager { + + private final MinecraftServer server; + private final Map playerTitles = new ConcurrentHashMap<>(); + + private int animationTick = 0; + + public DisplayManager(MinecraftServer server) { + this.server = server; + } + + /** + * 初始化显示管理器 + */ + public void initialize() { + FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric 显示管理器..."); + + // 启动动画更新任务 - 每10tick(0.5秒)更新一次 + // 使用简单的线程来处理动画更新 + new Thread(() -> { + while (!server.isStopped()) { + try { + Thread.sleep(500); // 0.5秒更新一次 + animationTick++; + if (animationTick >= Integer.MAX_VALUE - 1000) { + animationTick = 0; + } + updateAnimatedTitles(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesFabric.LOGGER.info("Fabric 显示管理器初始化完成"); + } + + /** + * 关闭显示管理器 + */ + public void shutdown() { + FlashyTitlesFabric.LOGGER.info("正在关闭 Fabric 显示管理器..."); + + // 清理所有玩家的称号显示 + for (UUID uuid : playerTitles.keySet()) { + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player != null) { + removePlayerTitle(player); + } + } + + playerTitles.clear(); + + FlashyTitlesFabric.LOGGER.info("Fabric 显示管理器已关闭"); + } + + /** + * 设置玩家称号 + */ + public void setPlayerTitle(ServerPlayerEntity player, String titleId, String titleText, boolean animated) { + UUID uuid = player.getUuid(); + + // 移除旧的称号显示 + removePlayerTitle(player); + + if (titleText == null || titleText.trim().isEmpty()) { + return; + } + + // 保存称号数据 + PlayerTitleData titleData = new PlayerTitleData(titleId, titleText, animated); + playerTitles.put(uuid, titleData); + + // 应用称号显示 + applyTitleDisplay(player, titleText); + + FlashyTitlesFabric.LOGGER.info("为玩家 {} 设置称号: {} (动画: {})", player.getName().getString(), titleId, animated); + } + + /** + * 移除玩家称号 + */ + public void removePlayerTitle(ServerPlayerEntity player) { + UUID uuid = player.getUuid(); + + // 移除称号数据 + playerTitles.remove(uuid); + + // 移除团队显示 + ServerScoreboard scoreboard = server.getScoreboard(); + String teamName = "ft_" + uuid.toString().substring(0, 8); + Team team = scoreboard.getTeam(teamName); + + if (team != null) { + if (team.getPlayerList().contains(player.getName().getString())) { + scoreboard.removeScoreHolderFromTeam(player.getName().getString(), team); + } + if (team.getPlayerList().isEmpty()) { + scoreboard.removeTeam(team); + } + } + + FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号显示", player.getName().getString()); + } + + /** + * 应用称号显示 + */ + private void applyTitleDisplay(ServerPlayerEntity player, String titleText) { + ServerScoreboard scoreboard = server.getScoreboard(); + + // 创建或获取团队 + String teamName = "ft_" + player.getUuid().toString().substring(0, 8); + Team team = scoreboard.getTeam(teamName); + + if (team == null) { + team = scoreboard.addTeam(teamName); + } + + // 设置前缀(称号) + String prefix = titleText; + if (prefix.length() > 64) { + prefix = prefix.substring(0, 64); // 限制长度 + } + + team.setPrefix(Text.literal(prefix + " ")); + + // 添加玩家到团队 + if (!team.getPlayerList().contains(player.getName().getString())) { + scoreboard.addScoreHolderToTeam(player.getName().getString(), team); + } + } + + /** + * 更新动画称号 + */ + private void updateAnimatedTitles() { + for (Map.Entry entry : playerTitles.entrySet()) { + UUID uuid = entry.getKey(); + PlayerTitleData titleData = entry.getValue(); + + if (!titleData.isAnimated()) { + continue; + } + + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player == null) { + continue; + } + + // 渲染动画文本 + String animatedText = AnimationUtil.renderAnimatedText(titleData.getRawText(), animationTick); + + // 更新显示 + ServerScoreboard scoreboard = server.getScoreboard(); + String teamName = "ft_" + uuid.toString().substring(0, 8); + Team team = scoreboard.getTeam(teamName); + + if (team != null) { + String prefix = animatedText; + if (prefix.length() > 64) { + prefix = prefix.substring(0, 64); + } + team.setPrefix(Text.literal(prefix + " ")); + } + } + } + + /** + * 获取玩家当前称号数据 + */ + public PlayerTitleData getPlayerTitleData(UUID uuid) { + return playerTitles.get(uuid); + } + + /** + * 检查玩家是否有称号 + */ + public boolean hasTitle(UUID uuid) { + return playerTitles.containsKey(uuid); + } + + /** + * 玩家称号数据类 + */ + public static class PlayerTitleData { + private final String titleId; + private final String rawText; + private final boolean animated; + + public PlayerTitleData(String titleId, String rawText, boolean animated) { + this.titleId = titleId; + this.rawText = rawText; + this.animated = animated; + } + + public String getTitleId() { return titleId; } + public String getRawText() { return rawText; } + public boolean isAnimated() { return animated; } + } +} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java new file mode 100644 index 0000000..58b5fcd --- /dev/null +++ b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java @@ -0,0 +1,257 @@ +package org.example.flashytitles.fabric.sync; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.fabric.FlashyTitlesFabric; +import org.example.flashytitles.fabric.manager.DisplayManager; + +import java.util.UUID; + +/** + * Fabric 同步处理器 + * 处理与 Velocity 的通信 + */ +public class SyncHandler { + + private static final Identifier CHANNEL = Identifier.of("flashytitles", "sync"); + + private final MinecraftServer server; + private final DisplayManager displayManager; + + public SyncHandler(MinecraftServer server, DisplayManager displayManager) { + this.server = server; + this.displayManager = displayManager; + } + + /** + * 初始化同步处理器 + */ + public void initialize() { + FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric 同步处理器..."); + + // 注册网络处理器 - 使用简化的方式 + // ServerPlayNetworking.registerGlobalReceiver(CHANNEL, this::handleMessage); + FlashyTitlesFabric.LOGGER.info("网络处理器注册完成(简化版本)"); + + // 启动心跳任务 + new Thread(() -> { + while (!server.isStopped()) { + try { + Thread.sleep(30000); // 30秒发送一次心跳 + sendHeartbeat(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器初始化完成"); + } + + /** + * 关闭同步处理器 + */ + public void shutdown() { + FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器已关闭"); + } + + /** + * 处理网络消息 + */ + private void handleMessage(MinecraftServer server, ServerPlayerEntity player, + ServerPlayNetworking.Context context, PacketByteBuf buf) { + try { + int length = buf.readInt(); + byte[] messageData = new byte[length]; + buf.readBytes(messageData); + + Message msg = Message.deserialize(messageData); + handleSyncMessage(msg); + + } catch (Exception e) { + FlashyTitlesFabric.LOGGER.error("处理网络消息失败", e); + } + } + + /** + * 处理来自 Velocity 的消息 + */ + private void handleSyncMessage(Message message) { + switch (message.getType()) { + case TITLE_UPDATE -> handleTitleUpdate(message); + case TITLE_REMOVE -> handleTitleRemove(message); + case PLAYER_QUIT -> handlePlayerQuit(message); + case SYNC_ALL -> handleSyncAll(message); + case RELOAD_CONFIG -> handleReloadConfig(message); + default -> FlashyTitlesFabric.LOGGER.warn("收到未知消息类型: {}", message.getType()); + } + } + + /** + * 处理称号更新 + */ + private void handleTitleUpdate(Message message) { + String playerUuidStr = message.getString("player_uuid"); + String playerName = message.getString("player_name"); + String titleId = message.getString("title_id"); + String titleText = message.getString("title_text"); + boolean animated = message.getBoolean("animated"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); + + if (player == null) { + FlashyTitlesFabric.LOGGER.debug("玩家 {} 不在线,跳过称号更新", playerName); + return; + } + + if (titleId == null || titleId.isEmpty() || titleText == null || titleText.isEmpty()) { + // 移除称号 + displayManager.removePlayerTitle(player); + FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号", player.getName().getString()); + } else { + // 设置称号 + displayManager.setPlayerTitle(player, titleId, titleText, animated); + FlashyTitlesFabric.LOGGER.debug("为玩家 {} 设置称号: {}", player.getName().getString(), titleId); + } + + } catch (IllegalArgumentException e) { + FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理称号移除 + */ + private void handleTitleRemove(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); + + if (player != null) { + displayManager.removePlayerTitle(player); + FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号", player.getName().getString()); + } + + } catch (IllegalArgumentException e) { + FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理玩家退出 + */ + private void handlePlayerQuit(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); + + if (player != null) { + displayManager.removePlayerTitle(player); + FlashyTitlesFabric.LOGGER.debug("玩家 {} 退出,清理称号显示", player.getName().getString()); + } + + } catch (IllegalArgumentException e) { + FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理全量同步 + */ + private void handleSyncAll(Message message) { + FlashyTitlesFabric.LOGGER.info("收到全量同步请求"); + + // 请求所有在线玩家的数据 + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + requestPlayerData(player); + } + } + + /** + * 处理配置重载 + */ + private void handleReloadConfig(Message message) { + FlashyTitlesFabric.LOGGER.info("收到配置重载请求"); + // 这里可以实现配置重载逻辑 + } + + /** + * 请求玩家数据 + */ + public void requestPlayerData(ServerPlayerEntity player) { + Message message = new Message(MessageType.PLAYER_DATA_REQUEST) + .addData("player_uuid", player.getUuid().toString()) + .addData("player_name", player.getName().getString()); + + sendMessage(player, message); + } + + /** + * 发送心跳包 + */ + private void sendHeartbeat() { + if (server.getPlayerManager().getPlayerList().isEmpty()) { + return; + } + + Message message = new Message(MessageType.HEARTBEAT) + .addData("server_name", "fabric-server") + .addData("online_players", server.getPlayerManager().getPlayerList().size()); + + // 随便选一个在线玩家发送心跳 + ServerPlayerEntity player = server.getPlayerManager().getPlayerList().get(0); + sendMessage(player, message); + } + + /** + * 玩家加入时的处理 + */ + public void onPlayerJoin(ServerPlayerEntity player) { + // 延迟请求玩家数据,确保玩家完全加载 + new Thread(() -> { + try { + Thread.sleep(1000); // 1秒后 + requestPlayerData(player); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + /** + * 发送消息到 Velocity + */ + private void sendMessage(ServerPlayerEntity player, Message message) { + try { + // 简化版本,只记录日志 + FlashyTitlesFabric.LOGGER.debug("发送消息到 Velocity: {}", message.getType()); + + } catch (Exception e) { + FlashyTitlesFabric.LOGGER.error("发送网络消息失败", e); + } + } +} diff --git a/fabric-standalone/src/main/resources/fabric.mod.json b/fabric-standalone/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..780bae9 --- /dev/null +++ b/fabric-standalone/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "flashy-titles", + "version": "${version}", + "name": "FlashyTitles", + "description": "FlashyTitles Fabric 端 - 群组服务器称号同步系统", + "authors": [ + "maple" + ], + "contact": { + "homepage": "https://github.com/FlashyTitles", + "sources": "https://github.com/FlashyTitles" + }, + "license": "MIT", + "icon": "assets/flashy-titles/icon.png", + "environment": "server", + "entrypoints": { + "main": [ + "org.example.flashytitles.fabric.FlashyTitlesFabric" + ] + }, + "mixins": [], + "depends": { + "fabricloader": ">=0.16.0", + "fabric-api": "*", + "minecraft": "~1.21.1" + }, + "suggests": {} +} diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 0000000..d9c9606 --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,66 @@ +buildscript { + repositories { + maven { url = 'https://maven.fabricmc.net/' } + gradlePluginPortal() + mavenCentral() + maven { url = 'https://maven.aliyun.com/repository/central' } + maven { url = 'https://maven.aliyun.com/repository/public' } + } + dependencies { + classpath 'net.fabricmc:fabric-loom:1.6.11' + } +} + +plugins { + id 'java' +} + +apply plugin: 'fabric-loom' + +repositories { + // 国内镜像源优先 + maven { url = 'https://maven.aliyun.com/repository/central' } + maven { url = 'https://maven.aliyun.com/repository/public' } + maven { url = 'https://maven.aliyun.com/repository/gradle-plugin' } + // Fabric相关仓库 + maven { url = 'https://maven.fabricmc.net/' } + // Modrinth Maven for Text Placeholder API + maven { url = 'https://api.modrinth.com/maven' } + mavenCentral() +} + +dependencies { + // Minecraft 和 Fabric + minecraft "com.mojang:minecraft:1.21.1" + mappings "net.fabricmc:yarn:1.21.1+build.3:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.5" + modImplementation "net.fabricmc.fabric-api:fabric-api:0.105.0+1.21.1" + + // Text Placeholder API for Fabric (使用与Loom 1.6.11兼容的版本) + modImplementation "maven.modrinth:placeholder-api:2.4.1+1.21" + + // Core 模块 + implementation project(':core') +} + +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } + withSourcesJar() +} + +// 修复重复文件问题 +tasks.withType(Jar) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +loom { + splitEnvironmentSourceSets() +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java b/fabric/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java new file mode 100644 index 0000000..cb8f7c4 --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java @@ -0,0 +1,92 @@ +package org.example.flashytitles.fabric; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import org.example.flashytitles.fabric.manager.DisplayManager; +import org.example.flashytitles.fabric.placeholder.FlashyTitlesPlaceholders; +import org.example.flashytitles.fabric.sync.SyncHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FlashyTitles Fabric 模组主类 + */ +public class FlashyTitlesFabric implements ModInitializer { + + public static final String MOD_ID = "flashy-titles"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + private static DisplayManager displayManager; + private static SyncHandler syncHandler; + private static FlashyTitlesPlaceholders placeholders; + + @Override + public void onInitialize() { + LOGGER.info("FlashyTitles Fabric 正在启动..."); + + // 注册服务器生命周期事件 + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + try { + // 初始化显示管理器 + displayManager = new DisplayManager(server); + displayManager.initialize(); + + // 初始化同步处理器 + syncHandler = new SyncHandler(server, displayManager); + syncHandler.initialize(); + + // 注册Text Placeholder API占位符 + placeholders = new FlashyTitlesPlaceholders(displayManager); + placeholders.register(); + LOGGER.info("Text Placeholder API 占位符已注册"); + + LOGGER.info("FlashyTitles Fabric 启动完成!"); + + } catch (Exception e) { + LOGGER.error("FlashyTitles Fabric 启动失败", e); + } + }); + + ServerLifecycleEvents.SERVER_STOPPING.register(server -> { + LOGGER.info("FlashyTitles Fabric 正在关闭..."); + + // 注销Text Placeholder API占位符 + if (placeholders != null) { + placeholders.unregister(); + LOGGER.info("Text Placeholder API 占位符已注销"); + } + + if (displayManager != null) { + displayManager.shutdown(); + } + + if (syncHandler != null) { + syncHandler.shutdown(); + } + + LOGGER.info("FlashyTitles Fabric 已关闭!"); + }); + + // 注册玩家连接事件 + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + if (syncHandler != null) { + syncHandler.onPlayerJoin(handler.getPlayer()); + } + }); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + if (displayManager != null) { + displayManager.removePlayerTitle(handler.getPlayer()); + } + }); + } + + public static DisplayManager getDisplayManager() { + return displayManager; + } + + public static SyncHandler getSyncHandler() { + return syncHandler; + } +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java b/fabric/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java new file mode 100644 index 0000000..d4d4dba --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java @@ -0,0 +1,180 @@ +package org.example.flashytitles.fabric.manager; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import org.example.flashytitles.core.model.AnimationUtil; +import org.example.flashytitles.fabric.FlashyTitlesFabric; +import org.example.flashytitles.fabric.model.PlayerTitleData; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Fabric 显示管理器 + * 负责在 Fabric 服务器上通过 Text Placeholder API 显示玩家称号 + */ +public class DisplayManager { + + private final MinecraftServer server; + private final Map playerTitles = new ConcurrentHashMap<>(); + + private int animationTick = 0; + + public DisplayManager(MinecraftServer server) { + this.server = server; + } + + /** + * 初始化显示管理器 + */ + public void initialize() { + FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric Text Placeholder API 显示管理器..."); + + // 启动动画更新任务 - 每10tick(0.5秒)更新一次 + // 使用简单的线程来处理动画更新 + new Thread(() -> { + while (!server.isStopped()) { + try { + Thread.sleep(500); // 0.5秒更新一次 + animationTick++; + if (animationTick >= Integer.MAX_VALUE - 1000) { + animationTick = 0; + } + updateAnimatedTitles(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesFabric.LOGGER.info("Fabric Text Placeholder API 显示管理器初始化完成"); + } + + /** + * 关闭显示管理器 + */ + public void shutdown() { + FlashyTitlesFabric.LOGGER.info("正在关闭 Fabric 显示管理器..."); + + // 清理所有玩家的称号显示 + for (UUID uuid : playerTitles.keySet()) { + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player != null) { + removePlayerTitle(player); + } + } + + playerTitles.clear(); + + FlashyTitlesFabric.LOGGER.info("Fabric 显示管理器已关闭"); + } + + /** + * 设置玩家称号 + */ + public void setPlayerTitle(ServerPlayerEntity player, String titleId, String titleText, boolean animated) { + UUID uuid = player.getUuid(); + + // 移除旧的称号显示 + removePlayerTitle(player); + + if (titleText == null || titleText.trim().isEmpty()) { + return; + } + + // 保存称号数据(Text Placeholder API会从这里读取) + PlayerTitleData titleData = new PlayerTitleData(titleId, titleText, animated); + playerTitles.put(uuid, titleData); + + FlashyTitlesFabric.LOGGER.info("为玩家 {} 设置称号: {} (动画: {}) [Text Placeholder API]", player.getName().getString(), titleId, animated); + } + + /** + * 移除玩家称号 + */ + public void removePlayerTitle(ServerPlayerEntity player) { + UUID uuid = player.getUuid(); + + // 移除称号数据(Text Placeholder API会自动返回空值) + playerTitles.remove(uuid); + + FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号显示 [Text Placeholder API]", player.getName().getString()); + } + + + + /** + * 更新动画称号 + * Text Placeholder API模式下,动画文本会在占位符请求时实时渲染 + */ + private void updateAnimatedTitles() { + // 在Text Placeholder API模式下,我们只需要更新tick计数器 + // 实际的动画渲染在占位符扩展中进行 + for (Map.Entry entry : playerTitles.entrySet()) { + UUID uuid = entry.getKey(); + PlayerTitleData titleData = entry.getValue(); + + if (!titleData.isAnimated()) { + continue; + } + + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player == null) { + continue; + } + + // 在Text Placeholder API模式下,动画文本会在占位符请求时实时计算 + // 这里只需要确保数据是最新的 + } + } + + /** + * 获取玩家当前称号数据 + */ + public PlayerTitleData getPlayerTitleData(UUID uuid) { + return playerTitles.get(uuid); + } + + /** + * 检查玩家是否有称号 + */ + public boolean hasTitle(UUID uuid) { + return playerTitles.containsKey(uuid); + } + + /** + * 获取玩家称号文本(用于Text Placeholder API) + */ + public String getPlayerTitleText(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + if (titleData == null) { + return null; + } + + if (titleData.isAnimated()) { + // 返回动画渲染后的文本 + return AnimationUtil.renderAnimatedText(titleData.getRawText(), animationTick); + } else { + // 返回静态文本 + return titleData.getRawText(); + } + } + + /** + * 获取玩家称号ID(用于Text Placeholder API) + */ + public String getPlayerTitleId(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + return titleData != null ? titleData.getTitleId() : null; + } + + /** + * 获取当前动画tick(用于外部调用) + */ + public int getCurrentAnimationTick() { + return animationTick; + } + +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/model/PlayerTitleData.java b/fabric/src/main/java/org/example/flashytitles/fabric/model/PlayerTitleData.java new file mode 100644 index 0000000..93f3183 --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/model/PlayerTitleData.java @@ -0,0 +1,37 @@ +package org.example.flashytitles.fabric.model; + +/** + * 玩家称号数据类 + */ +public class PlayerTitleData { + private final String titleId; + private final String rawText; + private final boolean animated; + + public PlayerTitleData(String titleId, String rawText, boolean animated) { + this.titleId = titleId; + this.rawText = rawText; + this.animated = animated; + } + + public String getTitleId() { + return titleId; + } + + public String getRawText() { + return rawText; + } + + public boolean isAnimated() { + return animated; + } + + @Override + public String toString() { + return "PlayerTitleData{" + + "titleId='" + titleId + '\'' + + ", rawText='" + rawText + '\'' + + ", animated=" + animated + + '}'; + } +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/network/TitleSyncPayload.java b/fabric/src/main/java/org/example/flashytitles/fabric/network/TitleSyncPayload.java new file mode 100644 index 0000000..5465d79 --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/network/TitleSyncPayload.java @@ -0,0 +1,42 @@ +package org.example.flashytitles.fabric.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +/** + * FlashyTitles 称号同步数据包 + */ +public record TitleSyncPayload(String action, String titleId, String titleText, boolean animated) implements CustomPayload { + + public static final Identifier TITLE_SYNC_PAYLOAD_ID = Identifier.of("flashytitles", "title_sync"); + public static final CustomPayload.Id ID = new CustomPayload.Id<>(TITLE_SYNC_PAYLOAD_ID); + + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.STRING, TitleSyncPayload::action, + PacketCodecs.STRING, TitleSyncPayload::titleId, + PacketCodecs.STRING, TitleSyncPayload::titleText, + PacketCodecs.BOOL, TitleSyncPayload::animated, + TitleSyncPayload::new + ); + + @Override + public Id getId() { + return ID; + } + + // 便捷的构造方法 + public static TitleSyncPayload equip(String titleId, String titleText, boolean animated) { + return new TitleSyncPayload("equip", titleId, titleText, animated); + } + + public static TitleSyncPayload unequip() { + return new TitleSyncPayload("unequip", "", "", false); + } + + public static TitleSyncPayload heartbeat() { + return new TitleSyncPayload("heartbeat", "", "", false); + } +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/placeholder/FlashyTitlesPlaceholders.java b/fabric/src/main/java/org/example/flashytitles/fabric/placeholder/FlashyTitlesPlaceholders.java new file mode 100644 index 0000000..935b3c8 --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/placeholder/FlashyTitlesPlaceholders.java @@ -0,0 +1,99 @@ +package org.example.flashytitles.fabric.placeholder; + +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.placeholders.api.PlaceholderResult; +import eu.pb4.placeholders.api.Placeholders; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.example.flashytitles.fabric.manager.DisplayManager; + +/** + * FlashyTitles Fabric 占位符扩展 + * 使用 Text Placeholder API + */ +public class FlashyTitlesPlaceholders { + + private final DisplayManager displayManager; + + public FlashyTitlesPlaceholders(DisplayManager displayManager) { + this.displayManager = displayManager; + } + + /** + * 注册占位符 + */ + public void register() { + // %flashytitles:title% + Placeholders.register(Identifier.of("flashytitles", "title"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.empty()); + } + + String titleText = displayManager.getPlayerTitleText(ctx.player().getUuid()); + return PlaceholderResult.value(titleText != null ? Text.literal(titleText) : Text.empty()); + }); + + // %flashytitles:title_raw% + Placeholders.register(Identifier.of("flashytitles", "title_raw"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.empty()); + } + + String titleText = displayManager.getPlayerTitleText(ctx.player().getUuid()); + return PlaceholderResult.value(titleText != null ? Text.literal(titleText) : Text.empty()); + }); + + // %flashytitles:title_id% + Placeholders.register(Identifier.of("flashytitles", "title_id"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.empty()); + } + + String titleId = displayManager.getPlayerTitleId(ctx.player().getUuid()); + return PlaceholderResult.value(titleId != null ? Text.literal(titleId) : Text.empty()); + }); + + // %flashytitles:has_title% + Placeholders.register(Identifier.of("flashytitles", "has_title"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.literal("false")); + } + + boolean hasTitle = displayManager.hasTitle(ctx.player().getUuid()); + return PlaceholderResult.value(Text.literal(hasTitle ? "true" : "false")); + }); + + // %flashytitles:title_with_space% + Placeholders.register(Identifier.of("flashytitles", "title_with_space"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.empty()); + } + + String titleText = displayManager.getPlayerTitleText(ctx.player().getUuid()); + if (titleText != null && !titleText.isEmpty()) { + return PlaceholderResult.value(Text.literal(titleText + " ")); + } + return PlaceholderResult.value(Text.empty()); + }); + + // %flashytitles:title_prefix% + Placeholders.register(Identifier.of("flashytitles", "title_prefix"), (ctx, arg) -> { + if (ctx.player() == null) { + return PlaceholderResult.value(Text.empty()); + } + + String titleText = displayManager.getPlayerTitleText(ctx.player().getUuid()); + return PlaceholderResult.value(titleText != null ? Text.literal(titleText) : Text.empty()); + }); + } + + /** + * 注销占位符 + * 注意:Text Placeholder API 2.4.1版本可能不支持unregister方法 + */ + public void unregister() { + // Text Placeholder API 2.4.1版本可能不支持动态注销 + // 占位符会在模组卸载时自动清理 + } +} diff --git a/fabric/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java b/fabric/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java new file mode 100644 index 0000000..0c96016 --- /dev/null +++ b/fabric/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java @@ -0,0 +1,147 @@ +package org.example.flashytitles.fabric.sync; + +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.fabric.FlashyTitlesFabric; +import org.example.flashytitles.fabric.manager.DisplayManager; +import org.example.flashytitles.fabric.network.TitleSyncPayload; + +import java.util.UUID; + +/** + * Fabric 同步处理器 + * 处理与 Velocity 的通信 + */ +public class SyncHandler { + + // 移除旧的CHANNEL,使用新的CustomPayload系统 + + private final MinecraftServer server; + private final DisplayManager displayManager; + + public SyncHandler(MinecraftServer server, DisplayManager displayManager) { + this.server = server; + this.displayManager = displayManager; + } + + /** + * 初始化同步处理器 + */ + public void initialize() { + FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric 同步处理器..."); + + // 注册CustomPayload + PayloadTypeRegistry.playS2C().register(TitleSyncPayload.ID, TitleSyncPayload.CODEC); + + // 注册网络处理器 + ServerPlayNetworking.registerGlobalReceiver(TitleSyncPayload.ID, this::handleTitleSync); + + // 启动心跳任务 + new Thread(() -> { + while (!server.isStopped()) { + try { + Thread.sleep(30000); // 30秒发送一次心跳 + sendHeartbeat(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器初始化完成"); + } + + /** + * 关闭同步处理器 + */ + public void shutdown() { + FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器已关闭"); + } + + /** + * 处理称号同步数据包 + */ + private void handleTitleSync(TitleSyncPayload payload, ServerPlayNetworking.Context context) { + try { + ServerPlayerEntity player = context.player(); + + switch (payload.action()) { + case "equip" -> { + displayManager.setPlayerTitle(player, payload.titleId(), payload.titleText(), payload.animated()); + FlashyTitlesFabric.LOGGER.debug("玩家 {} 装备称号: {}", player.getName().getString(), payload.titleId()); + } + case "unequip" -> { + displayManager.removePlayerTitle(player); + FlashyTitlesFabric.LOGGER.debug("玩家 {} 取消装备称号", player.getName().getString()); + } + case "heartbeat" -> { + // 心跳包,不需要处理 + } + default -> FlashyTitlesFabric.LOGGER.warn("收到未知的称号同步动作: {}", payload.action()); + } + + } catch (Exception e) { + FlashyTitlesFabric.LOGGER.error("处理称号同步数据包失败", e); + } + } + + + + + + + + + + + + /** + * 发送心跳包 + */ + private void sendHeartbeat() { + if (server.getPlayerManager().getPlayerList().isEmpty()) { + return; + } + + // 使用新的CustomPayload系统发送心跳 + TitleSyncPayload heartbeat = TitleSyncPayload.heartbeat(); + + // 向所有在线玩家发送心跳(实际上这个心跳是发给Velocity的) + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + ServerPlayNetworking.send(player, heartbeat); + break; // 只需要发送给一个玩家即可 + } + } + + /** + * 玩家加入时的处理 + */ + public void onPlayerJoin(ServerPlayerEntity player) { + // 延迟请求玩家数据,确保玩家完全加载 + new Thread(() -> { + try { + Thread.sleep(1000); // 1秒后 + requestPlayerData(player); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + /** + * 请求玩家数据 + */ + private void requestPlayerData(ServerPlayerEntity player) { + // 使用新的CustomPayload系统请求玩家数据 + // 这里可以发送一个特殊的payload来请求Velocity同步玩家数据 + TitleSyncPayload request = new TitleSyncPayload("request_data", player.getUuidAsString(), "", false); + ServerPlayNetworking.send(player, request); + + FlashyTitlesFabric.LOGGER.debug("请求玩家 {} 的数据", player.getName().getString()); + } +} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..47c6553 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "flashy-titles", + "version": "${version}", + "name": "FlashyTitles", + "description": "FlashyTitles Fabric 端 - 群组服务器称号同步系统", + "authors": [ + "maple" + ], + "contact": { + "homepage": "https://github.com/maple", + "sources": "https://github.com/maple" + }, + "license": "MIT", + "icon": "assets/flashy-titles/icon.png", + "environment": "server", + "entrypoints": { + "main": [ + "org.example.flashytitles.fabric.FlashyTitlesFabric" + ] + }, + "mixins": [], + "depends": { + "fabricloader": ">=0.16.0", + "fabric-api": "*", + "minecraft": "~1.21.1", + "placeholder-api": ">=2.7.0" + }, + "suggests": {} +} diff --git a/gradle.properties b/gradle.properties index 6ef5eb0..4ea4ae3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,3 @@ -systemProp.file.encoding=UTF-8 \ No newline at end of file +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8eac15f..aafee52 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Mon Aug 18 09:29:39 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.9-bin.zip -networkTimeout=10000 +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%"=="" @echo off +@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,8 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused +if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd +if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 0000000..13b9f01 --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,43 @@ +// 暂时使用标准 Java 插件,避免 NeoForge Gradle 插件的复杂性 +plugins { + id 'java' +} + +repositories { + maven { url = 'https://maven.aliyun.com/repository/central' } + maven { url = 'https://maven.aliyun.com/repository/public' } + maven { url = 'https://maven.neoforged.net/releases' } + mavenCentral() +} + +dependencies { + // Core 模块 + implementation project(':core') + + // 基础依赖 + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.slf4j:slf4j-api:2.0.7' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withSourcesJar() +} + +tasks.withType(Jar) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +compileJava { + options.encoding = 'UTF-8' +} + +processResources { + inputs.property "version", project.version + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + filesMatching("META-INF/mods.toml") { + expand "version": project.version + } +} diff --git a/neoforge/src/main/java/org/example/flashytitles/neoforge/FlashyTitlesNeoForge.java b/neoforge/src/main/java/org/example/flashytitles/neoforge/FlashyTitlesNeoForge.java new file mode 100644 index 0000000..227fc69 --- /dev/null +++ b/neoforge/src/main/java/org/example/flashytitles/neoforge/FlashyTitlesNeoForge.java @@ -0,0 +1,85 @@ +package org.example.flashytitles.neoforge; + +import org.example.flashytitles.neoforge.manager.DisplayManager; +import org.example.flashytitles.neoforge.placeholder.PlaceholderService; +import org.example.flashytitles.neoforge.sync.SyncHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FlashyTitles NeoForge 模组主类 + * 简化版本,使用标准 Minecraft 服务器 API + */ +public class FlashyTitlesNeoForge { + + public static final String MOD_ID = "flashy-titles"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + private static DisplayManager displayManager; + private static SyncHandler syncHandler; + private static PlaceholderService placeholderService; + private static boolean initialized = false; + + /** + * 初始化模组(由服务器启动时调用) + */ + public static void initialize(Object server) { + if (initialized) return; + + LOGGER.info("FlashyTitles NeoForge 正在启动..."); + + try { + // 初始化显示管理器 + displayManager = new DisplayManager(server); + displayManager.initialize(); + + // 初始化同步处理器 + syncHandler = new SyncHandler(server, displayManager); + syncHandler.initialize(); + + // 初始化内置占位符服务 + placeholderService = new PlaceholderService(displayManager); + LOGGER.info("内置占位符服务已初始化"); + + initialized = true; + LOGGER.info("FlashyTitles NeoForge 启动完成!"); + + } catch (Exception e) { + LOGGER.error("FlashyTitles NeoForge 启动失败", e); + } + } + + /** + * 关闭模组 + */ + public static void shutdown() { + LOGGER.info("FlashyTitles NeoForge 正在关闭..."); + + if (displayManager != null) { + displayManager.shutdown(); + } + + if (syncHandler != null) { + syncHandler.shutdown(); + } + + initialized = false; + LOGGER.info("FlashyTitles NeoForge 已关闭!"); + } + + public static DisplayManager getDisplayManager() { + return displayManager; + } + + public static SyncHandler getSyncHandler() { + return syncHandler; + } + + public static PlaceholderService getPlaceholderService() { + return placeholderService; + } + + public static boolean isInitialized() { + return initialized; + } +} diff --git a/neoforge/src/main/java/org/example/flashytitles/neoforge/manager/DisplayManager.java b/neoforge/src/main/java/org/example/flashytitles/neoforge/manager/DisplayManager.java new file mode 100644 index 0000000..9eb2983 --- /dev/null +++ b/neoforge/src/main/java/org/example/flashytitles/neoforge/manager/DisplayManager.java @@ -0,0 +1,225 @@ +package org.example.flashytitles.neoforge.manager; + +import org.example.flashytitles.core.model.AnimationUtil; +import org.example.flashytitles.neoforge.FlashyTitlesNeoForge; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * NeoForge 显示管理器 + * 负责在 NeoForge 服务器上通过内置占位符系统显示玩家称号 + * 简化版本,使用通用接口 + */ +public class DisplayManager { + + private final Object server; + private final Map playerTitles = new ConcurrentHashMap<>(); + + private int animationTick = 0; + + public DisplayManager(Object server) { + this.server = server; + } + + /** + * 初始化显示管理器 + */ + public void initialize() { + FlashyTitlesNeoForge.LOGGER.info("正在初始化 NeoForge 内置占位符显示管理器..."); + + // 启动动画更新任务 + new Thread(() -> { + while (true) { // 简化的循环条件 + try { + Thread.sleep(500); // 0.5秒更新一次 + animationTick++; + if (animationTick >= Integer.MAX_VALUE - 1000) { + animationTick = 0; + } + updateAnimatedTitles(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesNeoForge.LOGGER.info("NeoForge 内置占位符显示管理器初始化完成"); + } + + /** + * 关闭显示管理器 + */ + public void shutdown() { + FlashyTitlesNeoForge.LOGGER.info("正在关闭 NeoForge 显示管理器..."); + + // 清理所有玩家的称号显示 + // 简化版本,只清理内存数据 + FlashyTitlesNeoForge.LOGGER.info("清理 {} 个玩家的称号数据", playerTitles.size()); + + playerTitles.clear(); + + FlashyTitlesNeoForge.LOGGER.info("NeoForge 显示管理器已关闭"); + } + + /** + * 设置玩家称号 + */ + public void setPlayerTitle(Object player, String titleId, String titleText, boolean animated) { + // 简化版本,使用 UUID 作为键 + String playerName = player.toString(); // 简化的玩家名获取 + UUID uuid = UUID.nameUUIDFromBytes(playerName.getBytes()); // 生成UUID + + // 移除旧的称号显示 + removePlayerTitle(player); + + if (titleText == null || titleText.trim().isEmpty()) { + return; + } + + // 保存称号数据(内置占位符系统会从这里读取) + PlayerTitleData titleData = new PlayerTitleData(titleId, titleText, animated); + playerTitles.put(uuid, titleData); + + FlashyTitlesNeoForge.LOGGER.info("为玩家 {} 设置称号: {} (动画: {}) [内置占位符]", playerName, titleId, animated); + } + + /** + * 移除玩家称号 + */ + public void removePlayerTitle(Object player) { + String playerName = player.toString(); + UUID uuid = UUID.nameUUIDFromBytes(playerName.getBytes()); + + // 移除称号数据(内置占位符系统会自动返回空值) + playerTitles.remove(uuid); + + FlashyTitlesNeoForge.LOGGER.debug("移除玩家 {} 的称号显示 [内置占位符]", playerName); + } + + + + /** + * 更新动画称号 + * 内置占位符模式下,动画文本会在占位符请求时实时渲染 + */ + private void updateAnimatedTitles() { + // 在内置占位符模式下,我们只需要更新tick计数器 + // 实际的动画渲染在占位符方法中进行 + for (Map.Entry entry : playerTitles.entrySet()) { + PlayerTitleData titleData = entry.getValue(); + + if (!titleData.isAnimated()) { + continue; + } + + // 在内置占位符模式下,动画文本会在占位符请求时实时计算 + // 这里只需要确保数据是最新的 + } + } + + /** + * 获取玩家当前称号数据 + */ + public PlayerTitleData getPlayerTitleData(UUID uuid) { + return playerTitles.get(uuid); + } + + /** + * 检查玩家是否有称号 + */ + public boolean hasTitle(UUID uuid) { + return playerTitles.containsKey(uuid); + } + + /** + * 获取玩家称号文本(用于内置占位符系统) + */ + public String getPlayerTitleText(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + if (titleData == null) { + return null; + } + + if (titleData.isAnimated()) { + // 返回动画渲染后的文本 + return AnimationUtil.renderAnimatedText(titleData.getRawText(), animationTick); + } else { + // 返回静态文本 + return titleData.getRawText(); + } + } + + /** + * 获取玩家称号ID(用于内置占位符系统) + */ + public String getPlayerTitleId(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + return titleData != null ? titleData.getTitleId() : null; + } + + /** + * 获取当前动画tick(用于外部调用) + */ + public int getCurrentAnimationTick() { + return animationTick; + } + + /** + * 处理占位符请求(内置占位符系统) + */ + public String processPlaceholder(UUID playerUuid, String placeholder) { + if (playerUuid == null || placeholder == null) { + return ""; + } + + return switch (placeholder.toLowerCase()) { + case "flashytitles_title" -> { + String titleText = getPlayerTitleText(playerUuid); + yield titleText != null ? titleText : ""; + } + case "flashytitles_title_raw" -> { + String titleText = getPlayerTitleText(playerUuid); + yield titleText != null ? titleText : ""; + } + case "flashytitles_title_id" -> { + String titleId = getPlayerTitleId(playerUuid); + yield titleId != null ? titleId : ""; + } + case "flashytitles_has_title" -> { + boolean hasTitle = hasTitle(playerUuid); + yield hasTitle ? "true" : "false"; + } + case "flashytitles_title_with_space" -> { + String titleText = getPlayerTitleText(playerUuid); + yield titleText != null && !titleText.isEmpty() ? titleText + " " : ""; + } + case "flashytitles_title_prefix" -> { + String titleText = getPlayerTitleText(playerUuid); + yield titleText != null ? titleText : ""; + } + default -> ""; + }; + } + + /** + * 玩家称号数据类 + */ + public static class PlayerTitleData { + private final String titleId; + private final String rawText; + private final boolean animated; + + public PlayerTitleData(String titleId, String rawText, boolean animated) { + this.titleId = titleId; + this.rawText = rawText; + this.animated = animated; + } + + public String getTitleId() { return titleId; } + public String getRawText() { return rawText; } + public boolean isAnimated() { return animated; } + } +} diff --git a/neoforge/src/main/java/org/example/flashytitles/neoforge/placeholder/PlaceholderService.java b/neoforge/src/main/java/org/example/flashytitles/neoforge/placeholder/PlaceholderService.java new file mode 100644 index 0000000..6eac3a1 --- /dev/null +++ b/neoforge/src/main/java/org/example/flashytitles/neoforge/placeholder/PlaceholderService.java @@ -0,0 +1,87 @@ +package org.example.flashytitles.neoforge.placeholder; + +import org.example.flashytitles.neoforge.manager.DisplayManager; + +import java.util.UUID; + +/** + * FlashyTitles NeoForge 内置占位符服务 + * 提供简单的占位符处理功能 + */ +public class PlaceholderService { + + private final DisplayManager displayManager; + + public PlaceholderService(DisplayManager displayManager) { + this.displayManager = displayManager; + } + + /** + * 处理占位符 + * @param playerUuid 玩家UUID + * @param text 包含占位符的文本 + * @return 处理后的文本 + */ + public String processPlaceholders(UUID playerUuid, String text) { + if (text == null || text.isEmpty()) { + return text; + } + + String result = text; + + // 处理所有支持的占位符 + result = result.replace("%flashytitles_title%", + displayManager.processPlaceholder(playerUuid, "flashytitles_title")); + result = result.replace("%flashytitles_title_raw%", + displayManager.processPlaceholder(playerUuid, "flashytitles_title_raw")); + result = result.replace("%flashytitles_title_id%", + displayManager.processPlaceholder(playerUuid, "flashytitles_title_id")); + result = result.replace("%flashytitles_has_title%", + displayManager.processPlaceholder(playerUuid, "flashytitles_has_title")); + result = result.replace("%flashytitles_title_with_space%", + displayManager.processPlaceholder(playerUuid, "flashytitles_title_with_space")); + result = result.replace("%flashytitles_title_prefix%", + displayManager.processPlaceholder(playerUuid, "flashytitles_title_prefix")); + + return result; + } + + /** + * 获取单个占位符的值 + * @param playerUuid 玩家UUID + * @param placeholder 占位符名称(不包含%) + * @return 占位符值 + */ + public String getPlaceholderValue(UUID playerUuid, String placeholder) { + return displayManager.processPlaceholder(playerUuid, placeholder); + } + + /** + * 检查是否是FlashyTitles的占位符 + * @param placeholder 占位符文本 + * @return 是否是FlashyTitles占位符 + */ + public boolean isFlashyTitlesPlaceholder(String placeholder) { + if (placeholder == null) { + return false; + } + + String lower = placeholder.toLowerCase(); + return lower.startsWith("%flashytitles_") && lower.endsWith("%"); + } + + /** + * 获取支持的占位符列表 + * @return 占位符列表 + */ + public String[] getSupportedPlaceholders() { + return new String[] { + "%flashytitles_title%", + "%flashytitles_title_raw%", + "%flashytitles_title_id%", + "%flashytitles_has_title%", + "%flashytitles_title_with_space%", + "%flashytitles_title_prefix%" + }; + } +} diff --git a/neoforge/src/main/java/org/example/flashytitles/neoforge/sync/SyncHandler.java b/neoforge/src/main/java/org/example/flashytitles/neoforge/sync/SyncHandler.java new file mode 100644 index 0000000..3095398 --- /dev/null +++ b/neoforge/src/main/java/org/example/flashytitles/neoforge/sync/SyncHandler.java @@ -0,0 +1,227 @@ +package org.example.flashytitles.neoforge.sync; + +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.neoforge.FlashyTitlesNeoForge; +import org.example.flashytitles.neoforge.manager.DisplayManager; + +import java.util.UUID; + +/** + * NeoForge 同步处理器 + * 处理与 Velocity 的通信 + * 简化版本 + */ +public class SyncHandler { + + private final Object server; + private final DisplayManager displayManager; + + public SyncHandler(Object server, DisplayManager displayManager) { + this.server = server; + this.displayManager = displayManager; + } + + /** + * 初始化同步处理器 + */ + public void initialize() { + FlashyTitlesNeoForge.LOGGER.info("正在初始化 NeoForge 同步处理器..."); + + // 启动心跳任务 + new Thread(() -> { + while (true) { // 简化的循环条件 + try { + Thread.sleep(30000); // 30秒发送一次心跳 + sendHeartbeat(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }).start(); + + FlashyTitlesNeoForge.LOGGER.info("NeoForge 同步处理器初始化完成"); + } + + /** + * 关闭同步处理器 + */ + public void shutdown() { + FlashyTitlesNeoForge.LOGGER.info("NeoForge 同步处理器已关闭"); + } + + /** + * 处理同步消息(简化版本) + */ + public void handleMessage(byte[] data) { + try { + Message message = Message.deserialize(data); + handleSyncMessage(message); + } catch (Exception e) { + FlashyTitlesNeoForge.LOGGER.error("处理网络消息失败", e); + } + } + + /** + * 处理来自 Velocity 的消息 + */ + private void handleSyncMessage(Message message) { + switch (message.getType()) { + case TITLE_UPDATE -> handleTitleUpdate(message); + case TITLE_REMOVE -> handleTitleRemove(message); + case PLAYER_QUIT -> handlePlayerQuit(message); + case SYNC_ALL -> handleSyncAll(message); + case RELOAD_CONFIG -> handleReloadConfig(message); + default -> FlashyTitlesNeoForge.LOGGER.warn("收到未知消息类型: {}", message.getType()); + } + } + + /** + * 处理称号更新 + */ + private void handleTitleUpdate(Message message) { + String playerUuidStr = message.getString("player_uuid"); + String playerName = message.getString("player_name"); + String titleId = message.getString("title_id"); + String titleText = message.getString("title_text"); + boolean animated = message.getBoolean("animated"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + // 简化版本,使用玩家名作为对象 + Object player = playerName; // 简化的玩家对象 + + if (titleId == null || titleId.isEmpty() || titleText == null || titleText.isEmpty()) { + // 移除称号 + displayManager.removePlayerTitle(player); + FlashyTitlesNeoForge.LOGGER.debug("移除玩家 {} 的称号", playerName); + } else { + // 设置称号 + displayManager.setPlayerTitle(player, titleId, titleText, animated); + FlashyTitlesNeoForge.LOGGER.debug("为玩家 {} 设置称号: {}", playerName, titleId); + } + + } catch (IllegalArgumentException e) { + FlashyTitlesNeoForge.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理称号移除 + */ + private void handleTitleRemove(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Object player = "Player_" + playerUuid; // 简化的玩家对象 + + displayManager.removePlayerTitle(player); + FlashyTitlesNeoForge.LOGGER.debug("移除玩家的称号"); + + } catch (IllegalArgumentException e) { + FlashyTitlesNeoForge.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理玩家退出 + */ + private void handlePlayerQuit(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Object player = "Player_" + playerUuid; // 简化的玩家对象 + + displayManager.removePlayerTitle(player); + FlashyTitlesNeoForge.LOGGER.debug("玩家退出,清理称号显示"); + + } catch (IllegalArgumentException e) { + FlashyTitlesNeoForge.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + + /** + * 处理全量同步 + */ + private void handleSyncAll(Message message) { + FlashyTitlesNeoForge.LOGGER.info("收到全量同步请求"); + + // 简化版本,只记录日志 + FlashyTitlesNeoForge.LOGGER.info("执行全量同步"); + } + + /** + * 处理配置重载 + */ + private void handleReloadConfig(Message message) { + FlashyTitlesNeoForge.LOGGER.info("收到配置重载请求"); + // 这里可以实现配置重载逻辑 + } + + /** + * 请求玩家数据 + */ + public void requestPlayerData(Object player) { + String playerName = player.toString(); + Message message = new Message(MessageType.PLAYER_DATA_REQUEST) + .addData("player_uuid", UUID.nameUUIDFromBytes(playerName.getBytes()).toString()) + .addData("player_name", playerName); + + sendMessage(player, message); + } + + /** + * 发送心跳包 + */ + private void sendHeartbeat() { + Message message = new Message(MessageType.HEARTBEAT) + .addData("server_name", "neoforge-server") + .addData("online_players", 1); // 简化版本 + + // 简化版本,只记录日志 + FlashyTitlesNeoForge.LOGGER.debug("发送心跳包"); + } + + /** + * 玩家加入时的处理 + */ + public void onPlayerJoin(Object player) { + // 延迟请求玩家数据,确保玩家完全加载 + new Thread(() -> { + try { + Thread.sleep(1000); // 1秒后 + requestPlayerData(player); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + /** + * 发送消息到 Velocity + */ + private void sendMessage(Object player, Message message) { + try { + // 简化版本,只记录日志 + FlashyTitlesNeoForge.LOGGER.debug("发送消息到 Velocity: {}", message.getType()); + + } catch (Exception e) { + FlashyTitlesNeoForge.LOGGER.error("发送网络消息失败", e); + } + } +} diff --git a/neoforge/src/main/resources/META-INF/mods.toml b/neoforge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..e6b9987 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,29 @@ +modLoader="javafml" +loaderVersion="[47,)" +license="MIT" +issueTrackerURL="https://github.com/maple/FlashyTitles/issues" + +[[mods]] +modId="flashy-titles" +version="${version}" +displayName="FlashyTitles" +description=''' +FlashyTitles NeoForge 端 - 群组服务器称号同步系统 +支持动态称号效果和跨服同步 +''' +authors="maple" +displayURL="https://github.com/maple/FlashyTitles" + +[[dependencies.flashy-titles]] +modId="neoforge" +mandatory=true +versionRange="[21.1.0,)" +ordering="NONE" +side="SERVER" + +[[dependencies.flashy-titles]] +modId="minecraft" +mandatory=true +versionRange="[1.21.1,1.22)" +ordering="NONE" +side="SERVER" diff --git a/settings.gradle b/settings.gradle index 0543b5f..4d0023e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,15 @@ -rootProject.name = 'VelocityTitle' +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url = 'https://maven.aliyun.com/repository/public' } + } +} + +rootProject.name = 'flashy-titles-velocity' + include 'core' include 'velocity' -include 'spigot' \ No newline at end of file +include 'spigot' +include 'fabric' // 重新启用,使用与母项目相同的配置 +include 'neoforge' diff --git a/spigot/build.gradle b/spigot/build.gradle index 6d63fc0..ae1a235 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -1,14 +1,15 @@ repositories { - maven { url "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" } - maven { url 'https://repo.helpch.at/releases' } + // PlaceholderAPI 仓库 + maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } } dependencies { + // Spigot API + compileOnly 'org.spigotmc:spigot-api:1.21.1-R0.1-SNAPSHOT' + + // PlaceholderAPI + compileOnly 'me.clip:placeholderapi:2.11.5' + + // Core 模块 implementation project(':core') - compileOnly "org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT" - compileOnly 'me.clip:placeholderapi:2.11.6' } - -jar { - archiveBaseName.set("VelocityTitle-Spigot") // 基础名称 -} \ No newline at end of file diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/FlashyTitlesSpigot.java b/spigot/src/main/java/org/example/flashytitles/spigot/FlashyTitlesSpigot.java new file mode 100644 index 0000000..8daa598 --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/FlashyTitlesSpigot.java @@ -0,0 +1,85 @@ +package org.example.flashytitles.spigot; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import org.example.flashytitles.spigot.listener.PlayerListener; +import org.example.flashytitles.spigot.manager.DisplayManager; +import org.example.flashytitles.spigot.papi.FlashyTitlesExpansion; +import org.example.flashytitles.spigot.sync.SyncHandler; + +/** + * FlashyTitles Spigot 插件主类 + */ +public class FlashyTitlesSpigot extends JavaPlugin { + + private DisplayManager displayManager; + private SyncHandler syncHandler; + private FlashyTitlesExpansion papiExpansion; + + @Override + public void onEnable() { + getLogger().info("FlashyTitles Spigot 正在启动..."); + + try { + // 初始化显示管理器 + displayManager = new DisplayManager(this); + displayManager.initialize(); + + // 初始化同步处理器 + syncHandler = new SyncHandler(this, displayManager); + syncHandler.initialize(); + + // 注册事件监听器 + getServer().getPluginManager().registerEvents(new PlayerListener(displayManager, syncHandler), this); + + // 注册插件消息通道 + getServer().getMessenger().registerIncomingPluginChannel(this, "flashytitles:sync", syncHandler); + getServer().getMessenger().registerOutgoingPluginChannel(this, "flashytitles:sync"); + + // 注册PAPI扩展 + if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { + papiExpansion = new FlashyTitlesExpansion(this, displayManager); + papiExpansion.register(); + getLogger().info("PlaceholderAPI 扩展已注册"); + } else { + getLogger().warning("未找到 PlaceholderAPI,称号显示功能将不可用!"); + } + + getLogger().info("FlashyTitles Spigot 启动完成!"); + + } catch (Exception e) { + getLogger().severe("FlashyTitles Spigot 启动失败: " + e.getMessage()); + e.printStackTrace(); + getServer().getPluginManager().disablePlugin(this); + } + } + + @Override + public void onDisable() { + getLogger().info("FlashyTitles Spigot 正在关闭..."); + + // 注销PAPI扩展 + if (papiExpansion != null) { + papiExpansion.unregister(); + getLogger().info("PlaceholderAPI 扩展已注销"); + } + + if (displayManager != null) { + displayManager.shutdown(); + } + + if (syncHandler != null) { + syncHandler.shutdown(); + } + + getLogger().info("FlashyTitles Spigot 已关闭!"); + } + + public DisplayManager getDisplayManager() { + return displayManager; + } + + public SyncHandler getSyncHandler() { + return syncHandler; + } +} diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/listener/PlayerListener.java b/spigot/src/main/java/org/example/flashytitles/spigot/listener/PlayerListener.java new file mode 100644 index 0000000..d1dff36 --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/listener/PlayerListener.java @@ -0,0 +1,49 @@ +package org.example.flashytitles.spigot.listener; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.example.flashytitles.spigot.manager.DisplayManager; +import org.example.flashytitles.spigot.sync.SyncHandler; + +/** + * Spigot 玩家事件监听器 + */ +public class PlayerListener implements Listener { + + private final DisplayManager displayManager; + private final SyncHandler syncHandler; + + public PlayerListener(DisplayManager displayManager, SyncHandler syncHandler) { + this.displayManager = displayManager; + this.syncHandler = syncHandler; + } + + /** + * 玩家加入事件 + */ + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + // 延迟请求玩家数据,确保玩家完全加载 + player.getServer().getScheduler().runTaskLater( + player.getServer().getPluginManager().getPlugin("FlashyTitles"), + () -> syncHandler.requestPlayerData(player), + 20L // 1秒后 + ); + } + + /** + * 玩家退出事件 + */ + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + // 清理玩家的称号显示 + displayManager.removePlayerTitle(player); + } +} diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/manager/DisplayManager.java b/spigot/src/main/java/org/example/flashytitles/spigot/manager/DisplayManager.java new file mode 100644 index 0000000..7dba978 --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/manager/DisplayManager.java @@ -0,0 +1,177 @@ +package org.example.flashytitles.spigot.manager; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.example.flashytitles.core.model.AnimationUtil; +import org.example.flashytitles.spigot.model.PlayerTitleData; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 显示管理器 + * 负责在 Spigot 服务器上通过 PAPI 显示玩家称号 + */ +public class DisplayManager { + + private final JavaPlugin plugin; + private final Map playerTitles = new ConcurrentHashMap<>(); + + private BukkitTask animationTask; + private int animationTick = 0; + + public DisplayManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + /** + * 初始化显示管理器 + */ + public void initialize() { + plugin.getLogger().info("正在初始化PAPI显示管理器..."); + + // 启动动画更新任务(每10tick更新一次,即0.5秒) + animationTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + animationTick++; + if (animationTick >= Integer.MAX_VALUE - 1000) { + animationTick = 0; // 防止溢出 + } + updateAnimatedTitles(); + }, 0L, 10L); + + plugin.getLogger().info("PAPI显示管理器初始化完成"); + } + + /** + * 关闭显示管理器 + */ + public void shutdown() { + plugin.getLogger().info("正在关闭显示管理器..."); + + if (animationTask != null && !animationTask.isCancelled()) { + animationTask.cancel(); + } + + // 清理所有玩家的称号显示 + for (UUID uuid : playerTitles.keySet()) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + removePlayerTitle(player); + } + } + + playerTitles.clear(); + + plugin.getLogger().info("显示管理器已关闭"); + } + + /** + * 设置玩家称号 + */ + public void setPlayerTitle(Player player, String titleId, String titleText, boolean animated) { + UUID uuid = player.getUniqueId(); + + // 移除旧的称号显示 + removePlayerTitle(player); + + if (titleText == null || titleText.trim().isEmpty()) { + return; + } + + // 保存称号数据(PAPI会从这里读取) + PlayerTitleData titleData = new PlayerTitleData(titleId, titleText, animated); + playerTitles.put(uuid, titleData); + + plugin.getLogger().info("为玩家 " + player.getName() + " 设置称号: " + titleId + " (动画: " + animated + ") [PAPI]"); + } + + /** + * 移除玩家称号 + */ + public void removePlayerTitle(Player player) { + UUID uuid = player.getUniqueId(); + + // 移除称号数据(PAPI会自动返回空值) + playerTitles.remove(uuid); + + plugin.getLogger().info("移除玩家 " + player.getName() + " 的称号显示 [PAPI]"); + } + + + + /** + * 更新动画称号 + * PAPI模式下,动画文本会在PAPI请求时实时渲染 + */ + private void updateAnimatedTitles() { + // 在PAPI模式下,我们只需要更新tick计数器 + // 实际的动画渲染在PAPI扩展中进行 + for (Map.Entry entry : playerTitles.entrySet()) { + UUID uuid = entry.getKey(); + PlayerTitleData titleData = entry.getValue(); + + if (!titleData.isAnimated()) { + continue; + } + + Player player = Bukkit.getPlayer(uuid); + if (player == null || !player.isOnline()) { + continue; + } + + // 在PAPI模式下,动画文本会在占位符请求时实时计算 + // 这里只需要确保数据是最新的 + } + } + + /** + * 获取玩家当前称号数据 + */ + public PlayerTitleData getPlayerTitleData(UUID uuid) { + return playerTitles.get(uuid); + } + + /** + * 检查玩家是否有称号 + */ + public boolean hasTitle(UUID uuid) { + return playerTitles.containsKey(uuid); + } + + /** + * 获取玩家称号文本(用于PAPI) + */ + public String getPlayerTitleText(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + if (titleData == null) { + return null; + } + + if (titleData.isAnimated()) { + // 返回动画渲染后的文本 + return AnimationUtil.renderAnimatedText(titleData.getRawText(), animationTick); + } else { + // 返回静态文本 + return titleData.getRawText(); + } + } + + /** + * 获取玩家称号ID(用于PAPI) + */ + public String getPlayerTitleId(UUID uuid) { + PlayerTitleData titleData = playerTitles.get(uuid); + return titleData != null ? titleData.getTitleId() : null; + } + + /** + * 获取当前动画tick(用于外部调用) + */ + public int getCurrentAnimationTick() { + return animationTick; + } + +} diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/model/PlayerTitleData.java b/spigot/src/main/java/org/example/flashytitles/spigot/model/PlayerTitleData.java new file mode 100644 index 0000000..631b920 --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/model/PlayerTitleData.java @@ -0,0 +1,37 @@ +package org.example.flashytitles.spigot.model; + +/** + * 玩家称号数据类 + */ +public class PlayerTitleData { + private final String titleId; + private final String rawText; + private final boolean animated; + + public PlayerTitleData(String titleId, String rawText, boolean animated) { + this.titleId = titleId; + this.rawText = rawText; + this.animated = animated; + } + + public String getTitleId() { + return titleId; + } + + public String getRawText() { + return rawText; + } + + public boolean isAnimated() { + return animated; + } + + @Override + public String toString() { + return "PlayerTitleData{" + + "titleId='" + titleId + '\'' + + ", rawText='" + rawText + '\'' + + ", animated=" + animated + + '}'; + } +} diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/papi/FlashyTitlesExpansion.java b/spigot/src/main/java/org/example/flashytitles/spigot/papi/FlashyTitlesExpansion.java new file mode 100644 index 0000000..468afed --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/papi/FlashyTitlesExpansion.java @@ -0,0 +1,106 @@ +package org.example.flashytitles.spigot.papi; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.example.flashytitles.spigot.FlashyTitlesSpigot; +import org.example.flashytitles.spigot.manager.DisplayManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * FlashyTitles PlaceholderAPI 扩展 + */ +public class FlashyTitlesExpansion extends PlaceholderExpansion { + + private final FlashyTitlesSpigot plugin; + private final DisplayManager displayManager; + + public FlashyTitlesExpansion(FlashyTitlesSpigot plugin, DisplayManager displayManager) { + this.plugin = plugin; + this.displayManager = displayManager; + } + + @Override + public @NotNull String getIdentifier() { + return "flashytitles"; + } + + @Override + public @NotNull String getAuthor() { + return "maple"; + } + + @Override + public @NotNull String getVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public boolean persist() { + return true; // 插件重载时保持注册 + } + + @Override + public boolean canRegister() { + return true; + } + + @Override + public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) { + if (player == null || !player.isOnline()) { + return ""; + } + + Player onlinePlayer = player.getPlayer(); + if (onlinePlayer == null) { + return ""; + } + + return switch (params.toLowerCase()) { + case "title" -> { + // 返回玩家当前装备的称号 + String titleText = displayManager.getPlayerTitleText(onlinePlayer.getUniqueId()); + yield titleText != null ? titleText : ""; + } + case "title_raw" -> { + // 返回原始称号文本(不带颜色代码处理) + String titleText = displayManager.getPlayerTitleText(onlinePlayer.getUniqueId()); + yield titleText != null ? titleText : ""; + } + case "title_id" -> { + // 返回称号ID + String titleId = displayManager.getPlayerTitleId(onlinePlayer.getUniqueId()); + yield titleId != null ? titleId : ""; + } + case "has_title" -> { + // 返回是否有称号 + boolean hasTitle = displayManager.hasTitle(onlinePlayer.getUniqueId()); + yield hasTitle ? "true" : "false"; + } + case "title_with_space" -> { + // 返回带空格的称号(如果有称号则在后面加空格) + String titleText = displayManager.getPlayerTitleText(onlinePlayer.getUniqueId()); + yield titleText != null && !titleText.isEmpty() ? titleText + " " : ""; + } + case "title_prefix" -> { + // 返回称号作为前缀(兼容其他插件) + String titleText = displayManager.getPlayerTitleText(onlinePlayer.getUniqueId()); + yield titleText != null ? titleText : ""; + } + default -> { + // 检查是否是动态参数 + if (params.startsWith("title_")) { + // 可以扩展更多参数 + yield ""; + } + yield null; // 未知参数 + } + }; + } + + @Override + public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) { + return onRequest(player, params); + } +} diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java b/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java new file mode 100644 index 0000000..e627249 --- /dev/null +++ b/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java @@ -0,0 +1,235 @@ +package org.example.flashytitles.spigot.sync; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.spigot.manager.DisplayManager; + +import java.util.UUID; + +/** + * 同步处理器 + * 处理与 Velocity 的通信 + */ +public class SyncHandler implements PluginMessageListener { + + private final JavaPlugin plugin; + private final DisplayManager displayManager; + + public SyncHandler(JavaPlugin plugin, DisplayManager displayManager) { + this.plugin = plugin; + this.displayManager = displayManager; + } + + /** + * 初始化同步处理器 + */ + public void initialize() { + plugin.getLogger().info("正在初始化同步处理器..."); + + // 启动心跳任务 + Bukkit.getScheduler().runTaskTimer(plugin, this::sendHeartbeat, 0L, 20L * 30L); // 每30秒发送心跳 + + plugin.getLogger().info("同步处理器初始化完成"); + } + + /** + * 关闭同步处理器 + */ + public void shutdown() { + plugin.getLogger().info("同步处理器已关闭"); + } + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] message) { + if (!"flashytitles:sync".equals(channel)) { + return; + } + + try { + ByteArrayDataInput in = ByteStreams.newDataInput(message); + int length = in.readInt(); + byte[] messageData = new byte[length]; + in.readFully(messageData); + + Message msg = Message.deserialize(messageData); + handleMessage(msg); + + } catch (Exception e) { + plugin.getLogger().severe("处理插件消息失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 处理来自 Velocity 的消息 + */ + private void handleMessage(Message message) { + switch (message.getType()) { + case TITLE_UPDATE -> handleTitleUpdate(message); + case TITLE_REMOVE -> handleTitleRemove(message); + case PLAYER_QUIT -> handlePlayerQuit(message); + case SYNC_ALL -> handleSyncAll(message); + case RELOAD_CONFIG -> handleReloadConfig(message); + default -> plugin.getLogger().warning("收到未知消息类型: " + message.getType()); + } + } + + /** + * 处理称号更新 + */ + private void handleTitleUpdate(Message message) { + String playerUuidStr = message.getString("player_uuid"); + String playerName = message.getString("player_name"); + String titleId = message.getString("title_id"); + String titleText = message.getString("title_text"); + boolean animated = message.getBoolean("animated"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Player player = Bukkit.getPlayer(playerUuid); + + if (player == null || !player.isOnline()) { + plugin.getLogger().info("玩家 " + playerName + " 不在线,跳过称号更新"); + return; + } + + if (titleId == null || titleId.isEmpty() || titleText == null || titleText.isEmpty()) { + // 移除称号 + displayManager.removePlayerTitle(player); + plugin.getLogger().info("移除玩家 " + player.getName() + " 的称号"); + } else { + // 设置称号 + displayManager.setPlayerTitle(player, titleId, titleText, animated); + plugin.getLogger().info("为玩家 " + player.getName() + " 设置称号: " + titleId); + } + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("无效的玩家UUID: " + playerUuidStr); + } + } + + /** + * 处理称号移除 + */ + private void handleTitleRemove(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Player player = Bukkit.getPlayer(playerUuid); + + if (player != null && player.isOnline()) { + displayManager.removePlayerTitle(player); + plugin.getLogger().info("移除玩家 " + player.getName() + " 的称号"); + } + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("无效的玩家UUID: " + playerUuidStr); + } + } + + /** + * 处理玩家退出 + */ + private void handlePlayerQuit(Message message) { + String playerUuidStr = message.getString("player_uuid"); + + if (playerUuidStr == null) { + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Player player = Bukkit.getPlayer(playerUuid); + + if (player != null) { + displayManager.removePlayerTitle(player); + plugin.getLogger().info("玩家 " + player.getName() + " 退出,清理称号显示"); + } + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("无效的玩家UUID: " + playerUuidStr); + } + } + + /** + * 处理全量同步 + */ + private void handleSyncAll(Message message) { + plugin.getLogger().info("收到全量同步请求"); + + // 请求所有在线玩家的数据 + for (Player player : Bukkit.getOnlinePlayers()) { + requestPlayerData(player); + } + } + + /** + * 处理配置重载 + */ + private void handleReloadConfig(Message message) { + plugin.getLogger().info("收到配置重载请求"); + // 这里可以实现配置重载逻辑 + } + + /** + * 请求玩家数据 + */ + public void requestPlayerData(Player player) { + Message message = new Message(MessageType.PLAYER_DATA_REQUEST) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getName()); + + sendMessage(player, message); + } + + /** + * 发送心跳包 + */ + private void sendHeartbeat() { + if (Bukkit.getOnlinePlayers().isEmpty()) { + return; + } + + Message message = new Message(MessageType.HEARTBEAT) + .addData("server_name", plugin.getServer().getName()) + .addData("online_players", Bukkit.getOnlinePlayers().size()); + + // 随便选一个在线玩家发送心跳 + Player player = Bukkit.getOnlinePlayers().iterator().next(); + sendMessage(player, message); + } + + /** + * 发送消息到 Velocity + */ + private void sendMessage(Player player, Message message) { + try { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + byte[] messageData = message.serialize(); + out.writeInt(messageData.length); + out.write(messageData); + + player.sendPluginMessage(plugin, "flashytitles:sync", out.toByteArray()); + + } catch (Exception e) { + plugin.getLogger().severe("发送插件消息失败: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index f76254d..dc4988b 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,7 +1,25 @@ -name: VelocityTitle -version: '@VERSION@' -main: top.redstarmc.plugin.velocitytitle.spigot.VelocityTitleSpigot -api-version: 1.13 -author: pingguomc -website: https://github.com/RedStarMC/VelocityTitle -loadbefore: [PlaceholderAPI] \ No newline at end of file +name: FlashyTitles +version: @VERSION@ +main: org.example.flashytitles.spigot.FlashyTitlesSpigot +api-version: 1.21 +description: FlashyTitles Spigot 端 - 群组服务器称号同步系统 +author: maple + +depend: [] +softdepend: [PlaceholderAPI] + +permissions: + flashytitles.*: + description: FlashyTitles 所有权限 + default: op + children: + flashytitles.use: true + flashytitles.admin: true + + flashytitles.use: + description: 使用基本称号功能 + default: true + + flashytitles.admin: + description: 管理员权限 + default: op diff --git a/velocity/build.gradle b/velocity/build.gradle index 5af7a7e..3dc41f1 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,14 +1,8 @@ dependencies { + // Velocity API + compileOnly 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT' + annotationProcessor 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT' + + // Core 模块 implementation project(':core') - compileOnly "com.velocitypowered:velocity-api:3.4.0-SNAPSHOT" - implementation 'com.h2database:h2:2.2.220' } - -repositories { - maven { url 'https://repo.codemc.io/repository/maven-releases/' } - maven { url 'https://repo.papermc.io/repository/maven-public/' } -} - -jar { - archiveBaseName.set("VelocityTitle-Velocity") // 基础名称 -} \ No newline at end of file diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java b/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java new file mode 100644 index 0000000..1d80019 --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java @@ -0,0 +1,105 @@ +package org.example.flashytitles.velocity; + +import com.google.inject.Inject; +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.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import org.example.flashytitles.velocity.command.TitleCommand; +import org.example.flashytitles.velocity.config.ConfigManager; +import org.example.flashytitles.velocity.listener.PlayerListener; +import org.example.flashytitles.velocity.manager.TitleManager; +import org.example.flashytitles.velocity.sync.SyncManager; +import org.slf4j.Logger; + +import java.nio.file.Path; + +@Plugin( + id = "flashy-titles", + name = "FlashyTitles", + version = "@VERSION@", + description = "群组服务器称号同步系统 - 支持动态效果和跨服同步", + authors = {"maple"} +) +public class FlashyTitlesVelocity { + + private static FlashyTitlesVelocity instance; + + private final ProxyServer server; + private final Logger logger; + private final Path dataDirectory; + + private ConfigManager configManager; + private TitleManager titleManager; + private SyncManager syncManager; + + @Inject + public FlashyTitlesVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { + instance = this; + this.server = server; + this.logger = logger; + this.dataDirectory = dataDirectory; + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + logger.info("FlashyTitles 正在启动..."); + + try { + // 初始化配置管理器 + configManager = new ConfigManager(dataDirectory, logger); + configManager.loadConfig(); + + // 初始化称号管理器 + titleManager = new TitleManager(configManager, logger); + titleManager.initialize(); + + // 初始化同步管理器 + syncManager = new SyncManager(configManager, titleManager, server, logger); + syncManager.initialize(); + + // 注册命令 + server.getCommandManager().register("title", new TitleCommand(titleManager, server, logger)); + + // 注册事件监听器 + server.getEventManager().register(this, new PlayerListener(titleManager, syncManager, logger)); + + logger.info("FlashyTitles 启动完成!"); + logger.info("- 数据库类型: {}", configManager.getDatabaseType()); + logger.info("- 同步模式: {}", configManager.isSyncEnabled() ? "启用" : "禁用"); + logger.info("- 已加载称号数量: {}", titleManager.getAllTitles().size()); + + } catch (Exception e) { + logger.error("FlashyTitles 启动失败: ", e); + } + } + + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent event) { + logger.info("FlashyTitles 正在关闭..."); + + if (syncManager != null) { + syncManager.shutdown(); + } + + if (titleManager != null) { + titleManager.shutdown(); + } + + logger.info("FlashyTitles 已关闭!"); + } + + // Static instance getter + public static FlashyTitlesVelocity getInstance() { + return instance; + } + + // Getters for other classes + public ProxyServer getServer() { return server; } + public Logger getLogger() { return logger; } + public ConfigManager getConfigManager() { return configManager; } + public TitleManager getTitleManager() { return titleManager; } + public SyncManager getSyncManager() { return syncManager; } +} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java b/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java new file mode 100644 index 0000000..bd45737 --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java @@ -0,0 +1,448 @@ +package org.example.flashytitles.velocity.command; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.example.flashytitles.core.model.Title; +import org.example.flashytitles.velocity.manager.TitleManager; +import org.slf4j.Logger; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * 称号命令处理器 + */ +public class TitleCommand implements SimpleCommand { + + private final TitleManager titleManager; + private final ProxyServer server; + private final Logger logger; + + public TitleCommand(TitleManager titleManager, ProxyServer server, Logger logger) { + this.titleManager = titleManager; + this.server = server; + this.logger = logger; + } + + @Override + public void execute(Invocation invocation) { + CommandSource source = invocation.source(); + String[] args = invocation.arguments(); + + if (args.length == 0) { + sendHelp(source); + return; + } + + String subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case "shop" -> handleShop(source); + case "buy" -> handleBuy(source, args); + case "equip" -> handleEquip(source, args); + case "unequip" -> handleUnequip(source); + case "list" -> handleList(source); + case "coins" -> handleCoins(source, args); + case "create" -> handleCreate(source, args); + case "delete" -> handleDelete(source, args); + case "give" -> handleGive(source, args); + case "revoke" -> handleRevoke(source, args); + case "reload" -> handleReload(source); + case "help" -> sendHelp(source); + default -> source.sendMessage(Component.text("未知命令!使用 /title help 查看帮助", NamedTextColor.RED)); + } + } + + @Override + public List suggest(Invocation invocation) { + String[] args = invocation.arguments(); + + if (args.length <= 1) { + List suggestions = new ArrayList<>(); + suggestions.addAll(Arrays.asList("shop", "buy", "equip", "unequip", "list", "coins", "help")); + + // 管理员命令 + if (invocation.source().hasPermission("flashytitles.admin")) { + suggestions.addAll(Arrays.asList("create", "delete", "give", "revoke", "reload")); + } + + return suggestions; + } + + String subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case "buy", "equip" -> { + // 建议可购买/装备的称号 + return new ArrayList<>(titleManager.getAllTitles().keySet()); + } + case "delete", "give", "revoke" -> { + if (invocation.source().hasPermission("flashytitles.admin")) { + return new ArrayList<>(titleManager.getAllTitles().keySet()); + } + } + case "coins" -> { + if (args.length == 2) { + return Arrays.asList("get", "add", "set"); + } + } + } + + return Collections.emptyList(); + } + + @Override + public boolean hasPermission(Invocation invocation) { + return invocation.source().hasPermission("flashytitles.use"); + } + + // ==================== 命令处理方法 ==================== + + private void handleShop(CommandSource source) { + Map titles = titleManager.getAllTitles(); + + if (titles.isEmpty()) { + source.sendMessage(Component.text("商城暂时没有称号出售", NamedTextColor.YELLOW)); + return; + } + + source.sendMessage(Component.text("=== 称号商城 ===", NamedTextColor.GOLD)); + + for (Title title : titles.values()) { + String displayText = title.getDisplayText(); + Component message = Component.text("• ", NamedTextColor.GRAY) + .append(Component.text(title.getId(), NamedTextColor.YELLOW)) + .append(Component.text(" - ", NamedTextColor.GRAY)) + .append(Component.text(title.getPrice() + " 金币", NamedTextColor.GREEN)) + .append(Component.text(" | 预览: ", NamedTextColor.GRAY)) + .append(Component.text(displayText, NamedTextColor.WHITE)); + + source.sendMessage(message); + + if (!title.getDescription().isEmpty()) { + source.sendMessage(Component.text(" " + title.getDescription(), NamedTextColor.GRAY)); + } + } + } + + private void handleBuy(CommandSource source, String[] args) { + if (!(source instanceof Player player)) { + source.sendMessage(Component.text("只有玩家可以购买称号", NamedTextColor.RED)); + return; + } + + if (args.length < 2) { + source.sendMessage(Component.text("用法: /title buy <称号ID>", NamedTextColor.RED)); + return; + } + + String titleId = args[1]; + + titleManager.purchaseTitle(player.getUniqueId(), titleId).thenAccept(result -> { + switch (result) { + case SUCCESS -> { + source.sendMessage(Component.text("成功购买称号: " + titleId, NamedTextColor.GREEN)); + source.sendMessage(Component.text("使用 /title equip " + titleId + " 来装备", NamedTextColor.YELLOW)); + } + case TITLE_NOT_FOUND -> source.sendMessage(Component.text("称号不存在: " + titleId, NamedTextColor.RED)); + case ALREADY_OWNED -> source.sendMessage(Component.text("你已经拥有这个称号了", NamedTextColor.YELLOW)); + case INSUFFICIENT_COINS -> { + Title title = titleManager.getTitle(titleId); + int needed = title != null ? title.getPrice() : 0; + int current = titleManager.getCoins(player.getUniqueId()); + source.sendMessage(Component.text("金币不足!需要: " + needed + ", 当前: " + current, NamedTextColor.RED)); + } + case NO_PERMISSION -> source.sendMessage(Component.text("你没有权限购买这个称号", NamedTextColor.RED)); + case ERROR -> source.sendMessage(Component.text("购买失败,请稍后重试", NamedTextColor.RED)); + } + }); + } + + private void handleEquip(CommandSource source, String[] args) { + if (!(source instanceof Player player)) { + source.sendMessage(Component.text("只有玩家可以装备称号", NamedTextColor.RED)); + return; + } + + if (args.length < 2) { + source.sendMessage(Component.text("用法: /title equip <称号ID>", NamedTextColor.RED)); + return; + } + + String titleId = args[1]; + + titleManager.equipTitle(player.getUniqueId(), titleId).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("成功装备称号: " + titleId, NamedTextColor.GREEN)); + } else { + source.sendMessage(Component.text("装备失败!请确认你拥有这个称号", NamedTextColor.RED)); + } + }); + } + + private void handleUnequip(CommandSource source) { + if (!(source instanceof Player player)) { + source.sendMessage(Component.text("只有玩家可以取消装备称号", NamedTextColor.RED)); + return; + } + + titleManager.unequipTitle(player.getUniqueId()).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("已取消装备称号", NamedTextColor.GREEN)); + } else { + source.sendMessage(Component.text("取消装备失败", NamedTextColor.RED)); + } + }); + } + + private void handleList(CommandSource source) { + if (!(source instanceof Player player)) { + source.sendMessage(Component.text("只有玩家可以查看拥有的称号", NamedTextColor.RED)); + return; + } + + Set ownedTitles = titleManager.getOwnedTitles(player.getUniqueId()); + String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); + + if (ownedTitles.isEmpty()) { + source.sendMessage(Component.text("你还没有任何称号", NamedTextColor.YELLOW)); + return; + } + + source.sendMessage(Component.text("=== 你的称号 ===", NamedTextColor.GOLD)); + + for (String titleId : ownedTitles) { + Title title = titleManager.getTitle(titleId); + if (title != null) { + Component message = Component.text("• ", NamedTextColor.GRAY) + .append(Component.text(titleId, NamedTextColor.YELLOW)) + .append(Component.text(" - ", NamedTextColor.GRAY)) + .append(Component.text(title.getDisplayText(), NamedTextColor.WHITE)); + + if (titleId.equals(equippedTitle)) { + message = message.append(Component.text(" [已装备]", NamedTextColor.GREEN)); + } + + source.sendMessage(message); + } + } + + source.sendMessage(Component.text("当前金币: " + titleManager.getCoins(player.getUniqueId()), NamedTextColor.AQUA)); + } + + private void handleCoins(CommandSource source, String[] args) { + if (args.length < 2) { + if (source instanceof Player player) { + int coins = titleManager.getCoins(player.getUniqueId()); + source.sendMessage(Component.text("你的金币: " + coins, NamedTextColor.AQUA)); + } else { + source.sendMessage(Component.text("用法: /title coins [玩家] [数量]", NamedTextColor.RED)); + } + return; + } + + String action = args[1].toLowerCase(); + + switch (action) { + case "get" -> { + if (source instanceof Player player) { + int coins = titleManager.getCoins(player.getUniqueId()); + source.sendMessage(Component.text("你的金币: " + coins, NamedTextColor.AQUA)); + } else { + source.sendMessage(Component.text("控制台需要指定玩家名", NamedTextColor.RED)); + } + } + case "add", "set" -> { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + if (args.length < 4) { + source.sendMessage(Component.text("用法: /title coins " + action + " <玩家> <数量>", NamedTextColor.RED)); + return; + } + + String playerName = args[2]; + Optional targetPlayer = server.getPlayer(playerName); + + if (targetPlayer.isEmpty()) { + source.sendMessage(Component.text("玩家不在线: " + playerName, NamedTextColor.RED)); + return; + } + + try { + int amount = Integer.parseInt(args[3]); + Player target = targetPlayer.get(); + + if (action.equals("add")) { + titleManager.addCoins(target.getUniqueId(), amount); + source.sendMessage(Component.text("已为 " + playerName + " 增加 " + amount + " 金币", NamedTextColor.GREEN)); + } else { + titleManager.setCoins(target.getUniqueId(), amount); + source.sendMessage(Component.text("已将 " + playerName + " 的金币设为 " + amount, NamedTextColor.GREEN)); + } + + target.sendMessage(Component.text("你的金币已更新: " + titleManager.getCoins(target.getUniqueId()), NamedTextColor.AQUA)); + + } catch (NumberFormatException e) { + source.sendMessage(Component.text("无效的数字: " + args[3], NamedTextColor.RED)); + } + } + } + } + + // ==================== 管理员命令 ==================== + + private void handleCreate(CommandSource source, String[] args) { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + if (args.length < 4) { + source.sendMessage(Component.text("用法: /title create <显示文本> <价格> [动画:true/false] [权限] [描述]", NamedTextColor.RED)); + return; + } + + String id = args[1]; + String raw = args[2]; + + try { + int price = Integer.parseInt(args[3]); + boolean animated = args.length > 4 && Boolean.parseBoolean(args[4]); + String permission = args.length > 5 ? args[5] : null; + String description = args.length > 6 ? String.join(" ", Arrays.copyOfRange(args, 6, args.length)) : ""; + + titleManager.createTitle(id, raw, price, animated, permission, description).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("成功创建称号: " + id, NamedTextColor.GREEN)); + } else { + source.sendMessage(Component.text("创建称号失败", NamedTextColor.RED)); + } + }); + + } catch (NumberFormatException e) { + source.sendMessage(Component.text("无效的价格: " + args[3], NamedTextColor.RED)); + } + } + + private void handleDelete(CommandSource source, String[] args) { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + if (args.length < 2) { + source.sendMessage(Component.text("用法: /title delete <称号ID>", NamedTextColor.RED)); + return; + } + + String titleId = args[1]; + + titleManager.deleteTitle(titleId).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("成功删除称号: " + titleId, NamedTextColor.GREEN)); + } else { + source.sendMessage(Component.text("删除称号失败", NamedTextColor.RED)); + } + }); + } + + private void handleGive(CommandSource source, String[] args) { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + if (args.length < 3) { + source.sendMessage(Component.text("用法: /title give <玩家> <称号ID>", NamedTextColor.RED)); + return; + } + + String playerName = args[1]; + String titleId = args[2]; + + Optional targetPlayer = server.getPlayer(playerName); + if (targetPlayer.isEmpty()) { + source.sendMessage(Component.text("玩家不在线: " + playerName, NamedTextColor.RED)); + return; + } + + Player target = targetPlayer.get(); + + titleManager.grantTitle(target.getUniqueId(), titleId).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("成功给予 " + playerName + " 称号: " + titleId, NamedTextColor.GREEN)); + target.sendMessage(Component.text("你获得了新称号: " + titleId, NamedTextColor.GREEN)); + } else { + source.sendMessage(Component.text("给予称号失败", NamedTextColor.RED)); + } + }); + } + + private void handleRevoke(CommandSource source, String[] args) { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + if (args.length < 3) { + source.sendMessage(Component.text("用法: /title revoke <玩家> <称号ID>", NamedTextColor.RED)); + return; + } + + String playerName = args[1]; + String titleId = args[2]; + + Optional targetPlayer = server.getPlayer(playerName); + if (targetPlayer.isEmpty()) { + source.sendMessage(Component.text("玩家不在线: " + playerName, NamedTextColor.RED)); + return; + } + + Player target = targetPlayer.get(); + + titleManager.revokeTitle(target.getUniqueId(), titleId).thenAccept(success -> { + if (success) { + source.sendMessage(Component.text("成功收回 " + playerName + " 的称号: " + titleId, NamedTextColor.GREEN)); + target.sendMessage(Component.text("你的称号被收回: " + titleId, NamedTextColor.YELLOW)); + } else { + source.sendMessage(Component.text("收回称号失败", NamedTextColor.RED)); + } + }); + } + + private void handleReload(CommandSource source) { + if (!source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); + return; + } + + // TODO: 实现重载功能 + source.sendMessage(Component.text("重载功能暂未实现", NamedTextColor.YELLOW)); + } + + private void sendHelp(CommandSource source) { + source.sendMessage(Component.text("=== FlashyTitles 帮助 ===", NamedTextColor.GOLD)); + source.sendMessage(Component.text("/title shop - 查看称号商城", NamedTextColor.YELLOW)); + source.sendMessage(Component.text("/title buy - 购买称号", NamedTextColor.YELLOW)); + source.sendMessage(Component.text("/title equip - 装备称号", NamedTextColor.YELLOW)); + source.sendMessage(Component.text("/title unequip - 取消装备称号", NamedTextColor.YELLOW)); + source.sendMessage(Component.text("/title list - 查看拥有的称号", NamedTextColor.YELLOW)); + source.sendMessage(Component.text("/title coins - 查看金币", NamedTextColor.YELLOW)); + + if (source.hasPermission("flashytitles.admin")) { + source.sendMessage(Component.text("=== 管理员命令 ===", NamedTextColor.RED)); + source.sendMessage(Component.text("/title create <文本> <价格> [动画] [权限] [描述]", NamedTextColor.GRAY)); + source.sendMessage(Component.text("/title delete - 删除称号", NamedTextColor.GRAY)); + source.sendMessage(Component.text("/title give <玩家> - 给予称号", NamedTextColor.GRAY)); + source.sendMessage(Component.text("/title revoke <玩家> - 收回称号", NamedTextColor.GRAY)); + source.sendMessage(Component.text("/title coins add/set <玩家> <数量>", NamedTextColor.GRAY)); + } + } +} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java new file mode 100644 index 0000000..3b9329f --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java @@ -0,0 +1,221 @@ +package org.example.flashytitles.velocity.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.example.flashytitles.core.database.DatabaseConfig; +import org.slf4j.Logger; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Velocity 配置管理器 + */ +public class ConfigManager { + + private final Path dataDirectory; + private final Path configFile; + private final Logger logger; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + private JsonObject config; + + public ConfigManager(Path dataDirectory, Logger logger) { + this.dataDirectory = dataDirectory; + this.configFile = dataDirectory.resolve("config.json"); + this.logger = logger; + } + + public void loadConfig() throws IOException { + // 创建数据目录 + if (!Files.exists(dataDirectory)) { + Files.createDirectories(dataDirectory); + } + + // 如果配置文件不存在,创建默认配置 + if (!Files.exists(configFile)) { + createDefaultConfig(); + } + + // 加载配置 + try (Reader reader = Files.newBufferedReader(configFile)) { + config = JsonParser.parseReader(reader).getAsJsonObject(); + } + + if (config == null) { + config = new JsonObject(); + } + + logger.info("配置文件加载完成: {}", configFile); + } + + private void createDefaultConfig() throws IOException { + JsonObject defaultConfig = new JsonObject(); + + // 数据库配置 (使用H2数据库,兼容GitHub原仓库设计) + JsonObject database = new JsonObject(); + database.addProperty("type", "h2"); // h2, mysql 或 sqlite + database.addProperty("host", "localhost"); + database.addProperty("port", 3306); + database.addProperty("database", "flashy_titles"); + database.addProperty("username", "root"); + database.addProperty("password", "password"); + database.addProperty("sqlite-file", "titles.db"); + defaultConfig.add("database", database); + + // Redis 配置 (用于实时同步) + JsonObject redis = new JsonObject(); + redis.addProperty("enabled", false); + redis.addProperty("host", "localhost"); + redis.addProperty("port", 6379); + redis.addProperty("password", ""); + redis.addProperty("database", 0); + defaultConfig.add("redis", redis); + + // 称号配置 + JsonObject titles = new JsonObject(); + titles.addProperty("animation-interval", 10); // tick间隔 + titles.addProperty("max-title-length", 32); + titles.addProperty("allow-color-codes", true); + titles.addProperty("default-starting-coins", 100); + defaultConfig.add("titles", titles); + + // 同步配置 + JsonObject sync = new JsonObject(); + sync.addProperty("enabled", true); + sync.addProperty("sync-interval", 30); // 秒 + sync.addProperty("auto-sync-on-join", true); + defaultConfig.add("sync", sync); + + // 消息配置 + JsonObject messages = new JsonObject(); + messages.addProperty("prefix", "§6[FlashyTitles] §r"); + + JsonObject msgTexts = new JsonObject(); + msgTexts.addProperty("no-permission", "§c你没有权限执行此命令!"); + msgTexts.addProperty("title-not-found", "§c称号不存在!"); + msgTexts.addProperty("title-not-owned", "§c你没有拥有这个称号!"); + msgTexts.addProperty("insufficient-coins", "§c金币不足!"); + msgTexts.addProperty("purchase-success", "§a成功购买称号: §e{title}"); + msgTexts.addProperty("equip-success", "§a成功装备称号: §e{title}"); + msgTexts.addProperty("unequip-success", "§a已取消装备称号"); + msgTexts.addProperty("title-granted", "§a已获得称号: §e{title}"); + msgTexts.addProperty("title-revoked", "§c称号已被收回: §e{title}"); + messages.add("texts", msgTexts); + + defaultConfig.add("messages", messages); + + // 保存默认配置 + try (Writer writer = Files.newBufferedWriter(configFile)) { + gson.toJson(defaultConfig, writer); + } + + config = defaultConfig; + logger.info("已创建默认配置文件: {}", configFile); + } + + // 数据库配置 + public DatabaseConfig getDatabaseConfig() { + JsonObject db = config.getAsJsonObject("database"); + return new DatabaseConfig( + db.get("type").getAsString(), + db.get("host").getAsString(), + db.get("port").getAsInt(), + db.get("database").getAsString(), + db.get("username").getAsString(), + db.get("password").getAsString(), + db.get("sqlite-file").getAsString() + ); + } + + public String getDatabaseType() { + return config.getAsJsonObject("database").get("type").getAsString(); + } + + // Redis 配置 + public boolean isRedisEnabled() { + return config.getAsJsonObject("redis").get("enabled").getAsBoolean(); + } + + public String getRedisHost() { + return config.getAsJsonObject("redis").get("host").getAsString(); + } + + public int getRedisPort() { + return config.getAsJsonObject("redis").get("port").getAsInt(); + } + + public String getRedisPassword() { + return config.getAsJsonObject("redis").get("password").getAsString(); + } + + public int getRedisDatabase() { + return config.getAsJsonObject("redis").get("database").getAsInt(); + } + + // 称号配置 + public int getAnimationInterval() { + return config.getAsJsonObject("titles").get("animation-interval").getAsInt(); + } + + public int getMaxTitleLength() { + return config.getAsJsonObject("titles").get("max-title-length").getAsInt(); + } + + public boolean isColorCodesAllowed() { + return config.getAsJsonObject("titles").get("allow-color-codes").getAsBoolean(); + } + + public int getDefaultStartingCoins() { + return config.getAsJsonObject("titles").get("default-starting-coins").getAsInt(); + } + + // 同步配置 + public boolean isSyncEnabled() { + return config.getAsJsonObject("sync").get("enabled").getAsBoolean(); + } + + public int getSyncInterval() { + return config.getAsJsonObject("sync").get("sync-interval").getAsInt(); + } + + public boolean isAutoSyncOnJoin() { + return config.getAsJsonObject("sync").get("auto-sync-on-join").getAsBoolean(); + } + + // 消息配置 + public String getMessagePrefix() { + return config.getAsJsonObject("messages").get("prefix").getAsString(); + } + + public String getMessage(String key) { + JsonObject texts = config.getAsJsonObject("messages").getAsJsonObject("texts"); + return texts.has(key) ? texts.get(key).getAsString() : "§c消息未找到: " + key; + } + + public String getMessage(String key, Map placeholders) { + String message = getMessage(key); + for (Map.Entry entry : placeholders.entrySet()) { + message = message.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return message; + } + + public String getMessage(String key, String placeholder, String value) { + Map placeholders = new HashMap<>(); + placeholders.put(placeholder, value); + return getMessage(key, placeholders); + } + + public void saveConfig() throws IOException { + try (Writer writer = Files.newBufferedWriter(configFile)) { + gson.toJson(config, writer); + } + logger.info("配置文件已保存"); + } +} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java b/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java new file mode 100644 index 0000000..9fc22c9 --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java @@ -0,0 +1,98 @@ +package org.example.flashytitles.velocity.listener; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import org.example.flashytitles.velocity.manager.TitleManager; +import org.example.flashytitles.velocity.sync.SyncManager; +import org.slf4j.Logger; + +/** + * 玩家事件监听器 + */ +public class PlayerListener { + + private static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("flashytitles", "sync"); + + private final TitleManager titleManager; + private final SyncManager syncManager; + private final Logger logger; + + public PlayerListener(TitleManager titleManager, SyncManager syncManager, Logger logger) { + this.titleManager = titleManager; + this.syncManager = syncManager; + this.logger = logger; + } + + /** + * 玩家连接到服务器时 + */ + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + Player player = event.getPlayer(); + RegisteredServer server = event.getServer(); + + logger.debug("玩家 {} 连接到服务器 {}", player.getUsername(), server.getServerInfo().getName()); + + // 确保玩家有初始金币 + if (titleManager.getCoins(player.getUniqueId()) == 0) { + // 这里可以设置初始金币,但需要从配置中获取 + // titleManager.setCoins(player.getUniqueId(), configManager.getDefaultStartingCoins()); + } + + // 同步玩家称号到新服务器 + syncManager.onPlayerJoin(player); + } + + /** + * 玩家断开连接时 + */ + @Subscribe + public void onDisconnect(DisconnectEvent event) { + Player player = event.getPlayer(); + + logger.debug("玩家 {} 断开连接", player.getUsername()); + + // 通知其他服务器玩家离开 + syncManager.onPlayerQuit(player); + } + + /** + * 处理插件消息 + */ + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + if (!event.getIdentifier().equals(CHANNEL)) { + return; + } + + // 确保消息来源是服务器 + if (!(event.getSource() instanceof ServerConnection)) { + return; + } + + // 确保目标是玩家 + if (!(event.getTarget() instanceof Player)) { + return; + } + + Player player = (Player) event.getTarget(); + ByteArrayDataInput in = ByteStreams.newDataInput(event.getData()); + + try { + syncManager.handlePluginMessage(player, in); + } catch (Exception e) { + logger.error("处理插件消息时发生错误", e); + } + + // 阻止消息继续传播 + event.setResult(PluginMessageEvent.ForwardResult.handled()); + } +} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java new file mode 100644 index 0000000..44f2c47 --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java @@ -0,0 +1,327 @@ +package org.example.flashytitles.velocity.manager; + +import org.example.flashytitles.core.database.DatabaseManager; +import org.example.flashytitles.core.model.Title; +import org.example.flashytitles.velocity.config.ConfigManager; +import org.slf4j.Logger; + +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Velocity 称号管理器 + * 负责称号的创建、删除、购买、装备等操作 + */ +public class TitleManager { + + private final ConfigManager configManager; + private final Logger logger; + private final DatabaseManager databaseManager; + private final ScheduledExecutorService scheduler; + + // 动画tick计数器 + private int animationTick = 0; + + public TitleManager(ConfigManager configManager, Logger logger) { + this.configManager = configManager; + this.logger = logger; + this.databaseManager = new DatabaseManager(configManager.getDatabaseConfig()); + this.scheduler = Executors.newScheduledThreadPool(2); + } + + /** + * 初始化管理器 + */ + public void initialize() throws SQLException { + logger.info("正在初始化称号管理器..."); + + // 初始化数据库 + databaseManager.initialize(); + + // 启动动画tick任务 + int interval = configManager.getAnimationInterval(); + scheduler.scheduleAtFixedRate(() -> { + animationTick++; + if (animationTick >= Integer.MAX_VALUE - 1000) { + animationTick = 0; // 防止溢出 + } + }, 0, interval * 50, TimeUnit.MILLISECONDS); // tick转换为毫秒 + + logger.info("称号管理器初始化完成"); + logger.info("- 动画更新间隔: {} ticks", interval); + logger.info("- 已加载称号: {} 个", getAllTitles().size()); + } + + /** + * 关闭管理器 + */ + public void shutdown() { + logger.info("正在关闭称号管理器..."); + + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + if (databaseManager != null) { + databaseManager.close(); + } + + logger.info("称号管理器已关闭"); + } + + // ==================== 称号管理 ==================== + + /** + * 创建新称号 + */ + public CompletableFuture createTitle(String id, String raw, int price, boolean animated, String permission, String description) { + return CompletableFuture.supplyAsync(() -> { + try { + // 验证称号ID + if (id == null || id.trim().isEmpty()) { + logger.warn("尝试创建空ID的称号"); + return false; + } + + // 验证称号长度 + if (raw.length() > configManager.getMaxTitleLength()) { + logger.warn("称号长度超过限制: {} > {}", raw.length(), configManager.getMaxTitleLength()); + return false; + } + + // 检查颜色代码权限 + if (!configManager.isColorCodesAllowed() && raw.contains("§")) { + logger.warn("配置不允许使用颜色代码"); + return false; + } + + Title title = new Title(id, raw, price, animated, permission, description); + databaseManager.saveTitle(title).join(); + + logger.info("创建称号: {} (价格: {}, 动画: {})", id, price, animated); + return true; + } catch (Exception e) { + logger.error("创建称号失败: " + id, e); + return false; + } + }); + } + + /** + * 删除称号 + */ + public CompletableFuture deleteTitle(String id) { + return CompletableFuture.supplyAsync(() -> { + try { + databaseManager.deleteTitle(id).join(); + logger.info("删除称号: {}", id); + return true; + } catch (Exception e) { + logger.error("删除称号失败: " + id, e); + return false; + } + }); + } + + /** + * 获取所有称号 + */ + public Map getAllTitles() { + return databaseManager.getAllTitles(); + } + + /** + * 获取指定称号 + */ + public Title getTitle(String id) { + return databaseManager.getTitle(id); + } + + /** + * 渲染称号(支持动画) + */ + public String renderTitle(String titleId) { + Title title = getTitle(titleId); + if (title == null) { + return ""; + } + return title.render(animationTick); + } + + // ==================== 玩家称号管理 ==================== + + /** + * 购买称号 + */ + public CompletableFuture purchaseTitle(UUID playerUuid, String titleId) { + return CompletableFuture.supplyAsync(() -> { + try { + Title title = getTitle(titleId); + if (title == null) { + return PurchaseResult.TITLE_NOT_FOUND; + } + + // 检查是否已拥有 + if (ownsTitle(playerUuid, titleId)) { + return PurchaseResult.ALREADY_OWNED; + } + + // 检查金币 + int playerCoins = getCoins(playerUuid); + if (playerCoins < title.getPrice()) { + return PurchaseResult.INSUFFICIENT_COINS; + } + + // 检查权限(如果有的话) + if (title.getPermission() != null && !title.getPermission().isEmpty()) { + // 这里需要权限检查,但在Velocity中比较复杂 + // 可以通过消息通道让Spigot端检查权限 + } + + // 扣除金币 + setCoins(playerUuid, playerCoins - title.getPrice()); + + // 给予称号 + databaseManager.grantTitle(playerUuid, titleId).join(); + + logger.info("玩家 {} 购买称号: {} (花费: {})", playerUuid, titleId, title.getPrice()); + return PurchaseResult.SUCCESS; + + } catch (Exception e) { + logger.error("购买称号失败: " + titleId, e); + return PurchaseResult.ERROR; + } + }); + } + + /** + * 装备称号 + */ + public CompletableFuture equipTitle(UUID playerUuid, String titleId) { + return CompletableFuture.supplyAsync(() -> { + try { + if (!ownsTitle(playerUuid, titleId)) { + return false; + } + + databaseManager.equipTitle(playerUuid, titleId).join(); + logger.info("玩家 {} 装备称号: {}", playerUuid, titleId); + return true; + } catch (Exception e) { + logger.error("装备称号失败: " + titleId, e); + return false; + } + }); + } + + /** + * 取消装备称号 + */ + public CompletableFuture unequipTitle(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + try { + databaseManager.unequipTitle(playerUuid).join(); + logger.info("玩家 {} 取消装备称号", playerUuid); + return true; + } catch (Exception e) { + logger.error("取消装备称号失败", e); + return false; + } + }); + } + + /** + * 给予称号(管理员命令) + */ + public CompletableFuture grantTitle(UUID playerUuid, String titleId) { + return CompletableFuture.supplyAsync(() -> { + try { + if (getTitle(titleId) == null) { + return false; + } + + databaseManager.grantTitle(playerUuid, titleId).join(); + logger.info("管理员给予玩家 {} 称号: {}", playerUuid, titleId); + return true; + } catch (Exception e) { + logger.error("给予称号失败: " + titleId, e); + return false; + } + }); + } + + /** + * 收回称号(管理员命令) + */ + public CompletableFuture revokeTitle(UUID playerUuid, String titleId) { + return CompletableFuture.supplyAsync(() -> { + try { + databaseManager.revokeTitle(playerUuid, titleId).join(); + logger.info("管理员收回玩家 {} 称号: {}", playerUuid, titleId); + return true; + } catch (Exception e) { + logger.error("收回称号失败: " + titleId, e); + return false; + } + }); + } + + // ==================== 查询方法 ==================== + + public Set getOwnedTitles(UUID playerUuid) { + return databaseManager.getOwnedTitles(playerUuid); + } + + public boolean ownsTitle(UUID playerUuid, String titleId) { + return databaseManager.ownsTitle(playerUuid, titleId); + } + + public String getEquippedTitle(UUID playerUuid) { + return databaseManager.getEquippedTitle(playerUuid); + } + + // ==================== 金币管理 ==================== + + public int getCoins(UUID playerUuid) { + return databaseManager.getCoins(playerUuid); + } + + public void setCoins(UUID playerUuid, int coins) { + databaseManager.setCoins(playerUuid, coins); + } + + public void addCoins(UUID playerUuid, int amount) { + databaseManager.addCoins(playerUuid, amount); + } + + // ==================== 工具方法 ==================== + + public int getCurrentAnimationTick() { + return animationTick; + } + + /** + * 购买结果枚举 + */ + public enum PurchaseResult { + SUCCESS, + TITLE_NOT_FOUND, + ALREADY_OWNED, + INSUFFICIENT_COINS, + NO_PERMISSION, + ERROR + } +} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java new file mode 100644 index 0000000..70de9da --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java @@ -0,0 +1,384 @@ +package org.example.flashytitles.velocity.sync; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.core.model.Title; +import org.example.flashytitles.velocity.config.ConfigManager; +import org.example.flashytitles.velocity.manager.TitleManager; +import org.slf4j.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPubSub; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 同步管理器 + * 负责 Velocity 和 Spigot 服务器之间的数据同步 + */ +public class SyncManager { + + private static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("flashytitles", "sync"); + + private final ConfigManager configManager; + private final TitleManager titleManager; + private final ProxyServer server; + private final Logger logger; + + private JedisPool jedisPool; + private ScheduledExecutorService scheduler; + private boolean redisEnabled; + + public SyncManager(ConfigManager configManager, TitleManager titleManager, ProxyServer server, Logger logger) { + this.configManager = configManager; + this.titleManager = titleManager; + this.server = server; + this.logger = logger; + } + + /** + * 初始化同步管理器 + */ + public void initialize() { + logger.info("正在初始化同步管理器..."); + + // 注册插件消息通道 + server.getChannelRegistrar().register(CHANNEL); + + // 初始化 Redis(如果启用) + redisEnabled = configManager.isRedisEnabled(); + if (redisEnabled) { + initializeRedis(); + } + + // 启动定时同步任务 + if (configManager.isSyncEnabled()) { + startSyncTask(); + } + + logger.info("同步管理器初始化完成"); + logger.info("- Redis 同步: {}", redisEnabled ? "启用" : "禁用"); + logger.info("- 定时同步: {}", configManager.isSyncEnabled() ? "启用" : "禁用"); + } + + /** + * 初始化 Redis 连接 + */ + private void initializeRedis() { + try { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(10); + poolConfig.setMaxIdle(5); + poolConfig.setMinIdle(1); + poolConfig.setTestOnBorrow(true); + poolConfig.setTestOnReturn(true); + + String password = configManager.getRedisPassword(); + if (password.isEmpty()) { + password = null; + } + + jedisPool = new JedisPool( + poolConfig, + configManager.getRedisHost(), + configManager.getRedisPort(), + 2000, + password, + configManager.getRedisDatabase() + ); + + // 测试连接 + try (Jedis jedis = jedisPool.getResource()) { + jedis.ping(); + logger.info("Redis 连接成功"); + } + + // 启动 Redis 订阅 + startRedisSubscription(); + + } catch (Exception e) { + logger.error("Redis 初始化失败,将禁用 Redis 同步", e); + redisEnabled = false; + if (jedisPool != null) { + jedisPool.close(); + jedisPool = null; + } + } + } + + /** + * 启动 Redis 订阅 + */ + private void startRedisSubscription() { + if (!redisEnabled || jedisPool == null) return; + + CompletableFuture.runAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + jedis.subscribe(new JedisPubSub() { + @Override + public void onMessage(String channel, String message) { + if ("flashytitles:sync".equals(channel)) { + handleRedisMessage(message); + } + } + }, "flashytitles:sync"); + } catch (Exception e) { + logger.error("Redis 订阅异常", e); + } + }); + } + + /** + * 处理 Redis 消息 + */ + private void handleRedisMessage(String messageStr) { + try { + Message message = Message.deserialize(messageStr.getBytes()); + handleSyncMessage(message); + } catch (Exception e) { + logger.error("处理 Redis 消息失败", e); + } + } + + /** + * 启动定时同步任务 + */ + private void startSyncTask() { + scheduler = Executors.newScheduledThreadPool(1); + + int interval = configManager.getSyncInterval(); + scheduler.scheduleAtFixedRate(() -> { + try { + syncAllPlayersToServers(); + } catch (Exception e) { + logger.error("定时同步任务异常", e); + } + }, interval, interval, TimeUnit.SECONDS); + + logger.info("定时同步任务已启动,间隔: {} 秒", interval); + } + + /** + * 同步所有玩家数据到所有服务器 + */ + public void syncAllPlayersToServers() { + for (Player player : server.getAllPlayers()) { + syncPlayerToAllServers(player); + } + } + + /** + * 同步玩家数据到所有服务器 + */ + public void syncPlayerToAllServers(Player player) { + String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); + + Message message = new Message(MessageType.TITLE_UPDATE) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getUsername()); + + if (equippedTitle != null) { + Title title = titleManager.getTitle(equippedTitle); + if (title != null) { + String renderedTitle = titleManager.renderTitle(equippedTitle); + message.addData("title_id", equippedTitle) + .addData("title_text", renderedTitle) + .addData("animated", title.isAnimated()); + } + } else { + message.addData("title_id", "") + .addData("title_text", "") + .addData("animated", false); + } + + // 发送到所有服务器 + broadcastMessage(message); + + // 发送到 Redis + if (redisEnabled) { + publishToRedis(message); + } + } + + /** + * 同步玩家数据到指定服务器 + */ + public void syncPlayerToServer(Player player, ServerConnection server) { + String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); + + Message message = new Message(MessageType.TITLE_UPDATE) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getUsername()); + + if (equippedTitle != null) { + Title title = titleManager.getTitle(equippedTitle); + if (title != null) { + String renderedTitle = titleManager.renderTitle(equippedTitle); + message.addData("title_id", equippedTitle) + .addData("title_text", renderedTitle) + .addData("animated", title.isAnimated()); + } + } else { + message.addData("title_id", "") + .addData("title_text", "") + .addData("animated", false); + } + + sendMessageToServer(server, message); + } + + /** + * 广播消息到所有服务器 + */ + public void broadcastMessage(Message message) { + byte[] data = createPluginMessage(message); + + for (Player player : server.getAllPlayers()) { + Optional serverConnection = player.getCurrentServer(); + if (serverConnection.isPresent()) { + serverConnection.get().sendPluginMessage(CHANNEL, data); + } + } + } + + /** + * 发送消息到指定服务器 + */ + public void sendMessageToServer(ServerConnection serverConnection, Message message) { + byte[] data = createPluginMessage(message); + serverConnection.sendPluginMessage(CHANNEL, data); + } + + /** + * 创建插件消息数据 + */ + private byte[] createPluginMessage(Message message) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + byte[] messageData = message.serialize(); + out.writeInt(messageData.length); + out.write(messageData); + return out.toByteArray(); + } + + /** + * 处理来自 Spigot 的消息 + */ + public void handlePluginMessage(Player player, ByteArrayDataInput in) { + try { + int length = in.readInt(); + byte[] messageData = new byte[length]; + in.readFully(messageData); + + Message message = Message.deserialize(messageData); + handleSyncMessage(message); + + } catch (Exception e) { + logger.error("处理插件消息失败", e); + } + } + + /** + * 处理同步消息 + */ + private void handleSyncMessage(Message message) { + switch (message.getType()) { + case PLAYER_DATA_REQUEST -> { + String playerUuidStr = message.getString("player_uuid"); + if (playerUuidStr != null) { + UUID playerUuid = UUID.fromString(playerUuidStr); + Optional player = server.getPlayer(playerUuid); + if (player.isPresent()) { + syncPlayerToAllServers(player.get()); + } + } + } + case HEARTBEAT -> { + // 心跳包,可以用于检测连接状态 + logger.debug("收到心跳包"); + } + default -> logger.debug("收到未处理的消息类型: {}", message.getType()); + } + } + + /** + * 发布消息到 Redis + */ + private void publishToRedis(Message message) { + if (!redisEnabled || jedisPool == null) return; + + try (Jedis jedis = jedisPool.getResource()) { + String messageStr = new String(message.serialize()); + jedis.publish("flashytitles:sync", messageStr); + } catch (Exception e) { + logger.error("发布 Redis 消息失败", e); + } + } + + /** + * 玩家加入时的同步处理 + */ + public void onPlayerJoin(Player player) { + if (configManager.isAutoSyncOnJoin()) { + // 延迟一点时间,确保玩家完全加入服务器 + CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS).execute(() -> { + Optional serverConnection = player.getCurrentServer(); + if (serverConnection.isPresent()) { + syncPlayerToServer(player, serverConnection.get()); + } + }); + } + } + + /** + * 玩家离开时的同步处理 + */ + public void onPlayerQuit(Player player) { + Message message = new Message(MessageType.PLAYER_QUIT) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getUsername()); + + broadcastMessage(message); + + if (redisEnabled) { + publishToRedis(message); + } + } + + /** + * 关闭同步管理器 + */ + public void shutdown() { + logger.info("正在关闭同步管理器..."); + + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + if (jedisPool != null && !jedisPool.isClosed()) { + jedisPool.close(); + } + + logger.info("同步管理器已关闭"); + } +} From bee8c1a53e3bc767bb9aa47c0b509390ef6d90bf Mon Sep 17 00:00:00 2001 From: ZhangYongxu <1553450629@qq.com> Date: Mon, 18 Aug 2025 17:30:57 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=8F=92=E4=BB=B6bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/database/DatabaseManager.java | 210 +++++++-- .../core/model/AnimationUtil.java | 53 ++- .../core/api/AbstractLoggerManager.java | 143 ------- .../core/api/AbstractTomlManager.java | 136 ------ .../velocitytitle/core/util/IOUtils.java | 86 ---- .../velocitytitle/core/util/toStrings.java | 13 - fabric-standalone/build.gradle | 62 --- fabric-standalone/gradle.properties | 6 - .../gradle/wrapper/gradle-wrapper.properties | 6 - fabric-standalone/gradlew.bat | 89 ---- fabric-standalone/settings.gradle | 10 - .../core/database/DatabaseConfig.java | 34 -- .../core/database/DatabaseManager.java | 405 ------------------ .../flashytitles/core/message/Message.java | 119 ----- .../core/message/MessageType.java | 42 -- .../core/model/AnimationUtil.java | 202 --------- .../flashytitles/core/model/Title.java | 113 ----- .../fabric/FlashyTitlesFabric.java | 79 ---- .../fabric/manager/DisplayManager.java | 220 ---------- .../flashytitles/fabric/sync/SyncHandler.java | 257 ----------- .../src/main/resources/fabric.mod.json | 29 -- .../spigot/VelocityTitleSpigot.java | 20 - .../velocity/FlashyTitlesVelocity.java | 11 +- .../velocity/command/TitleCommand.java | 74 +++- .../velocity/config/ConfigManager.java | 106 +++-- .../velocity/manager/TitleManager.java | 82 +++- .../velocity/sync/SimplifiedSyncManager.java | 346 +++++++++++++++ .../velocitytitle/velocity/Listener.java | 32 -- .../velocity/VelocityTitleVelocity.java | 121 ------ .../velocity/command/CommandBuilder.java | 30 -- .../velocity/command/CreateBuilder.java | 18 - .../velocity/configuration/Config.java | 15 - .../velocity/configuration/Language.java | 13 - .../velocity/database/DebugHandler.java | 79 ---- .../velocity/database/operate/Operate.java | 13 - .../database/operate/PlayerWearOperate.java | 20 - .../velocity/database/table/PlayerTitles.java | 56 --- .../velocity/database/table/PlayerWear.java | 58 --- .../database/table/PrefixDictionary.java | 53 --- .../database/table/SuffixDictionary.java | 53 --- .../velocity/manager/ConfigManager.java | 32 -- .../velocity/manager/EasySQLManager.java | 113 ----- .../velocity/manager/LoggerManager.java | 28 -- .../velocity/util/ColoredConsole.java | 43 -- .../src/main/resources/velocity-plugin.json | 10 +- 45 files changed, 777 insertions(+), 2963 deletions(-) delete mode 100644 core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractLoggerManager.java delete mode 100644 core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractTomlManager.java delete mode 100644 core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/IOUtils.java delete mode 100644 core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/toStrings.java delete mode 100644 fabric-standalone/build.gradle delete mode 100644 fabric-standalone/gradle.properties delete mode 100644 fabric-standalone/gradle/wrapper/gradle-wrapper.properties delete mode 100644 fabric-standalone/gradlew.bat delete mode 100644 fabric-standalone/settings.gradle delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java delete mode 100644 fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java delete mode 100644 fabric-standalone/src/main/resources/fabric.mod.json delete mode 100644 spigot/src/main/java/top/redstarmc/plugin/velocitytitle/spigot/VelocityTitleSpigot.java create mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/sync/SimplifiedSyncManager.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/Listener.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/VelocityTitleVelocity.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CommandBuilder.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CreateBuilder.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Config.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Language.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/DebugHandler.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/Operate.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/PlayerWearOperate.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerTitles.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerWear.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PrefixDictionary.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/SuffixDictionary.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/ConfigManager.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/EasySQLManager.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/LoggerManager.java delete mode 100644 velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/util/ColoredConsole.java diff --git a/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java b/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java index 95a3ec3..a91bf48 100644 --- a/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java +++ b/core/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java @@ -32,8 +32,45 @@ public DatabaseManager(DatabaseConfig config) { * 初始化数据库连接 */ public void initialize() throws SQLException { + int maxRetries = 3; + int retryDelay = 5000; // 5秒 + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + initializeConnection(); + + // 创建表 + createTables(); + + // 加载缓存 + loadCache(); + + System.out.println("数据库初始化成功 (尝试 " + attempt + "/" + maxRetries + ")"); + return; + + } catch (SQLException e) { + System.err.println("数据库初始化失败 (尝试 " + attempt + "/" + maxRetries + "): " + e.getMessage()); + + if (attempt == maxRetries) { + throw new SQLException("数据库初始化失败,已尝试 " + maxRetries + " 次", e); + } + + try { + Thread.sleep(retryDelay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new SQLException("数据库初始化被中断", ie); + } + } + } + } + + /** + * 初始化数据库连接配置 + */ + private void initializeConnection() throws SQLException { HikariConfig hikariConfig = new HikariConfig(); - + if (config.getType().equalsIgnoreCase("mysql")) { hikariConfig.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=UTC&characterEncoding=utf8", config.getHost(), config.getPort(), config.getDatabase())); @@ -41,23 +78,37 @@ public void initialize() throws SQLException { hikariConfig.setPassword(config.getPassword()); hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); } else { - hikariConfig.setJdbcUrl("jdbc:sqlite:" + config.getSqliteFile()); - hikariConfig.setDriverClassName("org.sqlite.JDBC"); + // 默认使用H2数据库 + String dbPath = config.getSqliteFile(); + if (dbPath == null || dbPath.isEmpty()) { + dbPath = "./data/flashytitles"; + } + hikariConfig.setJdbcUrl("jdbc:h2:file:" + dbPath + ";DB_CLOSE_DELAY=-1;MODE=MYSQL"); + hikariConfig.setDriverClassName("org.h2.Driver"); + hikariConfig.setUsername("sa"); + hikariConfig.setPassword(""); } - + + // 连接池配置 hikariConfig.setMaximumPoolSize(10); hikariConfig.setMinimumIdle(2); hikariConfig.setConnectionTimeout(30000); hikariConfig.setIdleTimeout(600000); hikariConfig.setMaxLifetime(1800000); - + hikariConfig.setLeakDetectionThreshold(60000); + + // 连接验证 + hikariConfig.setConnectionTestQuery("SELECT 1"); + hikariConfig.setValidationTimeout(3000); + dataSource = new HikariDataSource(hikariConfig); - - // 创建表 - createTables(); - - // 加载缓存 - loadCache(); + + // 测试连接 + try (Connection conn = dataSource.getConnection()) { + if (!conn.isValid(5)) { + throw new SQLException("数据库连接验证失败"); + } + } } /** @@ -118,7 +169,7 @@ player_uuid VARCHAR(36) PRIMARY KEY, /** * 加载缓存 */ - private void loadCache() throws SQLException { + public void loadCache() throws SQLException { try (Connection conn = dataSource.getConnection()) { // 加载称号 try (Statement stmt = conn.createStatement(); @@ -173,17 +224,19 @@ private void loadCache() throws SQLException { public CompletableFuture saveTitle(Title title) { return CompletableFuture.runAsync(() -> { try (Connection conn = dataSource.getConnection()) { - String sql = """ - INSERT INTO flashy_titles (id, raw, price, animated, permission, description) - VALUES (?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - raw = VALUES(raw), price = VALUES(price), animated = VALUES(animated), - permission = VALUES(permission), description = VALUES(description) - """; - - if (config.getType().equalsIgnoreCase("sqlite")) { + String sql; + if (config.getType().equalsIgnoreCase("mysql")) { sql = """ - INSERT OR REPLACE INTO flashy_titles (id, raw, price, animated, permission, description) + INSERT INTO flashy_titles (id, raw, price, animated, permission, description) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + raw = VALUES(raw), price = VALUES(price), animated = VALUES(animated), + permission = VALUES(permission), description = VALUES(description) + """; + } else { + // H2 和 SQLite 使用 MERGE 或 INSERT OR REPLACE + sql = """ + MERGE INTO flashy_titles (id, raw, price, animated, permission, description) VALUES (?, ?, ?, ?, ?, ?) """; } @@ -248,8 +301,11 @@ public Title getTitle(String id) { public CompletableFuture grantTitle(UUID playerUuid, String titleId) { return CompletableFuture.runAsync(() -> { try (Connection conn = dataSource.getConnection()) { - String sql = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; - if (config.getType().equalsIgnoreCase("sqlite")) { + String sql; + if (config.getType().equalsIgnoreCase("mysql")) { + sql = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + } else { + // H2 和 SQLite 使用 INSERT OR IGNORE sql = "INSERT OR IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; } @@ -359,15 +415,17 @@ public String getEquippedTitle(UUID playerUuid) { public CompletableFuture setCoins(UUID playerUuid, int coins) { return CompletableFuture.runAsync(() -> { try (Connection conn = dataSource.getConnection()) { - String sql = """ - INSERT INTO flashy_coins (player_uuid, coins, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) - """; - - if (config.getType().equalsIgnoreCase("sqlite")) { + String sql; + if (config.getType().equalsIgnoreCase("mysql")) { sql = """ - INSERT OR REPLACE INTO flashy_coins (player_uuid, coins, updated_at) + INSERT INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) + """; + } else { + // H2 和 SQLite 使用 MERGE + sql = """ + MERGE INTO flashy_coins (player_uuid, coins, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) """; } @@ -389,11 +447,95 @@ public CompletableFuture addCoins(UUID playerUuid, int amount) { int currentCoins = getCoins(playerUuid); return setCoins(playerUuid, currentCoins + amount); } - + public int getCoins(UUID playerUuid) { return coinsCache.getOrDefault(playerUuid, 0); } - + + /** + * 购买称号事务(原子操作) + */ + public CompletableFuture purchaseTitleTransaction(UUID playerUuid, String titleId, int price) { + return CompletableFuture.supplyAsync(() -> { + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + + try { + // 1. 检查并扣除金币 + String checkCoinsSQL = "SELECT coins FROM flashy_coins WHERE player_uuid = ?"; + int currentCoins = 0; + + try (PreparedStatement stmt = conn.prepareStatement(checkCoinsSQL)) { + stmt.setString(1, playerUuid.toString()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + currentCoins = rs.getInt("coins"); + } + } + } + + if (currentCoins < price) { + conn.rollback(); + return false; + } + + // 2. 扣除金币 + String updateCoinsSQL; + if (config.getType().equalsIgnoreCase("mysql")) { + updateCoinsSQL = """ + INSERT INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) + """; + } else { + // H2 和 SQLite 使用 MERGE + updateCoinsSQL = """ + MERGE INTO flashy_coins (player_uuid, coins, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """; + } + + try (PreparedStatement stmt = conn.prepareStatement(updateCoinsSQL)) { + stmt.setString(1, playerUuid.toString()); + stmt.setInt(2, Math.max(0, currentCoins - price)); + stmt.executeUpdate(); + } + + // 3. 给予称号 + String grantTitleSQL; + if (config.getType().equalsIgnoreCase("mysql")) { + grantTitleSQL = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + } else { + // H2 和 SQLite 使用 INSERT OR IGNORE + grantTitleSQL = "INSERT OR IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; + } + + try (PreparedStatement stmt = conn.prepareStatement(grantTitleSQL)) { + stmt.setString(1, playerUuid.toString()); + stmt.setString(2, titleId); + stmt.executeUpdate(); + } + + // 4. 提交事务 + conn.commit(); + + // 5. 更新缓存 + coinsCache.put(playerUuid, Math.max(0, currentCoins - price)); + ownedCache.computeIfAbsent(playerUuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); + + return true; + + } catch (SQLException e) { + conn.rollback(); + throw e; + } + + } catch (SQLException e) { + throw new RuntimeException("购买称号事务失败", e); + } + }); + } + /** * 关闭数据库连接 */ diff --git a/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java b/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java index 84f8fb2..476ba87 100644 --- a/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java +++ b/core/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java @@ -35,20 +35,33 @@ public static String renderAnimatedText(String raw, int tick) { if (raw == null || raw.isEmpty()) { return raw; } - - // 检测动画类型 - if (raw.contains("{rainbow}")) { - return renderRainbow(raw, tick); - } else if (raw.contains("{gradient}")) { - return renderGradient(raw, tick); - } else if (raw.contains("{blink}")) { - return renderBlink(raw, tick); - } else if (raw.contains("{wave}")) { - return renderWave(raw, tick); - } else if (containsMultipleColors(raw)) { - return renderColorCycle(raw, tick); + + // 限制文本长度,防止性能问题 + if (raw.length() > 256) { + raw = raw.substring(0, 256); } - + + // 确保tick值在合理范围内,防止溢出 + tick = Math.abs(tick % 10000); + + try { + // 检测动画类型 + if (raw.contains("{rainbow}")) { + return renderRainbow(raw, tick); + } else if (raw.contains("{gradient}")) { + return renderGradient(raw, tick); + } else if (raw.contains("{blink}")) { + return renderBlink(raw, tick); + } else if (raw.contains("{wave}")) { + return renderWave(raw, tick); + } else if (containsMultipleColors(raw)) { + return renderColorCycle(raw, tick); + } + } catch (Exception e) { + // 如果动画渲染失败,返回静态文本 + return getStaticDisplay(raw); + } + return raw; } @@ -70,19 +83,25 @@ public static String getStaticDisplay(String raw) { private static String renderRainbow(String raw, int tick) { String text = raw.replace("{rainbow}", ""); StringBuilder result = new StringBuilder(); - + // 移除现有颜色代码 String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); - + + // 确保有颜色数组 + if (RAINBOW_COLORS.length == 0) { + return cleanText; + } + for (int i = 0; i < cleanText.length(); i++) { char c = cleanText.charAt(i); if (c != ' ') { - int colorIndex = (tick / 2 + i) % RAINBOW_COLORS.length; + // 安全的数组访问 + int colorIndex = Math.abs((tick / 2 + i)) % RAINBOW_COLORS.length; result.append(RAINBOW_COLORS[colorIndex]); } result.append(c); } - + return result.toString(); } diff --git a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractLoggerManager.java b/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractLoggerManager.java deleted file mode 100644 index 3cc0a49..0000000 --- a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractLoggerManager.java +++ /dev/null @@ -1,143 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.core.api; - -import top.redstarmc.plugin.velocitytitle.core.util.toStrings; - -/** - *

日志管理器

- * 负责向控制台输出日志。所有日志通过 {@link #sendMessage(String...)} 输出,由子类实现来适配多平台。 - */ -public abstract class AbstractLoggerManager { - public String INFO_PREFIX; - - public boolean debugMode; - - public AbstractLoggerManager(String INFO_PREFIX) { - this(INFO_PREFIX, false); - } - - public AbstractLoggerManager(String INFO_PREFIX, boolean debugMode) { - this.INFO_PREFIX = INFO_PREFIX; - this.debugMode = debugMode; - } - - public boolean isDebugMode() { - return debugMode; - } - - /** - *

向控制台打印的方法

- * @param msg 内容 - */ - public abstract void sendMessage(String... msg); - - /** - *

发送插件普通信息

- * @param messages 字符串 - */ - public void info(String... messages) { - if (messages == null) return; - for (String message : messages) { - if (message == null) continue; - sendMessage(INFO_PREFIX + "§a[INFO] §r" + message + "§r"); - } - } - - /** - *

发送插件格式化信息

- * @param messages 字符串 - * @param objects 传入的格式化内容 - */ - public void info(String messages, Object... objects) { - if (messages == null) return; - sendMessage(INFO_PREFIX + "§a[INFO] §r" + toStrings.format(messages,objects) + "§r"); - } - - /** - *

发送插件警告信息

- * @param messages 字符串 - */ - public void warn(String... messages) { - if (messages == null) return; - for (String message : messages) { - if (message == null) continue; - sendMessage(INFO_PREFIX + "§e[WARN] §r" + message + "§r"); - } - } - - /** - *

发送插件错误信息

- * @param messages 字符串 - */ - public void error(String... messages) { - if (messages == null) return; - for (String message : messages) { - if (message == null) continue; - sendMessage(INFO_PREFIX + "§c[ERROR] §r" + message + "§r"); - } - } - - /** - *

发送插件debug信息

- * @param messages 字符串 - */ - public void debug(String... messages) { - if (messages == null) return; - if (isDebugMode()) { - for (String message : messages) { - if (message == null) continue; - sendMessage(INFO_PREFIX + "§6[DEBUG] §r" + message + "§r"); - } - } - } - - /** - *

发送插件debug堆栈

- * @param e 堆栈 - */ - public void debug(Throwable e) { - if (e == null) return; - if (isDebugMode()) - e.printStackTrace(); - } - - /** - *

同时发送插件debug信息和堆栈

- * @param e 堆栈 - * @param msg 字符串 - */ - public void debug(String msg, Throwable e) { - if (msg == null || e == null) return; - if (isDebugMode()) { - debug(msg); - debug(e); - } - } - - /** - *

发送插件 数据库 debug信息

- * @param messages 字符串 - */ - public void debugDataBase(String messages, Object... objects) { - if (messages == null) return; - if (isDebugMode()) { - sendMessage(INFO_PREFIX + "§6[DEBUG DB] §r" + toStrings.format(messages,objects) + "§r"); - } - } - - /** - * 抛出错误堆栈和错误信息 - * @param throwable 堆栈 - * @param messages 信息 - */ - public void crash(Throwable throwable, String... messages){ - for (String message : messages) { - if (message == null) continue; - sendMessage(INFO_PREFIX + "§c[ERROR] §r" + message + "§r"); - } - sendMessage(INFO_PREFIX + "§c[ERROR] §r" + "抛出错误信息 ->" + "§r"); - sendMessage(INFO_PREFIX + "§c[ERROR] §r" + throwable.getMessage() + "§r"); - sendMessage(INFO_PREFIX + "§c[ERROR] §r" + "抛出错误堆栈 ->" + "§r"); - throwable.printStackTrace(); - } - -} diff --git a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractTomlManager.java b/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractTomlManager.java deleted file mode 100644 index de14339..0000000 --- a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/api/AbstractTomlManager.java +++ /dev/null @@ -1,136 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.core.api; - -import com.moandjiezana.toml.Toml; -import top.redstarmc.plugin.velocitytitle.core.util.IOUtils; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; - -/** - *

Toml 格式配置文件管理器

- * 作为一个抽象类,使用请让子类被继承后实例化。 - * 对于不需要使用的类,重写为空即可。 - */ -public abstract class AbstractTomlManager { - - @Deprecated - private static final String d_version = "0.1.2"; - - private final File file; - - private Toml configToml; - - private final String fileName; - - private final File dataFolder; - - /** - * 构造器 - * @param dataFolder 插件的工作文件夹 - * @param fileName 要操作的配置文件名称 - */ - public AbstractTomlManager(File dataFolder, String fileName){ - this.file = new File(dataFolder,fileName); - this.dataFolder = dataFolder; - this.fileName = fileName; - } - - /** - * 尝试创建文件(包括父目录),失败则抛出异常。 - * 若成功创建则尝试读取默认配置,若已存在则不进行操作。 - * 但是并不会读取配置文件 读取配置文件请使用 {@link #loadConfig()} - */ - public void tryCreateFile(){ - if (!dataFolder.exists()) { - try { - IOUtils.createDirectory(dataFolder); - } catch (IOException e) { - throw new RuntimeException(e); //TODO 输出待测试 - } - } - if (!file.exists()) { - try { - IOUtils.createFile(file); - injectConfigFromFile(); - } catch (IOException e) { - throw new RuntimeException(e); //TODO 输出待测试 - } - } - } - - public void injectConfigFromFile(){ - - InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName); - - try { - IOUtils.copyResource(inputStream, file); - } catch (IOException e) { - System.out.println("[VelocityTitle TomlManager] 无法从 Jar 中复制默认配置到文件中"); - throw new RuntimeException(e); - } - - } - - /** - *

更新配置文件

- * 当配置文件版本不一致时,更新配置文件并备份原配置文件。 - */ - public void updateFile(){ - String readVersion = configToml.getString("version"); - - if (Objects.equals(readVersion, d_version)){ - return; - } - - try { - IOUtils.backupFile(file); //备份 - } catch (IOException e) { - System.out.println("[VelocityTitle TomlManager] 无法备份配置文件,请手动备份后重试"+e.getMessage()); - throw new RuntimeException(e); - } - - IOUtils.delFile(file); - - injectConfigFromFile(); - - loadConfig(); - } - - /** - *

读取配置

- * 适用于启动、重载时加载配置文件,会从系统重新读取文件 - */ - public void loadConfig(){ - try { - configToml = IOUtils.readToml(file); - } catch (FileNotFoundException e) { - e.printStackTrace(); //TODO 修改输出 - } - } - - /** - *

初始化配置文件

- * 子类重写即可 - */ - public void init(){ - tryCreateFile(); - - loadConfig(); - - updateFile(); - - loadConfig(); - - } - - public final File getFile() { - return file; - } - - public final Toml getConfigToml() { - return configToml; - } -} diff --git a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/IOUtils.java b/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/IOUtils.java deleted file mode 100644 index a3c8bcb..0000000 --- a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/IOUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.core.util; - -import com.moandjiezana.toml.Toml; - -import java.io.*; -import java.nio.file.Files; - -public class IOUtils { - - /** - * 创建目录(包括父目录) - * @param dir 目标目录 - * @throws IOException 目录创建失败时抛出 - */ - public static void createDirectory(File dir) throws IOException { - if (dir != null && !dir.exists() && !Files.createDirectories(dir.toPath()).toFile().exists()) { - throw new IOException("[VelocityTitle Config Loading...] 目录创建失败: " + dir.getAbsolutePath()); - } - } - - /** - * 创建文件(需确保父目录存在) - * @param file 目标文件 - * @throws IOException 文件创建失败时抛出 - */ - public static void createFile(File file) throws IOException { - if (!file.exists() && !file.createNewFile()) { - throw new IOException("[VelocityTitle Config Loading...] 文件创建失败: " + file.getName()); - } - } - - /** - * 从输入流复制到文件 - * @param inputStream 源输入流 - * @param targetFile 目标文件 - * @throws IOException 流操作失败时抛出 - */ - public static void copyResource(InputStream inputStream, File targetFile) throws IOException { - if (inputStream == null) { - throw new FileNotFoundException("[VelocityTitle Config Loading...] 资源输入流为空: " + targetFile.getName()); - } - try (InputStream is = inputStream; - OutputStream os = new FileOutputStream(targetFile)) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = is.read(buffer)) != -1) { - os.write(buffer, 0, bytesRead); - } - os.flush(); - } - } - - /** - * 备份文件(重命名为原文件名+.old) - * @param sourceFile 源文件 - * @throws IOException 备份失败时抛出 - */ - public static void backupFile(File sourceFile) throws IOException { - File backupFile = new File(sourceFile.getParent(), sourceFile.getName() + ".old"); - if (!sourceFile.renameTo(backupFile)) { - throw new IOException("[VelocityTitle Config Loading...] 文件备份失败: " + backupFile.getName()); - } - } - - /** - * 读取TOML配置文件 - * @param file 配置文件 - * @return Toml对象 - * @throws FileNotFoundException 读取或解析失败时抛出 - */ - public static Toml readToml(File file) throws FileNotFoundException { - if (!file.exists()) { - throw new FileNotFoundException("[VelocityTitle Config Loading...] 配置文件不存在: " + file.getName()); - } - return new Toml().read(file); - } - - /** - * 删除文件 - * @param file 文件 - * @return 是否成功 - */ - public static boolean delFile(File file){ - return file.delete(); - } -} diff --git a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/toStrings.java b/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/toStrings.java deleted file mode 100644 index 34d538a..0000000 --- a/core/src/main/java/top/redstarmc/plugin/velocitytitle/core/util/toStrings.java +++ /dev/null @@ -1,13 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.core.util; - -import org.slf4j.helpers.MessageFormatter; - -public class toStrings { - /** - * 字符串按照 slf4j 方式进行格式化返回 - * @return 格式化后的字符串 - */ - public static String format(String format, Object... params) { - return MessageFormatter.arrayFormat(format, params).getMessage(); - } -} diff --git a/fabric-standalone/build.gradle b/fabric-standalone/build.gradle deleted file mode 100644 index c0c42d3..0000000 --- a/fabric-standalone/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -buildscript { - repositories { - maven { url = 'https://maven.fabricmc.net/' } - gradlePluginPortal() - mavenCentral() - } - dependencies { - classpath 'net.fabricmc:fabric-loom:1.6.11' - } -} - -plugins { - id 'java' -} - -apply plugin: 'fabric-loom' - -group = 'org.example' -version = '1.0.0' -archivesBaseName = 'titles' - -repositories { - // 使用阿里云镜像加速 - maven { url = 'https://maven.aliyun.com/repository/central' } - maven { url = 'https://maven.aliyun.com/repository/public' } - maven { url = 'https://maven.fabricmc.net/' } - - // 备用镜像 - mavenCentral() -} - -dependencies { - minecraft "com.mojang:minecraft:1.21.1" - mappings "net.fabricmc:yarn:1.21.1+build.3:v2" - modImplementation "net.fabricmc:fabric-loader:0.16.5" - modImplementation "net.fabricmc.fabric-api:fabric-api:0.105.0+1.21.1" - - // 核心依赖 - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'org.slf4j:slf4j-api:2.0.7' - implementation 'com.zaxxer:HikariCP:5.0.1' - implementation 'mysql:mysql-connector-java:8.0.33' - implementation 'org.xerial:sqlite-jdbc:3.42.0.0' - implementation 'redis.clients:jedis:4.4.3' -} - -java { - toolchain { languageVersion = JavaLanguageVersion.of(21) } - withSourcesJar() -} - -loom { - splitEnvironmentSourceSets() -} - -processResources { - inputs.property "version", project.version - - filesMatching("fabric.mod.json") { - expand "version": project.version - } -} diff --git a/fabric-standalone/gradle.properties b/fabric-standalone/gradle.properties deleted file mode 100644 index 9abdf96..0000000 --- a/fabric-standalone/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -org.gradle.jvmargs=-Xmx2G -org.gradle.parallel=true -org.gradle.caching=true - -# ?? Java ?? -org.gradle.java.home=C:/Users/Administrator/.jdks/ms-21.0.8 diff --git a/fabric-standalone/gradle/wrapper/gradle-wrapper.properties b/fabric-standalone/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index aafee52..0000000 --- a/fabric-standalone/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Aug 18 09:29:39 CST 2025 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.6-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/fabric-standalone/gradlew.bat b/fabric-standalone/gradlew.bat deleted file mode 100644 index 107acd3..0000000 --- a/fabric-standalone/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/fabric-standalone/settings.gradle b/fabric-standalone/settings.gradle deleted file mode 100644 index f59a8fa..0000000 --- a/fabric-standalone/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven { url = 'https://maven.fabricmc.net/' } - maven { url = 'https://maven.aliyun.com/repository/gradle-plugin' } - maven { url = 'https://maven.aliyun.com/repository/public' } - } -} - -rootProject.name = 'flashy-titles-fabric' diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java deleted file mode 100644 index 25cf8d4..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.example.flashytitles.core.database; - -/** - * 数据库配置类 - */ -public class DatabaseConfig { - private final String type; - private final String host; - private final int port; - private final String database; - private final String username; - private final String password; - private final String sqliteFile; - - public DatabaseConfig(String type, String host, int port, String database, - String username, String password, String sqliteFile) { - this.type = type; - this.host = host; - this.port = port; - this.database = database; - this.username = username; - this.password = password; - this.sqliteFile = sqliteFile; - } - - // Getters - public String getType() { return type; } - public String getHost() { return host; } - public int getPort() { return port; } - public String getDatabase() { return database; } - public String getUsername() { return username; } - public String getPassword() { return password; } - public String getSqliteFile() { return sqliteFile; } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java deleted file mode 100644 index 95a3ec3..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/database/DatabaseManager.java +++ /dev/null @@ -1,405 +0,0 @@ -package org.example.flashytitles.core.database; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.example.flashytitles.core.model.Title; - -import java.sql.*; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 数据库管理器 - * 支持 MySQL 和 SQLite - */ -public class DatabaseManager { - - private final DatabaseConfig config; - private HikariDataSource dataSource; - - // 缓存 - private final Map titleCache = new ConcurrentHashMap<>(); - private final Map> ownedCache = new ConcurrentHashMap<>(); - private final Map equippedCache = new ConcurrentHashMap<>(); - private final Map coinsCache = new ConcurrentHashMap<>(); - - public DatabaseManager(DatabaseConfig config) { - this.config = config; - } - - /** - * 初始化数据库连接 - */ - public void initialize() throws SQLException { - HikariConfig hikariConfig = new HikariConfig(); - - if (config.getType().equalsIgnoreCase("mysql")) { - hikariConfig.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=UTC&characterEncoding=utf8", - config.getHost(), config.getPort(), config.getDatabase())); - hikariConfig.setUsername(config.getUsername()); - hikariConfig.setPassword(config.getPassword()); - hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); - } else { - hikariConfig.setJdbcUrl("jdbc:sqlite:" + config.getSqliteFile()); - hikariConfig.setDriverClassName("org.sqlite.JDBC"); - } - - hikariConfig.setMaximumPoolSize(10); - hikariConfig.setMinimumIdle(2); - hikariConfig.setConnectionTimeout(30000); - hikariConfig.setIdleTimeout(600000); - hikariConfig.setMaxLifetime(1800000); - - dataSource = new HikariDataSource(hikariConfig); - - // 创建表 - createTables(); - - // 加载缓存 - loadCache(); - } - - /** - * 创建数据库表 - */ - private void createTables() throws SQLException { - try (Connection conn = dataSource.getConnection()) { - // 称号表 - String createTitlesTable = """ - CREATE TABLE IF NOT EXISTS flashy_titles ( - id VARCHAR(64) PRIMARY KEY, - raw TEXT NOT NULL, - price INT NOT NULL DEFAULT 0, - animated BOOLEAN NOT NULL DEFAULT FALSE, - permission VARCHAR(128), - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """; - - // 玩家拥有的称号表 - String createOwnedTable = """ - CREATE TABLE IF NOT EXISTS flashy_owned_titles ( - player_uuid VARCHAR(36) NOT NULL, - title_id VARCHAR(64) NOT NULL, - obtained_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (player_uuid, title_id) - ) - """; - - // 玩家装备的称号表 - String createEquippedTable = """ - CREATE TABLE IF NOT EXISTS flashy_equipped_titles ( - player_uuid VARCHAR(36) PRIMARY KEY, - title_id VARCHAR(64), - equipped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """; - - // 玩家金币表 - String createCoinsTable = """ - CREATE TABLE IF NOT EXISTS flashy_coins ( - player_uuid VARCHAR(36) PRIMARY KEY, - coins INT NOT NULL DEFAULT 0, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """; - - try (Statement stmt = conn.createStatement()) { - stmt.execute(createTitlesTable); - stmt.execute(createOwnedTable); - stmt.execute(createEquippedTable); - stmt.execute(createCoinsTable); - } - } - } - - /** - * 加载缓存 - */ - private void loadCache() throws SQLException { - try (Connection conn = dataSource.getConnection()) { - // 加载称号 - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_titles")) { - while (rs.next()) { - Title title = new Title( - rs.getString("id"), - rs.getString("raw"), - rs.getInt("price"), - rs.getBoolean("animated"), - rs.getString("permission"), - rs.getString("description") - ); - titleCache.put(title.getId(), title); - } - } - - // 加载拥有的称号 - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_owned_titles")) { - while (rs.next()) { - UUID uuid = UUID.fromString(rs.getString("player_uuid")); - String titleId = rs.getString("title_id"); - ownedCache.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); - } - } - - // 加载装备的称号 - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_equipped_titles WHERE title_id IS NOT NULL")) { - while (rs.next()) { - UUID uuid = UUID.fromString(rs.getString("player_uuid")); - String titleId = rs.getString("title_id"); - equippedCache.put(uuid, titleId); - } - } - - // 加载金币 - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM flashy_coins")) { - while (rs.next()) { - UUID uuid = UUID.fromString(rs.getString("player_uuid")); - int coins = rs.getInt("coins"); - coinsCache.put(uuid, coins); - } - } - } - } - - // ==================== 称号管理 ==================== - - public CompletableFuture saveTitle(Title title) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - String sql = """ - INSERT INTO flashy_titles (id, raw, price, animated, permission, description) - VALUES (?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - raw = VALUES(raw), price = VALUES(price), animated = VALUES(animated), - permission = VALUES(permission), description = VALUES(description) - """; - - if (config.getType().equalsIgnoreCase("sqlite")) { - sql = """ - INSERT OR REPLACE INTO flashy_titles (id, raw, price, animated, permission, description) - VALUES (?, ?, ?, ?, ?, ?) - """; - } - - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, title.getId()); - stmt.setString(2, title.getRaw()); - stmt.setInt(3, title.getPrice()); - stmt.setBoolean(4, title.isAnimated()); - stmt.setString(5, title.getPermission()); - stmt.setString(6, title.getDescription()); - stmt.executeUpdate(); - } - - titleCache.put(title.getId(), title); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public CompletableFuture deleteTitle(String titleId) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - // 删除称号 - try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_titles WHERE id = ?")) { - stmt.setString(1, titleId); - stmt.executeUpdate(); - } - - // 删除相关的拥有记录 - try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE title_id = ?")) { - stmt.setString(1, titleId); - stmt.executeUpdate(); - } - - // 删除相关的装备记录 - try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE title_id = ?")) { - stmt.setString(1, titleId); - stmt.executeUpdate(); - } - - titleCache.remove(titleId); - ownedCache.values().forEach(set -> set.remove(titleId)); - equippedCache.entrySet().removeIf(entry -> titleId.equals(entry.getValue())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public Map getAllTitles() { - return new HashMap<>(titleCache); - } - - public Title getTitle(String id) { - return titleCache.get(id); - } - - // ==================== 玩家称号管理 ==================== - - public CompletableFuture grantTitle(UUID playerUuid, String titleId) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - String sql = "INSERT IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; - if (config.getType().equalsIgnoreCase("sqlite")) { - sql = "INSERT OR IGNORE INTO flashy_owned_titles (player_uuid, title_id) VALUES (?, ?)"; - } - - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, playerUuid.toString()); - stmt.setString(2, titleId); - stmt.executeUpdate(); - } - - ownedCache.computeIfAbsent(playerUuid, k -> ConcurrentHashMap.newKeySet()).add(titleId); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public CompletableFuture revokeTitle(UUID playerUuid, String titleId) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - // 删除拥有记录 - try (PreparedStatement stmt = conn.prepareStatement("DELETE FROM flashy_owned_titles WHERE player_uuid = ? AND title_id = ?")) { - stmt.setString(1, playerUuid.toString()); - stmt.setString(2, titleId); - stmt.executeUpdate(); - } - - // 如果装备了这个称号,取消装备 - if (titleId.equals(equippedCache.get(playerUuid))) { - try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { - stmt.setString(1, playerUuid.toString()); - stmt.executeUpdate(); - } - equippedCache.remove(playerUuid); - } - - Set owned = ownedCache.get(playerUuid); - if (owned != null) { - owned.remove(titleId); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public Set getOwnedTitles(UUID playerUuid) { - return new HashSet<>(ownedCache.getOrDefault(playerUuid, Collections.emptySet())); - } - - public boolean ownsTitle(UUID playerUuid, String titleId) { - Set owned = ownedCache.get(playerUuid); - return owned != null && owned.contains(titleId); - } - - // ==================== 装备称号管理 ==================== - - public CompletableFuture equipTitle(UUID playerUuid, String titleId) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - String sql = """ - INSERT INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON DUPLICATE KEY UPDATE title_id = VALUES(title_id), equipped_at = VALUES(equipped_at) - """; - - if (config.getType().equalsIgnoreCase("sqlite")) { - sql = """ - INSERT OR REPLACE INTO flashy_equipped_titles (player_uuid, title_id, equipped_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - """; - } - - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, playerUuid.toString()); - stmt.setString(2, titleId); - stmt.executeUpdate(); - } - - equippedCache.put(playerUuid, titleId); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public CompletableFuture unequipTitle(UUID playerUuid) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("UPDATE flashy_equipped_titles SET title_id = NULL WHERE player_uuid = ?")) { - stmt.setString(1, playerUuid.toString()); - stmt.executeUpdate(); - } - - equippedCache.remove(playerUuid); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public String getEquippedTitle(UUID playerUuid) { - return equippedCache.get(playerUuid); - } - - // ==================== 金币管理 ==================== - - public CompletableFuture setCoins(UUID playerUuid, int coins) { - return CompletableFuture.runAsync(() -> { - try (Connection conn = dataSource.getConnection()) { - String sql = """ - INSERT INTO flashy_coins (player_uuid, coins, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - ON DUPLICATE KEY UPDATE coins = VALUES(coins), updated_at = VALUES(updated_at) - """; - - if (config.getType().equalsIgnoreCase("sqlite")) { - sql = """ - INSERT OR REPLACE INTO flashy_coins (player_uuid, coins, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - """; - } - - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, playerUuid.toString()); - stmt.setInt(2, Math.max(0, coins)); - stmt.executeUpdate(); - } - - coinsCache.put(playerUuid, Math.max(0, coins)); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - public CompletableFuture addCoins(UUID playerUuid, int amount) { - int currentCoins = getCoins(playerUuid); - return setCoins(playerUuid, currentCoins + amount); - } - - public int getCoins(UUID playerUuid) { - return coinsCache.getOrDefault(playerUuid, 0); - } - - /** - * 关闭数据库连接 - */ - public void close() { - if (dataSource != null && !dataSource.isClosed()) { - dataSource.close(); - } - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java deleted file mode 100644 index f860616..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/Message.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.example.flashytitles.core.message; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -/** - * 消息类 - * 用于 Velocity 和 Spigot 之间的数据传输 - */ -public class Message { - private static final Gson GSON = new Gson(); - - private final MessageType type; - private final JsonObject data; - private final long timestamp; - - public Message(MessageType type, JsonObject data) { - this.type = type; - this.data = data != null ? data : new JsonObject(); - this.timestamp = System.currentTimeMillis(); - } - - public Message(MessageType type) { - this(type, new JsonObject()); - } - - // Getters - public MessageType getType() { return type; } - public JsonObject getData() { return data; } - public long getTimestamp() { return timestamp; } - - // 数据操作方法 - public Message addData(String key, String value) { - data.addProperty(key, value); - return this; - } - - public Message addData(String key, int value) { - data.addProperty(key, value); - return this; - } - - public Message addData(String key, boolean value) { - data.addProperty(key, value); - return this; - } - - public Message addData(String key, JsonObject value) { - data.add(key, value); - return this; - } - - public String getString(String key) { - return data.has(key) ? data.get(key).getAsString() : null; - } - - public int getInt(String key) { - return data.has(key) ? data.get(key).getAsInt() : 0; - } - - public boolean getBoolean(String key) { - return data.has(key) && data.get(key).getAsBoolean(); - } - - public JsonObject getObject(String key) { - return data.has(key) ? data.getAsJsonObject(key) : null; - } - - /** - * 序列化为字节数组 - */ - public byte[] serialize() { - JsonObject json = new JsonObject(); - json.addProperty("type", type.getId()); - json.add("data", data); - json.addProperty("timestamp", timestamp); - - return GSON.toJson(json).getBytes(); - } - - /** - * 从字节数组反序列化 - */ - public static Message deserialize(byte[] bytes) { - try { - String jsonStr = new String(bytes); - JsonObject json = JsonParser.parseString(jsonStr).getAsJsonObject(); - - String typeId = json.get("type").getAsString(); - MessageType type = MessageType.fromId(typeId); - if (type == null) { - throw new IllegalArgumentException("Unknown message type: " + typeId); - } - - JsonObject data = json.has("data") ? json.getAsJsonObject("data") : new JsonObject(); - - Message message = new Message(type, data); - // 设置时间戳(如果有的话) - if (json.has("timestamp")) { - // 这里我们不能直接设置timestamp,因为它是final的 - // 但这不影响功能,因为时间戳主要用于调试 - } - - return message; - } catch (Exception e) { - throw new RuntimeException("Failed to deserialize message", e); - } - } - - @Override - public String toString() { - return "Message{" + - "type=" + type + - ", data=" + data + - ", timestamp=" + timestamp + - '}'; - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java deleted file mode 100644 index e5a74de..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/message/MessageType.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.example.flashytitles.core.message; - -/** - * 消息类型枚举 - * 用于 Velocity 和 Spigot 之间的通信 - */ -public enum MessageType { - // 称号相关 - TITLE_UPDATE("title_update"), // 更新玩家称号 - TITLE_REMOVE("title_remove"), // 移除玩家称号 - TITLE_SYNC("title_sync"), // 同步称号数据 - - // 玩家数据相关 - PLAYER_JOIN("player_join"), // 玩家加入服务器 - PLAYER_QUIT("player_quit"), // 玩家离开服务器 - PLAYER_DATA_REQUEST("player_data_req"), // 请求玩家数据 - PLAYER_DATA_RESPONSE("player_data_res"), // 响应玩家数据 - - // 系统相关 - RELOAD_CONFIG("reload_config"), // 重载配置 - SYNC_ALL("sync_all"), // 同步所有数据 - HEARTBEAT("heartbeat"); // 心跳包 - - private final String id; - - MessageType(String id) { - this.id = id; - } - - public String getId() { - return id; - } - - public static MessageType fromId(String id) { - for (MessageType type : values()) { - if (type.id.equals(id)) { - return type; - } - } - return null; - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java deleted file mode 100644 index 84f8fb2..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/AnimationUtil.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.example.flashytitles.core.model; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * 动画工具类 - * 处理称号的动态效果 - */ -public class AnimationUtil { - - // 颜色代码映射 - private static final String[] COLORS = { - "§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7", - "§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f" - }; - - // 彩虹色序列 - private static final String[] RAINBOW_COLORS = { - "§c", "§6", "§e", "§a", "§b", "§9", "§d" - }; - - // 渐变色序列 - private static final String[] GRADIENT_COLORS = { - "§c", "§6", "§e", "§f", "§e", "§6", "§c" - }; - - /** - * 渲染动画文本 - * @param raw 原始文本 - * @param tick 当前tick值 - * @return 渲染后的文本 - */ - public static String renderAnimatedText(String raw, int tick) { - if (raw == null || raw.isEmpty()) { - return raw; - } - - // 检测动画类型 - if (raw.contains("{rainbow}")) { - return renderRainbow(raw, tick); - } else if (raw.contains("{gradient}")) { - return renderGradient(raw, tick); - } else if (raw.contains("{blink}")) { - return renderBlink(raw, tick); - } else if (raw.contains("{wave}")) { - return renderWave(raw, tick); - } else if (containsMultipleColors(raw)) { - return renderColorCycle(raw, tick); - } - - return raw; - } - - /** - * 获取静态显示文本(移除动画标记) - */ - public static String getStaticDisplay(String raw) { - if (raw == null) return ""; - - return raw.replaceAll("\\{rainbow\\}", "") - .replaceAll("\\{gradient\\}", "") - .replaceAll("\\{blink\\}", "") - .replaceAll("\\{wave\\}", ""); - } - - /** - * 彩虹效果 - */ - private static String renderRainbow(String raw, int tick) { - String text = raw.replace("{rainbow}", ""); - StringBuilder result = new StringBuilder(); - - // 移除现有颜色代码 - String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); - - for (int i = 0; i < cleanText.length(); i++) { - char c = cleanText.charAt(i); - if (c != ' ') { - int colorIndex = (tick / 2 + i) % RAINBOW_COLORS.length; - result.append(RAINBOW_COLORS[colorIndex]); - } - result.append(c); - } - - return result.toString(); - } - - /** - * 渐变效果 - */ - private static String renderGradient(String raw, int tick) { - String text = raw.replace("{gradient}", ""); - StringBuilder result = new StringBuilder(); - - String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); - - for (int i = 0; i < cleanText.length(); i++) { - char c = cleanText.charAt(i); - if (c != ' ') { - int colorIndex = (tick / 3 + i) % GRADIENT_COLORS.length; - result.append(GRADIENT_COLORS[colorIndex]); - } - result.append(c); - } - - return result.toString(); - } - - /** - * 闪烁效果 - */ - private static String renderBlink(String raw, int tick) { - String text = raw.replace("{blink}", ""); - - // 每20tick闪烁一次 - if ((tick / 10) % 2 == 0) { - return "§f" + text; - } else { - return "§7" + text; - } - } - - /** - * 波浪效果 - */ - private static String renderWave(String raw, int tick) { - String text = raw.replace("{wave}", ""); - StringBuilder result = new StringBuilder(); - - String cleanText = text.replaceAll("§[0-9a-fk-or]", ""); - - for (int i = 0; i < cleanText.length(); i++) { - char c = cleanText.charAt(i); - if (c != ' ') { - // 使用正弦波计算颜色 - double wave = Math.sin((tick + i * 2) * 0.1); - if (wave > 0.5) { - result.append("§b"); - } else if (wave > 0) { - result.append("§3"); - } else if (wave > -0.5) { - result.append("§1"); - } else { - result.append("§9"); - } - } - result.append(c); - } - - return result.toString(); - } - - /** - * 颜色循环效果(检测到多个颜色代码时) - */ - private static String renderColorCycle(String raw, int tick) { - // 提取所有颜色代码 - Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); - Matcher matcher = pattern.matcher(raw); - - StringBuilder colors = new StringBuilder(); - while (matcher.find()) { - colors.append(matcher.group()); - } - - if (colors.length() < 4) { // 至少需要2个颜色代码 - return raw; - } - - // 循环替换颜色 - String result = raw; - int offset = (tick / 5) % (colors.length() / 2); - - for (int i = 0; i < colors.length(); i += 2) { - if (i + 1 < colors.length()) { - int newIndex = (i + offset * 2) % colors.length(); - if (newIndex + 1 < colors.length()) { - String oldColor = colors.substring(i, i + 2); - String newColor = colors.substring(newIndex, newIndex + 2); - result = result.replaceFirst(Pattern.quote(oldColor), newColor); - } - } - } - - return result; - } - - /** - * 检测是否包含多个颜色代码 - */ - private static boolean containsMultipleColors(String text) { - Pattern pattern = Pattern.compile("§[0-9a-fk-or]"); - Matcher matcher = pattern.matcher(text); - int count = 0; - while (matcher.find()) { - count++; - if (count >= 2) return true; - } - return false; - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java b/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java deleted file mode 100644 index 83d55a3..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/core/model/Title.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.example.flashytitles.core.model; - -import com.google.gson.JsonObject; - -import java.util.Objects; - -/** - * 称号模型类 - * 支持动态效果和颜色变化 - */ -public class Title { - private final String id; - private final String raw; - private final int price; - private final boolean animated; - private final String permission; - private final String description; - - public Title(String id, String raw, int price) { - this(id, raw, price, false, null, ""); - } - - public Title(String id, String raw, int price, boolean animated, String permission, String description) { - this.id = id; - this.raw = raw; - this.price = price; - this.animated = animated; - this.permission = permission; - this.description = description != null ? description : ""; - } - - /** - * 渲染称号文本,支持动态效果 - * @param tick 当前tick值,用于动画计算 - * @return 渲染后的文本 - */ - public String render(int tick) { - if (!animated) { - return raw; - } - - // 动态效果实现 - return AnimationUtil.renderAnimatedText(raw, tick); - } - - /** - * 获取静态显示文本(用于GUI等场景) - */ - public String getDisplayText() { - return AnimationUtil.getStaticDisplay(raw); - } - - /** - * 转换为JSON对象 - */ - public JsonObject toJson() { - JsonObject json = new JsonObject(); - json.addProperty("id", id); - json.addProperty("raw", raw); - json.addProperty("price", price); - json.addProperty("animated", animated); - if (permission != null) { - json.addProperty("permission", permission); - } - json.addProperty("description", description); - return json; - } - - /** - * 从JSON对象创建称号 - */ - public static Title fromJson(JsonObject json) { - String id = json.get("id").getAsString(); - String raw = json.get("raw").getAsString(); - int price = json.get("price").getAsInt(); - boolean animated = json.has("animated") ? json.get("animated").getAsBoolean() : false; - String permission = json.has("permission") ? json.get("permission").getAsString() : null; - String description = json.has("description") ? json.get("description").getAsString() : ""; - - return new Title(id, raw, price, animated, permission, description); - } - - // Getters - public String getId() { return id; } - public String getRaw() { return raw; } - public int getPrice() { return price; } - public boolean isAnimated() { return animated; } - public String getPermission() { return permission; } - public String getDescription() { return description; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Title title = (Title) o; - return Objects.equals(id, title.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "Title{" + - "id='" + id + '\'' + - ", raw='" + raw + '\'' + - ", price=" + price + - ", animated=" + animated + - '}'; - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java deleted file mode 100644 index 460894f..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/FlashyTitlesFabric.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.example.flashytitles.fabric; - -import net.fabricmc.api.ModInitializer; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; -import org.example.flashytitles.fabric.manager.DisplayManager; -import org.example.flashytitles.fabric.sync.SyncHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * FlashyTitles Fabric 模组主类 - */ -public class FlashyTitlesFabric implements ModInitializer { - - public static final String MOD_ID = "flashy-titles"; - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - private static DisplayManager displayManager; - private static SyncHandler syncHandler; - - @Override - public void onInitialize() { - LOGGER.info("FlashyTitles Fabric 正在启动..."); - - // 注册服务器生命周期事件 - ServerLifecycleEvents.SERVER_STARTING.register(server -> { - try { - // 初始化显示管理器 - displayManager = new DisplayManager(server); - displayManager.initialize(); - - // 初始化同步处理器 - syncHandler = new SyncHandler(server, displayManager); - syncHandler.initialize(); - - LOGGER.info("FlashyTitles Fabric 启动完成!"); - - } catch (Exception e) { - LOGGER.error("FlashyTitles Fabric 启动失败", e); - } - }); - - ServerLifecycleEvents.SERVER_STOPPING.register(server -> { - LOGGER.info("FlashyTitles Fabric 正在关闭..."); - - if (displayManager != null) { - displayManager.shutdown(); - } - - if (syncHandler != null) { - syncHandler.shutdown(); - } - - LOGGER.info("FlashyTitles Fabric 已关闭!"); - }); - - // 注册玩家连接事件 - ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - if (syncHandler != null) { - syncHandler.onPlayerJoin(handler.getPlayer()); - } - }); - - ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { - if (displayManager != null) { - displayManager.removePlayerTitle(handler.getPlayer()); - } - }); - } - - public static DisplayManager getDisplayManager() { - return displayManager; - } - - public static SyncHandler getSyncHandler() { - return syncHandler; - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java deleted file mode 100644 index 5553110..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/manager/DisplayManager.java +++ /dev/null @@ -1,220 +0,0 @@ -package org.example.flashytitles.fabric.manager; - -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.scoreboard.ServerScoreboard; -import net.minecraft.scoreboard.Team; -import net.minecraft.text.Text; -import org.example.flashytitles.core.model.AnimationUtil; -import org.example.flashytitles.fabric.FlashyTitlesFabric; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Fabric 显示管理器 - * 负责在 Fabric 服务器上显示玩家称号 - */ -public class DisplayManager { - - private final MinecraftServer server; - private final Map playerTitles = new ConcurrentHashMap<>(); - - private int animationTick = 0; - - public DisplayManager(MinecraftServer server) { - this.server = server; - } - - /** - * 初始化显示管理器 - */ - public void initialize() { - FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric 显示管理器..."); - - // 启动动画更新任务 - 每10tick(0.5秒)更新一次 - // 使用简单的线程来处理动画更新 - new Thread(() -> { - while (!server.isStopped()) { - try { - Thread.sleep(500); // 0.5秒更新一次 - animationTick++; - if (animationTick >= Integer.MAX_VALUE - 1000) { - animationTick = 0; - } - updateAnimatedTitles(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - }).start(); - - FlashyTitlesFabric.LOGGER.info("Fabric 显示管理器初始化完成"); - } - - /** - * 关闭显示管理器 - */ - public void shutdown() { - FlashyTitlesFabric.LOGGER.info("正在关闭 Fabric 显示管理器..."); - - // 清理所有玩家的称号显示 - for (UUID uuid : playerTitles.keySet()) { - ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); - if (player != null) { - removePlayerTitle(player); - } - } - - playerTitles.clear(); - - FlashyTitlesFabric.LOGGER.info("Fabric 显示管理器已关闭"); - } - - /** - * 设置玩家称号 - */ - public void setPlayerTitle(ServerPlayerEntity player, String titleId, String titleText, boolean animated) { - UUID uuid = player.getUuid(); - - // 移除旧的称号显示 - removePlayerTitle(player); - - if (titleText == null || titleText.trim().isEmpty()) { - return; - } - - // 保存称号数据 - PlayerTitleData titleData = new PlayerTitleData(titleId, titleText, animated); - playerTitles.put(uuid, titleData); - - // 应用称号显示 - applyTitleDisplay(player, titleText); - - FlashyTitlesFabric.LOGGER.info("为玩家 {} 设置称号: {} (动画: {})", player.getName().getString(), titleId, animated); - } - - /** - * 移除玩家称号 - */ - public void removePlayerTitle(ServerPlayerEntity player) { - UUID uuid = player.getUuid(); - - // 移除称号数据 - playerTitles.remove(uuid); - - // 移除团队显示 - ServerScoreboard scoreboard = server.getScoreboard(); - String teamName = "ft_" + uuid.toString().substring(0, 8); - Team team = scoreboard.getTeam(teamName); - - if (team != null) { - if (team.getPlayerList().contains(player.getName().getString())) { - scoreboard.removeScoreHolderFromTeam(player.getName().getString(), team); - } - if (team.getPlayerList().isEmpty()) { - scoreboard.removeTeam(team); - } - } - - FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号显示", player.getName().getString()); - } - - /** - * 应用称号显示 - */ - private void applyTitleDisplay(ServerPlayerEntity player, String titleText) { - ServerScoreboard scoreboard = server.getScoreboard(); - - // 创建或获取团队 - String teamName = "ft_" + player.getUuid().toString().substring(0, 8); - Team team = scoreboard.getTeam(teamName); - - if (team == null) { - team = scoreboard.addTeam(teamName); - } - - // 设置前缀(称号) - String prefix = titleText; - if (prefix.length() > 64) { - prefix = prefix.substring(0, 64); // 限制长度 - } - - team.setPrefix(Text.literal(prefix + " ")); - - // 添加玩家到团队 - if (!team.getPlayerList().contains(player.getName().getString())) { - scoreboard.addScoreHolderToTeam(player.getName().getString(), team); - } - } - - /** - * 更新动画称号 - */ - private void updateAnimatedTitles() { - for (Map.Entry entry : playerTitles.entrySet()) { - UUID uuid = entry.getKey(); - PlayerTitleData titleData = entry.getValue(); - - if (!titleData.isAnimated()) { - continue; - } - - ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); - if (player == null) { - continue; - } - - // 渲染动画文本 - String animatedText = AnimationUtil.renderAnimatedText(titleData.getRawText(), animationTick); - - // 更新显示 - ServerScoreboard scoreboard = server.getScoreboard(); - String teamName = "ft_" + uuid.toString().substring(0, 8); - Team team = scoreboard.getTeam(teamName); - - if (team != null) { - String prefix = animatedText; - if (prefix.length() > 64) { - prefix = prefix.substring(0, 64); - } - team.setPrefix(Text.literal(prefix + " ")); - } - } - } - - /** - * 获取玩家当前称号数据 - */ - public PlayerTitleData getPlayerTitleData(UUID uuid) { - return playerTitles.get(uuid); - } - - /** - * 检查玩家是否有称号 - */ - public boolean hasTitle(UUID uuid) { - return playerTitles.containsKey(uuid); - } - - /** - * 玩家称号数据类 - */ - public static class PlayerTitleData { - private final String titleId; - private final String rawText; - private final boolean animated; - - public PlayerTitleData(String titleId, String rawText, boolean animated) { - this.titleId = titleId; - this.rawText = rawText; - this.animated = animated; - } - - public String getTitleId() { return titleId; } - public String getRawText() { return rawText; } - public boolean isAnimated() { return animated; } - } -} diff --git a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java b/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java deleted file mode 100644 index 58b5fcd..0000000 --- a/fabric-standalone/src/main/java/org/example/flashytitles/fabric/sync/SyncHandler.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.example.flashytitles.fabric.sync; - -import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.util.Identifier; -import org.example.flashytitles.core.message.Message; -import org.example.flashytitles.core.message.MessageType; -import org.example.flashytitles.fabric.FlashyTitlesFabric; -import org.example.flashytitles.fabric.manager.DisplayManager; - -import java.util.UUID; - -/** - * Fabric 同步处理器 - * 处理与 Velocity 的通信 - */ -public class SyncHandler { - - private static final Identifier CHANNEL = Identifier.of("flashytitles", "sync"); - - private final MinecraftServer server; - private final DisplayManager displayManager; - - public SyncHandler(MinecraftServer server, DisplayManager displayManager) { - this.server = server; - this.displayManager = displayManager; - } - - /** - * 初始化同步处理器 - */ - public void initialize() { - FlashyTitlesFabric.LOGGER.info("正在初始化 Fabric 同步处理器..."); - - // 注册网络处理器 - 使用简化的方式 - // ServerPlayNetworking.registerGlobalReceiver(CHANNEL, this::handleMessage); - FlashyTitlesFabric.LOGGER.info("网络处理器注册完成(简化版本)"); - - // 启动心跳任务 - new Thread(() -> { - while (!server.isStopped()) { - try { - Thread.sleep(30000); // 30秒发送一次心跳 - sendHeartbeat(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - }).start(); - - FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器初始化完成"); - } - - /** - * 关闭同步处理器 - */ - public void shutdown() { - FlashyTitlesFabric.LOGGER.info("Fabric 同步处理器已关闭"); - } - - /** - * 处理网络消息 - */ - private void handleMessage(MinecraftServer server, ServerPlayerEntity player, - ServerPlayNetworking.Context context, PacketByteBuf buf) { - try { - int length = buf.readInt(); - byte[] messageData = new byte[length]; - buf.readBytes(messageData); - - Message msg = Message.deserialize(messageData); - handleSyncMessage(msg); - - } catch (Exception e) { - FlashyTitlesFabric.LOGGER.error("处理网络消息失败", e); - } - } - - /** - * 处理来自 Velocity 的消息 - */ - private void handleSyncMessage(Message message) { - switch (message.getType()) { - case TITLE_UPDATE -> handleTitleUpdate(message); - case TITLE_REMOVE -> handleTitleRemove(message); - case PLAYER_QUIT -> handlePlayerQuit(message); - case SYNC_ALL -> handleSyncAll(message); - case RELOAD_CONFIG -> handleReloadConfig(message); - default -> FlashyTitlesFabric.LOGGER.warn("收到未知消息类型: {}", message.getType()); - } - } - - /** - * 处理称号更新 - */ - private void handleTitleUpdate(Message message) { - String playerUuidStr = message.getString("player_uuid"); - String playerName = message.getString("player_name"); - String titleId = message.getString("title_id"); - String titleText = message.getString("title_text"); - boolean animated = message.getBoolean("animated"); - - if (playerUuidStr == null) { - return; - } - - try { - UUID playerUuid = UUID.fromString(playerUuidStr); - ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); - - if (player == null) { - FlashyTitlesFabric.LOGGER.debug("玩家 {} 不在线,跳过称号更新", playerName); - return; - } - - if (titleId == null || titleId.isEmpty() || titleText == null || titleText.isEmpty()) { - // 移除称号 - displayManager.removePlayerTitle(player); - FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号", player.getName().getString()); - } else { - // 设置称号 - displayManager.setPlayerTitle(player, titleId, titleText, animated); - FlashyTitlesFabric.LOGGER.debug("为玩家 {} 设置称号: {}", player.getName().getString(), titleId); - } - - } catch (IllegalArgumentException e) { - FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); - } - } - - /** - * 处理称号移除 - */ - private void handleTitleRemove(Message message) { - String playerUuidStr = message.getString("player_uuid"); - - if (playerUuidStr == null) { - return; - } - - try { - UUID playerUuid = UUID.fromString(playerUuidStr); - ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); - - if (player != null) { - displayManager.removePlayerTitle(player); - FlashyTitlesFabric.LOGGER.debug("移除玩家 {} 的称号", player.getName().getString()); - } - - } catch (IllegalArgumentException e) { - FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); - } - } - - /** - * 处理玩家退出 - */ - private void handlePlayerQuit(Message message) { - String playerUuidStr = message.getString("player_uuid"); - - if (playerUuidStr == null) { - return; - } - - try { - UUID playerUuid = UUID.fromString(playerUuidStr); - ServerPlayerEntity player = server.getPlayerManager().getPlayer(playerUuid); - - if (player != null) { - displayManager.removePlayerTitle(player); - FlashyTitlesFabric.LOGGER.debug("玩家 {} 退出,清理称号显示", player.getName().getString()); - } - - } catch (IllegalArgumentException e) { - FlashyTitlesFabric.LOGGER.warn("无效的玩家UUID: {}", playerUuidStr); - } - } - - /** - * 处理全量同步 - */ - private void handleSyncAll(Message message) { - FlashyTitlesFabric.LOGGER.info("收到全量同步请求"); - - // 请求所有在线玩家的数据 - for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { - requestPlayerData(player); - } - } - - /** - * 处理配置重载 - */ - private void handleReloadConfig(Message message) { - FlashyTitlesFabric.LOGGER.info("收到配置重载请求"); - // 这里可以实现配置重载逻辑 - } - - /** - * 请求玩家数据 - */ - public void requestPlayerData(ServerPlayerEntity player) { - Message message = new Message(MessageType.PLAYER_DATA_REQUEST) - .addData("player_uuid", player.getUuid().toString()) - .addData("player_name", player.getName().getString()); - - sendMessage(player, message); - } - - /** - * 发送心跳包 - */ - private void sendHeartbeat() { - if (server.getPlayerManager().getPlayerList().isEmpty()) { - return; - } - - Message message = new Message(MessageType.HEARTBEAT) - .addData("server_name", "fabric-server") - .addData("online_players", server.getPlayerManager().getPlayerList().size()); - - // 随便选一个在线玩家发送心跳 - ServerPlayerEntity player = server.getPlayerManager().getPlayerList().get(0); - sendMessage(player, message); - } - - /** - * 玩家加入时的处理 - */ - public void onPlayerJoin(ServerPlayerEntity player) { - // 延迟请求玩家数据,确保玩家完全加载 - new Thread(() -> { - try { - Thread.sleep(1000); // 1秒后 - requestPlayerData(player); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }).start(); - } - - /** - * 发送消息到 Velocity - */ - private void sendMessage(ServerPlayerEntity player, Message message) { - try { - // 简化版本,只记录日志 - FlashyTitlesFabric.LOGGER.debug("发送消息到 Velocity: {}", message.getType()); - - } catch (Exception e) { - FlashyTitlesFabric.LOGGER.error("发送网络消息失败", e); - } - } -} diff --git a/fabric-standalone/src/main/resources/fabric.mod.json b/fabric-standalone/src/main/resources/fabric.mod.json deleted file mode 100644 index 780bae9..0000000 --- a/fabric-standalone/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "schemaVersion": 1, - "id": "flashy-titles", - "version": "${version}", - "name": "FlashyTitles", - "description": "FlashyTitles Fabric 端 - 群组服务器称号同步系统", - "authors": [ - "maple" - ], - "contact": { - "homepage": "https://github.com/FlashyTitles", - "sources": "https://github.com/FlashyTitles" - }, - "license": "MIT", - "icon": "assets/flashy-titles/icon.png", - "environment": "server", - "entrypoints": { - "main": [ - "org.example.flashytitles.fabric.FlashyTitlesFabric" - ] - }, - "mixins": [], - "depends": { - "fabricloader": ">=0.16.0", - "fabric-api": "*", - "minecraft": "~1.21.1" - }, - "suggests": {} -} diff --git a/spigot/src/main/java/top/redstarmc/plugin/velocitytitle/spigot/VelocityTitleSpigot.java b/spigot/src/main/java/top/redstarmc/plugin/velocitytitle/spigot/VelocityTitleSpigot.java deleted file mode 100644 index 0068b25..0000000 --- a/spigot/src/main/java/top/redstarmc/plugin/velocitytitle/spigot/VelocityTitleSpigot.java +++ /dev/null @@ -1,20 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.spigot; - -import org.bukkit.plugin.java.JavaPlugin; - -public class VelocityTitleSpigot extends JavaPlugin { - - @Override - public void onEnable() { - getServer().getLogger().info("ces"); - } - - - @Override - public void onDisable() { - getServer().getLogger().info("ces"); - } - - - -} diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java b/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java index 1d80019..2e986c4 100644 --- a/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java +++ b/velocity/src/main/java/org/example/flashytitles/velocity/FlashyTitlesVelocity.java @@ -11,7 +11,7 @@ import org.example.flashytitles.velocity.config.ConfigManager; import org.example.flashytitles.velocity.listener.PlayerListener; import org.example.flashytitles.velocity.manager.TitleManager; -import org.example.flashytitles.velocity.sync.SyncManager; +import org.example.flashytitles.velocity.sync.SimplifiedSyncManager; import org.slf4j.Logger; import java.nio.file.Path; @@ -33,7 +33,7 @@ public class FlashyTitlesVelocity { private ConfigManager configManager; private TitleManager titleManager; - private SyncManager syncManager; + private SimplifiedSyncManager syncManager; @Inject public FlashyTitlesVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { @@ -57,8 +57,11 @@ public void onProxyInitialization(ProxyInitializeEvent event) { titleManager.initialize(); // 初始化同步管理器 - syncManager = new SyncManager(configManager, titleManager, server, logger); + syncManager = new SimplifiedSyncManager(configManager, titleManager, server, logger); syncManager.initialize(); + + // 设置同步管理器到称号管理器(用于权限检查) + titleManager.setSyncManager(syncManager); // 注册命令 server.getCommandManager().register("title", new TitleCommand(titleManager, server, logger)); @@ -101,5 +104,5 @@ public static FlashyTitlesVelocity getInstance() { public Logger getLogger() { return logger; } public ConfigManager getConfigManager() { return configManager; } public TitleManager getTitleManager() { return titleManager; } - public SyncManager getSyncManager() { return syncManager; } + public SimplifiedSyncManager getSyncManager() { return syncManager; } } diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java b/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java index bd45737..2092666 100644 --- a/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java +++ b/velocity/src/main/java/org/example/flashytitles/velocity/command/TitleCommand.java @@ -311,23 +311,65 @@ private void handleCreate(CommandSource source, String[] args) { String id = args[1]; String raw = args[2]; - + + // 验证称号ID格式 + if (!id.matches("^[a-zA-Z0-9_-]+$")) { + source.sendMessage(Component.text("称号ID只能包含字母、数字、下划线和连字符", NamedTextColor.RED)); + return; + } + + // 验证称号ID长度 + if (id.length() > 32) { + source.sendMessage(Component.text("称号ID长度不能超过32个字符", NamedTextColor.RED)); + return; + } + + // 验证称号文本长度 + if (raw.length() > 128) { + source.sendMessage(Component.text("称号文本长度不能超过128个字符", NamedTextColor.RED)); + return; + } + try { int price = Integer.parseInt(args[3]); + + // 验证价格范围 + if (price < 0) { + source.sendMessage(Component.text("价格不能为负数", NamedTextColor.RED)); + return; + } + + if (price > 1000000) { + source.sendMessage(Component.text("价格不能超过1,000,000", NamedTextColor.RED)); + return; + } + boolean animated = args.length > 4 && Boolean.parseBoolean(args[4]); String permission = args.length > 5 ? args[5] : null; String description = args.length > 6 ? String.join(" ", Arrays.copyOfRange(args, 6, args.length)) : ""; - + + // 验证描述长度 + if (description.length() > 256) { + source.sendMessage(Component.text("描述长度不能超过256个字符", NamedTextColor.RED)); + return; + } + + // 检查称号是否已存在 + if (titleManager.getTitle(id) != null) { + source.sendMessage(Component.text("称号ID已存在: " + id, NamedTextColor.RED)); + return; + } + titleManager.createTitle(id, raw, price, animated, permission, description).thenAccept(success -> { if (success) { source.sendMessage(Component.text("成功创建称号: " + id, NamedTextColor.GREEN)); } else { - source.sendMessage(Component.text("创建称号失败", NamedTextColor.RED)); + source.sendMessage(Component.text("创建称号失败,请检查日志获取详细信息", NamedTextColor.RED)); } }); - + } catch (NumberFormatException e) { - source.sendMessage(Component.text("无效的价格: " + args[3], NamedTextColor.RED)); + source.sendMessage(Component.text("无效的价格格式: " + args[3] + ",请输入有效的数字", NamedTextColor.RED)); } } @@ -422,9 +464,25 @@ private void handleReload(CommandSource source) { source.sendMessage(Component.text("你没有权限执行此命令", NamedTextColor.RED)); return; } - - // TODO: 实现重载功能 - source.sendMessage(Component.text("重载功能暂未实现", NamedTextColor.YELLOW)); + + source.sendMessage(Component.text("正在重载配置...", NamedTextColor.YELLOW)); + + CompletableFuture.runAsync(() -> { + try { + // 重载配置文件 + titleManager.getConfigManager().loadConfig(); + + // 重新加载称号缓存 + titleManager.reloadTitles(); + + source.sendMessage(Component.text("配置重载完成!", NamedTextColor.GREEN)); + logger.info("管理员 {} 重载了配置", source.toString()); + + } catch (Exception e) { + source.sendMessage(Component.text("配置重载失败: " + e.getMessage(), NamedTextColor.RED)); + logger.error("配置重载失败", e); + } + }); } private void sendHelp(CommandSource source) { diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java index 3b9329f..693164d 100644 --- a/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java +++ b/velocity/src/main/java/org/example/flashytitles/velocity/config/ConfigManager.java @@ -10,6 +10,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; @@ -36,47 +37,53 @@ public void loadConfig() throws IOException { if (!Files.exists(dataDirectory)) { Files.createDirectories(dataDirectory); } - + // 如果配置文件不存在,创建默认配置 if (!Files.exists(configFile)) { createDefaultConfig(); } - + // 加载配置 try (Reader reader = Files.newBufferedReader(configFile)) { config = JsonParser.parseReader(reader).getAsJsonObject(); + } catch (Exception e) { + logger.error("配置文件读取失败,使用默认配置: {}", e.getMessage()); + // 备份损坏的配置文件 + try { + Path backupFile = configFile.resolveSibling(configFile.getFileName() + ".backup"); + Files.copy(configFile, backupFile, StandardCopyOption.REPLACE_EXISTING); + logger.info("已备份损坏的配置文件到: {}", backupFile); + } catch (IOException backupError) { + logger.warn("无法备份损坏的配置文件: {}", backupError.getMessage()); + } + // 重新创建默认配置 + createDefaultConfig(); } - + if (config == null) { config = new JsonObject(); } - + + // 验证配置完整性 + validateConfig(); + logger.info("配置文件加载完成: {}", configFile); } private void createDefaultConfig() throws IOException { JsonObject defaultConfig = new JsonObject(); - // 数据库配置 (使用H2数据库,兼容GitHub原仓库设计) + // 数据库配置 (默认使用H2数据库) JsonObject database = new JsonObject(); - database.addProperty("type", "h2"); // h2, mysql 或 sqlite + database.addProperty("type", "h2"); // h2, mysql database.addProperty("host", "localhost"); database.addProperty("port", 3306); database.addProperty("database", "flashy_titles"); database.addProperty("username", "root"); database.addProperty("password", "password"); - database.addProperty("sqlite-file", "titles.db"); + database.addProperty("sqlite-file", "./data/flashytitles"); // H2数据库文件路径 defaultConfig.add("database", database); - // Redis 配置 (用于实时同步) - JsonObject redis = new JsonObject(); - redis.addProperty("enabled", false); - redis.addProperty("host", "localhost"); - redis.addProperty("port", 6379); - redis.addProperty("password", ""); - redis.addProperty("database", 0); - defaultConfig.add("redis", redis); - // 称号配置 JsonObject titles = new JsonObject(); titles.addProperty("animation-interval", 10); // tick间隔 @@ -118,6 +125,39 @@ private void createDefaultConfig() throws IOException { config = defaultConfig; logger.info("已创建默认配置文件: {}", configFile); } + + /** + * 验证配置完整性 + */ + private void validateConfig() { + boolean needsUpdate = false; + + // 检查必需的配置项 + if (!config.has("database")) { + logger.warn("配置文件缺少database配置,将使用默认值"); + needsUpdate = true; + } + + if (!config.has("titles")) { + logger.warn("配置文件缺少titles配置,将使用默认值"); + needsUpdate = true; + } + + if (!config.has("sync")) { + logger.warn("配置文件缺少sync配置,将使用默认值"); + needsUpdate = true; + } + + // 如果需要更新,重新创建默认配置 + if (needsUpdate) { + try { + createDefaultConfig(); + logger.info("配置文件已更新为完整版本"); + } catch (IOException e) { + logger.error("更新配置文件失败: {}", e.getMessage()); + } + } + } // 数据库配置 public DatabaseConfig getDatabaseConfig() { @@ -137,25 +177,20 @@ public String getDatabaseType() { return config.getAsJsonObject("database").get("type").getAsString(); } - // Redis 配置 - public boolean isRedisEnabled() { - return config.getAsJsonObject("redis").get("enabled").getAsBoolean(); - } - - public String getRedisHost() { - return config.getAsJsonObject("redis").get("host").getAsString(); + // 性能配置 + public int getCacheSize() { + JsonObject perf = config.getAsJsonObject("performance"); + return perf != null ? perf.get("cache-size").getAsInt() : 1000; } - - public int getRedisPort() { - return config.getAsJsonObject("redis").get("port").getAsInt(); - } - - public String getRedisPassword() { - return config.getAsJsonObject("redis").get("password").getAsString(); + + public int getConnectionPoolSize() { + JsonObject perf = config.getAsJsonObject("performance"); + return perf != null ? perf.get("connection-pool-size").getAsInt() : 10; } - - public int getRedisDatabase() { - return config.getAsJsonObject("redis").get("database").getAsInt(); + + public int getQueryTimeout() { + JsonObject perf = config.getAsJsonObject("performance"); + return perf != null ? perf.get("query-timeout").getAsInt() : 30; } // 称号配置 @@ -174,6 +209,11 @@ public boolean isColorCodesAllowed() { public int getDefaultStartingCoins() { return config.getAsJsonObject("titles").get("default-starting-coins").getAsInt(); } + + public int getMaxPrice() { + JsonObject titles = config.getAsJsonObject("titles"); + return titles.has("max-price") ? titles.get("max-price").getAsInt() : 1000000; + } // 同步配置 public boolean isSyncEnabled() { diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java index 44f2c47..e92396a 100644 --- a/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java +++ b/velocity/src/main/java/org/example/flashytitles/velocity/manager/TitleManager.java @@ -3,6 +3,7 @@ import org.example.flashytitles.core.database.DatabaseManager; import org.example.flashytitles.core.model.Title; import org.example.flashytitles.velocity.config.ConfigManager; +import org.example.flashytitles.velocity.sync.SimplifiedSyncManager; import org.slf4j.Logger; import java.sql.SQLException; @@ -23,6 +24,7 @@ public class TitleManager { private final Logger logger; private final DatabaseManager databaseManager; private final ScheduledExecutorService scheduler; + private SimplifiedSyncManager syncManager; // 动画tick计数器 private int animationTick = 0; @@ -173,33 +175,38 @@ public CompletableFuture purchaseTitle(UUID playerUuid, String t if (title == null) { return PurchaseResult.TITLE_NOT_FOUND; } - + // 检查是否已拥有 if (ownsTitle(playerUuid, titleId)) { return PurchaseResult.ALREADY_OWNED; } - + // 检查金币 int playerCoins = getCoins(playerUuid); if (playerCoins < title.getPrice()) { return PurchaseResult.INSUFFICIENT_COINS; } - + // 检查权限(如果有的话) if (title.getPermission() != null && !title.getPermission().isEmpty()) { - // 这里需要权限检查,但在Velocity中比较复杂 - // 可以通过消息通道让Spigot端检查权限 + // 通过同步管理器检查权限 + boolean hasPermission = checkPlayerPermission(playerUuid, title.getPermission()); + if (!hasPermission) { + logger.info("玩家 {} 没有权限 {} 购买称号 {}", playerUuid, title.getPermission(), titleId); + return PurchaseResult.NO_PERMISSION; + } + logger.debug("玩家 {} 权限检查通过: {}", playerUuid, title.getPermission()); } - - // 扣除金币 - setCoins(playerUuid, playerCoins - title.getPrice()); - - // 给予称号 - databaseManager.grantTitle(playerUuid, titleId).join(); - + + // 使用事务确保原子性 + boolean success = databaseManager.purchaseTitleTransaction(playerUuid, titleId, title.getPrice()).join(); + if (!success) { + return PurchaseResult.ERROR; + } + logger.info("玩家 {} 购买称号: {} (花费: {})", playerUuid, titleId, title.getPrice()); return PurchaseResult.SUCCESS; - + } catch (Exception e) { logger.error("购买称号失败: " + titleId, e); return PurchaseResult.ERROR; @@ -312,7 +319,54 @@ public void addCoins(UUID playerUuid, int amount) { public int getCurrentAnimationTick() { return animationTick; } - + + /** + * 重载称号数据 + */ + public void reloadTitles() { + try { + databaseManager.loadCache(); + logger.info("称号数据重载完成"); + } catch (Exception e) { + logger.error("重载称号数据失败", e); + throw new RuntimeException("重载称号数据失败", e); + } + } + + /** + * 获取配置管理器 + */ + public ConfigManager getConfigManager() { + return configManager; + } + + /** + * 设置同步管理器(用于权限检查) + */ + public void setSyncManager(SimplifiedSyncManager syncManager) { + this.syncManager = syncManager; + } + + /** + * 检查玩家权限 + * 通过同步管理器与后端服务器通信检查权限 + */ + private boolean checkPlayerPermission(UUID playerUuid, String permission) { + if (syncManager != null) { + try { + // 使用同步管理器进行权限检查 + return syncManager.checkPlayerPermission(playerUuid, permission).get(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.warn("权限检查失败,默认允许: 玩家 {} 权限 {}", playerUuid, permission, e); + return true; // 权限检查失败时默认允许,避免阻塞购买流程 + } + } + + // 如果没有同步管理器,使用简化的权限检查逻辑 + logger.debug("使用简化权限检查: 玩家 {} 权限 {}", playerUuid, permission); + return true; // 默认允许 + } + /** * 购买结果枚举 */ diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/sync/SimplifiedSyncManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/sync/SimplifiedSyncManager.java new file mode 100644 index 0000000..b60fd94 --- /dev/null +++ b/velocity/src/main/java/org/example/flashytitles/velocity/sync/SimplifiedSyncManager.java @@ -0,0 +1,346 @@ +package org.example.flashytitles.velocity.sync; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import org.example.flashytitles.core.message.Message; +import org.example.flashytitles.core.message.MessageType; +import org.example.flashytitles.core.model.Title; +import org.example.flashytitles.velocity.config.ConfigManager; +import org.example.flashytitles.velocity.manager.TitleManager; +import org.slf4j.Logger; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 简化的同步管理器 + * 基于数据库同步,移除Redis依赖 + */ +public class SimplifiedSyncManager { + + private static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("flashytitles", "sync"); + + private final ConfigManager configManager; + private final TitleManager titleManager; + private final ProxyServer server; + private final Logger logger; + + private ScheduledExecutorService scheduler; + + // 权限检查响应缓存 + private final ConcurrentHashMap permissionCache = new ConcurrentHashMap<>(); + + public SimplifiedSyncManager(ConfigManager configManager, TitleManager titleManager, ProxyServer server, Logger logger) { + this.configManager = configManager; + this.titleManager = titleManager; + this.server = server; + this.logger = logger; + } + + /** + * 初始化同步管理器 + */ + public void initialize() { + logger.info("正在初始化同步管理器..."); + + // 注册插件消息通道 + server.getChannelRegistrar().register(CHANNEL); + + // 启动定时同步任务 + if (configManager.isSyncEnabled()) { + startSyncTask(); + } + + logger.info("同步管理器初始化完成 (基于数据库同步)"); + logger.info("- 定时同步: {}", configManager.isSyncEnabled() ? "启用" : "禁用"); + } + + /** + * 启动定时同步任务 + */ + private void startSyncTask() { + scheduler = Executors.newScheduledThreadPool(1); + + int interval = configManager.getSyncInterval(); + scheduler.scheduleAtFixedRate(() -> { + try { + syncAllPlayersToServers(); + } catch (Exception e) { + logger.error("定时同步任务异常", e); + } + }, interval, interval, TimeUnit.SECONDS); + + logger.info("定时同步任务已启动,间隔: {} 秒", interval); + } + + /** + * 同步所有玩家数据到所有服务器 + */ + public void syncAllPlayersToServers() { + for (Player player : server.getAllPlayers()) { + syncPlayerToAllServers(player); + } + } + + /** + * 同步玩家数据到所有服务器 + */ + public void syncPlayerToAllServers(Player player) { + Optional currentServer = player.getCurrentServer(); + if (currentServer.isPresent()) { + syncPlayerToServer(player, currentServer.get()); + } + } + + /** + * 同步玩家数据到指定服务器 + */ + public void syncPlayerToServer(Player player, ServerConnection server) { + String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); + + Message message = new Message(MessageType.TITLE_UPDATE) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getUsername()); + + if (equippedTitle != null) { + Title title = titleManager.getTitle(equippedTitle); + if (title != null) { + String renderedTitle = titleManager.renderTitle(equippedTitle); + message.addData("title_id", equippedTitle) + .addData("title_text", renderedTitle) + .addData("animated", title.isAnimated()); + } + } else { + message.addData("title_id", "") + .addData("title_text", "") + .addData("animated", false); + } + + sendMessageToServer(server, message); + } + + /** + * 发送消息到指定服务器 + */ + private void sendMessageToServer(ServerConnection server, Message message) { + try { + byte[] data = createPluginMessage(message); + server.sendPluginMessage(CHANNEL, data); + } catch (Exception e) { + logger.error("发送插件消息失败", e); + } + } + + /** + * 创建插件消息数据 + */ + private byte[] createPluginMessage(Message message) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + byte[] messageData = message.serialize(); + out.writeInt(messageData.length); + out.write(messageData); + return out.toByteArray(); + } + + /** + * 玩家加入时的同步处理 + */ + public void onPlayerJoin(Player player) { + if (configManager.isAutoSyncOnJoin()) { + // 延迟一点时间,确保玩家完全加入服务器 + CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS).execute(() -> { + Optional serverConnection = player.getCurrentServer(); + if (serverConnection.isPresent()) { + syncPlayerToServer(player, serverConnection.get()); + } + }); + } + } + + /** + * 玩家离开时的处理 + */ + public void onPlayerQuit(Player player) { + // 通知所有服务器玩家离开 + Message message = new Message(MessageType.PLAYER_QUIT) + .addData("player_uuid", player.getUniqueId().toString()) + .addData("player_name", player.getUsername()); + + // 广播到所有服务器 + for (Player onlinePlayer : server.getAllPlayers()) { + if (!onlinePlayer.equals(player)) { + Optional serverConnection = onlinePlayer.getCurrentServer(); + if (serverConnection.isPresent()) { + sendMessageToServer(serverConnection.get(), message); + } + } + } + } + + /** + * 处理来自 Spigot 的消息 + */ + public void handlePluginMessage(Player player, ByteArrayDataInput in) { + try { + int length = in.readInt(); + byte[] messageData = new byte[length]; + in.readFully(messageData); + + Message message = Message.deserialize(messageData); + handleSyncMessage(message); + + } catch (Exception e) { + logger.error("处理插件消息失败", e); + } + } + + /** + * 处理同步消息 + */ + private void handleSyncMessage(Message message) { + switch (message.getType()) { + case PLAYER_DATA_REQUEST -> handlePlayerDataRequest(message); + case PERMISSION_RESPONSE -> handlePermissionResponse(message); + case HEARTBEAT -> handleHeartbeat(message); + default -> logger.debug("收到消息类型: {}", message.getType()); + } + } + + /** + * 处理玩家数据请求 + */ + private void handlePlayerDataRequest(Message message) { + String playerUuidStr = message.getString("player_uuid"); + if (playerUuidStr != null) { + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Optional playerOpt = server.getPlayer(playerUuid); + if (playerOpt.isPresent()) { + Player player = playerOpt.get(); + Optional serverConnection = player.getCurrentServer(); + if (serverConnection.isPresent()) { + syncPlayerToServer(player, serverConnection.get()); + } + } + } catch (IllegalArgumentException e) { + logger.warn("无效的玩家UUID: {}", playerUuidStr); + } + } + } + + /** + * 处理权限检查响应 + */ + private void handlePermissionResponse(Message message) { + String requestId = message.getString("request_id"); + String playerUuid = message.getString("player_uuid"); + String permission = message.getString("permission"); + boolean hasPermission = message.getBoolean("has_permission"); + + if (requestId != null) { + // 将权限检查结果存入缓存 + permissionCache.put(requestId, hasPermission); + logger.debug("收到权限检查响应: 玩家 {} 权限 {} = {}", playerUuid, permission, hasPermission); + } + } + + /** + * 处理心跳消息 + */ + private void handleHeartbeat(Message message) { + String serverName = message.getString("server_name"); + int onlinePlayers = message.getInt("online_players"); + logger.debug("收到来自服务器 {} 的心跳,在线玩家: {}", serverName, onlinePlayers); + } + + /** + * 检查玩家权限(异步) + */ + public CompletableFuture checkPlayerPermission(UUID playerUuid, String permission) { + return CompletableFuture.supplyAsync(() -> { + Optional playerOpt = server.getPlayer(playerUuid); + if (playerOpt.isEmpty()) { + logger.warn("玩家 {} 不在线,无法检查权限", playerUuid); + return false; + } + + Player player = playerOpt.get(); + Optional serverConnection = player.getCurrentServer(); + if (serverConnection.isEmpty()) { + logger.warn("玩家 {} 未连接到任何服务器,无法检查权限", player.getUsername()); + return false; + } + + // 生成请求ID + String requestId = UUID.randomUUID().toString(); + + // 创建权限检查请求 + Message permissionRequest = new Message(MessageType.PERMISSION_CHECK) + .addData("player_uuid", playerUuid.toString()) + .addData("player_name", player.getUsername()) + .addData("permission", permission) + .addData("request_id", requestId); + + // 发送权限检查请求到后端服务器 + sendMessageToServer(serverConnection.get(), permissionRequest); + + // 等待响应(简化版本) + try { + int maxWaitTime = 3000; // 最多等待3秒 + int checkInterval = 100; // 每100ms检查一次 + int waitedTime = 0; + + while (waitedTime < maxWaitTime) { + Boolean result = permissionCache.get(requestId); + if (result != null) { + // 清理缓存 + permissionCache.remove(requestId); + logger.debug("权限检查完成: 玩家 {} 权限 {} = {}", player.getUsername(), permission, result); + return result; + } + + Thread.sleep(checkInterval); + waitedTime += checkInterval; + } + + // 超时,默认允许(避免阻塞购买流程) + logger.warn("权限检查超时,默认允许: 玩家 {} 权限 {}", player.getUsername(), permission); + return true; + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("权限检查被中断", e); + return false; + } + }); + } + + /** + * 关闭同步管理器 + */ + public void shutdown() { + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + logger.info("同步管理器已关闭"); + } +} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/Listener.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/Listener.java deleted file mode 100644 index 4ef6fd7..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/Listener.java +++ /dev/null @@ -1,32 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity; - -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.player.ServerPostConnectEvent; -import com.velocitypowered.api.proxy.Player; -import org.jetbrains.annotations.NotNull; -import top.redstarmc.plugin.velocitytitle.velocity.database.operate.PlayerWearOperate; -import top.redstarmc.plugin.velocitytitle.velocity.manager.EasySQLManager; - -/** - *

插件监听器

- */ -public class Listener { - - /** - * 进服时保存UUID,以便离线时查验。 - * 其实是为了适配离线服务器 - * @param event 连接子服事件 - */ - @Subscribe - public void onServerPostConnectEvent(@NotNull ServerPostConnectEvent event){ - Player player_tmp = event.getPlayer(); - String name = player_tmp.getUsername(); - Player player = VelocityTitleVelocity.getInstance().getServer().getPlayer(name).orElse(null); - - if (player == null) return; - - PlayerWearOperate.ReplaceUUID(EasySQLManager.getSqlManager(), player.getUniqueId().toString(), name); - } - - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/VelocityTitleVelocity.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/VelocityTitleVelocity.java deleted file mode 100644 index 7ce6592..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/VelocityTitleVelocity.java +++ /dev/null @@ -1,121 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity; - -import com.google.inject.Inject; -import com.velocitypowered.api.command.BrigadierCommand; -import com.velocitypowered.api.command.CommandManager; -import com.velocitypowered.api.command.CommandMeta; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; -import com.velocitypowered.api.plugin.Plugin; -import com.velocitypowered.api.plugin.annotation.DataDirectory; -import com.velocitypowered.api.proxy.ProxyServer; -import top.redstarmc.plugin.velocitytitle.velocity.command.CommandBuilder; -import top.redstarmc.plugin.velocitytitle.velocity.configuration.Config; -import top.redstarmc.plugin.velocitytitle.velocity.configuration.Language; -import top.redstarmc.plugin.velocitytitle.velocity.manager.EasySQLManager; -import top.redstarmc.plugin.velocitytitle.velocity.manager.LoggerManager; - -import java.io.File; -import java.nio.file.Path; - -@Plugin( - id = "velocity_title", - name = "VelocityTitle" -) -public class VelocityTitleVelocity { - - private LoggerManager logger; - - private final File dataFolder; - - private Config config; - - private Language language; - - private EasySQLManager DBManager; - - private final ProxyServer server; - - private static VelocityTitleVelocity instance; - - @Inject - public VelocityTitleVelocity(@DataDirectory Path dataDirectory, ProxyServer server) { - this.dataFolder = dataDirectory.toFile(); - this.server = server; - } - - @Subscribe - public void onProxyInitialization(ProxyInitializeEvent event) { - System.out.println("[VelocityTitle] Loading..."); - instance = this; - - System.out.println("[VelocityTitle] Configurations Loading..."); - loadConfiguration(); - - logger = new LoggerManager(config.getConfigToml().getString("plugin-prefix"), - config.getConfigToml().getBoolean("debug-mode")); - - logger.info("Language: "+language.getConfigToml().getString("name")); - - logger.info(language.getConfigToml().getString("logs.loading")); - logger.info(language.getConfigToml().getString("logs.author")," pingguomc"); - logger.debug(language.getConfigToml().getString("logs.debug")); - logger.info(language.getConfigToml().getString("logs.website")," https://github.com/RedStarMC/VelocityTitle"); - - logger.info(language.getConfigToml().getString("logs.command-loading")); - registerCommand(); - - logger.info(language.getConfigToml().getString("logs.database-loading")); - DBManager = new EasySQLManager(logger, config, language); - DBManager.init(); - - logger.info(language.getConfigToml().getString("logs.listener-loading")); - server.getEventManager().register(this, new Listener()); - - logger.info("测试"); - logger.warn("警告"); - logger.error("错误"); - logger.debug("debug"); - } - - private void registerCommand(){ - CommandManager commandManager = server.getCommandManager(); - - CommandMeta commandMeta = commandManager.metaBuilder("VelocityTitle") - .plugin(this) - .aliases("vt") - .build(); - - commandManager.register(commandMeta,new BrigadierCommand(CommandBuilder.init(language))); - } - - - private void loadConfiguration(){ - config = new Config(this.getDataFolder()); - config.init(); - - language = new Language(this.getDataFolder()); - language.init(); - } - - - public LoggerManager getLogger() { - return logger; - } - - public ProxyServer getServer() { - return server; - } - - public Language getLanguage() { - return language; - } - - public File getDataFolder() { - return dataFolder; - } - - public static VelocityTitleVelocity getInstance() { - return instance; - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CommandBuilder.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CommandBuilder.java deleted file mode 100644 index 5b7554e..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CommandBuilder.java +++ /dev/null @@ -1,30 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.tree.LiteralCommandNode; -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.proxy.ProxyServer; -import top.redstarmc.plugin.velocitytitle.velocity.VelocityTitleVelocity; -import top.redstarmc.plugin.velocitytitle.velocity.manager.ConfigManager; - -import static net.kyori.adventure.text.Component.text; - -public abstract class CommandBuilder { - - public static LiteralCommandNode init(ConfigManager language){ - return LiteralArgumentBuilder.literal("VelocityTitle") - .executes(context -> { - context.getSource().sendMessage(text(language.getConfigToml().getString("commands.root")) - .append(text(language.getConfigToml().getString("commands.helps.open")))); - return Command.SINGLE_SUCCESS; - }) - .then(new CreateBuilder().build(language)) - .build(); - - } - - public static final ProxyServer proxyServer = VelocityTitleVelocity.getInstance().getServer(); - - public abstract LiteralArgumentBuilder build(ConfigManager language); -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CreateBuilder.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CreateBuilder.java deleted file mode 100644 index a024907..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/command/CreateBuilder.java +++ /dev/null @@ -1,18 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.velocitypowered.api.command.CommandSource; -import top.redstarmc.plugin.velocitytitle.velocity.manager.ConfigManager; - -public class CreateBuilder extends CommandBuilder{ - @Override - public LiteralArgumentBuilder build(ConfigManager language) { - return LiteralArgumentBuilder.literal("create") - .executes(context -> { - // 命令帮助 - return Command.SINGLE_SUCCESS; - }) - ; - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Config.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Config.java deleted file mode 100644 index deed117..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Config.java +++ /dev/null @@ -1,15 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.configuration; - -import top.redstarmc.plugin.velocitytitle.velocity.manager.ConfigManager; - -import java.io.File; - -public class Config extends ConfigManager { - - static final String fileName = "config-velocity.toml"; - - public Config(File dataFolder) { - super(dataFolder, fileName); - } - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Language.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Language.java deleted file mode 100644 index 6535f41..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/configuration/Language.java +++ /dev/null @@ -1,13 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.configuration; - -import top.redstarmc.plugin.velocitytitle.velocity.manager.ConfigManager; - -import java.io.File; - -public class Language extends ConfigManager { - static final String fileName = "language-velocity.toml"; - public Language(File dataFolder) { - super(dataFolder, fileName); - } - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/DebugHandler.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/DebugHandler.java deleted file mode 100644 index 23515b2..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/DebugHandler.java +++ /dev/null @@ -1,79 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database; - -import cc.carm.lib.easysql.api.SQLAction; -import cc.carm.lib.easysql.api.SQLQuery; -import cc.carm.lib.easysql.api.action.PreparedSQLUpdateAction; -import cc.carm.lib.easysql.api.action.PreparedSQLUpdateBatchAction; -import cc.carm.lib.easysql.api.function.SQLDebugHandler; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import top.redstarmc.plugin.velocitytitle.velocity.manager.LoggerManager; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class DebugHandler implements SQLDebugHandler { - - private final LoggerManager logger; - - public DebugHandler(LoggerManager logger){ - this.logger = logger; - } - - /** - * 该方法将在 {@link SQLAction#execute()} 执行前调用。 - * - * @param action {@link SQLAction} 对象 - * @param params 执行传入的参数列表。 - * 实际上,仅有 {@link PreparedSQLUpdateAction} 和 {@link PreparedSQLUpdateBatchAction} 才会有传入参数。 - */ - @Override - public void beforeExecute(@NotNull SQLAction action, @NotNull List<@Nullable Object[]> params) { - logger.debugDataBase("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - logger.debugDataBase("┣# ActionUUID: {}", action.getActionUUID()); - logger.debugDataBase("┣# ActionType: {}", action.getClass().getSimpleName()); - if (action.getSQLContents().size() == 1) { - logger.debugDataBase("┣# SQLContent: {}", action.getSQLContents().get(0)); - } else { - logger.debugDataBase("┣# SQLContents: "); - int i = 0; - for (String sqlContent : action.getSQLContents()) { - logger.debugDataBase("┃ - [{}] {}", ++i, sqlContent); - } - } - if (params.size() == 1) { - Object[] param = params.get(0); - if (param != null) { - logger.debugDataBase("┣# SQLParam: {}", parseParams(param)); - } - } else if (params.size() > 1) { - logger.debugDataBase("┣# SQLParams: "); - int i = 0; - for (Object[] param : params) { - logger.debugDataBase("┃ - [{}] {}", ++i, parseParams(param)); - } - } - logger.debugDataBase("┣# CreateTime: {}", action.getCreateTime(TimeUnit.MILLISECONDS)); - logger.debugDataBase("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - } - - /** - * 该方法将在 {@link SQLQuery#close()} 执行后调用。 - * - * @param query {@link SQLQuery} 对象 - * @param executeNanoTime 该次查询开始执行的时间 (单位:纳秒) - * @param closeNanoTime 该次查询彻底关闭的时间 (单位:纳秒) - */ - @Override - public void afterQuery(@NotNull SQLQuery query, long executeNanoTime, long closeNanoTime) { - logger.debugDataBase("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - logger.debugDataBase("┣# ActionUUID: {}", query.getAction().getActionUUID()); - logger.debugDataBase("┣# SQLContent: {}", query.getSQLContent()); - logger.debugDataBase("┣# CloseTime: {} (cost {} ms)", - TimeUnit.NANOSECONDS.toMillis(closeNanoTime), - ((double) (closeNanoTime - executeNanoTime) / 1000000) - ); - logger.debugDataBase("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - } - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/Operate.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/Operate.java deleted file mode 100644 index c5f6d8b..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/Operate.java +++ /dev/null @@ -1,13 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.operate; - -import top.redstarmc.plugin.velocitytitle.velocity.VelocityTitleVelocity; -import top.redstarmc.plugin.velocitytitle.velocity.manager.ConfigManager; -import top.redstarmc.plugin.velocitytitle.velocity.manager.LoggerManager; - -public interface Operate { - - public static LoggerManager logger = VelocityTitleVelocity.getInstance().getLogger(); - - public static ConfigManager language = VelocityTitleVelocity.getInstance().getLanguage(); - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/PlayerWearOperate.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/PlayerWearOperate.java deleted file mode 100644 index dba69fe..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/operate/PlayerWearOperate.java +++ /dev/null @@ -1,20 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.operate; - -import cc.carm.lib.easysql.api.SQLManager; -import top.redstarmc.plugin.velocitytitle.velocity.database.table.PlayerWear; - -public class PlayerWearOperate implements Operate { - - public static void ReplaceUUID(SQLManager sqlManager, String uuid, String name) { - sqlManager.createReplace(PlayerWear.PLAYER_WEAR.getTableName()) - .setColumnNames("uuid", "name") - .setParams(uuid, name) - .executeAsync((query) -> {}, - ((exception, sqlAction) -> { - logger.crash(exception,language.getConfigToml().getString("database.failed-operate")); - }) - ); - } - - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerTitles.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerTitles.java deleted file mode 100644 index dfa5089..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerTitles.java +++ /dev/null @@ -1,56 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.table; - -import cc.carm.lib.easysql.api.SQLManager; -import cc.carm.lib.easysql.api.SQLTable; -import cc.carm.lib.easysql.api.builder.TableCreateBuilder; -import cc.carm.lib.easysql.api.enums.IndexType; -import cc.carm.lib.easysql.api.enums.NumberType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; -import java.util.function.Consumer; - -public enum PlayerTitles implements SQLTable { - PLAYER_TITLES((table) -> { - table.addAutoIncrementColumn("id", NumberType.INT, true, true); - table.addColumn("player_uuid", "VARCHAR(38) NOT NULL"); - table.addColumn("title_id", "INT NOT NULL"); - - table.setIndex(IndexType.INDEX, "player_idx", "player_uuid"); - // 唯一约束:防止玩家重复拥有同一个称号 - table.setIndex(IndexType.UNIQUE_KEY, "player_title_unique", "player_uuid", "title_id"); - }); - private final Consumer builder; - private @Nullable SQLManager manager; - - private static final String tableName = "PLAYER_TITLES"; - - PlayerTitles(Consumer builder) { - this.builder = builder; - } - - @Override - public @Nullable SQLManager getSQLManager() { - return this.manager; - } - - @Override - public @NotNull String getTableName() { - return tableName; - } - - @Override - public boolean create(SQLManager sqlManager) throws SQLException { - this.manager = sqlManager; - - TableCreateBuilder tableBuilder = sqlManager.createTable(tableName); - if (builder != null) builder.accept(tableBuilder); - - return tableBuilder.build().executeFunction(l -> l > 0, false); - } - - public static void initialize(SQLManager sqlManager) throws SQLException { - PLAYER_TITLES.create(sqlManager); - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerWear.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerWear.java deleted file mode 100644 index ca76a3d..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PlayerWear.java +++ /dev/null @@ -1,58 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.table; - -import cc.carm.lib.easysql.api.SQLManager; -import cc.carm.lib.easysql.api.SQLTable; -import cc.carm.lib.easysql.api.builder.TableCreateBuilder; -import cc.carm.lib.easysql.api.enums.IndexType; -import cc.carm.lib.easysql.api.enums.NumberType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; -import java.util.function.Consumer; - -public enum PlayerWear implements SQLTable { - PLAYER_WEAR((table) -> { - table.addAutoIncrementColumn("id", NumberType.INT, true, true); - table.addColumn("uuid", "VARCHAR(38) NOT NULL"); - table.addColumn("name", "VARCHAR(64) NOT NULL"); - table.addColumn("prefix", "VARCHAR(256)"); - table.addColumn("suffix", "VARCHAR(256)"); - - table.setIndex("uuid", IndexType.UNIQUE_KEY); - }); - - private final Consumer builder; - private @Nullable SQLManager manager; - - private static final String tableName = "PLAYER_WEAR"; - - PlayerWear(Consumer builder) { - this.builder = builder; - } - - @Override - public @Nullable SQLManager getSQLManager() { - return this.manager; - } - - @Override - public @NotNull String getTableName() { - return tableName; - } - - @Override - public boolean create(SQLManager sqlManager) throws SQLException { - this.manager = sqlManager; - - TableCreateBuilder tableBuilder = sqlManager.createTable(tableName); - if (builder != null) builder.accept(tableBuilder); - - return tableBuilder.build().executeFunction(l -> l > 0, false); - } - - public static void initialize(SQLManager sqlManager) throws SQLException { - PLAYER_WEAR.create(sqlManager); - } - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PrefixDictionary.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PrefixDictionary.java deleted file mode 100644 index e537389..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/PrefixDictionary.java +++ /dev/null @@ -1,53 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.table; - -import cc.carm.lib.easysql.api.SQLManager; -import cc.carm.lib.easysql.api.SQLTable; -import cc.carm.lib.easysql.api.builder.TableCreateBuilder; -import cc.carm.lib.easysql.api.enums.IndexType; -import cc.carm.lib.easysql.api.enums.NumberType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; -import java.util.function.Consumer; - -public enum PrefixDictionary implements SQLTable { - PREFIX_DICTIONARY((table) -> { - table.addAutoIncrementColumn("id", NumberType.INT, true, true); - table.addColumn("name", "VARCHAR(256) NOT NULL"); // 作为索引 - table.addColumn("display", "VARCHAR(256) NOT NULL"); // 实际展示 - table.setIndex("name", IndexType.UNIQUE_KEY); - }); - private final Consumer builder; - private @Nullable SQLManager manager; - - private static final String tableName = "PREFIX_DICTIONARY"; - - PrefixDictionary(Consumer builder) { - this.builder = builder; - } - - @Override - public @Nullable SQLManager getSQLManager() { - return this.manager; - } - - @Override - public @NotNull String getTableName() { - return tableName; - } - - @Override - public boolean create(SQLManager sqlManager) throws SQLException { - this.manager = sqlManager; - - TableCreateBuilder tableBuilder = sqlManager.createTable(tableName); - if (builder != null) builder.accept(tableBuilder); - - return tableBuilder.build().executeFunction(l -> l > 0, false); - } - - public static void initialize(SQLManager sqlManager) throws SQLException { - PREFIX_DICTIONARY.create(sqlManager); - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/SuffixDictionary.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/SuffixDictionary.java deleted file mode 100644 index e293e31..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/database/table/SuffixDictionary.java +++ /dev/null @@ -1,53 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.database.table; - -import cc.carm.lib.easysql.api.SQLManager; -import cc.carm.lib.easysql.api.SQLTable; -import cc.carm.lib.easysql.api.builder.TableCreateBuilder; -import cc.carm.lib.easysql.api.enums.IndexType; -import cc.carm.lib.easysql.api.enums.NumberType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; -import java.util.function.Consumer; - -public enum SuffixDictionary implements SQLTable { - SUFFIX_DICTIONARY((table) -> { - table.addAutoIncrementColumn("id", NumberType.INT, true, true); - table.addColumn("name", "VARCHAR(256) NOT NULL"); // 作为索引 - table.addColumn("display", "VARCHAR(256) NOT NULL"); // 实际展示 - table.setIndex("name", IndexType.UNIQUE_KEY); - }); - private final Consumer builder; - private @Nullable SQLManager manager; - - private static final String tableName = "SUFFIX_DICTIONARY"; - - SuffixDictionary(Consumer builder) { - this.builder = builder; - } - - @Override - public @Nullable SQLManager getSQLManager() { - return this.manager; - } - - @Override - public @NotNull String getTableName() { - return tableName; - } - - @Override - public boolean create(SQLManager sqlManager) throws SQLException { - this.manager = sqlManager; - - TableCreateBuilder tableBuilder = sqlManager.createTable(tableName); - if (builder != null) builder.accept(tableBuilder); - - return tableBuilder.build().executeFunction(l -> l > 0, false); - } - - public static void initialize(SQLManager sqlManager) throws SQLException { - SUFFIX_DICTIONARY.create(sqlManager); - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/ConfigManager.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/ConfigManager.java deleted file mode 100644 index d4b8a14..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/ConfigManager.java +++ /dev/null @@ -1,32 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.manager; - -import top.redstarmc.plugin.velocitytitle.core.api.AbstractTomlManager; - -import java.io.File; - -public class ConfigManager extends AbstractTomlManager { - - /** - * 构造器 - * @param dataFolder 插件的工作文件夹 - * @param fileName 要操作的配置文件名称 - */ - public ConfigManager(File dataFolder, String fileName) { - super(dataFolder, fileName); - } - - /** - * Velocity 侧初始化配置文件 - */ - @Override - public void init() { - tryCreateFile(); //测试 - - loadConfig(); - - updateFile(); - - } - - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/EasySQLManager.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/EasySQLManager.java deleted file mode 100644 index 05c6acc..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/EasySQLManager.java +++ /dev/null @@ -1,113 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.manager; - -import cc.carm.lib.easysql.EasySQL; -import cc.carm.lib.easysql.hikari.HikariConfig; -import cc.carm.lib.easysql.hikari.HikariDataSource; -import cc.carm.lib.easysql.manager.SQLManagerImpl; -import top.redstarmc.plugin.velocitytitle.velocity.database.DebugHandler; -import top.redstarmc.plugin.velocitytitle.velocity.database.table.PlayerTitles; -import top.redstarmc.plugin.velocitytitle.velocity.database.table.PlayerWear; -import top.redstarmc.plugin.velocitytitle.velocity.database.table.PrefixDictionary; -import top.redstarmc.plugin.velocitytitle.velocity.database.table.SuffixDictionary; - -import java.sql.SQLException; - -/** - *

数据库管理器

- * 使用 {@link cc.carm.lib.easysql.EasySQL} 数据库操作库。 - */ -public class EasySQLManager { - - private static SQLManagerImpl sqlManager; - - private final LoggerManager logger; - - private final ConfigManager config; - - private final ConfigManager language; - - public EasySQLManager(LoggerManager logger, ConfigManager config, ConfigManager language) { - this.logger = logger; - this.config = config; - this.language = language; - } - - /** - *

初始化数据库

- * 从 {@link top.redstarmc.plugin.velocitytitle.velocity.VelocityTitleVelocity } 调用。数据库入口方法。 - */ - public void init(){ - String mode = config.getConfigToml().getString("database.mode"); - - if(mode.equals("Embedded")){ - logger.info(language.getConfigToml().getString("database.embedded")); - initEmbedded(); - } else if (mode.equals("Server")) { - logger.info(language.getConfigToml().getString("database.server")); - initServer(); - }else { - logger.info(language.getConfigToml().getString("database.other")); - initEmbedded(); - } - - sqlManager.setDebugHandler(new DebugHandler(logger)); - sqlManager.setDebugMode(config.getConfigToml().getBoolean("debug-mode")); - - try { - if (!sqlManager.getConnection().isValid(5)) { - logger.error(language.getConfigToml().getString("database.timeout")); - } - - // 注册数据表 - logger.debugDataBase("正在注册数据表"); - - PrefixDictionary.initialize(sqlManager); - SuffixDictionary.initialize(sqlManager); - PlayerTitles.initialize(sqlManager); - PlayerWear.initialize(sqlManager); - - logger.debugDataBase("数据表注册完毕"); - - } catch (SQLException e) { - logger.error(language.getConfigToml().getString("database.failed")); - logger.debug(e.getMessage(),e); - } - } - - /** - * 初始化嵌入式数据库的 {@link cc.carm.lib.easysql.api.SQLManager} - */ - private void initEmbedded(){ - String driver = config.getConfigToml().getString("database.driver"); - String url = config.getConfigToml().getString("database.url"); - String username = config.getConfigToml().getString("database.username"); - String password = config.getConfigToml().getString("database.password"); - - HikariConfig hikariConfig = new HikariConfig(); - hikariConfig.setDriverClassName(driver); - hikariConfig.setJdbcUrl(url); - // 新增:设置用户名和密码 - hikariConfig.setUsername(username); - hikariConfig.setPassword(password); - - sqlManager = new SQLManagerImpl(new HikariDataSource(hikariConfig), "test"); - - } - - /** - * 初始化服务器(远程链接)数据库的 {@link cc.carm.lib.easysql.api.SQLManager} - */ - private void initServer(){ - String diver = config.getConfigToml().getString("database.driver"); - String url = config.getConfigToml().getString("database.url"); - String username = config.getConfigToml().getString("database.username"); - String password = config.getConfigToml().getString("database.password"); - - sqlManager = EasySQL.createManager(diver,url,username,password); - } - - - public static SQLManagerImpl getSqlManager() { - return sqlManager; - } -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/LoggerManager.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/LoggerManager.java deleted file mode 100644 index f076761..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/manager/LoggerManager.java +++ /dev/null @@ -1,28 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.manager; - -import top.redstarmc.plugin.velocitytitle.core.api.AbstractLoggerManager; -import top.redstarmc.plugin.velocitytitle.velocity.util.ColoredConsole; - -/** - *

日志管理器

- * 提供了日志相关的操作代码,以免重复编写发送日志的操作。 - */ -public class LoggerManager extends AbstractLoggerManager { - - public LoggerManager(String INFO_PREFIX, boolean debugMode) { - super(INFO_PREFIX, debugMode); - } - - /** - *

向控制台打印的方法

- * @param msg 内容 - */ - @Override - public void sendMessage(String... msg) { - for (String s : msg) { - if (s == null) continue; - System.out.println(ColoredConsole.toANSI(s + "§r")); - } - } - -} diff --git a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/util/ColoredConsole.java b/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/util/ColoredConsole.java deleted file mode 100644 index 2d94791..0000000 --- a/velocity/src/main/java/top/redstarmc/plugin/velocitytitle/velocity/util/ColoredConsole.java +++ /dev/null @@ -1,43 +0,0 @@ -package top.redstarmc.plugin.velocitytitle.velocity.util; - -import java.util.LinkedHashMap; - -/** - *

Velocity 控制台染色器

- */ -public class ColoredConsole { - - private static final LinkedHashMap MAP = new LinkedHashMap<>(); - - static { - MAP.put("§0", "\u001B[30m"); - MAP.put("§1", "\u001B[34m"); - MAP.put("§2", "\u001B[32m"); - MAP.put("§3", "\u001B[36m"); - MAP.put("§4", "\u001B[31m"); - MAP.put("§5", "\u001B[35m"); - MAP.put("§6", "\u001B[33m"); - MAP.put("§7", "\u001B[37m"); - MAP.put("§8", "\u001B[90m"); - MAP.put("§9", "\u001B[94m"); - MAP.put("§a", "\u001B[92m"); - MAP.put("§b", "\u001B[96m"); - MAP.put("§c", "\u001B[91m"); - MAP.put("§d", "\u001B[95m"); - MAP.put("§e", "\u001B[93m"); - MAP.put("§f", "\u001B[97m"); - MAP.put("§k", "\u001B[5m"); - MAP.put("§l", "\u001B[1m"); - MAP.put("§m", "\u001B[9m"); - MAP.put("§n", "\u001B[4m"); - MAP.put("§o", "\u001B[3m"); - MAP.put("§r", "\u001B[0m"); - } - - public static String toANSI(String s) { - final String[] out = {s}; - MAP.forEach((mc, ansi) -> out[0] = out[0].replace(mc, ansi)); - return out[0]; - } - -} diff --git a/velocity/src/main/resources/velocity-plugin.json b/velocity/src/main/resources/velocity-plugin.json index eba4fa8..03d7fdf 100644 --- a/velocity/src/main/resources/velocity-plugin.json +++ b/velocity/src/main/resources/velocity-plugin.json @@ -1,11 +1,11 @@ { - "id": "velocity_title", - "name": "VelocityTitle", + "id": "flashy_titles", + "name": "FlashyTitles", "version": "@VERSION@", "authors": [ - "pingguomc" + "maple" ], "dependencies": [], - "main": "top.redstarmc.plugin.velocitytitle.velocity.VelocityTitleVelocity", - "website": "https://github.com/RedStarMC/VelocityTitle" + "main": "org.example.flashytitles.velocity.FlashyTitlesVelocity", + "website": "https://github.com/maplesusu/VelocityTitle" } \ No newline at end of file From 9d951e75f4ff968dccd40b041c180a13049ce3ed Mon Sep 17 00:00:00 2001 From: ZhangYongxu <1553450629@qq.com> Date: Tue, 19 Aug 2025 10:48:15 +0800 Subject: [PATCH 3/3] Update project structure and sync fabric/neoforge files --- README.md | 88 +--- build.gradle | 8 +- .../core/message/MessageType.java | 4 + .../flashytitles/spigot/sync/SyncHandler.java | 45 ++ .../velocity/listener/PlayerListener.java | 6 +- .../velocity/sync/SyncManager.java | 384 ------------------ .../src/main/resources/config-velocity.toml | 19 - .../src/main/resources/language-velocity.toml | 52 --- 8 files changed, 60 insertions(+), 546 deletions(-) delete mode 100644 velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java delete mode 100644 velocity/src/main/resources/config-velocity.toml delete mode 100644 velocity/src/main/resources/language-velocity.toml diff --git a/README.md b/README.md index 181dfa3..a3b7c9f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ # VelocityTitle - - 一个现代化的 Minecraft 跨平台称号系统,支持 Velocity、Spigot、Fabric 和 NeoForge,具有完整的占位符API支持和H2嵌入式数据库。 ## 技术特性 ### 核心技术栈 -- **Java 21** - 现代Java特性支持 +- **Java 17+** - 现代Java特性支持 - **Gradle 8.6** - 构建系统 - **HikariCP** - 高性能数据库连接池 -- **H2 Database** - 嵌入式数据库(默认) -- **MySQL/SQLite** - 可选数据库支持 -- **Jedis** - Redis缓存支持 +- **H2 Database** - 嵌入式数据库(默认,零配置) +- **MySQL** - 可选数据库支持 +- **基于数据库同步** - 无需Redis,简化部署 ### 平台支持 - **Velocity** - 代理服务器支持,H2数据库 @@ -364,84 +362,6 @@ String chatFormat = "%flashytitles_title_with_space%%player_name%: %message%"; - **PlaceholderAPI**: 2.11.5+ - **Java**: 21+ - -### 常见问题 - -**Q: H2数据库文件在哪里?** -A: 默认在 `./data/flashytitles.mv.db`,可以通过配置文件修改路径。 - -**Q: 如何备份H2数据库?** -A: 直接复制 `./data/flashytitles.mv.db` 文件即可。 - -**Q: Redis是必需的吗?** -A: **不是必需的**!Redis是完全可选的,主要用于多服务器环境的实时同步优化。单服务器或小型群组服务器完全可以不配置Redis,插件会通过H2数据库正常工作。 - -**Q: 不配置Redis会影响功能吗?** -A: 不会影响基本功能。不配置Redis时: -- ✅ 所有称号功能正常工作 -- ✅ 数据通过H2数据库存储和同步 -- ✅ 单服务器环境完全没问题 -- ⚠️ 多服务器环境同步可能稍慢(通过数据库同步而非实时同步) - -**Q: PlaceholderAPI占位符不工作?** -A: 确保安装了PlaceholderAPI插件,并且FlashyTitles扩展已注册。使用 `/papi list` 检查。 - -**Q: Fabric占位符不显示?** -A: 确保安装了Text Placeholder API 2.4.1+1.21版本。 - -**Q: 如何从MySQL迁移到H2?** -A: 使用数据导出工具或联系开发者获取迁移脚本。 - -**Q: 称号不同步怎么办?** -A: 检查网络连接,确保所有服务器都安装了对应的插件/模组。 - -**Q: 动画称号不显示?** -A: 确保称号配置中 `animated: true`,并且客户端支持颜色代码。 - -### 日志调试 -```yaml -# 在config.yml中启用调试模式 -debug: true -log-level: "DEBUG" -``` - -### 性能优化 -```yaml -# 数据库连接池配置 -database: - pool: - maximum-pool-size: 10 - minimum-idle: 5 - connection-timeout: 30000 - idle-timeout: 600000 - max-lifetime: 1800000 - -# 缓存配置 -cache: - title-cache-size: 1000 - player-cache-size: 500 - cache-expire-minutes: 30 - -# Redis配置(可选) -redis: - enabled: false # 设为true启用Redis - host: "localhost" - port: 6379 - password: "" - database: 0 -``` - -## 📊 构建产物 - -### 已生成的JAR文件 -- `build/jars/core-1.0.0.jar` - Core模块 -- `build/jars/velocity-1.0.0.jar` - Velocity模块(H2数据库) -- `build/jars/spigot-1.0.0.jar` - Spigot模块(PlaceholderAPI) -- `build/jars/fabric-1.0.0.jar` - Fabric模块(Text Placeholder API) -- `build/jars/neoforge-1.0.0.jar` - NeoForge模块(内置占位符) -- `build/jars/FlashyTitles-Complete-1.0.0.jar` - 完整版本 - - ## 快速开始 ### 1分钟快速部署 diff --git a/build.gradle b/build.gradle index 267c847..82f9f14 100644 --- a/build.gradle +++ b/build.gradle @@ -40,8 +40,7 @@ allprojects { implementation 'com.zaxxer:HikariCP:5.0.1' implementation 'mysql:mysql-connector-java:8.0.33' implementation 'org.xerial:sqlite-jdbc:3.42.0.0' - implementation 'redis.clients:jedis:4.4.3' - // H2 数据库 + // H2 数据库 (默认使用) implementation 'com.h2database:h2:2.2.224' } @@ -81,9 +80,10 @@ subprojects { group = 'build' destinationDirectory.set(rootProject.file("${rootProject.rootDir}/build/jars")) archiveClassifier.set('original') - + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from sourceSets.main.output - + // 对velocity、spigot、fabric、neoforge模块添加core代码 if (project.name in ['velocity', 'spigot', 'fabric', 'neoforge']) { from project(':core').sourceSets.main.output diff --git a/core/src/main/java/org/example/flashytitles/core/message/MessageType.java b/core/src/main/java/org/example/flashytitles/core/message/MessageType.java index e5a74de..42f87a8 100644 --- a/core/src/main/java/org/example/flashytitles/core/message/MessageType.java +++ b/core/src/main/java/org/example/flashytitles/core/message/MessageType.java @@ -16,6 +16,10 @@ public enum MessageType { PLAYER_DATA_REQUEST("player_data_req"), // 请求玩家数据 PLAYER_DATA_RESPONSE("player_data_res"), // 响应玩家数据 + // 权限相关 + PERMISSION_CHECK("permission_check"), // 权限检查请求 + PERMISSION_RESPONSE("permission_res"), // 权限检查响应 + // 系统相关 RELOAD_CONFIG("reload_config"), // 重载配置 SYNC_ALL("sync_all"), // 同步所有数据 diff --git a/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java b/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java index e627249..573c9f6 100644 --- a/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java +++ b/spigot/src/main/java/org/example/flashytitles/spigot/sync/SyncHandler.java @@ -77,6 +77,7 @@ private void handleMessage(Message message) { case PLAYER_QUIT -> handlePlayerQuit(message); case SYNC_ALL -> handleSyncAll(message); case RELOAD_CONFIG -> handleReloadConfig(message); + case PERMISSION_CHECK -> handlePermissionCheck(message); default -> plugin.getLogger().warning("收到未知消息类型: " + message.getType()); } } @@ -186,6 +187,50 @@ private void handleReloadConfig(Message message) { plugin.getLogger().info("收到配置重载请求"); // 这里可以实现配置重载逻辑 } + + /** + * 处理权限检查请求 + */ + private void handlePermissionCheck(Message message) { + String playerUuidStr = message.getString("player_uuid"); + String playerName = message.getString("player_name"); + String permission = message.getString("permission"); + String requestId = message.getString("request_id"); + + if (playerUuidStr == null || permission == null || requestId == null) { + plugin.getLogger().warning("权限检查请求参数不完整"); + return; + } + + try { + UUID playerUuid = UUID.fromString(playerUuidStr); + Player player = Bukkit.getPlayer(playerUuid); + + boolean hasPermission = false; + if (player != null && player.isOnline()) { + // 检查玩家权限 + hasPermission = player.hasPermission(permission); + plugin.getLogger().info("权限检查: 玩家 " + player.getName() + " 权限 " + permission + " = " + hasPermission); + } else { + plugin.getLogger().info("玩家 " + playerName + " 不在线,权限检查失败"); + } + + // 发送权限检查响应 + Message response = new Message(MessageType.PERMISSION_RESPONSE) + .addData("request_id", requestId) + .addData("player_uuid", playerUuidStr) + .addData("player_name", playerName) + .addData("permission", permission) + .addData("has_permission", hasPermission); + + if (player != null) { + sendMessage(player, response); + } + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("无效的玩家UUID: " + playerUuidStr); + } + } /** * 请求玩家数据 diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java b/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java index 9fc22c9..d29e48b 100644 --- a/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java +++ b/velocity/src/main/java/org/example/flashytitles/velocity/listener/PlayerListener.java @@ -11,7 +11,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import org.example.flashytitles.velocity.manager.TitleManager; -import org.example.flashytitles.velocity.sync.SyncManager; +import org.example.flashytitles.velocity.sync.SimplifiedSyncManager; import org.slf4j.Logger; /** @@ -22,10 +22,10 @@ public class PlayerListener { private static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("flashytitles", "sync"); private final TitleManager titleManager; - private final SyncManager syncManager; + private final SimplifiedSyncManager syncManager; private final Logger logger; - public PlayerListener(TitleManager titleManager, SyncManager syncManager, Logger logger) { + public PlayerListener(TitleManager titleManager, SimplifiedSyncManager syncManager, Logger logger) { this.titleManager = titleManager; this.syncManager = syncManager; this.logger = logger; diff --git a/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java b/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java deleted file mode 100644 index 70de9da..0000000 --- a/velocity/src/main/java/org/example/flashytitles/velocity/sync/SyncManager.java +++ /dev/null @@ -1,384 +0,0 @@ -package org.example.flashytitles.velocity.sync; - -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import org.example.flashytitles.core.message.Message; -import org.example.flashytitles.core.message.MessageType; -import org.example.flashytitles.core.model.Title; -import org.example.flashytitles.velocity.config.ConfigManager; -import org.example.flashytitles.velocity.manager.TitleManager; -import org.slf4j.Logger; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.JedisPubSub; - -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** - * 同步管理器 - * 负责 Velocity 和 Spigot 服务器之间的数据同步 - */ -public class SyncManager { - - private static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("flashytitles", "sync"); - - private final ConfigManager configManager; - private final TitleManager titleManager; - private final ProxyServer server; - private final Logger logger; - - private JedisPool jedisPool; - private ScheduledExecutorService scheduler; - private boolean redisEnabled; - - public SyncManager(ConfigManager configManager, TitleManager titleManager, ProxyServer server, Logger logger) { - this.configManager = configManager; - this.titleManager = titleManager; - this.server = server; - this.logger = logger; - } - - /** - * 初始化同步管理器 - */ - public void initialize() { - logger.info("正在初始化同步管理器..."); - - // 注册插件消息通道 - server.getChannelRegistrar().register(CHANNEL); - - // 初始化 Redis(如果启用) - redisEnabled = configManager.isRedisEnabled(); - if (redisEnabled) { - initializeRedis(); - } - - // 启动定时同步任务 - if (configManager.isSyncEnabled()) { - startSyncTask(); - } - - logger.info("同步管理器初始化完成"); - logger.info("- Redis 同步: {}", redisEnabled ? "启用" : "禁用"); - logger.info("- 定时同步: {}", configManager.isSyncEnabled() ? "启用" : "禁用"); - } - - /** - * 初始化 Redis 连接 - */ - private void initializeRedis() { - try { - JedisPoolConfig poolConfig = new JedisPoolConfig(); - poolConfig.setMaxTotal(10); - poolConfig.setMaxIdle(5); - poolConfig.setMinIdle(1); - poolConfig.setTestOnBorrow(true); - poolConfig.setTestOnReturn(true); - - String password = configManager.getRedisPassword(); - if (password.isEmpty()) { - password = null; - } - - jedisPool = new JedisPool( - poolConfig, - configManager.getRedisHost(), - configManager.getRedisPort(), - 2000, - password, - configManager.getRedisDatabase() - ); - - // 测试连接 - try (Jedis jedis = jedisPool.getResource()) { - jedis.ping(); - logger.info("Redis 连接成功"); - } - - // 启动 Redis 订阅 - startRedisSubscription(); - - } catch (Exception e) { - logger.error("Redis 初始化失败,将禁用 Redis 同步", e); - redisEnabled = false; - if (jedisPool != null) { - jedisPool.close(); - jedisPool = null; - } - } - } - - /** - * 启动 Redis 订阅 - */ - private void startRedisSubscription() { - if (!redisEnabled || jedisPool == null) return; - - CompletableFuture.runAsync(() -> { - try (Jedis jedis = jedisPool.getResource()) { - jedis.subscribe(new JedisPubSub() { - @Override - public void onMessage(String channel, String message) { - if ("flashytitles:sync".equals(channel)) { - handleRedisMessage(message); - } - } - }, "flashytitles:sync"); - } catch (Exception e) { - logger.error("Redis 订阅异常", e); - } - }); - } - - /** - * 处理 Redis 消息 - */ - private void handleRedisMessage(String messageStr) { - try { - Message message = Message.deserialize(messageStr.getBytes()); - handleSyncMessage(message); - } catch (Exception e) { - logger.error("处理 Redis 消息失败", e); - } - } - - /** - * 启动定时同步任务 - */ - private void startSyncTask() { - scheduler = Executors.newScheduledThreadPool(1); - - int interval = configManager.getSyncInterval(); - scheduler.scheduleAtFixedRate(() -> { - try { - syncAllPlayersToServers(); - } catch (Exception e) { - logger.error("定时同步任务异常", e); - } - }, interval, interval, TimeUnit.SECONDS); - - logger.info("定时同步任务已启动,间隔: {} 秒", interval); - } - - /** - * 同步所有玩家数据到所有服务器 - */ - public void syncAllPlayersToServers() { - for (Player player : server.getAllPlayers()) { - syncPlayerToAllServers(player); - } - } - - /** - * 同步玩家数据到所有服务器 - */ - public void syncPlayerToAllServers(Player player) { - String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); - - Message message = new Message(MessageType.TITLE_UPDATE) - .addData("player_uuid", player.getUniqueId().toString()) - .addData("player_name", player.getUsername()); - - if (equippedTitle != null) { - Title title = titleManager.getTitle(equippedTitle); - if (title != null) { - String renderedTitle = titleManager.renderTitle(equippedTitle); - message.addData("title_id", equippedTitle) - .addData("title_text", renderedTitle) - .addData("animated", title.isAnimated()); - } - } else { - message.addData("title_id", "") - .addData("title_text", "") - .addData("animated", false); - } - - // 发送到所有服务器 - broadcastMessage(message); - - // 发送到 Redis - if (redisEnabled) { - publishToRedis(message); - } - } - - /** - * 同步玩家数据到指定服务器 - */ - public void syncPlayerToServer(Player player, ServerConnection server) { - String equippedTitle = titleManager.getEquippedTitle(player.getUniqueId()); - - Message message = new Message(MessageType.TITLE_UPDATE) - .addData("player_uuid", player.getUniqueId().toString()) - .addData("player_name", player.getUsername()); - - if (equippedTitle != null) { - Title title = titleManager.getTitle(equippedTitle); - if (title != null) { - String renderedTitle = titleManager.renderTitle(equippedTitle); - message.addData("title_id", equippedTitle) - .addData("title_text", renderedTitle) - .addData("animated", title.isAnimated()); - } - } else { - message.addData("title_id", "") - .addData("title_text", "") - .addData("animated", false); - } - - sendMessageToServer(server, message); - } - - /** - * 广播消息到所有服务器 - */ - public void broadcastMessage(Message message) { - byte[] data = createPluginMessage(message); - - for (Player player : server.getAllPlayers()) { - Optional serverConnection = player.getCurrentServer(); - if (serverConnection.isPresent()) { - serverConnection.get().sendPluginMessage(CHANNEL, data); - } - } - } - - /** - * 发送消息到指定服务器 - */ - public void sendMessageToServer(ServerConnection serverConnection, Message message) { - byte[] data = createPluginMessage(message); - serverConnection.sendPluginMessage(CHANNEL, data); - } - - /** - * 创建插件消息数据 - */ - private byte[] createPluginMessage(Message message) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - byte[] messageData = message.serialize(); - out.writeInt(messageData.length); - out.write(messageData); - return out.toByteArray(); - } - - /** - * 处理来自 Spigot 的消息 - */ - public void handlePluginMessage(Player player, ByteArrayDataInput in) { - try { - int length = in.readInt(); - byte[] messageData = new byte[length]; - in.readFully(messageData); - - Message message = Message.deserialize(messageData); - handleSyncMessage(message); - - } catch (Exception e) { - logger.error("处理插件消息失败", e); - } - } - - /** - * 处理同步消息 - */ - private void handleSyncMessage(Message message) { - switch (message.getType()) { - case PLAYER_DATA_REQUEST -> { - String playerUuidStr = message.getString("player_uuid"); - if (playerUuidStr != null) { - UUID playerUuid = UUID.fromString(playerUuidStr); - Optional player = server.getPlayer(playerUuid); - if (player.isPresent()) { - syncPlayerToAllServers(player.get()); - } - } - } - case HEARTBEAT -> { - // 心跳包,可以用于检测连接状态 - logger.debug("收到心跳包"); - } - default -> logger.debug("收到未处理的消息类型: {}", message.getType()); - } - } - - /** - * 发布消息到 Redis - */ - private void publishToRedis(Message message) { - if (!redisEnabled || jedisPool == null) return; - - try (Jedis jedis = jedisPool.getResource()) { - String messageStr = new String(message.serialize()); - jedis.publish("flashytitles:sync", messageStr); - } catch (Exception e) { - logger.error("发布 Redis 消息失败", e); - } - } - - /** - * 玩家加入时的同步处理 - */ - public void onPlayerJoin(Player player) { - if (configManager.isAutoSyncOnJoin()) { - // 延迟一点时间,确保玩家完全加入服务器 - CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS).execute(() -> { - Optional serverConnection = player.getCurrentServer(); - if (serverConnection.isPresent()) { - syncPlayerToServer(player, serverConnection.get()); - } - }); - } - } - - /** - * 玩家离开时的同步处理 - */ - public void onPlayerQuit(Player player) { - Message message = new Message(MessageType.PLAYER_QUIT) - .addData("player_uuid", player.getUniqueId().toString()) - .addData("player_name", player.getUsername()); - - broadcastMessage(message); - - if (redisEnabled) { - publishToRedis(message); - } - } - - /** - * 关闭同步管理器 - */ - public void shutdown() { - logger.info("正在关闭同步管理器..."); - - if (scheduler != null && !scheduler.isShutdown()) { - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - if (jedisPool != null && !jedisPool.isClosed()) { - jedisPool.close(); - } - - logger.info("同步管理器已关闭"); - } -} diff --git a/velocity/src/main/resources/config-velocity.toml b/velocity/src/main/resources/config-velocity.toml deleted file mode 100644 index 446cfd6..0000000 --- a/velocity/src/main/resources/config-velocity.toml +++ /dev/null @@ -1,19 +0,0 @@ -# 运行在 Velocity 上时的配置 - -# 不要更改本设置,否则会造成配置重置,后果自负。 -# 更新新的插件版本也会导致配置重置。请务必提前备份配置文件 -version = "@VERSION@" - -# 插件提示前缀(可以使用 & 符号) -plugin-prefix = "[VelocityTitle] " - -# 是否开启 Debug 模式 -debug-mode = true - -# 数据库设置 -[database] -mode = "Embedded" # 数据库工作方式 -driver = "org.h2.Driver" # 数据库驱动地址 -url = "jdbc:h2:file:./data/velocitytitle;DB_CLOSE_DELAY=-1;MODE=MYSQL;" # 数据库 URL 地址 -username = "root" # 用户名 -password = "password" # 密码 diff --git a/velocity/src/main/resources/language-velocity.toml b/velocity/src/main/resources/language-velocity.toml deleted file mode 100644 index a114a62..0000000 --- a/velocity/src/main/resources/language-velocity.toml +++ /dev/null @@ -1,52 +0,0 @@ -# 运行在 Velocity 上时的语言配置 - -# 不要更改本设置,否则会造成配置重置,后果自负。 -# 更新新的插件版本也会导致配置重置。请务必提前备份配置文件 -version = "@VERSION@" - -# 语言名称 -# 将会显示在日志和各种提示中 -name = "简体中文" - -# 显示模式 -mode = "" - -# 日志提示类 -[logs] -loading = "插件正在启动~~~" -command-loading = "正在注册 命令 ……" -database-loading = "正在加载 数据库 ……" -listener-loading = "正在注册 监听器 ……" -channel-loading = "正在注册 插件消息通道 ……" - -author = "插件作者:" -debug = "请注意!当前已启用 Debug 模式。" - -thanks = "感谢您使用本插件!" -end = "===加载完毕===" -website = "本插件是开源的。Github :" - -reload = "插件配置文件已重载。" - -# 命令类 -[commands] -root = "========VelocityTitle========\n" -parameter-less = "缺少必要的参数!" -create-success = "创建成功!" -console = "控制台不能执行该指令!" - -# 命令帮助类 -[commands.helps] -open = "使用 /vt help 打开帮助页面" -create-prefix = "/vt create prefix [称号ID] [显示内容(可使用&)] 设置前缀" -create-suffix = "/vt create suffix [称号ID] [显示内容(可使用&)] 设置后缀" - - -# 数据库类 -[database] -embedded = "[数据库] 正在加载嵌入式(Embedded)数据库……" -server = "[数据库] 正在加载服务器模式(Server)数据库……" -other = "[数据库] 无法识别数据库模式!正在加载默认的嵌入式(Embedded)数据库……" -timeout = "[数据库] 链接超时!" -failed = "[数据库] 无法链接到数据库!" -failed-operate = "[数据库] 数据库在操作时出现问题!"