├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_zh.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── io │ └── github │ └── syferie │ └── magicblock │ ├── MagicBlockPlugin.java │ ├── api │ ├── IMagicBlock.java │ └── IMagicFood.java │ ├── block │ ├── BlockBindManager.java │ └── BlockManager.java │ ├── command │ ├── CommandManager.java │ └── handler │ │ └── TabCompleter.java │ ├── database │ └── DatabaseManager.java │ ├── food │ ├── FoodManager.java │ └── FoodService.java │ ├── gui │ ├── BlockSelectionGUI.java │ └── GUIManager.java │ ├── hook │ └── PlaceholderHook.java │ ├── listener │ └── BlockListener.java │ ├── manager │ └── MagicBlockIndexManager.java │ ├── metrics │ └── Metrics.java │ └── util │ ├── Constants.java │ ├── LanguageManager.java │ ├── MessageUtil.java │ ├── MinecraftLangManager.java │ ├── PerformanceMonitor.java │ ├── Statistics.java │ └── UpdateChecker.java └── resources ├── config.yml ├── foodconf.yml ├── lang_en.yml ├── lang_zh_CN.yml ├── minecraftLanguage ├── en_gb └── zh_cn └── plugin.yml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **请在提问前务必阅读并遵守以下规则,否则你的 issue 可能会被直接关闭:** 11 | - 必须附上服务器核心及版本、插件版本信息。 12 | - 必须提供可复现的具体步骤/描述(可辅以视频、GIF、图片等辅助材料)。如为概率性或偶发问题,需自行查明并准确描述复现方式。 13 | - 必须附上所有与问题相关的日志、配置等文件。若为配置相关问题,请附完整 config 文件。 14 | - 请先仔细检查 config 文件,确保没有你需要的配置项,或无法通过组合现有配置实现你的需求。 15 | - 信息不充分、不符合上述要求的 issue 将被直接关闭,不再另行解释。 16 | 17 | --- 18 | 19 | ### 1. 服务器及插件信息 20 | 21 | - **服务器核心及版本(如:Paper-1.20.2-353):** 22 | - **插件名称及版本(如:ExamplePlugin v1.2.3):** 23 | 24 | ### 2. 问题描述 25 | 26 | 27 | 28 | ### 3. 复现步骤 29 | 30 | 31 | 32 | 1. 33 | 2. 34 | 3. 35 | 36 | ### 4. 相关日志及文件 37 | 38 | - **服务器日志(请附出错相关部分或完整日志):** 39 | - **相关配置文件(如 config.yml,务必完整粘贴或上传附件):** 40 | 41 | ### 5. 配置项确认 42 | 43 | - [ ] 我已仔细阅读并确认 config 文件中没有相关配置项,或无法通过配置实现我的需求。 44 | 45 | --- 46 | 47 | **未按上述模板填写/信息不全者,issue 将被直接关闭。请节省你我时间,谢谢合作!** 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feat]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 1. 你的需求或建议是什么? 11 | 12 | 13 | 14 | ### 2. 实现该功能的背景和意义 15 | 16 | 17 | 18 | ### 3. 你期望的具体实现方式 19 | 20 | 21 | 22 | ### 4. 补充信息 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Plugin 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - 'README.md' 8 | - 'docs/**' 9 | 10 | permissions: 11 | contents: write 12 | packages: write 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | version: ${{ steps.read_version.outputs.version }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Prepare Build 23 | run: | 24 | if [ -f gradle.properties ]; then 25 | sed -i '/org.gradle.java.home/d' gradle.properties 26 | fi 27 | chmod +x gradlew 28 | 29 | - name: Set up JDK 17 30 | uses: actions/setup-java@v4 31 | with: 32 | java-version: '17' 33 | distribution: 'temurin' 34 | cache: 'gradle' 35 | 36 | - name: List Files 37 | run: | 38 | ls -l 39 | 40 | - name: Convert Line Endings 41 | run: | 42 | sudo apt-get install -y dos2unix 43 | dos2unix build.gradle 44 | 45 | - name: Read Version 46 | id: read_version 47 | run: | 48 | # 更强健的版本提取逻辑,支持四位版本号 49 | VERSION=$(grep "^version" build.gradle | sed -E "s/^version\s*['\"]?([^'\"]+)['\"]?.*$/\1/" | xargs) 50 | 51 | # 如果第一种方法失败,尝试其他方法 52 | if [ -z "$VERSION" ] || [ "$VERSION" = "version" ]; then 53 | VERSION=$(grep "^version" build.gradle | cut -d"'" -f2) 54 | fi 55 | 56 | # 再次检查,使用 awk 作为备用方法 57 | if [ -z "$VERSION" ] || [ "$VERSION" = "version" ]; then 58 | VERSION=$(awk -F"'" '/^version/ {print $2}' build.gradle) 59 | fi 60 | 61 | # 最终检查 62 | if [ -z "$VERSION" ] || [ "$VERSION" = "version" ]; then 63 | echo "Version not found in build.gradle. Exiting." 64 | echo "Debug: build.gradle content:" 65 | cat build.gradle | head -10 66 | exit 1 67 | fi 68 | 69 | echo "version=$VERSION" >> $GITHUB_OUTPUT 70 | echo "Detected Version: $VERSION" 71 | echo "Version validation: Four-part version support enabled" 72 | 73 | # 验证版本格式 74 | if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then 75 | echo "✅ Version format validation passed: $VERSION" 76 | if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 77 | echo "🎯 Four-part version detected: $VERSION" 78 | else 79 | echo "📝 Three-part version detected: $VERSION" 80 | fi 81 | else 82 | echo "❌ Invalid version format: $VERSION" 83 | exit 1 84 | fi 85 | 86 | - name: Build with Gradle 87 | run: ./gradlew clean build --info --stacktrace 88 | env: 89 | GRADLE_OPTS: "-Dorg.gradle.logging.level=info" 90 | VERSION: ${{ steps.read_version.outputs.version }} 91 | 92 | - name: Upload Artifact 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: plugin-jar 96 | path: ${{ github.workspace }}/build/libs/MagicBlock-${{ steps.read_version.outputs.version }}.jar 97 | retention-days: 1 98 | 99 | release: 100 | needs: build 101 | runs-on: ubuntu-latest 102 | steps: 103 | - uses: actions/checkout@v4 104 | 105 | - name: Download Artifact 106 | uses: actions/download-artifact@v4 107 | with: 108 | name: plugin-jar 109 | path: . 110 | 111 | - name: List Downloaded Artifacts 112 | run: ls -l 113 | 114 | - name: Create Release 115 | id: create_release 116 | uses: softprops/action-gh-release@v1 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | with: 120 | tag_name: v${{ needs.build.outputs.version }} 121 | name: Release ${{ needs.build.outputs.version }} 122 | draft: false 123 | prerelease: false 124 | files: MagicBlock-${{ needs.build.outputs.version }}.jar 125 | body: | 126 | ## MagicBlock v${{ needs.build.outputs.version }} 127 | 128 | ### 📦 下载 129 | - **插件文件**: MagicBlock-${{ needs.build.outputs.version }}.jar 130 | 131 | ### 🔧 安装说明 132 | 1. 下载 JAR 文件 133 | 2. 将文件放入服务器的 `plugins` 文件夹 134 | 3. 重启服务器或使用 `/reload` 命令 135 | 136 | ### 📋 版本信息 137 | - **版本号**: ${{ needs.build.outputs.version }} 138 | - **支持版本**: Minecraft 1.18.2+ 139 | - **Java版本**: 17+ 140 | 141 | ### 🆕 更新内容 142 | 请查看 [更新日志](https://github.com/${{ github.repository }}/commits/main) 了解详细更改。 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | .gradle/ 4 | 5 | *.iml 6 | *.ipr 7 | *.iws 8 | 9 | # IntelliJ 10 | out/ 11 | 12 | # Compiled class file 13 | *.class 14 | 15 | # Log file 16 | *.log 17 | 18 | # BlueJ files 19 | *.ctxt 20 | 21 | # Package Files # 22 | *.jar 23 | *.war 24 | *.nar 25 | *.ear 26 | *.zip 27 | *.tar.gz 28 | *.rar 29 | 30 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 31 | hs_err_pid* 32 | 33 | *~ 34 | 35 | # temporary files which can be created if a process still has a handle open of a deleted file 36 | .fuse_hidden* 37 | 38 | # KDE directory preferences 39 | .directory 40 | 41 | # Linux trash folder which might appear on any partition or disk 42 | .Trash-* 43 | 44 | # .nfs files are created when an open file is removed but is still being accessed 45 | .nfs* 46 | 47 | # General 48 | .DS_Store 49 | .AppleDouble 50 | .LSOverride 51 | 52 | # Icon must end with two \r 53 | Icon 54 | 55 | # Thumbnails 56 | ._* 57 | 58 | # Files that might appear in the root of a volume 59 | .DocumentRevisions-V100 60 | .fseventsd 61 | .Spotlight-V100 62 | .TemporaryItems 63 | .Trashes 64 | .VolumeIcon.icns 65 | .com.apple.timemachine.donotpresent 66 | 67 | # Directories potentially created on remote AFP share 68 | .AppleDB 69 | .AppleDesktop 70 | Network Trash Folder 71 | Temporary Items 72 | .apdisk 73 | 74 | # Windows thumbnail cache files 75 | Thumbs.db 76 | Thumbs.db:encryptable 77 | ehthumbs.db 78 | ehthumbs_vista.db 79 | 80 | # Dump file 81 | *.stackdump 82 | 83 | # Folder config file 84 | [Dd]esktop.ini 85 | 86 | # Recycle Bin used on file shares 87 | $RECYCLE.BIN/ 88 | 89 | # Windows Installer files 90 | *.cab 91 | *.msi 92 | *.msix 93 | *.msm 94 | *.msp 95 | 96 | # Windows shortcuts 97 | *.lnk 98 | 99 | target/ 100 | build/ 101 | libs/ 102 | 103 | pom.xml.tag 104 | pom.xml.releaseBackup 105 | pom.xml.versionsBackup 106 | pom.xml.next 107 | 108 | release.properties 109 | dependency-reduced-pom.xml 110 | buildNumber.properties 111 | .mvn/timing.properties 112 | .mvn/wrapper/maven-wrapper.jar 113 | .flattened-pom.xml 114 | 115 | # Common working directory 116 | run/ 117 | /.settings/ 118 | /.classpath 119 | /.project 120 | /paper/ 121 | 122 | bin/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Syferie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MagicBlock Plugin 2 | 3 | [点击查看中文版介绍](README_zh.md) 4 | 5 | MagicBlock is a feature-rich Minecraft plugin that enables players to use magic blocks and magic food items with limited usage counts. These special items can be bound to specific players and managed through an intuitive GUI interface. 6 | 7 | ## Features 8 | * Magic Block System 9 | * Configurable usage counts for blocks 10 | * Block binding system 11 | * Intuitive GUI interface 12 | * Block search functionality 13 | * Magic Food System 14 | * Reusable food items 15 | * Custom food effects 16 | * Multi-language Support 17 | * English (en) 18 | * Simplified Chinese (zh_CN) 19 | * PlaceholderAPI Support 20 | * Detailed Usage Statistics 21 | * Permission System 22 | * Performance Optimization System 23 | * Smart caching mechanism 24 | * Asynchronous database operations 25 | * Batch processing optimization 26 | * Real-time performance monitoring 27 | 28 | ## Requirements 29 | * Minecraft Server Version: 1.19+ 30 | * Optional Dependency: PlaceholderAPI 31 | 32 | ## Commands 33 | Main Command: 34 | * `/magicblock` or `/mb` - Main plugin command 35 | 36 | Subcommands: 37 | * `/mb help` - Show help information 38 | * `/mb get [times]` - Get a magic block (use -1 for infinite uses) 39 | * `/mb give [times]` - Give a magic block to a player 40 | * `/mb getfood [times]` - Get magic food 41 | * `/mb settimes ` - Set uses for held magic block 42 | * `/mb addtimes ` - Add uses to held magic block 43 | * `/mb list` - View bound blocks 44 | * `/mb reload` - Reload plugin configuration 45 | * `/mb performance` or `/mb perf` - View plugin performance report 46 | 47 | ## Permissions 48 | ### Administrator Permission 49 | * `magicblock.admin` 50 | * Includes all permissions 51 | * Default: OP only 52 | * Includes sub-permissions: 53 | * `magicblock.use` 54 | * `magicblock.give` 55 | * `magicblock.reload` 56 | * `magicblock.settimes` 57 | * `magicblock.addtimes` 58 | * `magicblock.food` 59 | 60 | ### Basic Permissions 61 | * `magicblock.use` 62 | * Grants permission to use the basic features of MagicBlock 63 | * Default: `true` (enabled by default for all users) 64 | * Administrators can deny this permission to specific users or groups to prevent them from using MagicBlock functionalities 65 | * Command: `/mb get` 66 | 67 | ### Management Permissions 68 | * `magicblock.give` 69 | * Allows giving magic blocks to others 70 | * Default: OP only 71 | * Command: `/mb give [times]` 72 | * `magicblock.reload` 73 | * Allows reloading plugin configuration 74 | * Default: OP only 75 | * Command: `/mb reload` 76 | * `magicblock.settimes` 77 | * Allows setting magic block uses 78 | * Default: OP only 79 | * Command: `/mb settimes ` 80 | * `magicblock.addtimes` 81 | * Allows adding magic block uses 82 | * Default: OP only 83 | * Command: `/mb addtimes ` 84 | 85 | ### Feature Permissions 86 | * `magicblock.food` 87 | * Allows using magic food 88 | * Default: All players 89 | * Command: `/mb getfood [times]` 90 | * `magicblock.list` 91 | * Allows viewing bound block list 92 | * Default: All players 93 | * Command: `/mb list` 94 | * `magicblock.performance` 95 | * Allows viewing plugin performance reports 96 | * Default: OP only 97 | * Command: `/mb performance` or `/mb perf` 98 | 99 | ### Special Block Permissions 100 | * `magicblock.vip` - Allows using VIP-exclusive blocks 101 | * `magicblock.mvp` - Allows using MVP-exclusive blocks 102 | 103 | ## Basic Usage 104 | ### Magic Block Usage 105 | 1. Get magic block: Use `/mb get` command 106 | 2. Bind block: Sneak + Right-click 107 | 3. Place block: Place normally 108 | 4. Change block type: Sneak + Left-click to open GUI 109 | 5. View bound blocks: Use `/mb list` command 110 | 111 | ### GUI Operations 112 | * Left-click: Select block type 113 | * Search button: Search for specific blocks 114 | * Page buttons: Browse more block options 115 | 116 | ### Bound List Operations 117 | * Left-click: Retrieve bound block 118 | * Double right-click: Hide block from list (doesn't unbind) 119 | 120 | ## Configuration Files 121 | ### config.yml Main Settings 122 | ```yaml 123 | # Debug mode 124 | debug-mode: false 125 | 126 | # Language setting 127 | language: "en" # Options: "en" or "zh_CN" 128 | 129 | # Default usage count 130 | default-block-times: 1000000000 131 | 132 | # Blacklisted worlds 133 | blacklisted-worlds: 134 | - world_nether 135 | - world_the_end 136 | 137 | # Performance optimization settings 138 | performance: 139 | # Lore cache settings 140 | lore-cache: 141 | enabled: true # Enable caching 142 | duration: 5000 # Cache duration (milliseconds) 143 | max-size: 1000 # Maximum cache entries 144 | 145 | # Statistics save settings 146 | statistics: 147 | batch-threshold: 50 # Batch save threshold 148 | save-interval: 30000 # Auto-save interval (milliseconds) 149 | 150 | # Database optimization 151 | database-optimization: 152 | async-operations: true # Asynchronous database operations 153 | batch-updates: true # Batch updates 154 | ``` 155 | 156 | ### foodconf.yml Food Configuration 157 | ```yaml 158 | # Food configuration example 159 | foods: 160 | GOLDEN_APPLE: 161 | heal: 4 162 | saturation: 9.6 163 | effects: 164 | REGENERATION: 165 | duration: 100 166 | amplifier: 1 167 | ``` 168 | 169 | ## Usage Examples 170 | 1. Basic player permissions: 171 | ```yaml 172 | permissions: 173 | - magicblock.use # Allows placing and interacting with magic blocks 174 | - magicblock.break # Allows breaking magic blocks 175 | - magicblock.list # Allows viewing bound blocks list 176 | ``` 177 | 178 | 2. VIP player permissions: 179 | ```yaml 180 | permissions: 181 | - magicblock.use 182 | - magicblock.break 183 | - magicblock.list 184 | - magicblock.group.vip-material # Access to VIP materials 185 | ``` 186 | 187 | 3. Player who can use but not break magic blocks: 188 | ```yaml 189 | permissions: 190 | - magicblock.use # Can place and interact 191 | - magicblock.list # Can view bound blocks 192 | # Note: No magicblock.break permission 193 | ``` 194 | 195 | 4. Administrator permissions: 196 | ```yaml 197 | permissions: 198 | - magicblock.admin # Includes all permissions 199 | ``` 200 | 201 | 5. Performance monitoring permissions: 202 | ```yaml 203 | permissions: 204 | - magicblock.performance 205 | ``` 206 | 207 | ## PlaceholderAPI Variables 208 | Supported variables: 209 | * `%magicblock_block_uses%` - Total magic block uses 210 | * `%magicblock_food_uses%` - Total magic food uses 211 | * `%magicblock_remaining_uses%` - Remaining uses of held magic block 212 | * `%magicblock_has_block%` - Whether player has magic block 213 | * `%magicblock_has_food%` - Whether player has magic food 214 | * `%magicblock_max_uses%` - Maximum uses of held magic block 215 | * `%magicblock_uses_progress%` - Usage progress (percentage) 216 | 217 | ## Customization 218 | ### Item Group Permissions 219 | Configure available block types for different permission groups: 220 | ```yaml 221 | group: 222 | vip-material: 223 | - DIAMOND_BLOCK 224 | - EMERALD_BLOCK 225 | mvp-material: 226 | - BEACON 227 | - DRAGON_EGG 228 | ``` 229 | 230 | ### Statistics 231 | * Plugin automatically records magic block and food usage 232 | * Supports displaying statistics via PlaceholderAPI 233 | 234 | ### Performance Monitoring 235 | * Use `/mb performance` to view detailed performance reports 236 | * Real-time monitoring of cache hit rates, database operation times, and other key metrics 237 | * Smart performance suggestions to help optimize server configuration 238 | * Supported performance metrics: 239 | * Lore system performance (update count, cache hit rate, average time) 240 | * Database operation statistics (operation count, average time, async operations) 241 | * Task scheduling status (current active tasks) 242 | * Runtime statistics 243 | 244 | ## Important Notes 245 | 1. Magic blocks disappear when uses are depleted 246 | 2. Bound blocks can only be used/broken by the binding player 247 | 3. Blocks cannot be used in blacklisted worlds 248 | 4. Blocks are unaffected by pistons 249 | 5. Explosions don't destroy magic blocks 250 | 6. Binding system requires no extra permissions beyond `magicblock.use` 251 | 7. Infinite use blocks require `magicblock.give` or `magicblock.settimes` 252 | 8. VIP/MVP blocks need configured block lists 253 | 254 | ## Troubleshooting 255 | Common issues: 256 | 1. Cannot use commands: Check permission nodes 257 | 2. Cannot place blocks: Check blacklisted worlds 258 | 3. GUI won't open: Verify holding magic block 259 | 4. Cannot bind block: Check if already bound 260 | 261 | ## License 262 | Modified MIT License: 263 | 1. Free Use 264 | * Use on any server 265 | * Modify source code 266 | * Distribute modified versions 267 | 2. Restrictions 268 | * No commercial use 269 | * No selling plugin/modifications 270 | * Must retain original author information 271 | 3. Disclaimer 272 | * Provided "as is" without warranty 273 | * Author not liable for any damages 274 | 275 | ## Support 276 | For issues or suggestions: 277 | * GitHub Issues (Include reproducible steps for bugs) 278 | * QQ Group: [134484522] 279 | 280 | © 2024 MagicBlock. All Rights Reserved. 281 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # MagicBlock Plugin 2 | 3 | [Switch to English](README.md) 4 | 5 | MagicBlock 是一个功能丰富的 Minecraft 插件,允许玩家使用具有有限使用次数的魔法方块和魔法食物。这些特殊物品可以被绑定到特定玩家,并且可以通过直观的 GUI 界面进行管理。 6 | 7 | ## 功能特点 8 | * 魔法方块系统 9 | * 可配置使用次数的方块 10 | * 方块绑定系统 11 | * 直观的 GUI 界面 12 | * 方块搜索功能 13 | * 魔法食物系统 14 | * 可重复使用的食物 15 | * 自定义食物效果 16 | * 多语言支持 17 | * 英语 (en) 18 | * 简体中文 (zh_CN) 19 | * PlaceholderAPI 支持 20 | * 详细的使用统计 21 | * 权限系统 22 | * 性能优化系统 23 | * 智能缓存机制 24 | * 异步数据库操作 25 | * 批量处理优化 26 | * 实时性能监控 27 | 28 | ## 安装要求 29 | * Minecraft 服务器版本: 1.19+ 30 | * 可选依赖: PlaceholderAPI 31 | 32 | ## 命令系统 33 | 主命令: 34 | * `/magicblock` 或 `/mb` - 插件主命令 35 | 36 | 子命令: 37 | * `/mb help` - 显示帮助信息 38 | * `/mb get [次数]` - 获取一个魔法方块(次数为-1时获取无限次数的魔术方块) 39 | * `/mb give <玩家> [次数]` - 给予玩家魔法方块 40 | * `/mb getfood <食物> [次数]` - 获取魔法食物 41 | * `/mb settimes <次数>` - 设置手持魔法方块的使用次数 42 | * `/mb addtimes <次数>` - 增加手持魔法方块的使用次数 43 | * `/mb list` - 查看已绑定的方块 44 | * `/mb reload` - 重载插件配置 45 | * `/mb performance` 或 `/mb perf` - 查看插件性能报告 46 | 47 | ## 权限节点 48 | ### 管理员权限 49 | * `magicblock.admin` 50 | * 包含所有权限 51 | * 默认仅 OP 拥有 52 | * 包含以下子权限: 53 | * `magicblock.use` 54 | * `magicblock.give` 55 | * `magicblock.reload` 56 | * `magicblock.settimes` 57 | * `magicblock.addtimes` 58 | * `magicblock.food` 59 | 60 | ### 基础权限 61 | * `magicblock.use` 62 | * 授予使用魔术方块基本功能的权限 63 | * 默认值:`true` (默认对所有用户启用) 64 | * 管理员可以将此权限对特定用户或用户组设置为 `false`,以禁止他们使用魔术方块的功能 65 | * 命令: `/mb get` 66 | 67 | ### 管理类权限 68 | * `magicblock.give` 69 | * 允许给予其他玩家魔法方块 70 | * 默认仅 OP 拥有 71 | * 命令: `/mb give <玩家> [次数]` 72 | * `magicblock.reload` 73 | * 允许重载插件配置 74 | * 默认仅 OP 拥有 75 | * 命令: `/mb reload` 76 | * `magicblock.settimes` 77 | * 允许设置魔法方块使用次数 78 | * 默认仅 OP 拥有 79 | * 命令: `/mb settimes <次数>` 80 | * `magicblock.addtimes` 81 | * 允许增加魔法方块使用次数 82 | * 默认仅 OP 拥有 83 | * 命令: `/mb addtimes <次数>` 84 | 85 | ### 功能权限 86 | * `magicblock.food` 87 | * 允许使用魔法食物 88 | * 默认所有玩家拥有 89 | * 命令: `/mb getfood <食物> [次数]` 90 | * `magicblock.list` 91 | * 允许查看已绑定的方块列表 92 | * 默认所有玩家拥有 93 | * 命令: `/mb list` 94 | * `magicblock.performance` 95 | * 允许查看插件性能报告 96 | * 默认仅 OP 拥有 97 | * 命令: `/mb performance` 或 `/mb perf` 98 | 99 | ### 特殊方块权限 100 | * `magicblock.vip` - 允许使用VIP专属方块 101 | * `magicblock.mvp` - 允许使用MVP专属方块 102 | 103 | ## 基本操作说明 104 | ### 魔法方块使用 105 | 1. 获取魔法方块:使用 `/mb get` 命令 106 | 2. 绑定方块:潜行 + 右键点击 107 | 3. 放置方块:直接放置即可 108 | 4. 更改方块类型:潜行 + 左键打开GUI界面 109 | 5. 查看绑定方块:使用 `/mb list` 命令 110 | 111 | ### GUI 界面操作 112 | * 左键点击:选择方块类型 113 | * 使用搜索按钮:可以搜索特定方块 114 | * 翻页按钮:浏览更多方块选项 115 | 116 | ### 绑定列表操作 117 | * 左键点击:找回绑定的方块 118 | * 右键双击:从列表中隐藏方块(不会解除绑定) 119 | 120 | ## 配置文件 121 | ### config.yml 主要配置项 122 | ```yaml 123 | # 调试模式 124 | debug-mode: false 125 | 126 | # 语言设置 127 | language: "en" # 可选 "en" 或 "zh_CN" 128 | 129 | # 默认使用次数 130 | default-block-times: 1000000000 131 | 132 | # 黑名单世界 133 | blacklisted-worlds: 134 | - world_nether 135 | - world_the_end 136 | 137 | # 性能优化设置 138 | performance: 139 | # Lore 缓存设置 140 | lore-cache: 141 | enabled: true # 启用缓存 142 | duration: 5000 # 缓存时间(毫秒) 143 | max-size: 1000 # 最大缓存条目数 144 | 145 | # 统计保存设置 146 | statistics: 147 | batch-threshold: 50 # 批量保存阈值 148 | save-interval: 30000 # 自动保存间隔(毫秒) 149 | 150 | # 数据库优化 151 | database-optimization: 152 | async-operations: true # 异步数据库操作 153 | batch-updates: true # 批量更新 154 | ``` 155 | 156 | ### foodconf.yml 食物配置 157 | ```yaml 158 | # 食物配置示例 159 | foods: 160 | GOLDEN_APPLE: 161 | heal: 4 162 | saturation: 9.6 163 | effects: 164 | REGENERATION: 165 | duration: 100 166 | amplifier: 1 167 | ``` 168 | 169 | ## 使用示例 170 | 1. 给予玩家基础使用权限: 171 | ```yaml 172 | permissions: 173 | - magicblock.use # 允许放置和交互魔术方块 174 | - magicblock.break # 允许破坏魔术方块 175 | - magicblock.list # 允许查看绑定方块列表 176 | ``` 177 | 178 | 2. 给予玩家VIP权限: 179 | ```yaml 180 | permissions: 181 | - magicblock.use 182 | - magicblock.break 183 | - magicblock.list 184 | - magicblock.group.vip-material # 访问VIP材料 185 | ``` 186 | 187 | 3. 只能使用但不能破坏魔术方块的玩家: 188 | ```yaml 189 | permissions: 190 | - magicblock.use # 可以放置和交互 191 | - magicblock.list # 可以查看绑定方块 192 | # 注意:没有 magicblock.break 权限 193 | ``` 194 | 195 | 4. 给予玩家管理员权限: 196 | ```yaml 197 | permissions: 198 | - magicblock.admin # 包含所有权限 199 | ``` 200 | 201 | 5. 给予玩家性能监控权限: 202 | ```yaml 203 | permissions: 204 | - magicblock.performance 205 | ``` 206 | 207 | ## PlaceholderAPI 变量 208 | 支持的变量: 209 | * `%magicblock_block_uses%` - 显示玩家使用魔法方块的总次数 210 | * `%magicblock_food_uses%` - 显示玩家使用魔法食物的总次数 211 | * `%magicblock_remaining_uses%` - 显示当前手持魔法方块的剩余使用次数 212 | * `%magicblock_has_block%` - 显示玩家是否持有魔法方块 213 | * `%magicblock_has_food%` - 显示玩家是否持有魔法食物 214 | * `%magicblock_max_uses%` - 显示当前手持魔法方块的最大使用次数 215 | * `%magicblock_uses_progress%` - 显示使用进度(百分比) 216 | 217 | ## 定制功能 218 | ### 物品组权限 219 | 可以通过配置文件为不同权限组设置可用的方块类型: 220 | ```yaml 221 | group: 222 | vip-material: 223 | - DIAMOND_BLOCK 224 | - EMERALD_BLOCK 225 | mvp-material: 226 | - BEACON 227 | - DRAGON_EGG 228 | ``` 229 | 230 | ### 统计功能 231 | * 插件会自动记录玩家使用魔法方块和魔法食物的次数 232 | * 支持通过 PlaceholderAPI 在计分板等地方显示统计信息 233 | 234 | ### 性能监控功能 235 | * 使用 `/mb performance` 查看详细的性能报告 236 | * 实时监控缓存命中率、数据库操作时间等关键指标 237 | * 智能性能建议,帮助优化服务器配置 238 | * 支持的性能指标: 239 | * Lore 系统性能(更新次数、缓存命中率、平均时间) 240 | * 数据库操作统计(操作次数、平均时间、异步操作数) 241 | * 任务调度状态(当前活跃任务数) 242 | * 运行时间统计 243 | 244 | ## 注意事项 245 | 1. 魔法方块在使用次数耗尽后会自动消失 246 | 2. 绑定的方块只能被绑定者使用和破坏 247 | 3. 方块不能在黑名单世界中使用 248 | 4. 方块不受活塞影响 249 | 5. 爆炸不会破坏魔法方块 250 | 6. 绑定系统不需要额外权限,任何拥有 `magicblock.use` 的玩家都可以使用 251 | 7. 无限次数方块的创建需要 `magicblock.give` 或 `magicblock.settimes` 权限 252 | 8. VIP和MVP方块需要在配置文件中设置相应的方块列表 253 | 254 | ## 问题排查 255 | 常见问题: 256 | 1. 无法使用命令:检查权限节点设置 257 | 2. 方块无法放置:检查是否在黑名单世界 258 | 3. GUI无法打开:确认是否手持魔法方块 259 | 4. 方块无法绑定:检查是否已被其他玩家绑定 260 | 261 | ## 许可协议 262 | 本插件采用修改版MIT许可证: 263 | 1. 允许自由使用 264 | * 可以在任何服务器上使用本插件 265 | * 允许修改源代码 266 | * 允许分发修改后的版本 267 | 2. 限制条款 268 | * 禁止将插件或其修改版本用于商业用途 269 | * 禁止销售插件或其修改版本 270 | * 二次开发时必须保留原作者信息 271 | 3. 免责声明 272 | * 本插件按"原样"提供,不提供任何形式的保证 273 | * 作者不对使用本插件造成的任何损失负责 274 | 275 | ## 技术支持 276 | 如有问题或建议,请通过以下方式联系: 277 | * GitHub Issues,BUG反馈请在能够进行复现的情况下反馈,否则无法修复,功能建议并不是提了就会添加,是否能够实现需要根据实际情况决定。 278 | * QQ交流群:[134484522] 279 | 280 | 281 | © 2024 MagicBlock. All Rights Reserved. 282 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'com.github.johnrengelman.shadow' version '7.1.2' 4 | } 5 | 6 | group 'io.github.syferie.magicblock' 7 | version '3.1.6.1' 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } 12 | maven { url 'https://repo.extendedclip.com/releases/' } 13 | maven { url 'https://repo.papermc.io/repository/maven-public/' } 14 | maven { url 'https://maven.chengzhimeow.cn/releases/' } 15 | maven { 16 | name = "jitpack" 17 | url = "https://jitpack.io" 18 | } 19 | } 20 | 21 | dependencies { 22 | compileOnly 'org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT' 23 | compileOnly 'me.clip:placeholderapi:2.11.6' 24 | compileOnly 'org.jetbrains:annotations:24.0.0' 25 | implementation "com.github.technicallycoded:FoliaLib:main-SNAPSHOT" 26 | implementation 'com.zaxxer:HikariCP:5.0.1' 27 | implementation 'com.google.code.gson:gson:2.10.1' 28 | 29 | // 测试依赖 30 | testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' 31 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 32 | } 33 | 34 | java { 35 | toolchain.languageVersion.set(JavaLanguageVersion.of(17)) 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | tasks.withType(JavaCompile) { 43 | options.encoding = 'UTF-8' 44 | } 45 | 46 | shadowJar { 47 | // 重定位依赖包以避免冲突 48 | relocate "com.alibaba.fastjson2", "io.github.syferie.magicblock.lib.fastjson" 49 | relocate "com.tcoded.folialib", "io.github.syferie.magicblock.lib.folialib" 50 | relocate "com.zaxxer.hikari", "io.github.syferie.magicblock.lib.hikari" 51 | relocate "org.intellij.lang.annotations", "io.github.syferie.magicblock.lib.intellij.annotations" 52 | relocate "org.jetbrains.annotations", "io.github.syferie.magicblock.lib.jetbrains.annotations" 53 | relocate "org.slf4j", "io.github.syferie.magicblock.lib.slf4j" 54 | relocate "com.google.gson", "io.github.syferie.magicblock.lib.gson" 55 | 56 | // 排除不必要的文件 57 | exclude "META-INF/" 58 | exclude "schema/" 59 | 60 | archiveFileName = "MagicBlock-${version}.jar" 61 | } 62 | 63 | jar { 64 | enabled = false 65 | } 66 | 67 | artifacts { 68 | archives shadowJar 69 | } 70 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Syferie/MagicBlock/48bab6ca4a49d572078a2b367658ea55c0fb09a4/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Syferie/MagicBlock/48bab6ca4a49d572078a2b367658ea55c0fb09a4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'magicblock' 2 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/api/IMagicBlock.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.api; 2 | 3 | import org.bukkit.inventory.ItemStack; 4 | 5 | /** 6 | * 魔法方块接口 7 | */ 8 | public interface IMagicBlock { 9 | /** 10 | * 设置使用次数 11 | * @param item 物品 12 | * @param times 次数 13 | */ 14 | void setUseTimes(ItemStack item, int times); 15 | 16 | /** 17 | * 获取使用次数 18 | * @param item 物品 19 | * @return 剩余使用次数 20 | */ 21 | int getUseTimes(ItemStack item); 22 | 23 | /** 24 | * 减少使用次数 25 | * @param item 物品 26 | * @return 剩余使用次数 27 | */ 28 | int decrementUseTimes(ItemStack item); 29 | 30 | /** 31 | * 更新物品说明 32 | * @param item 物品 33 | * @param remainingTimes 剩余次数 34 | */ 35 | void updateLore(ItemStack item, int remainingTimes); 36 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/api/IMagicFood.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.api; 2 | 3 | import org.bukkit.Material; 4 | import org.bukkit.inventory.ItemStack; 5 | 6 | /** 7 | * 魔法食物接口 8 | */ 9 | public interface IMagicFood { 10 | /** 11 | * 创建魔法食物 12 | * @param material 食物类型 13 | * @return 魔法食物物品 14 | */ 15 | ItemStack createMagicFood(Material material); 16 | 17 | /** 18 | * 设置使用次数 19 | * @param item 物品 20 | * @param times 次数 21 | */ 22 | void setUseTimes(ItemStack item, int times); 23 | 24 | /** 25 | * 获取使用次数 26 | * @param item 物品 27 | * @return 剩余使用次数 28 | */ 29 | int getUseTimes(ItemStack item); 30 | 31 | /** 32 | * 减少使用次数 33 | * @param item 物品 34 | * @return 剩余使用次数 35 | */ 36 | int decrementUseTimes(ItemStack item); 37 | 38 | /** 39 | * 更新物品说明 40 | * @param item 物品 41 | * @param remainingTimes 剩余次数 42 | */ 43 | void updateLore(ItemStack item, int remainingTimes); 44 | 45 | /** 46 | * 检查是否是魔法食物 47 | * @param item 物品 48 | * @return 是否是魔法食物 49 | */ 50 | boolean isMagicFood(ItemStack item); 51 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/block/BlockManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.block; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import io.github.syferie.magicblock.api.IMagicBlock; 5 | import io.github.syferie.magicblock.util.Constants; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.NamespacedKey; 10 | import org.bukkit.inventory.ItemStack; 11 | import org.bukkit.inventory.meta.ItemMeta; 12 | import org.bukkit.persistence.PersistentDataContainer; 13 | import org.bukkit.persistence.PersistentDataType; 14 | import org.bukkit.entity.Player; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.UUID; 19 | import java.util.Objects; 20 | import java.util.Set; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.Map; 23 | 24 | public class BlockManager implements IMagicBlock { 25 | private final MagicBlockPlugin plugin; 26 | private final NamespacedKey useTimesKey; 27 | private final NamespacedKey maxTimesKey; 28 | 29 | // 性能优化:Lore 缓存 30 | private final Map> loreCache = new ConcurrentHashMap<>(); 31 | private final Map loreCacheTime = new ConcurrentHashMap<>(); 32 | 33 | public BlockManager(MagicBlockPlugin plugin) { 34 | this.plugin = plugin; 35 | this.useTimesKey = new NamespacedKey(plugin, Constants.BLOCK_TIMES_KEY); 36 | this.maxTimesKey = new NamespacedKey(plugin, "magicblock_maxtimes"); 37 | } 38 | 39 | @Override 40 | public void setUseTimes(ItemStack item, int times) { 41 | ItemMeta meta = item.getItemMeta(); 42 | if (meta == null) return; 43 | 44 | // 设置当前使用次数 45 | if (times == -1) { 46 | // 如果是无限次数,设置一个非常大的值(20亿次) 47 | int infiniteValue = Integer.MAX_VALUE - 100; 48 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, infiniteValue); 49 | meta.getPersistentDataContainer().set(maxTimesKey, PersistentDataType.INTEGER, infiniteValue); 50 | } else { 51 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, times); 52 | // 如果最大使用次数还没有设置,才设置它 53 | if (!meta.getPersistentDataContainer().has(maxTimesKey, PersistentDataType.INTEGER)) { 54 | meta.getPersistentDataContainer().set(maxTimesKey, PersistentDataType.INTEGER, times); 55 | } 56 | } 57 | 58 | // 更新物品说明 59 | updateLore(item, times == -1 ? Integer.MAX_VALUE - 100 : times); 60 | item.setItemMeta(meta); 61 | } 62 | 63 | @Override 64 | public int getUseTimes(ItemStack item) { 65 | ItemMeta meta = item.getItemMeta(); 66 | if (meta == null) return 0; 67 | 68 | PersistentDataContainer container = meta.getPersistentDataContainer(); 69 | return container.getOrDefault(useTimesKey, PersistentDataType.INTEGER, 0); 70 | } 71 | 72 | @Override 73 | public int decrementUseTimes(ItemStack item) { 74 | int currentTimes = getUseTimes(item); 75 | ItemMeta meta = item.getItemMeta(); 76 | if (meta == null) return currentTimes; 77 | 78 | // 正常减少次数 79 | currentTimes--; 80 | final int finalCurrentTimes = currentTimes; // 为 lambda 表达式创建 final 变量 81 | PersistentDataContainer container = meta.getPersistentDataContainer(); 82 | container.set(useTimesKey, PersistentDataType.INTEGER, currentTimes); 83 | item.setItemMeta(meta); 84 | 85 | // 检查是否是"无限"次数(大数值) 86 | int maxTimes = getMaxUseTimes(item); 87 | if (maxTimes == Integer.MAX_VALUE - 100) { 88 | updateLore(item, currentTimes); 89 | return currentTimes; 90 | } 91 | 92 | // 性能优化:延迟数据库更新,减少频繁写入 93 | if (isBlockBound(item)) { 94 | UUID boundPlayer = getBoundPlayer(item); 95 | if (boundPlayer != null) { 96 | final UUID finalBoundPlayer = boundPlayer; 97 | final ItemStack finalItem = item.clone(); // 创建物品副本避免并发问题 98 | // 使用异步任务更新绑定数据,避免阻塞主线程 99 | plugin.getFoliaLib().getScheduler().runAsync(task -> { 100 | updateBindingDataAsync(finalBoundPlayer, finalItem, finalCurrentTimes); 101 | }); 102 | } 103 | } 104 | 105 | updateLore(item, currentTimes); 106 | return currentTimes; 107 | } 108 | 109 | public void setInfiniteUse(ItemStack item) { 110 | ItemMeta meta = item.getItemMeta(); 111 | int defaultTimes = plugin.getDefaultBlockTimes(); 112 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, defaultTimes); 113 | item.setItemMeta(meta); 114 | } 115 | 116 | @Override 117 | public void updateLore(ItemStack item, int remainingTimes) { 118 | long startTime = System.nanoTime(); // 开始计时 119 | 120 | ItemMeta meta = item.getItemMeta(); 121 | if (meta == null) return; 122 | 123 | // 获取物品的最大使用次数 124 | int maxTimes = getMaxUseTimes(item); 125 | if (maxTimes <= 0) return; 126 | 127 | // 检查是否是"无限"次数(大数值) 128 | boolean isInfinite = maxTimes == Integer.MAX_VALUE - 100; 129 | 130 | // 性能优化:生成缓存键 131 | String cacheKey = generateLoreCacheKey(item, remainingTimes, maxTimes, isInfinite); 132 | 133 | // 检查缓存 134 | List cachedLore = getCachedLore(cacheKey); 135 | if (cachedLore != null) { 136 | plugin.getPerformanceMonitor().recordCacheHit(); 137 | meta.setLore(new ArrayList<>(cachedLore)); // 创建副本避免并发修改 138 | item.setItemMeta(meta); 139 | 140 | // 记录性能数据 141 | long duration = (System.nanoTime() - startTime) / 1_000_000; // 转换为毫秒 142 | plugin.getPerformanceMonitor().recordLoreUpdate(duration); 143 | return; 144 | } 145 | 146 | // 缓存未命中 147 | plugin.getPerformanceMonitor().recordCacheMiss(); 148 | 149 | List lore = new ArrayList<>(); 150 | 151 | // 添加魔法方块标识 152 | lore.add(plugin.getMagicLore()); 153 | 154 | // 获取物品所有者(如果已绑定)用于PAPI变量解析 155 | Player owner = null; 156 | UUID boundPlayer = null; 157 | if (isBlockBound(item)) { 158 | boundPlayer = getBoundPlayer(item); 159 | if (boundPlayer != null) { 160 | owner = Bukkit.getPlayer(boundPlayer); 161 | } 162 | } 163 | 164 | // 添加装饰性lore(如果启用) 165 | if (plugin.getConfig().getBoolean("display.decorative-lore.enabled", true)) { 166 | List configLore = plugin.getConfig().getStringList("display.decorative-lore.lines"); 167 | for (String line : configLore) { 168 | String processedLine = ChatColor.translateAlternateColorCodes('&', line); 169 | // 如果服务器安装了PlaceholderAPI,处理变量 170 | if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { 171 | // 将当前物品的使用次数信息传递给PAPI处理器 172 | // 这样即使进度条显示被禁用,仍然可以通过PAPI变量使用进度条 173 | processedLine = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(owner, processedLine); 174 | } 175 | lore.add(processedLine); 176 | } 177 | } 178 | 179 | // 添加绑定信息(如果启用且已绑定) 180 | if (plugin.getConfig().getBoolean("display.show-info.bound-player", true) && boundPlayer != null) { 181 | String bindLorePrefix = plugin.getBlockBindManager().getBindLorePrefix(); 182 | if (owner != null) { 183 | lore.add(bindLorePrefix + owner.getName()); 184 | } else { 185 | lore.add(bindLorePrefix + boundPlayer.toString()); 186 | } 187 | } 188 | 189 | // 添加使用次数(如果启用) 190 | if (plugin.getConfig().getBoolean("display.show-info.usage-count", true)) { 191 | StringBuilder usageText = new StringBuilder(); 192 | usageText.append(ChatColor.GRAY).append(plugin.getUsageLorePrefix()).append(" "); 193 | if (isInfinite) { 194 | usageText.append(ChatColor.AQUA).append("∞") 195 | .append(ChatColor.GRAY).append("/") 196 | .append(ChatColor.GRAY).append("∞"); 197 | } else { 198 | usageText.append(ChatColor.AQUA).append(remainingTimes) 199 | .append(ChatColor.GRAY).append("/") 200 | .append(ChatColor.GRAY).append(maxTimes); 201 | } 202 | lore.add(usageText.toString()); 203 | } 204 | 205 | // 添加进度条(如果启用且不是无限次数) 206 | if (!isInfinite && plugin.getConfig().getBoolean("display.show-info.progress-bar", true)) { 207 | double usedPercentage = (double) remainingTimes / maxTimes; 208 | int barLength = 10; 209 | int filledBars = (int) Math.round(usedPercentage * barLength); 210 | 211 | StringBuilder progressBar = new StringBuilder(); 212 | progressBar.append(ChatColor.GRAY).append("["); 213 | for (int i = 0; i < barLength; i++) { 214 | if (i < filledBars) { 215 | progressBar.append(ChatColor.GREEN).append("■"); 216 | } else { 217 | progressBar.append(ChatColor.GRAY).append("■"); 218 | } 219 | } 220 | progressBar.append(ChatColor.GRAY).append("]"); 221 | lore.add(progressBar.toString()); 222 | } 223 | 224 | meta.setLore(lore); 225 | item.setItemMeta(meta); 226 | 227 | // 缓存生成的 lore 228 | cacheLore(cacheKey, lore); 229 | 230 | // 记录性能数据 231 | long duration = (System.nanoTime() - startTime) / 1_000_000; // 转换为毫秒 232 | plugin.getPerformanceMonitor().recordLoreUpdate(duration); 233 | } 234 | 235 | public boolean isMagicBlock(ItemStack item) { 236 | if (item == null || !item.hasItemMeta()) return false; 237 | ItemMeta meta = item.getItemMeta(); 238 | // 使用插件的hasMagicLore方法进行检查,该方法已经增强以处理格式代码 239 | return plugin.hasMagicLore(meta); 240 | } 241 | 242 | public boolean isBlockBound(ItemStack item) { 243 | return plugin.getBlockBindManager().isBlockBound(item); 244 | } 245 | 246 | public UUID getBoundPlayer(ItemStack item) { 247 | return plugin.getBlockBindManager().getBoundPlayer(item); 248 | } 249 | 250 | public void setMaxUseTimes(ItemStack item, int maxTimes) { 251 | ItemMeta meta = item.getItemMeta(); 252 | if (meta == null) return; 253 | 254 | meta.getPersistentDataContainer().set(maxTimesKey, PersistentDataType.INTEGER, maxTimes); 255 | item.setItemMeta(meta); 256 | } 257 | 258 | public int getMaxUseTimes(ItemStack item) { 259 | if (!isMagicBlock(item)) return 0; 260 | ItemMeta meta = item.getItemMeta(); 261 | if (meta == null) return 0; 262 | 263 | PersistentDataContainer container = meta.getPersistentDataContainer(); 264 | Integer maxTimes = container.get(maxTimesKey, PersistentDataType.INTEGER); 265 | 266 | // 如果没有存储的最大次数,则使用默认值 267 | if (maxTimes == null) { 268 | maxTimes = plugin.getDefaultBlockTimes(); 269 | // 存储默认值作为最大次数 270 | setMaxUseTimes(item, maxTimes); 271 | } 272 | 273 | return maxTimes; 274 | } 275 | 276 | // 性能优化:缓存相关方法 277 | private String generateLoreCacheKey(ItemStack item, int remainingTimes, int maxTimes, boolean isInfinite) { 278 | StringBuilder keyBuilder = new StringBuilder(); 279 | keyBuilder.append(item.getType().name()) 280 | .append("_") 281 | .append(remainingTimes) 282 | .append("_") 283 | .append(maxTimes) 284 | .append("_") 285 | .append(isInfinite); 286 | 287 | // 添加绑定状态到缓存键 288 | if (isBlockBound(item)) { 289 | UUID boundPlayer = getBoundPlayer(item); 290 | if (boundPlayer != null) { 291 | keyBuilder.append("_bound_").append(boundPlayer.toString()); 292 | } 293 | } 294 | 295 | return keyBuilder.toString(); 296 | } 297 | 298 | private List getCachedLore(String cacheKey) { 299 | // 检查是否启用缓存 300 | if (!plugin.getConfig().getBoolean("performance.lore-cache.enabled", true)) { 301 | return null; 302 | } 303 | 304 | Long cacheTime = loreCacheTime.get(cacheKey); 305 | long cacheDuration = plugin.getConfig().getLong("performance.lore-cache.duration", 5000); 306 | 307 | if (cacheTime == null || System.currentTimeMillis() - cacheTime > cacheDuration) { 308 | // 缓存过期,清理 309 | loreCache.remove(cacheKey); 310 | loreCacheTime.remove(cacheKey); 311 | return null; 312 | } 313 | return loreCache.get(cacheKey); 314 | } 315 | 316 | private void cacheLore(String cacheKey, List lore) { 317 | // 检查是否启用缓存 318 | if (!plugin.getConfig().getBoolean("performance.lore-cache.enabled", true)) { 319 | return; 320 | } 321 | 322 | loreCache.put(cacheKey, new ArrayList<>(lore)); // 存储副本 323 | loreCacheTime.put(cacheKey, System.currentTimeMillis()); 324 | 325 | // 定期清理过期缓存(简单的清理策略) 326 | int maxSize = plugin.getConfig().getInt("performance.lore-cache.max-size", 1000); 327 | if (loreCache.size() > maxSize) { 328 | cleanExpiredCache(); 329 | } 330 | } 331 | 332 | private void cleanExpiredCache() { 333 | long currentTime = System.currentTimeMillis(); 334 | long cacheDuration = plugin.getConfig().getLong("performance.lore-cache.duration", 5000); 335 | 336 | loreCacheTime.entrySet().removeIf(entry -> { 337 | boolean expired = currentTime - entry.getValue() > cacheDuration; 338 | if (expired) { 339 | loreCache.remove(entry.getKey()); 340 | } 341 | return expired; 342 | }); 343 | } 344 | 345 | // 性能优化:异步更新绑定数据 346 | private void updateBindingDataAsync(UUID boundPlayer, ItemStack item, int currentTimes) { 347 | try { 348 | String uuid = boundPlayer.toString(); 349 | if (plugin.getBlockBindManager().getBindConfig().contains("bindings." + uuid)) { 350 | Set blocks = Objects.requireNonNull(plugin.getBlockBindManager().getBindConfig() 351 | .getConfigurationSection("bindings." + uuid)).getKeys(false); 352 | for (String blockId : blocks) { 353 | String path = "bindings." + uuid + "." + blockId; 354 | String material = plugin.getBlockBindManager().getBindConfig().getString(path + ".material"); 355 | if (material != null && material.equals(item.getType().name())) { 356 | // 更新使用次数 357 | plugin.getBlockBindManager().getBindConfig().set(path + ".uses", currentTimes); 358 | plugin.getBlockBindManager().saveBindConfig(); 359 | break; 360 | } 361 | } 362 | } 363 | } catch (Exception e) { 364 | plugin.getLogger().warning("异步更新绑定数据时出错: " + e.getMessage()); 365 | } 366 | } 367 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/command/CommandManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.command; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | 5 | import me.clip.placeholderapi.PlaceholderAPI; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.Material; 10 | import org.bukkit.command.Command; 11 | import org.bukkit.command.CommandExecutor; 12 | import org.bukkit.command.CommandSender; 13 | import org.bukkit.command.ConsoleCommandSender; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.inventory.ItemStack; 16 | 17 | import java.util.UUID; 18 | 19 | public class CommandManager implements CommandExecutor { 20 | private final MagicBlockPlugin plugin; 21 | 22 | public CommandManager(MagicBlockPlugin plugin) { 23 | this.plugin = plugin; 24 | } 25 | 26 | @Override 27 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 28 | if (args.length == 0) { 29 | if (sender instanceof Player) { 30 | sendHelpMessage((Player) sender); 31 | } else { 32 | sender.sendMessage(ChatColor.RED + "用法: /mb give <玩家> [次数]"); 33 | } 34 | return true; 35 | } 36 | 37 | switch (args[0].toLowerCase()) { 38 | case "help": 39 | if (sender instanceof Player) { 40 | sendHelpMessage((Player) sender); 41 | } else { 42 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 43 | } 44 | break; 45 | case "list": 46 | if (sender instanceof Player) { 47 | handleList((Player) sender); 48 | } else { 49 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 50 | } 51 | break; 52 | case "get": 53 | if (sender instanceof Player) { 54 | handleGet((Player) sender, args); 55 | } else { 56 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 57 | } 58 | break; 59 | case "reload": 60 | handleReload(sender); 61 | break; 62 | case "settimes": 63 | if (sender instanceof Player) { 64 | handleSetTimes((Player) sender, args); 65 | } else { 66 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 67 | } 68 | break; 69 | case "addtimes": 70 | if (sender instanceof Player) { 71 | handleAddTimes((Player) sender, args); 72 | } else { 73 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 74 | } 75 | break; 76 | case "getfood": 77 | if (sender instanceof Player) { 78 | handleGetFood((Player) sender, args); 79 | } else { 80 | sender.sendMessage(ChatColor.RED + "此命令只能由玩家执行。"); 81 | } 82 | break; 83 | case "give": 84 | handleGive(sender, args); 85 | break; 86 | case "performance": 87 | case "perf": 88 | handlePerformance(sender); 89 | break; 90 | default: 91 | if (sender instanceof Player) { 92 | sendHelpMessage((Player) sender); 93 | } else { 94 | sender.sendMessage(ChatColor.RED + "未知的命令。"); 95 | } 96 | break; 97 | } 98 | 99 | return true; 100 | } 101 | 102 | private void sendHelpMessage(Player player) { 103 | plugin.sendMessage(player, "commands.help.title"); 104 | plugin.sendMessage(player, "commands.help.help"); 105 | 106 | // 只显示玩家有权限的命令 107 | if (player.hasPermission("magicblock.get")) { 108 | plugin.sendMessage(player, "commands.help.get"); 109 | } 110 | if (player.hasPermission("magicblock.give")) { 111 | plugin.sendMessage(player, "commands.help.give"); 112 | } 113 | if (player.hasPermission("magicblock.getfood")) { 114 | plugin.sendMessage(player, "commands.help.getfood"); 115 | } 116 | if (player.hasPermission("magicblock.settimes")) { 117 | plugin.sendMessage(player, "commands.help.settimes"); 118 | } 119 | if (player.hasPermission("magicblock.addtimes")) { 120 | plugin.sendMessage(player, "commands.help.addtimes"); 121 | } 122 | 123 | // list 命令默认所有玩家都可以使用 124 | plugin.sendMessage(player, "commands.help.list"); 125 | 126 | if (player.hasPermission("magicblock.reload")) { 127 | plugin.sendMessage(player, "commands.help.reload"); 128 | } 129 | 130 | if (player.hasPermission("magicblock.performance")) { 131 | plugin.sendMessage(player, "commands.help.performance"); 132 | } 133 | 134 | // 基础功能提示 135 | plugin.sendMessage(player, "commands.help.tip"); 136 | plugin.sendMessage(player, "commands.help.gui-tip"); 137 | } 138 | 139 | private void handleGive(CommandSender sender, String[] args) { 140 | if (!sender.hasPermission("magicblock.give")) { 141 | sender.sendMessage(ChatColor.RED + "你没有权限使用此命令。"); 142 | return; 143 | } 144 | 145 | if (args.length < 2) { 146 | sender.sendMessage(ChatColor.RED + "用法: /mb give <玩家> [次数]"); 147 | return; 148 | } 149 | 150 | // 处理变量替换 151 | String targetPlayerName = args[1]; 152 | if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null && sender instanceof Player) { 153 | targetPlayerName = PlaceholderAPI.setPlaceholders((Player) sender, targetPlayerName); 154 | } 155 | 156 | Player target = Bukkit.getPlayer(targetPlayerName); 157 | if (target == null) { 158 | sender.sendMessage(ChatColor.RED + "找不到玩家: " + targetPlayerName); 159 | return; 160 | } 161 | 162 | String timesArg = args.length > 2 ? args[2] : String.valueOf(plugin.getDefaultBlockTimes()); 163 | // 如果有PlaceholderAPI,处理次数中的变量 164 | if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null && sender instanceof Player) { 165 | timesArg = PlaceholderAPI.setPlaceholders((Player) sender, timesArg); 166 | } 167 | 168 | int times; 169 | try { 170 | times = Integer.parseInt(timesArg); 171 | // 如果指定-1,则设置为无限次数 172 | if (times != -1 && times <= 0) { 173 | sender.sendMessage(ChatColor.RED + "无效的次数: " + timesArg + ",使用默认值。"); 174 | times = plugin.getDefaultBlockTimes(); 175 | } 176 | } catch (NumberFormatException e) { 177 | sender.sendMessage(ChatColor.RED + "无效的次数: " + timesArg + ",使用默认值。"); 178 | times = plugin.getDefaultBlockTimes(); 179 | } 180 | 181 | ItemStack specialBlock = plugin.createMagicBlock(); 182 | plugin.getBlockManager().setUseTimes(specialBlock, times); 183 | plugin.getBlockManager().updateLore(specialBlock, times); 184 | 185 | target.getInventory().addItem(specialBlock); 186 | if (times == -1) { 187 | plugin.sendMessage(target, "commands.get.success-infinite"); 188 | } else { 189 | plugin.sendMessage(target, "commands.get.success", times); 190 | } 191 | 192 | // 根据发送者类型显示不同的消息 193 | if (sender instanceof ConsoleCommandSender) { 194 | if (times == -1) { 195 | plugin.sendMessage(sender, "commands.give.success.console-infinite", target.getName()); 196 | } else { 197 | plugin.sendMessage(sender, "commands.give.success.console", target.getName(), times); 198 | } 199 | } else if (sender instanceof Player && sender != target) { 200 | if (times == -1) { 201 | plugin.sendMessage(sender, "commands.give.success.player-infinite", target.getName()); 202 | } else { 203 | plugin.sendMessage(sender, "commands.give.success.player", target.getName(), times); 204 | } 205 | } 206 | } 207 | 208 | private void handleGet(Player player, String[] args) { 209 | if (!player.hasPermission("magicblock.get")) { 210 | plugin.sendMessage(player, "commands.get.no-permission"); 211 | return; 212 | } 213 | 214 | // 检查使用权限 215 | if (!player.hasPermission("magicblock.use")) { 216 | plugin.sendMessage(player, "messages.no-permission-use"); 217 | return; 218 | } 219 | 220 | int times = plugin.getDefaultBlockTimes(); 221 | if (args.length > 1) { 222 | try { 223 | times = Integer.parseInt(args[1]); 224 | // 如果指定-1,则设置为无限次数 225 | if (times != -1 && times <= 0) { 226 | plugin.sendMessage(player, "commands.get.invalid-number"); 227 | times = plugin.getDefaultBlockTimes(); 228 | } 229 | } catch (NumberFormatException e) { 230 | plugin.sendMessage(player, "commands.get.invalid-number"); 231 | times = plugin.getDefaultBlockTimes(); 232 | } 233 | } 234 | 235 | ItemStack specialBlock = plugin.createMagicBlock(); 236 | plugin.getBlockManager().setUseTimes(specialBlock, times); 237 | plugin.getBlockManager().updateLore(specialBlock, times); 238 | 239 | player.getInventory().addItem(specialBlock); 240 | if (times == -1) { 241 | plugin.sendMessage(player, "commands.get.success-infinite"); 242 | } else { 243 | plugin.sendMessage(player, "commands.get.success", times); 244 | } 245 | } 246 | 247 | private void handleGetFood(Player player, String[] args) { 248 | if (!player.hasPermission("magicblock.getfood")) { 249 | plugin.sendMessage(player, "commands.getfood.no-permission"); 250 | return; 251 | } 252 | 253 | if (args.length < 2) { 254 | plugin.sendMessage(player, "commands.getfood.usage"); 255 | return; 256 | } 257 | 258 | Material material; 259 | try { 260 | material = Material.valueOf(args[1].toUpperCase()); 261 | } catch (IllegalArgumentException e) { 262 | plugin.sendMessage(player, "commands.getfood.invalid-food"); 263 | return; 264 | } 265 | 266 | if (!plugin.getFoodConfig().contains("foods." + material.name())) { 267 | plugin.sendMessage(player, "commands.getfood.invalid-food"); 268 | return; 269 | } 270 | 271 | ItemStack food = plugin.getMagicFood().createMagicFood(material); 272 | if (food == null) { 273 | plugin.sendMessage(player, "commands.getfood.invalid-food"); 274 | return; 275 | } 276 | 277 | // 处理使用次数 278 | int times; 279 | if (args.length >= 3) { 280 | try { 281 | times = Integer.parseInt(args[2]); 282 | if (times < -1) { 283 | plugin.sendMessage(player, "commands.getfood.invalid-number"); 284 | return; 285 | } 286 | } catch (NumberFormatException e) { 287 | plugin.sendMessage(player, "commands.getfood.invalid-number"); 288 | return; 289 | } 290 | } else { 291 | // 如果没有指定次数,使用配置文件中的默认值 292 | times = plugin.getFoodConfig().getInt("default-food-times", 64); 293 | } 294 | 295 | // 设置使用次数和最大次数 296 | plugin.getMagicFood().setMaxUseTimes(food, times); 297 | plugin.getMagicFood().setUseTimes(food, times); 298 | plugin.getMagicFood().updateLore(food, times); // 确保更新Lore显示 299 | 300 | player.getInventory().addItem(food); 301 | if (times == -1) { 302 | plugin.sendMessage(player, "commands.getfood.success-infinite"); 303 | } else { 304 | plugin.sendMessage(player, "commands.getfood.success", times); 305 | } 306 | } 307 | 308 | private void handleReload(CommandSender sender) { 309 | if (!sender.hasPermission("magicblock.reload")) { 310 | plugin.sendMessage(sender, "commands.reload.no-permission"); 311 | return; 312 | } 313 | 314 | try { 315 | // 执行完整的重载 316 | plugin.reloadPluginAllowedMaterials(); 317 | plugin.sendMessage(sender, "commands.reload.success"); 318 | } catch (Exception e) { 319 | plugin.getLogger().severe("重载配置时发生错误: " + e.getMessage()); 320 | e.printStackTrace(); 321 | plugin.sendMessage(sender, "commands.reload.error"); 322 | } 323 | } 324 | 325 | private void handleSetTimes(Player player, String[] args) { 326 | if (!player.hasPermission("magicblock.settimes")) { 327 | plugin.sendMessage(player, "commands.settimes.no-permission"); 328 | return; 329 | } 330 | 331 | // 检查使用权限 332 | if (!player.hasPermission("magicblock.use")) { 333 | plugin.sendMessage(player, "messages.no-permission-use"); 334 | return; 335 | } 336 | 337 | if (args.length < 2) { 338 | plugin.sendMessage(player, "commands.settimes.usage"); 339 | return; 340 | } 341 | 342 | ItemStack item = player.getInventory().getItemInMainHand(); 343 | if (!plugin.hasMagicLore(item.getItemMeta())) { 344 | plugin.sendMessage(player, "commands.settimes.must-hold"); 345 | return; 346 | } 347 | 348 | int times; 349 | try { 350 | times = Integer.parseInt(args[1]); 351 | } catch (NumberFormatException e) { 352 | plugin.sendMessage(player, "commands.settimes.invalid-number"); 353 | return; 354 | } 355 | 356 | plugin.getBlockManager().setUseTimes(item, times); 357 | plugin.sendMessage(player, "commands.settimes.success", times); 358 | } 359 | 360 | private void handleAddTimes(Player player, String[] args) { 361 | if (!player.hasPermission("magicblock.addtimes")) { 362 | plugin.sendMessage(player, "commands.addtimes.no-permission"); 363 | return; 364 | } 365 | 366 | // 检查使用权限 367 | if (!player.hasPermission("magicblock.use")) { 368 | plugin.sendMessage(player, "messages.no-permission-use"); 369 | return; 370 | } 371 | 372 | if (args.length < 2) { 373 | plugin.sendMessage(player, "commands.addtimes.usage"); 374 | return; 375 | } 376 | 377 | ItemStack item = player.getInventory().getItemInMainHand(); 378 | if (!plugin.getBlockManager().isMagicBlock(item)) { 379 | plugin.sendMessage(player, "commands.addtimes.must-hold"); 380 | return; 381 | } 382 | 383 | // 检查是否是绑定的方块且是否属于该玩家 384 | UUID boundPlayer = plugin.getBlockBindManager().getBoundPlayer(item); 385 | if (boundPlayer != null && !boundPlayer.equals(player.getUniqueId())) { 386 | plugin.sendMessage(player, "messages.not-bound-to-you"); 387 | return; 388 | } 389 | 390 | int addTimes; 391 | try { 392 | addTimes = Integer.parseInt(args[1]); 393 | if (addTimes <= 0) { 394 | plugin.sendMessage(player, "commands.addtimes.invalid-number"); 395 | return; 396 | } 397 | } catch (NumberFormatException e) { 398 | plugin.sendMessage(player, "commands.addtimes.invalid-number"); 399 | return; 400 | } 401 | 402 | // 获取当前使用次数和最大使用次数 403 | int currentTimes = plugin.getBlockManager().getUseTimes(item); 404 | int maxTimes = plugin.getBlockManager().getMaxUseTimes(item); 405 | 406 | // 检查是否是无限次数 407 | if (currentTimes == Integer.MAX_VALUE - 100) { 408 | plugin.sendMessage(player, "commands.addtimes.unlimited"); 409 | return; 410 | } 411 | 412 | // 计算新的使用次数和最大次数 413 | int newTimes = currentTimes + addTimes; 414 | int newMaxTimes = maxTimes + addTimes; 415 | 416 | // 设置新的使用次数和最大次数 417 | plugin.getBlockManager().setUseTimes(item, newTimes); 418 | plugin.getBlockManager().setMaxUseTimes(item, newMaxTimes); 419 | 420 | // 如果是绑定的方块,更新配置中的使用次数 421 | if (plugin.getBlockBindManager().isBlockBound(item)) { 422 | plugin.getBlockBindManager().updateBlockMaterial(item); 423 | } 424 | 425 | // 发送成功消息 426 | plugin.sendMessage(player, "commands.addtimes.success", addTimes, newTimes); 427 | } 428 | 429 | private void handleList(Player player) { 430 | // 检查使用权限 431 | if (!player.hasPermission("magicblock.use")) { 432 | plugin.sendMessage(player, "messages.no-permission-use"); 433 | return; 434 | } 435 | 436 | plugin.getBlockBindManager().openBindList(player); 437 | } 438 | 439 | private void handlePerformance(CommandSender sender) { 440 | if (!sender.hasPermission("magicblock.performance")) { 441 | plugin.sendMessage(sender, "commands.performance.no-permission"); 442 | return; 443 | } 444 | 445 | // 发送性能报告 446 | plugin.getPerformanceMonitor().sendPerformanceReport(sender); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/command/handler/TabCompleter.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.command.handler; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.command.Command; 6 | import org.bukkit.command.CommandSender; 7 | import org.bukkit.entity.Player; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | public class TabCompleter implements org.bukkit.command.TabCompleter { 15 | private final MagicBlockPlugin plugin; 16 | private final List commands = Arrays.asList("get", "reload", "settimes", "addtimes", "getfood", "help", "give", "list", "performance", "perf"); 17 | 18 | public TabCompleter(MagicBlockPlugin plugin) { 19 | this.plugin = plugin; 20 | } 21 | 22 | @Override 23 | public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { 24 | List completions = new ArrayList<>(); 25 | 26 | if (command.getName().equalsIgnoreCase("magicblock")) { 27 | if (args.length == 1) { 28 | // 基础命令 29 | List availableCommands = new ArrayList<>(); 30 | availableCommands.add("help"); 31 | 32 | // 根据权限添加命令 33 | if (sender.hasPermission("magicblock.get")) { 34 | availableCommands.add("get"); 35 | } 36 | if (sender.hasPermission("magicblock.give")) { 37 | availableCommands.add("give"); 38 | } 39 | if (sender.hasPermission("magicblock.getfood")) { 40 | availableCommands.add("getfood"); 41 | } 42 | if (sender.hasPermission("magicblock.settimes")) { 43 | availableCommands.add("settimes"); 44 | } 45 | if (sender.hasPermission("magicblock.addtimes")) { 46 | availableCommands.add("addtimes"); 47 | } 48 | if (sender.hasPermission("magicblock.list")) { 49 | availableCommands.add("list"); 50 | } 51 | if (sender.hasPermission("magicblock.reload")) { 52 | availableCommands.add("reload"); 53 | } 54 | if (sender.hasPermission("magicblock.performance")) { 55 | availableCommands.add("performance"); 56 | availableCommands.add("perf"); 57 | } 58 | 59 | // 过滤并返回匹配的命令 60 | String input = args[0].toLowerCase(); 61 | completions.addAll(availableCommands.stream() 62 | .filter(cmd -> cmd.startsWith(input)) 63 | .collect(Collectors.toList())); 64 | } else if (args.length == 2) { 65 | // 针对特定命令的第二个参数提供补全 66 | switch (args[0].toLowerCase()) { 67 | case "give": 68 | if (sender.hasPermission("magicblock.give")) { 69 | // 返回在线玩家列表 70 | String input = args[1].toLowerCase(); 71 | completions.addAll(Bukkit.getOnlinePlayers().stream() 72 | .map(Player::getName) 73 | .filter(name -> name.toLowerCase().startsWith(input)) 74 | .collect(Collectors.toList())); 75 | } 76 | break; 77 | case "getfood": 78 | if (sender.hasPermission("magicblock.getfood")) { 79 | // 从配置文件中获取可用食物列表 80 | String input = args[1].toLowerCase(); 81 | if (plugin.getFoodConfig().contains("foods")) { 82 | completions.addAll(plugin.getFoodConfig().getConfigurationSection("foods").getKeys(false).stream() 83 | .filter(food -> food.toLowerCase().startsWith(input)) 84 | .collect(Collectors.toList())); 85 | } 86 | } 87 | break; 88 | } 89 | } 90 | } 91 | 92 | return completions; 93 | } 94 | } 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/database/DatabaseManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.database; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import io.github.syferie.magicblock.MagicBlockPlugin; 6 | import org.bukkit.Material; 7 | import org.bukkit.configuration.file.FileConfiguration; 8 | import org.bukkit.entity.Player; 9 | 10 | import java.sql.*; 11 | import java.util.*; 12 | import java.util.logging.Level; 13 | 14 | /** 15 | * 数据库管理器,用于处理与MySQL数据库的连接和操作 16 | */ 17 | public class DatabaseManager { 18 | private final MagicBlockPlugin plugin; 19 | private HikariDataSource dataSource; 20 | private final String tablePrefix; 21 | private final String bindingsTable; 22 | 23 | /** 24 | * 构造函数 25 | * @param plugin 插件实例 26 | */ 27 | public DatabaseManager(MagicBlockPlugin plugin) { 28 | this.plugin = plugin; 29 | FileConfiguration config = plugin.getConfig(); 30 | this.tablePrefix = config.getString("database.table-prefix", "mb_"); 31 | this.bindingsTable = tablePrefix + "bindings"; 32 | 33 | if (config.getBoolean("database.enabled", false)) { 34 | setupDatabase(); 35 | } 36 | } 37 | 38 | /** 39 | * 设置数据库连接 40 | */ 41 | private void setupDatabase() { 42 | FileConfiguration config = plugin.getConfig(); 43 | 44 | try { 45 | // 检查HikariCP是否可用 46 | try { 47 | Class.forName("com.zaxxer.hikari.HikariConfig"); 48 | } catch (ClassNotFoundException e) { 49 | plugin.getLogger().severe("HikariCP依赖缺失!请安装HikariCP到服务器的lib目录或使用带有依赖的插件版本。"); 50 | plugin.getLogger().severe("数据库功能将被禁用。"); 51 | return; 52 | } 53 | 54 | // 配置HikariCP连接池 55 | HikariConfig hikariConfig = new HikariConfig(); 56 | hikariConfig.setJdbcUrl("jdbc:mysql://" + 57 | config.getString("database.host", "localhost") + ":" + 58 | config.getInt("database.port", 3306) + "/" + 59 | config.getString("database.database", "magicblock") + 60 | "?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"); 61 | hikariConfig.setUsername(config.getString("database.username", "root")); 62 | hikariConfig.setPassword(config.getString("database.password", "")); 63 | // 使用合理的默认值,适合少量数据的插件 64 | hikariConfig.setMaximumPoolSize(3); // 少量数据只需要少量连接 65 | hikariConfig.setMinimumIdle(1); // 最小空闲连接 66 | hikariConfig.setMaxLifetime(1800000); // 30分钟 67 | hikariConfig.setConnectionTimeout(5000); // 5秒 68 | hikariConfig.setIdleTimeout(600000); // 10分钟空闲超时 69 | hikariConfig.setPoolName("MagicBlockHikariPool"); 70 | 71 | // 添加连接测试查询 72 | hikariConfig.setConnectionTestQuery("SELECT 1"); 73 | 74 | // 缓存预编译语句 75 | hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); 76 | hikariConfig.addDataSourceProperty("prepStmtCacheSize", "25"); 77 | hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 78 | 79 | // 创建数据源 80 | dataSource = new HikariDataSource(hikariConfig); 81 | 82 | // 创建表 83 | createTables(); 84 | 85 | plugin.getLogger().info(plugin.getLanguageManager().getMessage("general.database-connected")); 86 | } catch (Exception e) { 87 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 88 | dataSource = null; 89 | } 90 | } 91 | 92 | /** 93 | * 创建必要的数据库表 94 | */ 95 | private void createTables() { 96 | String createBindingsTable = "CREATE TABLE IF NOT EXISTS " + bindingsTable + " (" + 97 | "id INT AUTO_INCREMENT PRIMARY KEY, " + 98 | "player_uuid VARCHAR(36) NOT NULL, " + 99 | "player_name VARCHAR(16) NOT NULL, " + 100 | "block_id VARCHAR(36) NOT NULL, " + 101 | "material VARCHAR(50) NOT NULL, " + 102 | "uses INT NOT NULL, " + 103 | "max_uses INT NOT NULL, " + 104 | "hidden BOOLEAN DEFAULT FALSE, " + 105 | "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + 106 | "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + 107 | "INDEX idx_player_uuid (player_uuid), " + 108 | "INDEX idx_block_id (block_id)" + 109 | ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; 110 | 111 | try (Connection conn = getConnection(); 112 | Statement stmt = conn.createStatement()) { 113 | stmt.execute(createBindingsTable); 114 | plugin.getLogger().info(plugin.getLanguageManager().getMessage("general.database-tables-created")); 115 | } catch (SQLException e) { 116 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 117 | } 118 | } 119 | 120 | /** 121 | * 获取数据库连接 122 | * @return 数据库连接 123 | * @throws SQLException 如果获取连接失败 124 | */ 125 | public Connection getConnection() throws SQLException { 126 | if (dataSource == null) { 127 | throw new SQLException("Database is not enabled or connection failed"); 128 | } 129 | return dataSource.getConnection(); 130 | } 131 | 132 | /** 133 | * 检查数据库是否已启用 134 | * @return 如果数据库已启用则返回true 135 | */ 136 | public boolean isEnabled() { 137 | return dataSource != null; 138 | } 139 | 140 | /** 141 | * 关闭数据库连接 142 | */ 143 | public void close() { 144 | if (dataSource != null && !dataSource.isClosed()) { 145 | dataSource.close(); 146 | } 147 | } 148 | 149 | /** 150 | * 保存方块绑定数据到数据库 151 | * @param playerUUID 玩家UUID 152 | * @param playerName 玩家名称 153 | * @param blockId 方块ID 154 | * @param material 方块材质 155 | * @param uses 使用次数 156 | * @param maxUses 最大使用次数 157 | * @return 是否保存成功 158 | */ 159 | public boolean saveBinding(UUID playerUUID, String playerName, String blockId, String material, int uses, int maxUses) { 160 | if (!isEnabled()) return false; 161 | 162 | String sql = "INSERT INTO " + bindingsTable + 163 | " (player_uuid, player_name, block_id, material, uses, max_uses) VALUES (?, ?, ?, ?, ?, ?) " + 164 | "ON DUPLICATE KEY UPDATE material = ?, uses = ?, max_uses = ?"; 165 | 166 | try (Connection conn = getConnection(); 167 | PreparedStatement stmt = conn.prepareStatement(sql)) { 168 | stmt.setString(1, playerUUID.toString()); 169 | stmt.setString(2, playerName); 170 | stmt.setString(3, blockId); 171 | stmt.setString(4, material); 172 | stmt.setInt(5, uses); 173 | stmt.setInt(6, maxUses); 174 | stmt.setString(7, material); 175 | stmt.setInt(8, uses); 176 | stmt.setInt(9, maxUses); 177 | 178 | return stmt.executeUpdate() > 0; 179 | } catch (SQLException e) { 180 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 181 | return false; 182 | } 183 | } 184 | 185 | /** 186 | * 更新方块绑定数据 187 | * @param playerUUID 玩家UUID 188 | * @param blockId 方块ID 189 | * @param material 方块材质 190 | * @param uses 使用次数 191 | * @param maxUses 最大使用次数 192 | * @return 是否更新成功 193 | */ 194 | public boolean updateBinding(UUID playerUUID, String blockId, String material, int uses, int maxUses) { 195 | if (!isEnabled()) return false; 196 | 197 | String sql = "UPDATE " + bindingsTable + 198 | " SET material = ?, uses = ?, max_uses = ? " + 199 | "WHERE player_uuid = ? AND block_id = ?"; 200 | 201 | try (Connection conn = getConnection(); 202 | PreparedStatement stmt = conn.prepareStatement(sql)) { 203 | stmt.setString(1, material); 204 | stmt.setInt(2, uses); 205 | stmt.setInt(3, maxUses); 206 | stmt.setString(4, playerUUID.toString()); 207 | stmt.setString(5, blockId); 208 | 209 | return stmt.executeUpdate() > 0; 210 | } catch (SQLException e) { 211 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 212 | return false; 213 | } 214 | } 215 | 216 | /** 217 | * 获取玩家的所有绑定方块 218 | * @param playerUUID 玩家UUID 219 | * @return 绑定方块的Map,键为方块ID,值为方块数据 220 | */ 221 | public Map> getPlayerBindings(UUID playerUUID) { 222 | if (!isEnabled()) return new HashMap<>(); 223 | 224 | Map> bindings = new HashMap<>(); 225 | String sql = "SELECT * FROM " + bindingsTable + " WHERE player_uuid = ? AND hidden = FALSE"; 226 | 227 | try (Connection conn = getConnection(); 228 | PreparedStatement stmt = conn.prepareStatement(sql)) { 229 | stmt.setString(1, playerUUID.toString()); 230 | 231 | try (ResultSet rs = stmt.executeQuery()) { 232 | while (rs.next()) { 233 | String blockId = rs.getString("block_id"); 234 | Map blockData = new HashMap<>(); 235 | blockData.put("material", rs.getString("material")); 236 | blockData.put("uses", rs.getInt("uses")); 237 | blockData.put("max_uses", rs.getInt("max_uses")); 238 | blockData.put("hidden", rs.getBoolean("hidden")); 239 | 240 | bindings.put(blockId, blockData); 241 | } 242 | } 243 | } catch (SQLException e) { 244 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 245 | } 246 | 247 | return bindings; 248 | } 249 | 250 | /** 251 | * 获取特定方块的绑定数据 252 | * @param blockId 方块ID 253 | * @return 方块数据的Map 254 | */ 255 | public Map getBlockBinding(String blockId) { 256 | if (!isEnabled()) return null; 257 | 258 | String sql = "SELECT * FROM " + bindingsTable + " WHERE block_id = ?"; 259 | 260 | try (Connection conn = getConnection(); 261 | PreparedStatement stmt = conn.prepareStatement(sql)) { 262 | stmt.setString(1, blockId); 263 | 264 | try (ResultSet rs = stmt.executeQuery()) { 265 | if (rs.next()) { 266 | Map blockData = new HashMap<>(); 267 | blockData.put("player_uuid", UUID.fromString(rs.getString("player_uuid"))); 268 | blockData.put("player_name", rs.getString("player_name")); 269 | blockData.put("material", rs.getString("material")); 270 | blockData.put("uses", rs.getInt("uses")); 271 | blockData.put("max_uses", rs.getInt("max_uses")); 272 | blockData.put("hidden", rs.getBoolean("hidden")); 273 | 274 | return blockData; 275 | } 276 | } 277 | } catch (SQLException e) { 278 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 279 | } 280 | 281 | return null; 282 | } 283 | 284 | /** 285 | * 设置方块的隐藏状态 286 | * @param playerUUID 玩家UUID 287 | * @param blockId 方块ID 288 | * @param hidden 是否隐藏 289 | * @return 是否设置成功 290 | */ 291 | public boolean setBlockHidden(UUID playerUUID, String blockId, boolean hidden) { 292 | if (!isEnabled()) return false; 293 | 294 | String sql = "UPDATE " + bindingsTable + 295 | " SET hidden = ? " + 296 | "WHERE player_uuid = ? AND block_id = ?"; 297 | 298 | try (Connection conn = getConnection(); 299 | PreparedStatement stmt = conn.prepareStatement(sql)) { 300 | stmt.setBoolean(1, hidden); 301 | stmt.setString(2, playerUUID.toString()); 302 | stmt.setString(3, blockId); 303 | 304 | return stmt.executeUpdate() > 0; 305 | } catch (SQLException e) { 306 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 307 | return false; 308 | } 309 | } 310 | 311 | /** 312 | * 删除方块绑定 313 | * @param playerUUID 玩家UUID 314 | * @param blockId 方块ID 315 | * @return 是否删除成功 316 | */ 317 | public boolean deleteBinding(UUID playerUUID, String blockId) { 318 | if (!isEnabled()) return false; 319 | 320 | String sql = "DELETE FROM " + bindingsTable + 321 | " WHERE player_uuid = ? AND block_id = ?"; 322 | 323 | try (Connection conn = getConnection(); 324 | PreparedStatement stmt = conn.prepareStatement(sql)) { 325 | stmt.setString(1, playerUUID.toString()); 326 | stmt.setString(2, blockId); 327 | 328 | return stmt.executeUpdate() > 0; 329 | } catch (SQLException e) { 330 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 331 | return false; 332 | } 333 | } 334 | 335 | /** 336 | * 从文件配置迁移数据到数据库 337 | * @param bindConfig 绑定配置 338 | * @return 是否迁移成功 339 | */ 340 | public boolean migrateFromFile(FileConfiguration bindConfig) { 341 | if (!isEnabled() || bindConfig == null) return false; 342 | 343 | plugin.getLogger().info(plugin.getLanguageManager().getMessage("general.database-migration-start")); 344 | 345 | try { 346 | if (bindConfig.contains("bindings")) { 347 | for (String uuidStr : bindConfig.getConfigurationSection("bindings").getKeys(false)) { 348 | UUID playerUUID = UUID.fromString(uuidStr); 349 | String playerName = plugin.getServer().getOfflinePlayer(playerUUID).getName(); 350 | if (playerName == null) playerName = "Unknown"; 351 | 352 | for (String blockId : bindConfig.getConfigurationSection("bindings." + uuidStr).getKeys(false)) { 353 | String path = "bindings." + uuidStr + "." + blockId; 354 | String material = bindConfig.getString(path + ".material"); 355 | int uses = bindConfig.getInt(path + ".uses"); 356 | int maxUses = bindConfig.getInt(path + ".max_uses", uses); 357 | boolean hidden = bindConfig.getBoolean(path + ".hidden", false); 358 | 359 | // 保存到数据库 360 | saveBinding(playerUUID, playerName, blockId, material, uses, maxUses); 361 | 362 | // 设置隐藏状态 363 | if (hidden) { 364 | setBlockHidden(playerUUID, blockId, true); 365 | } 366 | } 367 | } 368 | } 369 | 370 | plugin.getLogger().info(plugin.getLanguageManager().getMessage("general.database-migration-complete")); 371 | return true; 372 | } catch (Exception e) { 373 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 374 | return false; 375 | } 376 | } 377 | 378 | /** 379 | * 清理使用次数为0的方块 380 | * @param playerUUID 玩家UUID 381 | */ 382 | public void cleanupZeroUsageBlocks(UUID playerUUID) { 383 | if (!isEnabled()) return; 384 | 385 | // 如果配置为移除耗尽的方块 386 | if (plugin.getConfig().getBoolean("remove-depleted-blocks", false)) { 387 | String sql = "DELETE FROM " + bindingsTable + 388 | " WHERE player_uuid = ? AND uses <= 0"; 389 | 390 | try (Connection conn = getConnection(); 391 | PreparedStatement stmt = conn.prepareStatement(sql)) { 392 | stmt.setString(1, playerUUID.toString()); 393 | stmt.executeUpdate(); 394 | } catch (SQLException e) { 395 | plugin.getLogger().log(Level.SEVERE, plugin.getLanguageManager().getMessage("general.database-error", e.getMessage()), e); 396 | } 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/food/FoodManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.food; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import io.github.syferie.magicblock.api.IMagicFood; 5 | import io.github.syferie.magicblock.util.Constants; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.Material; 10 | import org.bukkit.NamespacedKey; 11 | import org.bukkit.Sound; 12 | import org.bukkit.Particle; 13 | import org.bukkit.configuration.ConfigurationSection; 14 | import org.bukkit.enchantments.Enchantment; 15 | import org.bukkit.entity.Player; 16 | import org.bukkit.event.EventHandler; 17 | import org.bukkit.event.Listener; 18 | import org.bukkit.event.player.PlayerItemConsumeEvent; 19 | import org.bukkit.inventory.EquipmentSlot; 20 | import org.bukkit.inventory.ItemFlag; 21 | import org.bukkit.inventory.ItemStack; 22 | import org.bukkit.inventory.meta.ItemMeta; 23 | import org.bukkit.persistence.PersistentDataContainer; 24 | import org.bukkit.persistence.PersistentDataType; 25 | import org.bukkit.potion.PotionEffect; 26 | import org.bukkit.potion.PotionEffectType; 27 | 28 | import java.util.ArrayList; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.UUID; 33 | 34 | public class FoodManager implements Listener, IMagicFood { 35 | private final MagicBlockPlugin plugin; 36 | private final NamespacedKey useTimesKey; 37 | private final NamespacedKey maxTimesKey; 38 | private final Map foodUses = new HashMap<>(); 39 | 40 | public FoodManager(MagicBlockPlugin plugin) { 41 | this.plugin = plugin; 42 | this.useTimesKey = new NamespacedKey(plugin, Constants.FOOD_TIMES_KEY); 43 | this.maxTimesKey = new NamespacedKey(plugin, "magicfood_maxtimes"); 44 | } 45 | 46 | @Override 47 | public ItemStack createMagicFood(Material material) { 48 | if (!plugin.getFoodConfig().contains("foods." + material.name())) { 49 | return null; 50 | } 51 | 52 | ItemStack item = new ItemStack(material); 53 | ItemMeta meta = item.getItemMeta(); 54 | if (meta == null) return null; 55 | 56 | // 获取食物名称 57 | String foodName =plugin.getMinecraftLangManager().getItemStackName(item); 58 | 59 | // 使用配置的名称格式 60 | String nameFormat = plugin.getFoodConfig().getString("display.food-name-format", "&b✦ %s &b✦"); 61 | meta.setDisplayName(ChatColor.translateAlternateColorCodes('&', 62 | String.format(nameFormat, foodName))); 63 | 64 | List lore = new ArrayList<>(); 65 | // 添加特殊标识 66 | lore.add(plugin.getFoodConfig().getString("special-lore", "§7MagicFood")); 67 | 68 | // 添加装饰性lore 69 | if (plugin.getFoodConfig().getBoolean("display.decorative-lore.enabled", true)) { 70 | ConfigurationSection foodSection = plugin.getFoodConfig().getConfigurationSection("foods." + material.name()); 71 | if (foodSection != null) { 72 | List decorativeLore = plugin.getFoodConfig().getStringList("display.decorative-lore.lines"); 73 | for (String line : decorativeLore) { 74 | // 替换食物属性变量 75 | line = line.replace("%magicfood_food_level%", String.valueOf(foodSection.getInt("food-level", 0))) 76 | .replace("%magicfood_saturation%", String.valueOf(foodSection.getDouble("saturation", 0.0))) 77 | .replace("%magicfood_heal%", String.valueOf(foodSection.getDouble("heal", 0.0))); 78 | 79 | // 如果安装了PAPI,处理其他变量 80 | if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { 81 | line = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(null, line); 82 | } 83 | 84 | lore.add(ChatColor.translateAlternateColorCodes('&', line)); 85 | } 86 | } 87 | } 88 | 89 | meta.setLore(lore); 90 | item.setItemMeta(meta); 91 | 92 | return item; 93 | } 94 | 95 | @Override 96 | public void setUseTimes(ItemStack item, int times) { 97 | ItemMeta meta = item.getItemMeta(); 98 | if (meta == null) return; 99 | 100 | if (times == -1) { 101 | int infiniteValue = Integer.MAX_VALUE - 100; 102 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, infiniteValue); 103 | } else { 104 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, times); 105 | } 106 | 107 | item.setItemMeta(meta); 108 | } 109 | 110 | public void setMaxUseTimes(ItemStack item, int maxTimes) { 111 | ItemMeta meta = item.getItemMeta(); 112 | if (meta == null) return; 113 | 114 | if (maxTimes == -1) { 115 | meta.getPersistentDataContainer().set(maxTimesKey, PersistentDataType.INTEGER, Integer.MAX_VALUE - 100); 116 | } else { 117 | meta.getPersistentDataContainer().set(maxTimesKey, PersistentDataType.INTEGER, maxTimes); 118 | } 119 | item.setItemMeta(meta); 120 | } 121 | 122 | @Override 123 | public int decrementUseTimes(ItemStack item) { 124 | int currentTimes = getUseTimes(item); 125 | if (currentTimes <= 0) { 126 | return 0; // 返回0表示次数已经用尽 127 | } 128 | 129 | currentTimes--; 130 | setUseTimes(item, currentTimes); 131 | return currentTimes; 132 | } 133 | 134 | @Override 135 | public int getUseTimes(ItemStack item) { 136 | ItemMeta meta = item.getItemMeta(); 137 | if (meta == null) return 0; 138 | 139 | PersistentDataContainer container = meta.getPersistentDataContainer(); 140 | return container.getOrDefault(useTimesKey, PersistentDataType.INTEGER, 0); 141 | } 142 | 143 | public int getMaxUseTimes(ItemStack item) { 144 | ItemMeta meta = item.getItemMeta(); 145 | if (meta == null) return 0; 146 | 147 | PersistentDataContainer container = meta.getPersistentDataContainer(); 148 | Integer maxTimes = container.get(maxTimesKey, PersistentDataType.INTEGER); 149 | return maxTimes != null ? maxTimes : 0; 150 | } 151 | 152 | @Override 153 | public void updateLore(ItemStack item, int times) { 154 | ItemMeta meta = item.getItemMeta(); 155 | if (meta == null) return; 156 | 157 | List lore = new ArrayList<>(); 158 | 159 | // 获取物品的最大使用次数 160 | int maxTimes = getMaxUseTimes(item); 161 | if (maxTimes <= 0) return; 162 | 163 | // 检查是否是"无限"次数 164 | boolean isInfinite = maxTimes == Integer.MAX_VALUE - 100; 165 | 166 | // 添加特殊标识 167 | lore.add(plugin.getFoodConfig().getString("special-lore", "§7MagicFood")); 168 | 169 | // 添加装饰性lore 170 | if (plugin.getFoodConfig().getBoolean("display.decorative-lore.enabled", true)) { 171 | ConfigurationSection foodSection = plugin.getFoodConfig().getConfigurationSection("foods." + item.getType().name()); 172 | if (foodSection != null) { 173 | List decorativeLore = plugin.getFoodConfig().getStringList("display.decorative-lore.lines"); 174 | for (String line : decorativeLore) { 175 | // 替换食物属性变量 176 | line = line.replace("%magicfood_food_level%", String.valueOf(foodSection.getInt("food-level", 0))) 177 | .replace("%magicfood_saturation%", String.valueOf(foodSection.getDouble("saturation", 0.0))) 178 | .replace("%magicfood_heal%", String.valueOf(foodSection.getDouble("heal", 0.0))); 179 | 180 | // 如果安装了PAPI,处理其他变量 181 | if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { 182 | line = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(null, line); 183 | } 184 | 185 | lore.add(ChatColor.translateAlternateColorCodes('&', line)); 186 | } 187 | } 188 | } 189 | 190 | // 添加使用次数信息 191 | if (plugin.getFoodConfig().getBoolean("display.show-info.usage-count", true)) { 192 | StringBuilder usageText = new StringBuilder(); 193 | usageText.append(ChatColor.GRAY).append("Uses: "); 194 | if (isInfinite) { 195 | usageText.append(ChatColor.AQUA).append("∞") 196 | .append(ChatColor.GRAY).append("/") 197 | .append(ChatColor.GRAY).append("∞"); 198 | } else { 199 | usageText.append(ChatColor.AQUA).append(times) 200 | .append(ChatColor.GRAY).append("/") 201 | .append(ChatColor.GRAY).append(maxTimes); 202 | } 203 | lore.add(usageText.toString()); 204 | } 205 | 206 | // 添加进度条 207 | if (!isInfinite && plugin.getFoodConfig().getBoolean("display.show-info.progress-bar", true)) { 208 | StringBuilder progressBar = new StringBuilder(); 209 | progressBar.append(ChatColor.GRAY).append("["); 210 | 211 | int barLength = 10; 212 | double progress = (double) times / maxTimes; 213 | int filledBars = (int) Math.round(progress * barLength); 214 | 215 | for (int i = 0; i < barLength; i++) { 216 | if (i < filledBars) { 217 | progressBar.append(ChatColor.GREEN).append("■"); 218 | } else { 219 | progressBar.append(ChatColor.GRAY).append("■"); 220 | } 221 | } 222 | progressBar.append(ChatColor.GRAY).append("]"); 223 | lore.add(progressBar.toString()); 224 | } 225 | 226 | meta.setLore(lore); 227 | item.setItemMeta(meta); 228 | } 229 | 230 | private void applyFoodEffects(Player player, Material foodType) { 231 | ConfigurationSection foodSection = plugin.getFoodConfig().getConfigurationSection("foods." + foodType.name()); 232 | if (foodSection == null) return; 233 | 234 | // 恢复饥饿值 235 | int foodLevel = foodSection.getInt("food-level", 0); 236 | float saturation = (float) foodSection.getDouble("saturation", 0.0); 237 | double heal = foodSection.getDouble("heal", 0.0); 238 | 239 | // 应用饥饿值和饱食度 240 | int newFoodLevel = Math.min(player.getFoodLevel() + foodLevel, 20); 241 | player.setFoodLevel(newFoodLevel); 242 | player.setSaturation(Math.min(player.getSaturation() + saturation, 20.0f)); 243 | 244 | // 恢复生命值 245 | if (heal > 0) { 246 | // 使用兼容 1.18 的方式获取最大生命值 247 | double maxHealth = player.getAttribute(org.bukkit.attribute.Attribute.GENERIC_MAX_HEALTH).getValue(); 248 | double newHealth = Math.min(player.getHealth() + heal, maxHealth); 249 | player.setHealth(newHealth); 250 | } 251 | 252 | // 应用药水效果 253 | ConfigurationSection effectsSection = foodSection.getConfigurationSection("effects"); 254 | if (effectsSection != null) { 255 | for (String effectName : effectsSection.getKeys(false)) { 256 | PotionEffectType effectType = PotionEffectType.getByName(effectName); 257 | if (effectType != null) { 258 | ConfigurationSection effectSection = effectsSection.getConfigurationSection(effectName); 259 | if (effectSection != null) { 260 | int duration = effectSection.getInt("duration", 200); 261 | int amplifier = effectSection.getInt("amplifier", 0); 262 | player.addPotionEffect(new PotionEffect(effectType, duration, amplifier)); 263 | } 264 | } 265 | } 266 | } 267 | 268 | // 播放音效 269 | if (plugin.getFoodConfig().getBoolean("sound.enabled", true)) { 270 | String soundName = plugin.getFoodConfig().getString("sound.eat", "ENTITY_PLAYER_BURP"); 271 | float volume = (float) plugin.getFoodConfig().getDouble("sound.volume", 1.0); 272 | float pitch = (float) plugin.getFoodConfig().getDouble("sound.pitch", 1.0); 273 | try { 274 | Sound sound = Sound.valueOf(soundName); 275 | player.playSound(player.getLocation(), sound, volume, pitch); 276 | } catch (IllegalArgumentException ignored) {} 277 | } 278 | 279 | // 显示粒子效果 280 | if (plugin.getFoodConfig().getBoolean("particles.enabled", true)) { 281 | String particleType = plugin.getFoodConfig().getString("particles.type", "HEART"); 282 | int count = plugin.getFoodConfig().getInt("particles.count", 5); 283 | double spreadX = plugin.getFoodConfig().getDouble("particles.spread.x", 0.5); 284 | double spreadY = plugin.getFoodConfig().getDouble("particles.spread.y", 0.5); 285 | double spreadZ = plugin.getFoodConfig().getDouble("particles.spread.z", 0.5); 286 | try { 287 | Particle particle = Particle.valueOf(particleType); 288 | player.getWorld().spawnParticle(particle, 289 | player.getLocation().add(0, 1, 0), 290 | count, spreadX, spreadY, spreadZ); 291 | } catch (IllegalArgumentException ignored) {} 292 | } 293 | } 294 | 295 | @EventHandler 296 | public void onPlayerEat(PlayerItemConsumeEvent event) { 297 | ItemStack originalItem = event.getItem(); 298 | if (!isMagicFood(originalItem)) return; 299 | 300 | event.setCancelled(true); 301 | Player player = event.getPlayer(); 302 | 303 | // 检查是否允许在饱食度满时使用 304 | if (!plugin.getFoodConfig().getBoolean("allow-use-when-full", true) 305 | && player.getFoodLevel() >= 20) { 306 | plugin.sendMessage(player, "messages.food-full"); 307 | return; 308 | } 309 | 310 | // 创建物品的副本以避免并发修改问题 311 | ItemStack item = originalItem.clone(); 312 | 313 | // 检查当前使用次数 314 | int currentTimes = getUseTimes(item); 315 | if (currentTimes <= 0) { 316 | // 在 1.18 中,消耗物品总是在主手进行的 317 | removeItemFromHand(player, EquipmentSlot.HAND); 318 | return; 319 | } 320 | 321 | // 应用食物效果 322 | applyFoodEffects(player, item.getType()); 323 | plugin.getStatistics().logFoodUse(player, item); 324 | 325 | // 减少使用次数 326 | currentTimes--; 327 | 328 | // 更新物品状态 329 | if (currentTimes <= 0) { 330 | // 在 1.18 中,消耗物品总是在主手进行的 331 | removeItemFromHand(player, EquipmentSlot.HAND); 332 | plugin.sendMessage(player, "messages.food-removed"); 333 | } else { 334 | setUseTimes(item, currentTimes); 335 | updateLore(item, currentTimes); 336 | // 在 1.18 中,消耗物品总是在主手进行的 337 | updateItemInHand(player, EquipmentSlot.HAND, item); 338 | } 339 | } 340 | 341 | private void removeItemFromHand(Player player, EquipmentSlot hand) { 342 | if (hand == EquipmentSlot.HAND) { 343 | player.getInventory().setItemInMainHand(null); 344 | } else if (hand == EquipmentSlot.OFF_HAND) { 345 | player.getInventory().setItemInOffHand(null); 346 | } 347 | } 348 | 349 | private void updateItemInHand(Player player, EquipmentSlot hand, ItemStack item) { 350 | if (hand == EquipmentSlot.HAND) { 351 | player.getInventory().setItemInMainHand(item); 352 | } else if (hand == EquipmentSlot.OFF_HAND) { 353 | player.getInventory().setItemInOffHand(item); 354 | } 355 | } 356 | 357 | @Override 358 | public boolean isMagicFood(ItemStack item) { 359 | if (item != null && item.hasItemMeta()) { 360 | ItemMeta meta = item.getItemMeta(); 361 | String specialLore = plugin.getFoodConfig().getString("special-lore", "§7MagicFood"); 362 | return meta.hasLore() && meta.getLore().contains(specialLore); 363 | } 364 | return false; 365 | } 366 | 367 | public int getFoodUses(UUID playerUUID) { 368 | return foodUses.getOrDefault(playerUUID, 0); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/food/FoodService.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.food; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | 5 | import org.bukkit.ChatColor; 6 | import org.bukkit.NamespacedKey; 7 | import org.bukkit.inventory.ItemStack; 8 | import org.bukkit.inventory.meta.ItemMeta; 9 | import org.bukkit.persistence.PersistentDataContainer; 10 | import org.bukkit.persistence.PersistentDataType; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class FoodService { 16 | private final MagicBlockPlugin plugin; 17 | private final NamespacedKey useTimesKey; 18 | 19 | public FoodService(MagicBlockPlugin plugin) { 20 | this.plugin = plugin; 21 | this.useTimesKey = new NamespacedKey(plugin, "magicfood_usetimes"); 22 | } 23 | 24 | public void setMagicFoodUseTimes(ItemStack item, int times) { 25 | ItemMeta meta = item.getItemMeta(); 26 | if (meta != null) { 27 | meta.getPersistentDataContainer().set(useTimesKey, PersistentDataType.INTEGER, times); 28 | updateMagicFoodLore(item, times); 29 | item.setItemMeta(meta); 30 | } 31 | } 32 | 33 | public int getMagicFoodUseTimes(ItemStack item) { 34 | ItemMeta meta = item.getItemMeta(); 35 | if (meta != null) { 36 | PersistentDataContainer dataContainer = meta.getPersistentDataContainer(); 37 | return dataContainer.getOrDefault(useTimesKey, PersistentDataType.INTEGER, 0); 38 | } 39 | return 0; 40 | } 41 | 42 | public int decrementMagicFoodUseTimes(ItemStack item) { 43 | int currentTimes = getMagicFoodUseTimes(item); 44 | 45 | if (currentTimes <= 0) { 46 | return 0; 47 | } 48 | 49 | currentTimes--; 50 | setMagicFoodUseTimes(item, currentTimes); 51 | 52 | return currentTimes; 53 | } 54 | 55 | public void updateMagicFoodLore(ItemStack item, int remainingTimes) { 56 | ItemMeta meta = item.getItemMeta(); 57 | if (meta == null) { 58 | return; 59 | } 60 | 61 | List lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); 62 | String prefix = plugin.getFoodConfig().getString("food-usage-lore-prefix", "剩余使用次数"); 63 | String timesLore = ChatColor.GRAY + prefix + remainingTimes; 64 | boolean timesLoreFound = false; 65 | 66 | for (int i = 0; i < lore.size(); i++) { 67 | if (lore.get(i).contains(prefix)) { 68 | lore.set(i, timesLore); 69 | timesLoreFound = true; 70 | break; 71 | } 72 | } 73 | 74 | if (!timesLoreFound) { 75 | lore.add(timesLore); 76 | } 77 | 78 | meta.setLore(lore); 79 | item.setItemMeta(meta); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/gui/BlockSelectionGUI.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.gui; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.Material; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.event.inventory.InventoryClickEvent; 8 | import org.bukkit.inventory.Inventory; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.bukkit.inventory.meta.ItemMeta; 11 | import org.bukkit.ChatColor; 12 | 13 | import java.util.ArrayList; 14 | import java.util.EnumMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.UUID; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.stream.Collectors; 20 | 21 | public class BlockSelectionGUI { 22 | private final MagicBlockPlugin plugin; 23 | private final Map currentPage = new ConcurrentHashMap<>(); 24 | private final Map> searchResults = new ConcurrentHashMap<>(); 25 | private final Map originalItems = new ConcurrentHashMap<>(); 26 | private final Map lastGuiOpenTime = new ConcurrentHashMap<>(); 27 | private static final int ITEMS_PER_PAGE = 45; 28 | private static final long GUI_OPERATION_COOLDOWN = 500; // 0.5秒操作冷却时间 29 | 30 | public BlockSelectionGUI(MagicBlockPlugin plugin) { 31 | this.plugin = plugin; 32 | } 33 | 34 | public void openInventory(Player player) { 35 | // 检查使用权限 36 | if (!player.hasPermission("magicblock.use")) { 37 | plugin.sendMessage(player, "messages.no-permission-use"); 38 | return; 39 | } 40 | 41 | // 记录原始物品 42 | originalItems.put(player.getUniqueId(), player.getInventory().getItemInMainHand().clone()); 43 | // 重置搜索状态 44 | searchResults.remove(player.getUniqueId()); 45 | // 重置页码 46 | currentPage.put(player.getUniqueId(), 1); 47 | // 记录打开时间 48 | lastGuiOpenTime.put(player.getUniqueId(), System.currentTimeMillis()); 49 | // 打开界面 50 | updateInventory(player); 51 | } 52 | 53 | public void updateInventory(Player player) { 54 | Inventory gui = Bukkit.createInventory(null, 54, plugin.getMessage("gui.title")); 55 | UUID playerId = player.getUniqueId(); 56 | int page = currentPage.getOrDefault(playerId, 1); 57 | 58 | List materials = searchResults.getOrDefault(playerId, plugin.getAllowedMaterials()); 59 | int totalPages = (int) Math.ceil(materials.size() / (double) ITEMS_PER_PAGE); 60 | 61 | int startIndex = (page - 1) * ITEMS_PER_PAGE; 62 | int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, materials.size()); 63 | 64 | // 添加物品 65 | for (int i = startIndex; i < endIndex; i++) { 66 | Material material = materials.get(i); 67 | gui.setItem(i - startIndex, createMagicBlock(material)); 68 | } 69 | 70 | // 添加导航按钮和页码信息 71 | if (page > 1) { 72 | ItemStack prevButton = createNavigationItem(plugin.getMessage("gui.previous-page"), Material.ARROW); 73 | gui.setItem(45, prevButton); 74 | } 75 | 76 | // 页码显示 77 | ItemStack pageInfo = new ItemStack(Material.PAPER); 78 | ItemMeta pageInfoMeta = pageInfo.getItemMeta(); 79 | if (pageInfoMeta != null) { 80 | pageInfoMeta.setDisplayName(ChatColor.YELLOW + plugin.getMessage("gui.page-info", page, totalPages)); 81 | pageInfo.setItemMeta(pageInfoMeta); 82 | } 83 | gui.setItem(49, pageInfo); 84 | 85 | if (page < totalPages) { 86 | ItemStack nextButton = createNavigationItem(plugin.getMessage("gui.next-page"), Material.ARROW); 87 | gui.setItem(53, nextButton); 88 | } 89 | 90 | // 添加搜索按钮 91 | gui.setItem(47, createSearchButton()); 92 | 93 | // 添加关闭按钮 94 | ItemStack closeButton = new ItemStack(Material.BARRIER); 95 | ItemMeta closeMeta = closeButton.getItemMeta(); 96 | if (closeMeta != null) { 97 | closeMeta.setDisplayName(ChatColor.RED + plugin.getMessage("gui.close")); 98 | closeButton.setItemMeta(closeMeta); 99 | } 100 | gui.setItem(51, closeButton); 101 | 102 | player.openInventory(gui); 103 | } 104 | 105 | public void handleSearch(Player player, String query) { 106 | UUID playerId = player.getUniqueId(); 107 | List allMaterials = plugin.getAllowedMaterials(); 108 | 109 | if (query == null || query.trim().isEmpty()) { 110 | searchResults.remove(playerId); 111 | } else { 112 | String lowercaseQuery = query.toLowerCase(); 113 | List results = allMaterials.stream() 114 | .filter(material -> { 115 | String materialName = material.name().toLowerCase(); 116 | String localizedName = plugin.getMessage(plugin.getMinecraftLangManager().getItemStackName(new ItemStack(material))); 117 | return materialName.contains(lowercaseQuery) || 118 | localizedName.toLowerCase().contains(lowercaseQuery); 119 | }) 120 | .collect(Collectors.toList()); 121 | 122 | if (!results.isEmpty()) { 123 | searchResults.put(playerId, results); 124 | } else { 125 | searchResults.remove(playerId); 126 | plugin.sendMessage(player, "messages.no-results"); 127 | } 128 | } 129 | 130 | currentPage.put(playerId, 1); 131 | updateInventory(player); 132 | } 133 | 134 | public void handleInventoryClick(InventoryClickEvent event, Player player) { 135 | // 检查使用权限 136 | if (!player.hasPermission("magicblock.use")) { 137 | plugin.sendMessage(player, "messages.no-permission-use"); 138 | event.setCancelled(true); 139 | player.closeInventory(); 140 | return; 141 | } 142 | 143 | // 检查冷却时间 144 | long currentTime = System.currentTimeMillis(); 145 | long openTime = lastGuiOpenTime.getOrDefault(player.getUniqueId(), 0L); 146 | if (currentTime - openTime < GUI_OPERATION_COOLDOWN) { 147 | return; 148 | } 149 | 150 | // 检查点击的位置是否在GUI的有效范围内 151 | int slot = event.getRawSlot(); 152 | if (slot < 0 || slot >= event.getView().getTopInventory().getSize()) { 153 | return; 154 | } 155 | 156 | ItemStack clickedItem = event.getCurrentItem(); 157 | if (clickedItem == null || clickedItem.getType() == Material.AIR) { 158 | return; 159 | } 160 | 161 | UUID playerId = player.getUniqueId(); 162 | int page = currentPage.getOrDefault(playerId, 1); 163 | List materials = searchResults.getOrDefault(playerId, plugin.getAllowedMaterials()); 164 | int totalPages = (int) Math.ceil(materials.size() / (double) ITEMS_PER_PAGE); 165 | 166 | // 使用synchronized块来确保线程安全 167 | synchronized (this) { 168 | // 处理导航按钮点击 169 | if (clickedItem.getType() == Material.ARROW) { 170 | if (slot == 45 && page > 1) { 171 | currentPage.put(playerId, page - 1); 172 | updateInventory(player); 173 | } else if (slot == 53 && page < totalPages) { 174 | currentPage.put(playerId, page + 1); 175 | updateInventory(player); 176 | } 177 | return; 178 | } 179 | 180 | // 处理关闭按钮点击 181 | if (slot == 51 && clickedItem.getType() == Material.BARRIER) { 182 | player.closeInventory(); 183 | return; 184 | } 185 | 186 | // 处理搜索按钮点击 187 | if (slot == 47 && clickedItem.getType() == Material.COMPASS) { 188 | player.closeInventory(); 189 | plugin.sendMessage(player, "messages.search-prompt"); 190 | GUIManager.setPlayerSearching(player, true); 191 | return; 192 | } 193 | 194 | // 检查点击的物品是否在允许的材料列表中 195 | if (!plugin.getAllowedMaterials().contains(clickedItem.getType())) { 196 | return; 197 | } 198 | 199 | // 替换方块 200 | ItemStack originalItem = originalItems.get(playerId); 201 | if (originalItem != null && plugin.hasMagicLore(originalItem.getItemMeta())) { 202 | ItemStack newItem = originalItem.clone(); 203 | newItem.setType(clickedItem.getType()); 204 | 205 | // 保持原有的附魔和其他元数据 206 | ItemMeta originalMeta = originalItem.getItemMeta(); 207 | ItemMeta newMeta = newItem.getItemMeta(); 208 | if (originalMeta != null && newMeta != null) { 209 | String blockName = plugin.getMinecraftLangManager().getItemStackName(clickedItem); 210 | String nameFormat = plugin.getConfig().getString("display.block-name-format", "&b✦ %s &b✦"); 211 | newMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', 212 | String.format(nameFormat, blockName))); 213 | 214 | newMeta.setLore(originalMeta.getLore()); 215 | originalMeta.getEnchants().forEach((enchant, level) -> 216 | newMeta.addEnchant(enchant, level, true)); 217 | originalMeta.getItemFlags().forEach(newMeta::addItemFlags); 218 | newItem.setItemMeta(newMeta); 219 | } 220 | 221 | player.getInventory().setItemInMainHand(newItem); 222 | plugin.sendMessage(player, "messages.success-replace", plugin.getMinecraftLangManager().getItemStackName(clickedItem)); 223 | 224 | // 清理记录 225 | clearPlayerData(playerId); 226 | player.closeInventory(); 227 | } 228 | } 229 | } 230 | 231 | private ItemStack createSearchButton() { 232 | ItemStack searchButton = new ItemStack(Material.COMPASS); 233 | ItemMeta meta = searchButton.getItemMeta(); 234 | if (meta != null) { 235 | meta.setDisplayName(plugin.getMessage("gui.search-button")); 236 | List lore = new ArrayList<>(); 237 | lore.add(plugin.getMessage("gui.search-lore")); 238 | meta.setLore(lore); 239 | searchButton.setItemMeta(meta); 240 | } 241 | return searchButton; 242 | } 243 | 244 | private ItemStack createNavigationItem(String name, Material material) { 245 | ItemStack button = new ItemStack(material); 246 | ItemMeta meta = button.getItemMeta(); 247 | if (meta != null) { 248 | meta.setDisplayName(name); 249 | button.setItemMeta(meta); 250 | } 251 | return button; 252 | } 253 | 254 | private ItemStack createMagicBlock(Material material) { 255 | ItemStack block = new ItemStack(material); 256 | ItemMeta meta = block.getItemMeta(); 257 | if (meta != null) { 258 | String blockName = plugin.getMinecraftLangManager().getItemStackName(block); 259 | // 在原有名称两侧添加装饰符号 260 | String nameFormat = plugin.getConfig().getString("display.block-name-format", "&b✦ %s &b✦"); 261 | meta.setDisplayName(ChatColor.translateAlternateColorCodes('&', 262 | String.format(nameFormat, blockName))); 263 | meta.setLore(List.of(plugin.getMessage("gui.select-block"))); 264 | block.setItemMeta(meta); 265 | } 266 | return block; 267 | } 268 | 269 | public void clearPlayerData(UUID playerUUID) { 270 | currentPage.remove(playerUUID); 271 | searchResults.remove(playerUUID); 272 | originalItems.remove(playerUUID); 273 | lastGuiOpenTime.remove(playerUUID); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/gui/GUIManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.gui; 2 | 3 | import com.tcoded.folialib.FoliaLib; 4 | import io.github.syferie.magicblock.MagicBlockPlugin; 5 | import org.bukkit.Material; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.event.EventHandler; 8 | import org.bukkit.event.EventPriority; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.inventory.InventoryClickEvent; 11 | import org.bukkit.event.inventory.InventoryCloseEvent; 12 | import org.bukkit.event.player.AsyncPlayerChatEvent; 13 | import org.bukkit.inventory.ItemStack; 14 | 15 | import java.util.List; 16 | import java.util.UUID; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.Map; 19 | 20 | public class GUIManager implements Listener { 21 | private final MagicBlockPlugin plugin; 22 | private final BlockSelectionGUI blockSelectionGUI; 23 | private static final Map searchingPlayers = new ConcurrentHashMap<>(); 24 | private static final long GUI_CLICK_COOLDOWN = 300; 25 | private static final Map lastSearchClickTime = new ConcurrentHashMap<>(); 26 | private static final long SEARCH_CLICK_COOLDOWN = 600; 27 | private static final Map lastGuiOpenTime = new ConcurrentHashMap<>(); 28 | private static final long GUI_PROTECTION_TIME = 200; 29 | private final FoliaLib foliaLib; 30 | 31 | public GUIManager(MagicBlockPlugin plugin, List allowedMaterials) { 32 | this.plugin = plugin; 33 | this.blockSelectionGUI = new BlockSelectionGUI(plugin); 34 | this.foliaLib = plugin.getFoliaLib(); 35 | } 36 | 37 | public static void setPlayerSearching(Player player, boolean searching) { 38 | if (searching) { 39 | searchingPlayers.put(player.getUniqueId(), true); 40 | } else { 41 | searchingPlayers.remove(player.getUniqueId()); 42 | } 43 | } 44 | 45 | public static boolean isPlayerSearching(Player player) { 46 | return searchingPlayers.getOrDefault(player.getUniqueId(), false); 47 | } 48 | 49 | public BlockSelectionGUI getBlockSelectionGUI() { 50 | return blockSelectionGUI; 51 | } 52 | 53 | public void openBlockSelectionGUI(Player player) { 54 | ItemStack heldItem = player.getInventory().getItemInMainHand(); 55 | if (!plugin.hasMagicLore(heldItem.getItemMeta())) { 56 | plugin.sendMessage(player, "messages.must-hold-magic-block"); 57 | return; 58 | } 59 | lastGuiOpenTime.put(player.getUniqueId(), System.currentTimeMillis()); 60 | blockSelectionGUI.openInventory(player); 61 | } 62 | 63 | @EventHandler 64 | public void onPlayerChat(AsyncPlayerChatEvent event) { 65 | Player player = event.getPlayer(); 66 | if (isPlayerSearching(player)) { 67 | event.setCancelled(true); 68 | String input = event.getMessage(); 69 | 70 | if (input.equalsIgnoreCase("cancel")) { 71 | setPlayerSearching(player, false); 72 | // 使用FoliaLib在玩家实体上执行GUI操作 73 | foliaLib.getScheduler().runAtEntity( 74 | player, 75 | task -> blockSelectionGUI.openInventory(player) 76 | ); 77 | return; 78 | } 79 | 80 | // 使用FoliaLib在玩家实体上执行搜索操作 81 | foliaLib.getScheduler().runAtEntity( 82 | player, 83 | task -> { 84 | blockSelectionGUI.handleSearch(player, input); 85 | setPlayerSearching(player, false); 86 | } 87 | ); 88 | } 89 | } 90 | 91 | @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) 92 | public void onInventoryClick(InventoryClickEvent event) { 93 | if (!(event.getWhoClicked() instanceof Player)) return; 94 | Player player = (Player) event.getWhoClicked(); 95 | 96 | if (!event.getView().getTitle().equals(plugin.getMessage("gui.title"))) { 97 | return; 98 | } 99 | 100 | // 立即取消事件,防止传播 101 | event.setCancelled(true); 102 | 103 | // 使用computeIfAbsent来确保线程安全的获取上次打开时间 104 | long openTime = lastGuiOpenTime.computeIfAbsent(player.getUniqueId(), k -> 0L); 105 | long currentTime = System.currentTimeMillis(); 106 | 107 | if (currentTime - openTime < GUI_PROTECTION_TIME) { 108 | return; 109 | } 110 | 111 | ItemStack clickedItem = event.getCurrentItem(); 112 | if (clickedItem == null || clickedItem.getType() == Material.AIR) { 113 | return; 114 | } 115 | 116 | // 检查是否是搜索按钮 117 | if (clickedItem.getType() == Material.COMPASS) { 118 | // 使用原子操作检查冷却时间 119 | Long lastClick = lastSearchClickTime.get(player.getUniqueId()); 120 | if (lastClick != null && currentTime - lastClick < SEARCH_CLICK_COOLDOWN) { 121 | plugin.sendMessage(player, "messages.wait-cooldown"); 122 | return; 123 | } 124 | lastSearchClickTime.put(player.getUniqueId(), currentTime); 125 | } 126 | 127 | // 使用FoliaLib确保在主线程执行GUI操作 128 | foliaLib.getScheduler().runAtEntity( 129 | player, 130 | task -> blockSelectionGUI.handleInventoryClick(event, player) 131 | ); 132 | } 133 | 134 | @EventHandler 135 | public void onInventoryClose(InventoryCloseEvent event) { 136 | if (!(event.getPlayer() instanceof Player)) return; 137 | Player player = (Player) event.getPlayer(); 138 | 139 | if (event.getView().getTitle().equals("魔法方块选择") && !isPlayerSearching(player)) { 140 | // 只有在不是因为搜索而关闭GUI时才清理数据 141 | blockSelectionGUI.clearPlayerData(player.getUniqueId()); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/hook/PlaceholderHook.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.hook; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import me.clip.placeholderapi.expansion.PlaceholderExpansion; 5 | import org.bukkit.OfflinePlayer; 6 | import org.bukkit.inventory.ItemStack; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class PlaceholderHook extends PlaceholderExpansion { 10 | 11 | private final MagicBlockPlugin plugin; 12 | 13 | public PlaceholderHook(MagicBlockPlugin plugin) { 14 | this.plugin = plugin; 15 | } 16 | 17 | @Override 18 | public @NotNull String getIdentifier() { 19 | return "magicblock"; 20 | } 21 | 22 | @Override 23 | public @NotNull String getAuthor() { 24 | return "WeSif"; 25 | } 26 | 27 | @Override 28 | public @NotNull String getVersion() { 29 | return plugin.getDescription().getVersion(); 30 | } 31 | 32 | @Override 33 | public boolean persist() { 34 | return true; 35 | } 36 | 37 | @Override 38 | public String onRequest(OfflinePlayer player, @NotNull String params) { 39 | if (player == null) return ""; 40 | 41 | // 获取玩家使用魔法方块的总次数 42 | if (params.equalsIgnoreCase("block_uses")) { 43 | return String.valueOf(plugin.getPlayerUsage(player.getUniqueId())); 44 | } 45 | 46 | // 获取玩家使用魔法食物的总次数 47 | if (params.equalsIgnoreCase("food_uses")) { 48 | if (plugin.getMagicFood() != null) { 49 | return String.valueOf(plugin.getMagicFood().getFoodUses(player.getUniqueId())); 50 | } 51 | return "0"; 52 | } 53 | 54 | // 获取玩家剩余的魔法方块使用次数 55 | if (params.equalsIgnoreCase("remaining_uses")) { 56 | if (player.isOnline() && player.getPlayer() != null) { 57 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 58 | if (plugin.getBlockManager().getUseTimes(item) > 0) { 59 | return String.valueOf(plugin.getBlockManager().getUseTimes(item)); 60 | } 61 | } 62 | return "0"; 63 | } 64 | 65 | // 获取玩家是否持有魔法方块 66 | if (params.equalsIgnoreCase("has_block")) { 67 | if (player.isOnline() && player.getPlayer() != null) { 68 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 69 | return String.valueOf(plugin.getBlockManager().isMagicBlock(item)); 70 | } 71 | return "false"; 72 | } 73 | 74 | // 获取玩家是否持有魔法食物 75 | if (params.equalsIgnoreCase("has_food")) { 76 | if (player.isOnline() && player.getPlayer() != null && plugin.getMagicFood() != null) { 77 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 78 | return String.valueOf(plugin.getMagicFood().isMagicFood(item)); 79 | } 80 | return "false"; 81 | } 82 | 83 | // 获取玩家魔法方块的最大使用次数 84 | if (params.equalsIgnoreCase("max_uses")) { 85 | if (player.isOnline() && player.getPlayer() != null) { 86 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 87 | if (plugin.getBlockManager().isMagicBlock(item)) { 88 | return String.valueOf(plugin.getBlockManager().getMaxUseTimes(item)); 89 | } 90 | } 91 | return "0"; 92 | } 93 | 94 | // 获取玩家魔法方块的使用进度(百分比) 95 | if (params.equalsIgnoreCase("uses_progress")) { 96 | if (player.isOnline() && player.getPlayer() != null) { 97 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 98 | if (plugin.getBlockManager().isMagicBlock(item)) { 99 | int maxUses = plugin.getBlockManager().getMaxUseTimes(item); 100 | int remainingUses = plugin.getBlockManager().getUseTimes(item); 101 | if (maxUses > 0) { 102 | double progress = ((double)(maxUses - remainingUses) / maxUses) * 100; 103 | return String.format("%.1f", progress); 104 | } 105 | } 106 | } 107 | return "0.0"; 108 | } 109 | 110 | // 获取玩家魔法方块的进度条 111 | if (params.equalsIgnoreCase("progress_bar") || params.equalsIgnoreCase("progressbar")) { 112 | if (player.isOnline() && player.getPlayer() != null) { 113 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 114 | if (plugin.getBlockManager().isMagicBlock(item)) { 115 | int maxUses = plugin.getBlockManager().getMaxUseTimes(item); 116 | int remainingUses = plugin.getBlockManager().getUseTimes(item); 117 | 118 | // 检查是否是无限次数 119 | if (maxUses == Integer.MAX_VALUE - 100) { 120 | return "&a∞"; // 无限符号 121 | } 122 | 123 | if (maxUses > 0) { 124 | // 使用插件的进度条生成方法 125 | return plugin.getProgressBar(remainingUses, maxUses); 126 | } 127 | } 128 | } 129 | return "&7无进度条"; // 默认返回空进度条 130 | } 131 | 132 | // 获取自定义长度的进度条 133 | if (params.startsWith("progress_bar_") || params.startsWith("progressbar_")) { 134 | try { 135 | // 从参数中提取进度条长度 136 | int barLength = Integer.parseInt(params.substring(params.lastIndexOf('_') + 1)); 137 | if (barLength <= 0) barLength = 10; // 默认长度 138 | 139 | if (player.isOnline() && player.getPlayer() != null) { 140 | ItemStack item = player.getPlayer().getInventory().getItemInMainHand(); 141 | if (plugin.getBlockManager().isMagicBlock(item)) { 142 | int maxUses = plugin.getBlockManager().getMaxUseTimes(item); 143 | int remainingUses = plugin.getBlockManager().getUseTimes(item); 144 | 145 | // 检查是否是无限次数 146 | if (maxUses == Integer.MAX_VALUE - 100) { 147 | return "&a∞"; // 无限符号 148 | } 149 | 150 | if (maxUses > 0) { 151 | // 生成自定义长度的进度条 152 | double usedPercentage = (double) remainingUses / maxUses; 153 | int filledBars = (int) Math.round(usedPercentage * barLength); 154 | 155 | StringBuilder progressBar = new StringBuilder("&a"); 156 | for (int i = 0; i < barLength; i++) { 157 | if (i < filledBars) { 158 | progressBar.append("■"); // 实心方块 159 | } else { 160 | progressBar.append("□"); // 空心方块 161 | } 162 | } 163 | return progressBar.toString(); 164 | } 165 | } 166 | } 167 | return "&7" + "□".repeat(barLength); // 默认返回空进度条 168 | } catch (NumberFormatException e) { 169 | return "&c无效的进度条长度"; 170 | } 171 | } 172 | 173 | return null; 174 | } 175 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/manager/MagicBlockIndexManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.manager; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.Chunk; 6 | import org.bukkit.Location; 7 | import org.bukkit.World; 8 | import org.bukkit.block.Block; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.bukkit.persistence.PersistentDataContainer; 11 | import org.bukkit.persistence.PersistentDataType; 12 | import org.bukkit.NamespacedKey; 13 | 14 | import java.util.*; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | /** 18 | * 魔法方块索引管理器 19 | * 实现高性能的魔法方块位置索引和查找 20 | * 21 | * 性能优化策略: 22 | * 1. 内存索引:O(1) 快速查找 23 | * 2. 区块缓存:减少内存使用 24 | * 3. 持久化存储:数据安全保障 25 | * 4. 智能过滤:早期事件过滤 26 | */ 27 | public class MagicBlockIndexManager { 28 | private final MagicBlockPlugin plugin; 29 | private final NamespacedKey magicBlockKey; 30 | 31 | // 第一层:全局内存索引 - 最快的查找 32 | private final Set globalMagicBlockIndex = ConcurrentHashMap.newKeySet(); 33 | 34 | // 第二层:区块级别索引 - 减少内存使用和提供区块过滤 35 | private final Map> chunkMagicBlocks = new ConcurrentHashMap<>(); 36 | 37 | // 第三层:世界级别索引 - 用于快速判断世界是否有魔法方块 38 | private final Set worldsWithMagicBlocks = ConcurrentHashMap.newKeySet(); 39 | 40 | // 性能统计 41 | private long totalLookups = 0; 42 | private long cacheHits = 0; 43 | private long cacheMisses = 0; 44 | 45 | public MagicBlockIndexManager(MagicBlockPlugin plugin) { 46 | this.plugin = plugin; 47 | this.magicBlockKey = new NamespacedKey(plugin, "magicblock_location"); 48 | 49 | // 启动时加载现有的魔法方块索引 50 | loadExistingMagicBlocks(); 51 | 52 | // 启动定期清理任务 53 | startCleanupTask(); 54 | } 55 | 56 | /** 57 | * 注册魔法方块到索引系统 58 | * 当魔法方块被放置时调用 59 | */ 60 | public void registerMagicBlock(Location location, ItemStack magicBlock) { 61 | String locationKey = serializeLocation(location); 62 | String chunkKey = getChunkKey(location); 63 | String worldName = location.getWorld().getName(); 64 | 65 | // 1. 添加到全局索引 66 | globalMagicBlockIndex.add(locationKey); 67 | 68 | // 2. 添加到区块索引 69 | chunkMagicBlocks.computeIfAbsent(chunkKey, k -> ConcurrentHashMap.newKeySet()) 70 | .add(locationKey); 71 | 72 | // 3. 标记世界包含魔法方块 73 | worldsWithMagicBlocks.add(worldName); 74 | 75 | // 4. 持久化存储(异步) 76 | plugin.getFoliaLib().getScheduler().runAsync(task -> { 77 | saveToPersistentStorage(location, magicBlock); 78 | }); 79 | 80 | plugin.debug("注册魔法方块: " + locationKey); 81 | } 82 | 83 | /** 84 | * 从索引系统移除魔法方块 85 | * 当魔法方块被破坏时调用 86 | */ 87 | public void unregisterMagicBlock(Location location) { 88 | String locationKey = serializeLocation(location); 89 | String chunkKey = getChunkKey(location); 90 | 91 | // 1. 从全局索引移除 92 | boolean removed = globalMagicBlockIndex.remove(locationKey); 93 | 94 | if (removed) { 95 | // 2. 从区块索引移除 96 | Set chunkBlocks = chunkMagicBlocks.get(chunkKey); 97 | if (chunkBlocks != null) { 98 | chunkBlocks.remove(locationKey); 99 | 100 | // 如果区块没有魔法方块了,清理区块索引 101 | if (chunkBlocks.isEmpty()) { 102 | chunkMagicBlocks.remove(chunkKey); 103 | } 104 | } 105 | 106 | // 3. 检查世界是否还有魔法方块 107 | checkAndCleanupWorld(location.getWorld().getName()); 108 | 109 | // 4. 从持久化存储移除(异步) 110 | plugin.getFoliaLib().getScheduler().runAsync(task -> { 111 | removeFromPersistentStorage(location); 112 | }); 113 | 114 | plugin.debug("移除魔法方块: " + locationKey); 115 | } 116 | } 117 | 118 | /** 119 | * 超高性能的魔法方块检查 120 | * O(1) 时间复杂度 121 | */ 122 | public boolean isMagicBlock(Location location) { 123 | totalLookups++; 124 | 125 | String locationKey = serializeLocation(location); 126 | boolean result = globalMagicBlockIndex.contains(locationKey); 127 | 128 | if (result) { 129 | cacheHits++; 130 | } else { 131 | cacheMisses++; 132 | } 133 | 134 | return result; 135 | } 136 | 137 | /** 138 | * 检查区块是否包含魔法方块 139 | * 用于早期事件过滤 140 | */ 141 | public boolean chunkHasMagicBlocks(Location location) { 142 | String chunkKey = getChunkKey(location); 143 | return chunkMagicBlocks.containsKey(chunkKey); 144 | } 145 | 146 | /** 147 | * 检查世界是否包含魔法方块 148 | * 用于最早期的事件过滤 149 | */ 150 | public boolean worldHasMagicBlocks(String worldName) { 151 | return worldsWithMagicBlocks.contains(worldName); 152 | } 153 | 154 | /** 155 | * 获取区块中的所有魔法方块位置 156 | */ 157 | public Set getMagicBlocksInChunk(Location location) { 158 | String chunkKey = getChunkKey(location); 159 | Set chunkBlocks = chunkMagicBlocks.get(chunkKey); 160 | return chunkBlocks != null ? new HashSet<>(chunkBlocks) : new HashSet<>(); 161 | } 162 | 163 | /** 164 | * 获取性能统计信息 165 | */ 166 | public Map getPerformanceStats() { 167 | Map stats = new HashMap<>(); 168 | stats.put("totalMagicBlocks", globalMagicBlockIndex.size()); 169 | stats.put("totalChunks", chunkMagicBlocks.size()); 170 | stats.put("totalWorlds", worldsWithMagicBlocks.size()); 171 | stats.put("totalLookups", totalLookups); 172 | stats.put("cacheHits", cacheHits); 173 | stats.put("cacheMisses", cacheMisses); 174 | 175 | double hitRate = totalLookups > 0 ? (double) cacheHits / totalLookups * 100 : 0; 176 | stats.put("cacheHitRate", hitRate); 177 | 178 | return stats; 179 | } 180 | 181 | // 辅助方法 182 | private String serializeLocation(Location loc) { 183 | return loc.getWorld().getName() + "," + 184 | loc.getBlockX() + "," + 185 | loc.getBlockY() + "," + 186 | loc.getBlockZ(); 187 | } 188 | 189 | private String getChunkKey(Location loc) { 190 | return loc.getWorld().getName() + "_" + loc.getChunk().getX() + "_" + loc.getChunk().getZ(); 191 | } 192 | 193 | private void checkAndCleanupWorld(String worldName) { 194 | // 检查世界是否还有魔法方块 195 | boolean hasBlocks = chunkMagicBlocks.keySet().stream() 196 | .anyMatch(chunkKey -> chunkKey.startsWith(worldName + "_")); 197 | 198 | if (!hasBlocks) { 199 | worldsWithMagicBlocks.remove(worldName); 200 | } 201 | } 202 | 203 | private void saveToPersistentStorage(Location location, ItemStack magicBlock) { 204 | // 保存到区块的持久化数据中 205 | String locationString = serializeLocation(location); 206 | PersistentDataContainer container = location.getChunk().getPersistentDataContainer(); 207 | 208 | // 获取现有的位置列表 209 | String existingData = container.get(magicBlockKey, PersistentDataType.STRING); 210 | Set locations = new HashSet<>(); 211 | 212 | if (existingData != null && !existingData.isEmpty()) { 213 | locations.addAll(Arrays.asList(existingData.split(";"))); 214 | } 215 | 216 | locations.add(locationString); 217 | 218 | // 保存更新后的位置列表 219 | String joinedLocations = String.join(";", locations); 220 | container.set(magicBlockKey, PersistentDataType.STRING, joinedLocations); 221 | } 222 | 223 | private void removeFromPersistentStorage(Location location) { 224 | String locationString = serializeLocation(location); 225 | PersistentDataContainer container = location.getChunk().getPersistentDataContainer(); 226 | 227 | String existingData = container.get(magicBlockKey, PersistentDataType.STRING); 228 | if (existingData == null) return; 229 | 230 | Set locations = new HashSet<>(Arrays.asList(existingData.split(";"))); 231 | locations.remove(locationString); 232 | 233 | if (locations.isEmpty()) { 234 | container.remove(magicBlockKey); 235 | } else { 236 | String joinedLocations = String.join(";", locations); 237 | container.set(magicBlockKey, PersistentDataType.STRING, joinedLocations); 238 | } 239 | } 240 | 241 | private void loadExistingMagicBlocks() { 242 | plugin.getLogger().info("正在加载现有魔法方块索引..."); 243 | 244 | int loadedCount = 0; 245 | for (World world : Bukkit.getWorlds()) { 246 | for (Chunk chunk : world.getLoadedChunks()) { 247 | PersistentDataContainer container = chunk.getPersistentDataContainer(); 248 | String locationsData = container.get(magicBlockKey, PersistentDataType.STRING); 249 | 250 | if (locationsData != null && !locationsData.isEmpty()) { 251 | String[] locations = locationsData.split(";"); 252 | for (String locationStr : locations) { 253 | try { 254 | String[] parts = locationStr.split(","); 255 | if (parts.length == 4) { 256 | World locWorld = Bukkit.getWorld(parts[0]); 257 | if (locWorld != null) { 258 | Location loc = new Location(locWorld, 259 | Integer.parseInt(parts[1]), 260 | Integer.parseInt(parts[2]), 261 | Integer.parseInt(parts[3])); 262 | 263 | // 验证方块是否仍然存在 264 | Block block = loc.getBlock(); 265 | if (!block.getType().isAir()) { 266 | // 添加到索引(不触发持久化) 267 | String locationKey = serializeLocation(loc); 268 | String chunkKey = getChunkKey(loc); 269 | 270 | globalMagicBlockIndex.add(locationKey); 271 | chunkMagicBlocks.computeIfAbsent(chunkKey, k -> ConcurrentHashMap.newKeySet()) 272 | .add(locationKey); 273 | worldsWithMagicBlocks.add(world.getName()); 274 | 275 | loadedCount++; 276 | } 277 | } 278 | } 279 | } catch (Exception e) { 280 | plugin.debug("加载魔法方块位置时出错: " + locationStr + " - " + e.getMessage()); 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | plugin.getLogger().info("已加载 " + loadedCount + " 个魔法方块到索引中"); 288 | } 289 | 290 | private void startCleanupTask() { 291 | // 每5分钟清理一次无效的索引 292 | plugin.getFoliaLib().getScheduler().runTimer(() -> { 293 | cleanupInvalidEntries(); 294 | }, 6000L, 6000L); // 5分钟 = 6000 ticks 295 | } 296 | 297 | private void cleanupInvalidEntries() { 298 | plugin.debug("开始清理无效的魔法方块索引..."); 299 | 300 | int removedCount = 0; 301 | Iterator iterator = globalMagicBlockIndex.iterator(); 302 | 303 | while (iterator.hasNext()) { 304 | String locationKey = iterator.next(); 305 | try { 306 | String[] parts = locationKey.split(","); 307 | if (parts.length == 4) { 308 | World world = Bukkit.getWorld(parts[0]); 309 | if (world != null) { 310 | Location loc = new Location(world, 311 | Integer.parseInt(parts[1]), 312 | Integer.parseInt(parts[2]), 313 | Integer.parseInt(parts[3])); 314 | 315 | // 检查方块是否仍然存在 316 | if (loc.getBlock().getType().isAir()) { 317 | // 方块不存在,从索引中移除 318 | iterator.remove(); 319 | 320 | // 同时从区块索引中移除 321 | String chunkKey = getChunkKey(loc); 322 | Set chunkBlocks = chunkMagicBlocks.get(chunkKey); 323 | if (chunkBlocks != null) { 324 | chunkBlocks.remove(locationKey); 325 | if (chunkBlocks.isEmpty()) { 326 | chunkMagicBlocks.remove(chunkKey); 327 | } 328 | } 329 | 330 | removedCount++; 331 | } 332 | } 333 | } 334 | } catch (Exception e) { 335 | // 无效的位置格式,移除 336 | iterator.remove(); 337 | removedCount++; 338 | } 339 | } 340 | 341 | if (removedCount > 0) { 342 | plugin.debug("清理了 " + removedCount + " 个无效的魔法方块索引"); 343 | } 344 | } 345 | 346 | /** 347 | * 重载索引系统 348 | */ 349 | public void reload() { 350 | plugin.getLogger().info("重载魔法方块索引系统..."); 351 | 352 | // 清空现有索引 353 | globalMagicBlockIndex.clear(); 354 | chunkMagicBlocks.clear(); 355 | worldsWithMagicBlocks.clear(); 356 | 357 | // 重新加载 358 | loadExistingMagicBlocks(); 359 | 360 | plugin.getLogger().info("魔法方块索引系统重载完成"); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/Constants.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | /** 4 | * 常量类 5 | */ 6 | public final class Constants { 7 | private Constants() {} 8 | 9 | // 插件名称 10 | public static final String PLUGIN_NAME = "MagicBlock"; 11 | 12 | // 数据键 13 | public static final String BLOCK_TIMES_KEY = "magicblock_usetimes"; 14 | public static final String FOOD_TIMES_KEY = "magicfood_usetimes"; 15 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/LanguageManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.ChatColor; 5 | import org.bukkit.configuration.file.FileConfiguration; 6 | import org.bukkit.configuration.file.YamlConfiguration; 7 | 8 | import java.io.*; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class LanguageManager { 14 | private final MagicBlockPlugin plugin; 15 | private FileConfiguration langConfig; 16 | private String currentLanguage; 17 | private static final Map SUPPORTED_LANGUAGES = new HashMap<>(); 18 | private static final Map LANGUAGE_TO_MINECRAFT = new HashMap<>(); 19 | 20 | static { 21 | SUPPORTED_LANGUAGES.put("en", "English"); 22 | SUPPORTED_LANGUAGES.put("zh_CN", "简体中文"); 23 | 24 | // 映射插件语言代码到Minecraft语言代码 25 | LANGUAGE_TO_MINECRAFT.put("en", "en_gb"); 26 | LANGUAGE_TO_MINECRAFT.put("zh_CN", "zh_cn"); 27 | } 28 | 29 | public LanguageManager(MagicBlockPlugin plugin) { 30 | this.plugin = plugin; 31 | initializeLanguageFiles(); 32 | loadLanguage(); 33 | } 34 | 35 | private void initializeLanguageFiles() { 36 | // 确保所有支持的语言文件都被生成 37 | for (String langCode : SUPPORTED_LANGUAGES.keySet()) { 38 | String fileName = "lang_" + langCode + ".yml"; 39 | File langFile = new File(plugin.getDataFolder(), fileName); 40 | if (!langFile.exists()) { 41 | plugin.saveResource(fileName, false); 42 | } 43 | } 44 | } 45 | 46 | private void loadLanguage() { 47 | // 从配置文件获取语言设置,默认英语 48 | currentLanguage = plugin.getConfig().getString("language", "en"); 49 | 50 | // 确保语言代码有效 51 | if (!SUPPORTED_LANGUAGES.containsKey(currentLanguage)) { 52 | plugin.getLogger().warning("不支持的语言: " + currentLanguage + ",使用默认语言(英语)。"); 53 | currentLanguage = "en"; 54 | } 55 | 56 | // 加载语言文件 57 | File langFile = new File(plugin.getDataFolder(), "lang_" + currentLanguage + ".yml"); 58 | 59 | try { 60 | // 加载默认语言文件 61 | InputStream defaultLangStream = plugin.getResource("lang_" + currentLanguage + ".yml"); 62 | if (defaultLangStream != null) { 63 | FileConfiguration defaultLang = YamlConfiguration.loadConfiguration( 64 | new InputStreamReader(defaultLangStream, StandardCharsets.UTF_8)); 65 | 66 | // 加载用户语言文件 67 | langConfig = YamlConfiguration.loadConfiguration( 68 | new InputStreamReader(new FileInputStream(langFile), StandardCharsets.UTF_8)); 69 | 70 | // 确保所有键都存在 71 | boolean needsSave = false; 72 | for (String key : defaultLang.getKeys(true)) { 73 | if (!langConfig.contains(key)) { 74 | langConfig.set(key, defaultLang.get(key)); 75 | needsSave = true; 76 | } 77 | } 78 | 79 | // 只在需要时保��文件 80 | if (needsSave) { 81 | langConfig.save(langFile); 82 | } 83 | } 84 | } catch (IOException e) { 85 | plugin.getLogger().severe("加载语言文件失败: " + e.getMessage()); 86 | e.printStackTrace(); 87 | } 88 | } 89 | 90 | public void reloadLanguage() { 91 | // 重新加载配置以获取最新的语言设置 92 | plugin.reloadConfig(); 93 | loadLanguage(); 94 | } 95 | 96 | public String getMessage(String path) { 97 | String message = langConfig.getString(path); 98 | if (message == null) { 99 | plugin.getLogger().warning("缺少语言键: " + path); 100 | return "Missing message: " + path; 101 | } 102 | return ChatColor.translateAlternateColorCodes('&', message); 103 | } 104 | 105 | public String getMessage(String path, Object... args) { 106 | String message = getMessage(path); 107 | 108 | // 如果有参数,替换占位符 109 | if (args != null && args.length > 0) { 110 | for (int i = 0; i < args.length; i++) { 111 | message = message.replace("{" + i + "}", String.valueOf(args[i])); 112 | } 113 | } 114 | 115 | // 检查是否有未替换的占位符,如果有,则移除它们 116 | // 这可以防止在消息中出现未替换的 {0}, {1} 等 117 | if (message.contains("{") && message.contains("}")) { 118 | message = message.replaceAll("\\{\\d+\\}", ""); 119 | } 120 | 121 | return message; 122 | } 123 | 124 | public String getCurrentLanguage() { 125 | return currentLanguage; 126 | } 127 | 128 | public Map getSupportedLanguages() { 129 | return SUPPORTED_LANGUAGES; 130 | } 131 | 132 | /** 133 | * 获取当前语言对应的Minecraft语言代码 134 | * @return Minecraft语言代码 135 | */ 136 | public String getMinecraftLanguageCode() { 137 | return LANGUAGE_TO_MINECRAFT.getOrDefault(currentLanguage, "en_gb"); 138 | } 139 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/MessageUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | 4 | /** 5 | * 消息工具类 6 | */ 7 | public final class MessageUtil { 8 | private MessageUtil() {} 9 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/MinecraftLangManager.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import io.github.syferie.magicblock.MagicBlockPlugin; 6 | import org.bukkit.Material; 7 | import org.bukkit.inventory.ItemStack; 8 | 9 | import java.io.*; 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class MinecraftLangManager { 15 | private final MagicBlockPlugin plugin; 16 | private String currentLanguage; 17 | private Map languageMap; 18 | private Map customTranslations; 19 | private final Gson gson = new Gson(); 20 | 21 | public MinecraftLangManager(MagicBlockPlugin plugin) { 22 | this.plugin = plugin; 23 | initializeLanguageFiles(); 24 | loadLanguage(); 25 | loadCustomTranslations(); 26 | } 27 | 28 | private void initializeLanguageFiles() { 29 | // 确保minecraftLanguage文件夹存在 30 | File langDir = new File(plugin.getDataFolder(), "minecraftLanguage"); 31 | if (!langDir.exists()) { 32 | langDir.mkdirs(); 33 | } 34 | 35 | // 从插件资源中复制语言文件到数据文件夹 36 | String[] supportedLanguages = {"en_gb", "zh_cn"}; 37 | for (String lang : supportedLanguages) { 38 | File langFile = new File(langDir, lang); 39 | if (!langFile.exists()) { 40 | copyLanguageFileFromResources(lang, langFile); 41 | } 42 | } 43 | } 44 | 45 | private void copyLanguageFileFromResources(String languageCode, File targetFile) { 46 | try { 47 | // 从插件JAR资源中读取语言文件 48 | String resourcePath = "minecraftLanguage/" + languageCode; 49 | InputStream resourceStream = plugin.getResource(resourcePath); 50 | 51 | if (resourceStream != null) { 52 | // 复制文件 53 | try (InputStream is = resourceStream; 54 | FileOutputStream fos = new FileOutputStream(targetFile)) { 55 | 56 | byte[] buffer = new byte[1024]; 57 | int length; 58 | while ((length = is.read(buffer)) > 0) { 59 | fos.write(buffer, 0, length); 60 | } 61 | } 62 | plugin.getLogger().info("成功从插件资源复制语言文件: " + languageCode); 63 | } else { 64 | plugin.getLogger().warning("找不到插件资源中的语言文件: " + resourcePath); 65 | // 尝试从外部文件系统读取(向后兼容) 66 | File sourceFile = new File("minecraftLanguage/" + languageCode); 67 | if (sourceFile.exists()) { 68 | try (FileInputStream fis = new FileInputStream(sourceFile); 69 | FileOutputStream fos = new FileOutputStream(targetFile)) { 70 | 71 | byte[] buffer = new byte[1024]; 72 | int length; 73 | while ((length = fis.read(buffer)) > 0) { 74 | fos.write(buffer, 0, length); 75 | } 76 | } 77 | plugin.getLogger().info("成功从外部文件复制语言文件: " + languageCode); 78 | } else { 79 | plugin.getLogger().warning("找不到语言文件: " + sourceFile.getAbsolutePath()); 80 | } 81 | } 82 | } catch (IOException e) { 83 | plugin.getLogger().severe("复制语言文件失败 " + languageCode + ": " + e.getMessage()); 84 | } 85 | } 86 | 87 | private void loadLanguage() { 88 | // 使用LanguageManager获取对应的Minecraft语言代码 89 | currentLanguage = plugin.getLanguageManager().getMinecraftLanguageCode(); 90 | 91 | // 加载语言文件 92 | File langFile = new File(plugin.getDataFolder(), "minecraftLanguage/" + currentLanguage); 93 | if (langFile.exists()) { 94 | loadLanguageFile(langFile); 95 | } else { 96 | plugin.getLogger().warning("语言文件不存在: " + langFile.getAbsolutePath() + ",使用默认英语"); 97 | // 尝试加载英语作为后备 98 | File fallbackFile = new File(plugin.getDataFolder(), "minecraftLanguage/en_gb"); 99 | if (fallbackFile.exists()) { 100 | loadLanguageFile(fallbackFile); 101 | } else { 102 | // 如果连英语文件都没有,创建一个空的映射 103 | languageMap = new HashMap<>(); 104 | plugin.getLogger().severe("无法加载任何语言文件!"); 105 | } 106 | } 107 | } 108 | 109 | private void loadLanguageFile(File langFile) { 110 | try (FileReader reader = new FileReader(langFile, StandardCharsets.UTF_8)) { 111 | JsonObject jsonObject = gson.fromJson(reader, JsonObject.class); 112 | languageMap = new HashMap<>(); 113 | 114 | // 将JSON对象转换为Map 115 | jsonObject.entrySet().forEach(entry -> 116 | languageMap.put(entry.getKey(), entry.getValue().getAsString()) 117 | ); 118 | 119 | plugin.getLogger().info("成功加载语言文件: " + langFile.getName() + " (包含 " + languageMap.size() + " 个条目)"); 120 | } catch (IOException e) { 121 | plugin.getLogger().severe("加载语言文件失败 " + langFile.getName() + ": " + e.getMessage()); 122 | languageMap = new HashMap<>(); 123 | } 124 | } 125 | 126 | private void loadCustomTranslations() { 127 | customTranslations = new HashMap<>(); 128 | 129 | // 从配置文件加载自定义翻译 130 | if (plugin.getConfig().contains("custom-block-translations")) { 131 | var customSection = plugin.getConfig().getConfigurationSection("custom-block-translations"); 132 | if (customSection != null) { 133 | for (String materialName : customSection.getKeys(false)) { 134 | String customName = customSection.getString(materialName); 135 | if (customName != null && !customName.trim().isEmpty()) { 136 | customTranslations.put(materialName.toUpperCase(), customName); 137 | plugin.getLogger().info("加载自定义翻译: " + materialName + " -> " + customName); 138 | } 139 | } 140 | } 141 | } 142 | 143 | plugin.getLogger().info("成功加载 " + customTranslations.size() + " 个自定义方块翻译"); 144 | } 145 | 146 | public String getItemStackName(ItemStack itemStack) { 147 | if (itemStack == null || languageMap == null) { 148 | return "Unknown Item"; 149 | } 150 | 151 | Material material = itemStack.getType(); 152 | String materialName = material.name(); 153 | 154 | // 1. 优先检查自定义翻译(最高优先级) 155 | if (customTranslations != null && customTranslations.containsKey(materialName)) { 156 | return customTranslations.get(materialName); 157 | } 158 | 159 | // 2. 尝试从语言文件获取方块翻译 160 | String key = "block.minecraft." + materialName.toLowerCase(); 161 | String name = languageMap.get(key); 162 | if (name != null) { 163 | return name; 164 | } 165 | 166 | // 3. 尝试从语言文件获取物品翻译 167 | key = "item.minecraft." + materialName.toLowerCase(); 168 | name = languageMap.get(key); 169 | if (name != null) { 170 | return name; 171 | } 172 | 173 | // 4. 如果都找不到,返回格式化的材料名称 174 | return formatMaterialName(materialName); 175 | } 176 | 177 | private String formatMaterialName(String materialName) { 178 | // 将下划线替换为空格,并将每个单词的首字母大写 179 | String[] words = materialName.toLowerCase().split("_"); 180 | StringBuilder result = new StringBuilder(); 181 | 182 | for (int i = 0; i < words.length; i++) { 183 | if (i > 0) { 184 | result.append(" "); 185 | } 186 | if (!words[i].isEmpty()) { 187 | result.append(Character.toUpperCase(words[i].charAt(0))); 188 | if (words[i].length() > 1) { 189 | result.append(words[i].substring(1)); 190 | } 191 | } 192 | } 193 | 194 | return result.toString(); 195 | } 196 | 197 | public String getCurrentLanguage() { 198 | return currentLanguage; 199 | } 200 | 201 | public int getLoadedTranslationsCount() { 202 | return languageMap != null ? languageMap.size() : 0; 203 | } 204 | 205 | public int getCustomTranslationsCount() { 206 | return customTranslations != null ? customTranslations.size() : 0; 207 | } 208 | 209 | public Map getCustomTranslations() { 210 | return customTranslations != null ? new HashMap<>(customTranslations) : new HashMap<>(); 211 | } 212 | 213 | public boolean hasCustomTranslation(String materialName) { 214 | return customTranslations != null && customTranslations.containsKey(materialName.toUpperCase()); 215 | } 216 | 217 | public String getCustomTranslation(String materialName) { 218 | return customTranslations != null ? customTranslations.get(materialName.toUpperCase()) : null; 219 | } 220 | 221 | /** 222 | * 重新加载自定义翻译(用于配置重载时) 223 | */ 224 | public void reloadCustomTranslations() { 225 | loadCustomTranslations(); 226 | } 227 | 228 | /** 229 | * 重新加载所有翻译数据(用于配置重载时) 230 | */ 231 | public void reload() { 232 | loadLanguage(); 233 | loadCustomTranslations(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/PerformanceMonitor.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.command.CommandSender; 5 | 6 | import java.util.concurrent.atomic.AtomicLong; 7 | import java.util.Map; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * 性能监控工具类 12 | * 用于跟踪和报告插件的性能指标 13 | */ 14 | public class PerformanceMonitor { 15 | private final MagicBlockPlugin plugin; 16 | 17 | // 性能计数器 18 | private final AtomicLong loreUpdates = new AtomicLong(0); 19 | private final AtomicLong loreCacheHits = new AtomicLong(0); 20 | private final AtomicLong loreCacheMisses = new AtomicLong(0); 21 | private final AtomicLong databaseOperations = new AtomicLong(0); 22 | private final AtomicLong asyncOperations = new AtomicLong(0); 23 | private final AtomicInteger activeTasks = new AtomicInteger(0); 24 | 25 | // 新增:位置检查性能计数器 26 | private final AtomicLong locationChecks = new AtomicLong(0); 27 | private final AtomicLong locationCacheHits = new AtomicLong(0); 28 | private final AtomicLong locationCacheMisses = new AtomicLong(0); 29 | private final AtomicLong physicsEvents = new AtomicLong(0); 30 | private final AtomicLong physicsEventsSkipped = new AtomicLong(0); 31 | 32 | // 时间统计 33 | private final AtomicLong totalLoreUpdateTime = new AtomicLong(0); 34 | private final AtomicLong totalDatabaseTime = new AtomicLong(0); 35 | private final AtomicLong totalLocationCheckTime = new AtomicLong(0); 36 | 37 | private final long startTime; 38 | 39 | public PerformanceMonitor(MagicBlockPlugin plugin) { 40 | this.plugin = plugin; 41 | this.startTime = System.currentTimeMillis(); 42 | } 43 | 44 | // 记录 Lore 更新 45 | public void recordLoreUpdate(long duration) { 46 | loreUpdates.incrementAndGet(); 47 | totalLoreUpdateTime.addAndGet(duration); 48 | } 49 | 50 | // 记录缓存命中 51 | public void recordCacheHit() { 52 | loreCacheHits.incrementAndGet(); 53 | } 54 | 55 | // 记录缓存未命中 56 | public void recordCacheMiss() { 57 | loreCacheMisses.incrementAndGet(); 58 | } 59 | 60 | // 记录数据库操作 61 | public void recordDatabaseOperation(long duration) { 62 | databaseOperations.incrementAndGet(); 63 | totalDatabaseTime.addAndGet(duration); 64 | } 65 | 66 | // 记录异步操作 67 | public void recordAsyncOperation() { 68 | asyncOperations.incrementAndGet(); 69 | } 70 | 71 | // 增加活跃任务计数 72 | public void incrementActiveTasks() { 73 | activeTasks.incrementAndGet(); 74 | } 75 | 76 | // 减少活跃任务计数 77 | public void decrementActiveTasks() { 78 | activeTasks.decrementAndGet(); 79 | } 80 | 81 | // 新增:位置检查性能监控方法 82 | public void recordLocationCheck(long duration) { 83 | locationChecks.incrementAndGet(); 84 | totalLocationCheckTime.addAndGet(duration); 85 | } 86 | 87 | public void recordLocationCacheHit() { 88 | locationCacheHits.incrementAndGet(); 89 | } 90 | 91 | public void recordLocationCacheMiss() { 92 | locationCacheMisses.incrementAndGet(); 93 | } 94 | 95 | public void recordPhysicsEvent() { 96 | physicsEvents.incrementAndGet(); 97 | } 98 | 99 | public void recordPhysicsEventSkipped() { 100 | physicsEventsSkipped.incrementAndGet(); 101 | } 102 | 103 | // 获取性能报告 104 | public void sendPerformanceReport(CommandSender sender) { 105 | long uptime = System.currentTimeMillis() - startTime; 106 | long uptimeSeconds = uptime / 1000; 107 | long uptimeMinutes = uptimeSeconds / 60; 108 | long uptimeHours = uptimeMinutes / 60; 109 | 110 | sender.sendMessage("§6=== MagicBlock 性能报告 ==="); 111 | sender.sendMessage("§7运行时间: §a" + uptimeHours + "h " + (uptimeMinutes % 60) + "m " + (uptimeSeconds % 60) + "s"); 112 | sender.sendMessage(""); 113 | 114 | // Lore 性能统计 115 | long totalLoreOps = loreUpdates.get(); 116 | long cacheHits = loreCacheHits.get(); 117 | long cacheMisses = loreCacheMisses.get(); 118 | double cacheHitRate = totalLoreOps > 0 ? (double) cacheHits / (cacheHits + cacheMisses) * 100 : 0; 119 | double avgLoreTime = totalLoreOps > 0 ? (double) totalLoreUpdateTime.get() / totalLoreOps : 0; 120 | 121 | sender.sendMessage("§6Lore 系统:"); 122 | sender.sendMessage("§7 总更新次数: §a" + totalLoreOps); 123 | sender.sendMessage("§7 缓存命中率: §a" + String.format("%.1f%%", cacheHitRate)); 124 | sender.sendMessage("§7 平均更新时间: §a" + String.format("%.2fms", avgLoreTime)); 125 | sender.sendMessage(""); 126 | 127 | // 🚀 魔法方块索引系统性能统计 128 | Map indexStats = plugin.getIndexManager().getPerformanceStats(); 129 | 130 | sender.sendMessage("§6🚀 魔法方块索引系统:"); 131 | sender.sendMessage("§7 总魔法方块数: §a" + indexStats.get("totalMagicBlocks")); 132 | sender.sendMessage("§7 活跃区块数: §a" + indexStats.get("totalChunks")); 133 | sender.sendMessage("§7 活跃世界数: §a" + indexStats.get("totalWorlds")); 134 | sender.sendMessage("§7 总查找次数: §a" + indexStats.get("totalLookups")); 135 | sender.sendMessage("§7 索引命中率: §a" + String.format("%.1f%%", (Double) indexStats.get("cacheHitRate"))); 136 | sender.sendMessage(""); 137 | 138 | // 位置检查性能统计(旧系统,已弃用) 139 | long totalLocationOps = locationChecks.get(); 140 | double locCacheHitRate = 0; 141 | double avgLocationTime = 0; 142 | 143 | if (totalLocationOps > 0) { 144 | long locCacheHits = locationCacheHits.get(); 145 | long locCacheMisses = locationCacheMisses.get(); 146 | locCacheHitRate = totalLocationOps > 0 ? (double) locCacheHits / (locCacheHits + locCacheMisses) * 100 : 0; 147 | avgLocationTime = totalLocationOps > 0 ? (double) totalLocationCheckTime.get() / totalLocationOps : 0; 148 | 149 | sender.sendMessage("§6位置检查系统 (旧):"); 150 | sender.sendMessage("§7 总检查次数: §a" + totalLocationOps); 151 | sender.sendMessage("§7 缓存命中率: §a" + String.format("%.1f%%", locCacheHitRate)); 152 | sender.sendMessage("§7 平均检查时间: §a" + String.format("%.2fms", avgLocationTime)); 153 | sender.sendMessage(""); 154 | } 155 | 156 | // 物理事件统计 157 | long totalPhysicsEvents = physicsEvents.get(); 158 | long skippedPhysicsEvents = physicsEventsSkipped.get(); 159 | double physicsSkipRate = totalPhysicsEvents > 0 ? (double) skippedPhysicsEvents / totalPhysicsEvents * 100 : 0; 160 | 161 | sender.sendMessage("§6物理事件优化:"); 162 | sender.sendMessage("§7 总物理事件: §a" + totalPhysicsEvents); 163 | sender.sendMessage("§7 跳过事件数: §a" + skippedPhysicsEvents); 164 | sender.sendMessage("§7 优化跳过率: §a" + String.format("%.1f%%", physicsSkipRate)); 165 | sender.sendMessage(""); 166 | 167 | // 数据库性能统计 168 | long dbOps = databaseOperations.get(); 169 | double avgDbTime = dbOps > 0 ? (double) totalDatabaseTime.get() / dbOps : 0; 170 | 171 | sender.sendMessage("§6数据库系统:"); 172 | sender.sendMessage("§7 总操作次数: §a" + dbOps); 173 | sender.sendMessage("§7 平均操作时间: §a" + String.format("%.2fms", avgDbTime)); 174 | sender.sendMessage("§7 异步操作次数: §a" + asyncOperations.get()); 175 | sender.sendMessage(""); 176 | 177 | // 任务调度统计 178 | sender.sendMessage("§6任务调度:"); 179 | sender.sendMessage("§7 当前活跃任务: §a" + activeTasks.get()); 180 | sender.sendMessage(""); 181 | 182 | // 性能建议 183 | sender.sendMessage("§6性能建议:"); 184 | boolean hasIssues = false; 185 | 186 | if (cacheHitRate < 50 && totalLoreOps > 100) { 187 | sender.sendMessage("§c 建议增加 Lore 缓存时间以提高命中率"); 188 | hasIssues = true; 189 | } 190 | if (locCacheHitRate < 70 && totalLocationOps > 100) { 191 | sender.sendMessage("§c 建议增加位置缓存时间以提高位置检查性能"); 192 | hasIssues = true; 193 | } 194 | if (physicsSkipRate < 30 && totalPhysicsEvents > 100) { 195 | sender.sendMessage("§c 建议启用物理事件优化以减少不必要的检查"); 196 | hasIssues = true; 197 | } 198 | if (avgDbTime > 50 && dbOps > 10) { 199 | sender.sendMessage("§c 数据库操作较慢,建议检查数据库连接"); 200 | hasIssues = true; 201 | } 202 | if (activeTasks.get() > 20) { 203 | sender.sendMessage("§c 活跃任务过多,可能存在性能问题"); 204 | hasIssues = true; 205 | } 206 | 207 | // 正面反馈 208 | if (!hasIssues) { 209 | sender.sendMessage("§a 所有系统性能良好!"); 210 | } else { 211 | if (cacheHitRate > 80 && avgLoreTime < 1.0) { 212 | sender.sendMessage("§a Lore 系统性能良好!"); 213 | } 214 | if (locCacheHitRate > 80 && avgLocationTime < 0.5) { 215 | sender.sendMessage("§a 位置检查系统性能良好!"); 216 | } 217 | if (physicsSkipRate > 50) { 218 | sender.sendMessage("§a 物理事件优化效果显著!"); 219 | } 220 | } 221 | 222 | sender.sendMessage("§6========================"); 223 | } 224 | 225 | // 重置统计数据 226 | public void resetStats() { 227 | loreUpdates.set(0); 228 | loreCacheHits.set(0); 229 | loreCacheMisses.set(0); 230 | databaseOperations.set(0); 231 | asyncOperations.set(0); 232 | activeTasks.set(0); 233 | totalLoreUpdateTime.set(0); 234 | totalDatabaseTime.set(0); 235 | 236 | // 重置新增的统计数据 237 | locationChecks.set(0); 238 | locationCacheHits.set(0); 239 | locationCacheMisses.set(0); 240 | physicsEvents.set(0); 241 | physicsEventsSkipped.set(0); 242 | totalLocationCheckTime.set(0); 243 | } 244 | 245 | // 获取缓存命中率 246 | public double getCacheHitRate() { 247 | long hits = loreCacheHits.get(); 248 | long misses = loreCacheMisses.get(); 249 | long total = hits + misses; 250 | return total > 0 ? (double) hits / total * 100 : 0; 251 | } 252 | 253 | // 获取平均 Lore 更新时间 254 | public double getAverageLoreUpdateTime() { 255 | long updates = loreUpdates.get(); 256 | return updates > 0 ? (double) totalLoreUpdateTime.get() / updates : 0; 257 | } 258 | 259 | // 获取平均数据库操作时间 260 | public double getAverageDatabaseTime() { 261 | long ops = databaseOperations.get(); 262 | return ops > 0 ? (double) totalDatabaseTime.get() / ops : 0; 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/Statistics.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | import org.bukkit.configuration.file.FileConfiguration; 5 | import org.bukkit.configuration.file.YamlConfiguration; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.inventory.ItemStack; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.UUID; 13 | 14 | public class Statistics { 15 | private final MagicBlockPlugin plugin; 16 | private final File statsFile; 17 | private FileConfiguration stats; 18 | private final ConcurrentHashMap blockUses = new ConcurrentHashMap<>(); 19 | private final ConcurrentHashMap foodUses = new ConcurrentHashMap<>(); 20 | 21 | // 性能优化:批量保存和缓存 22 | private volatile boolean needsSave = false; 23 | private long lastSaveTime = 0; 24 | 25 | public Statistics(MagicBlockPlugin plugin) { 26 | this.plugin = plugin; 27 | this.statsFile = new File(plugin.getDataFolder(), "stats.yml"); 28 | loadStats(); 29 | } 30 | 31 | private void loadStats() { 32 | if (!statsFile.exists()) { 33 | try { 34 | statsFile.createNewFile(); 35 | } catch (IOException e) { 36 | plugin.getLogger().warning("无法创建统计文件: " + e.getMessage()); 37 | return; 38 | } 39 | } 40 | stats = YamlConfiguration.loadConfiguration(statsFile); 41 | } 42 | 43 | public void saveStats() { 44 | if (stats == null) return; 45 | 46 | try { 47 | stats.save(statsFile); 48 | } catch (IOException e) { 49 | plugin.getLogger().warning("无法保存统计数据: " + e.getMessage()); 50 | } 51 | } 52 | 53 | public void logBlockUse(Player player, ItemStack block) { 54 | UUID playerUUID = player.getUniqueId(); 55 | String path = "blocks." + playerUUID; 56 | int uses = stats.getInt(path, 0) + 1; 57 | stats.set(path, uses); 58 | blockUses.put(playerUUID, uses); 59 | 60 | // 性能优化:智能保存策略 61 | scheduleSmartSave(uses); 62 | } 63 | 64 | public void logFoodUse(Player player, ItemStack food) { 65 | UUID playerUUID = player.getUniqueId(); 66 | String path = "foods." + playerUUID; 67 | int uses = stats.getInt(path, 0) + 1; 68 | stats.set(path, uses); 69 | foodUses.put(playerUUID, uses); 70 | 71 | // 性能优化:智能保存策略 72 | scheduleSmartSave(uses); 73 | } 74 | 75 | // 性能优化:智能保存策略 76 | private void scheduleSmartSave(int totalUses) { 77 | needsSave = true; 78 | long currentTime = System.currentTimeMillis(); 79 | 80 | // 从配置读取性能设置 81 | int batchThreshold = plugin.getConfig().getInt("performance.statistics.batch-threshold", 50); 82 | long saveInterval = plugin.getConfig().getLong("performance.statistics.save-interval", 30000); 83 | 84 | // 条件1:达到批量保存阈值 85 | // 条件2:距离上次保存超过指定时间间隔 86 | if (totalUses % batchThreshold == 0 || 87 | (currentTime - lastSaveTime) > saveInterval) { 88 | 89 | plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { 90 | if (needsSave) { 91 | saveStats(); 92 | needsSave = false; 93 | lastSaveTime = System.currentTimeMillis(); 94 | } 95 | }); 96 | } 97 | } 98 | 99 | public int getBlockUses(UUID playerUUID) { 100 | return blockUses.getOrDefault(playerUUID, stats.getInt("blocks." + playerUUID, 0)); 101 | } 102 | 103 | public int getFoodUses(UUID playerUUID) { 104 | return foodUses.getOrDefault(playerUUID, stats.getInt("foods." + playerUUID, 0)); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/io/github/syferie/magicblock/util/UpdateChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.syferie.magicblock.util; 2 | 3 | import io.github.syferie.magicblock.MagicBlockPlugin; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.URL; 8 | import java.util.Scanner; 9 | 10 | public class UpdateChecker { 11 | private final MagicBlockPlugin plugin; 12 | private final int resourceId; 13 | 14 | public UpdateChecker(MagicBlockPlugin plugin, int resourceId) { 15 | this.plugin = plugin; 16 | this.resourceId = resourceId; 17 | } 18 | 19 | public void checkForUpdates() { 20 | plugin.getFoliaLib().getImpl().runAsync(task -> { 21 | try (InputStream inputStream = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId).openStream(); 22 | Scanner scanner = new Scanner(inputStream)) { 23 | if (scanner.hasNext()) { 24 | String latestVersion = scanner.next(); 25 | String currentVersion = plugin.getDescription().getVersion(); 26 | 27 | // 智能版本比较 28 | int comparison = compareVersions(currentVersion, latestVersion); 29 | if (comparison < 0) { 30 | // 当前版本较旧,有新版本可用 31 | plugin.getLogger().info(plugin.getMessage("general.update-found", latestVersion)); 32 | plugin.getLogger().info(plugin.getMessage("general.current-version", currentVersion)); 33 | plugin.getLogger().info(plugin.getMessage("general.download-link", resourceId)); 34 | } else if (comparison > 0) { 35 | // 当前版本较新(开发版本) 36 | plugin.getLogger().info(plugin.getMessage("general.dev-version", currentVersion, latestVersion)); 37 | } else { 38 | // 版本相同 39 | plugin.getLogger().info(plugin.getMessage("general.up-to-date")); 40 | } 41 | } 42 | } catch (IOException e) { 43 | plugin.getLogger().warning(plugin.getMessage("general.update-check-failed", e.getMessage())); 44 | if (plugin.getConfig().getBoolean("debug-mode")) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * 智能版本比较方法 53 | * 支持语义化版本号格式:major.minor.patch.build 54 | * 55 | * @param version1 当前版本 56 | * @param version2 远程版本 57 | * @return 负数:version1 < version2,0:相等,正数:version1 > version2 58 | */ 59 | protected int compareVersions(String version1, String version2) { 60 | if (version1 == null || version2 == null) { 61 | return version1 == null ? (version2 == null ? 0 : -1) : 1; 62 | } 63 | 64 | // 移除可能的前缀(如 "v") 65 | version1 = version1.replaceFirst("^v", ""); 66 | version2 = version2.replaceFirst("^v", ""); 67 | 68 | // 分割版本号 69 | String[] parts1 = version1.split("\\."); 70 | String[] parts2 = version2.split("\\."); 71 | 72 | // 获取最大长度,用于比较 73 | int maxLength = Math.max(parts1.length, parts2.length); 74 | 75 | for (int i = 0; i < maxLength; i++) { 76 | // 获取版本号的各个部分,如果不存在则默认为0 77 | int part1 = i < parts1.length ? parseVersionPart(parts1[i]) : 0; 78 | int part2 = i < parts2.length ? parseVersionPart(parts2[i]) : 0; 79 | 80 | if (part1 < part2) { 81 | return -1; 82 | } else if (part1 > part2) { 83 | return 1; 84 | } 85 | } 86 | 87 | return 0; // 版本相同 88 | } 89 | 90 | /** 91 | * 解析版本号的单个部分 92 | * 处理纯数字和包含字母的版本号(如 "1.0.0-SNAPSHOT") 93 | */ 94 | protected int parseVersionPart(String part) { 95 | if (part == null || part.isEmpty()) { 96 | return 0; 97 | } 98 | 99 | try { 100 | // 尝试解析为纯数字 101 | return Integer.parseInt(part); 102 | } catch (NumberFormatException e) { 103 | // 如果包含非数字字符,提取数字部分 104 | StringBuilder numPart = new StringBuilder(); 105 | for (char c : part.toCharArray()) { 106 | if (Character.isDigit(c)) { 107 | numPart.append(c); 108 | } else { 109 | break; // 遇到非数字字符就停止 110 | } 111 | } 112 | 113 | if (numPart.length() > 0) { 114 | return Integer.parseInt(numPart.toString()); 115 | } else { 116 | return 0; 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # ============================================================== 2 | # MagicBlock Plugin Configuration 3 | # ============================================================== 4 | # Plugin Author: Syferie 5 | # QQ Group: 134484522 6 | # Telegram Group: t.me/+ctO2LArww4NkNmI9 7 | # ============================================================== 8 | 9 | # General Settings 10 | # ------------------------------------------------------------- 11 | 12 | # Debug mode: Enables verbose logging for troubleshooting. 13 | debug-mode: false 14 | 15 | # Language setting for plugin messages and block names. 16 | # Available languages: 17 | # - en (English, will use en_gb for block names) 18 | # - zh_CN (Simplified Chinese, will use zh_cn for block names) 19 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 20 | language: "en" 21 | 22 | # Custom Block Translations 23 | # ------------------------------------------------------------- 24 | # Override or add custom translations for specific blocks. 25 | # These translations will be used regardless of the language setting. 26 | # Format: MATERIAL_NAME: "Custom Display Name" 27 | # 28 | # Examples: 29 | # - Override existing translations: GRASS_BLOCK: "草草方块" 30 | # - Add missing translations: HEAVY_CORE: "重型核心" 31 | # - Support new version blocks: CHERRY_BLOSSOM_BLOCK: "樱花方块" 32 | # 33 | # Note: Use the exact Material name from Minecraft (case-sensitive) 34 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 35 | custom-block-translations: 36 | # Example entries (remove # to enable): 37 | # HEAVY_CORE: "重型核心" 38 | # TRIAL_SPAWNER: "试炼刷怪笼" 39 | # COPPER_GRATE: "铜格栅" 40 | # GRASS_BLOCK: "草草方块" 41 | 42 | # Message prefix displayed before plugin messages. 43 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 44 | prefix: "§7[MagicBlock] " 45 | 46 | # Enable plugin usage statistics. 47 | # ✅ Hot-reloadable: Can be enabled/disabled with /mb reload 48 | enable-statistics: true 49 | 50 | # Automatically check for plugin updates on server start. 51 | # ⚠️ Requires restart: Only checked during plugin startup 52 | check-updates: true 53 | 54 | # Blacklisted worlds where Magic Blocks cannot be placed or used. 55 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 56 | blacklisted-worlds: 57 | - world_nether 58 | - world_the_end 59 | 60 | # Display Settings 61 | # ------------------------------------------------------------- 62 | 63 | # Format for displaying the name of a Magic Block in item form. 64 | # %s will be replaced with the block's material name. 65 | # ✅ Hot-reloadable: Changes take effect for newly created blocks with /mb reload 66 | display: 67 | block-name-format: "&b✦ %s &b✦" 68 | # Control which information lines are shown in the lore 69 | show-info: 70 | bound-player: true # Show "Bound to: PlayerName" 71 | usage-count: true # Show "Uses: X/Y" 72 | progress-bar: true # Show usage progress bar 73 | decorative-lore: 74 | # Enable decorative lore lines below the Magic Block's identifier. 75 | enabled: true 76 | # Decorative lore lines will be displayed between magic-lore and usage information 77 | # Supports color codes (&) and PlaceholderAPI variables if installed. 78 | # 79 | # Available MagicBlock Variables: 80 | # %magicblock_block_uses% - Total block uses by player 81 | # %magicblock_remaining_uses% - Current block's remaining uses 82 | # %magicblock_max_uses% - Current block's maximum uses 83 | # %magicblock_uses_progress% - Usage progress (percentage) 84 | # %magicblock_progress_bar% - Progress bar with default length (20 chars) 85 | # %magicblock_progressbar_10% - Progress bar with custom length (10 chars) 86 | # %magicblock_progressbar_15% - Progress bar with custom length (15 chars) 87 | # 88 | # Common Variables: 89 | # %player_name% - Bound player's name 90 | # %server_online% - Online players count 91 | # 92 | # Example with variables (remove # to use): 93 | # - "&7Bound to: %player_name%" 94 | # - "&7Total Uses: %magicblock_block_uses%" 95 | # - "&7Progress: %magicblock_uses_progress%%" 96 | # - "&7Progress Bar: %magicblock_progress_bar%" 97 | # - "&7Custom Bar: %magicblock_progressbar_15%" 98 | lines: 99 | - "&7This is a magical block." 100 | - "&7It is imbued with the power of magic." 101 | - "&7Use it to change the world." 102 | 103 | # GUI Settings 104 | # ------------------------------------------------------------- 105 | 106 | # Number of rows for the Magic Block selection GUI. 107 | # ✅ Hot-reloadable: Changes take effect for newly opened GUIs with /mb reload 108 | gui: 109 | rows: 6 110 | 111 | # Magic Block Properties 112 | # ------------------------------------------------------------- 113 | 114 | # Custom lore to identify Magic Blocks. Use § for color codes, not &. 115 | # Important: This lore must be unique and not commonly found on other items. 116 | # ⚠️ Requires restart: Changing this affects existing blocks recognition 117 | magic-lore: "§7MagicBlock" 118 | 119 | # Prefix text for displaying the remaining usage times of a Magic Block. 120 | # ✅ Hot-reloadable: Changes take effect for newly created blocks with /mb reload 121 | usage-lore-prefix: "§7Uses:" 122 | 123 | # Default usage times for Magic Blocks obtained through the /mb get command. 124 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 125 | default-block-times: 1000000000 126 | 127 | # Whether to enable the block binding system 128 | # If set to true, players can bind blocks to themselves and retrieve them later 129 | # If set to false, the binding system will be disabled and blocks won't be bound to players 130 | # ⚠️ Requires restart: Core system feature that affects plugin initialization 131 | enable-binding-system: true 132 | 133 | # Whether to allow other players to use blocks that are bound to another player 134 | # If set to true, players can use blocks even if they are bound to someone else 135 | # This only affects block usage, not the binding system or UI 136 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 137 | allow-use-bound-blocks: false 138 | 139 | # Whether to remove the magic block when its uses are depleted 140 | # If set to true, the block will be removed from the player's inventory and bound list when uses reach 0 141 | # If set to false, the block will remain but cannot be used (default behavior) 142 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 143 | remove-depleted-blocks: false 144 | 145 | # Performance Settings 146 | # ------------------------------------------------------------- 147 | # These settings help optimize plugin performance for high-traffic servers 148 | # ✅ Hot-reloadable: Most performance settings take effect immediately with /mb reload 149 | 150 | performance: 151 | # Lore caching settings 152 | lore-cache: 153 | # Enable lore caching to reduce string operations (recommended: true) 154 | enabled: true 155 | # Cache duration in milliseconds (default: 5000 = 5 seconds) 156 | duration: 5000 157 | # Maximum cache size before cleanup (default: 1000) 158 | max-size: 1000 159 | 160 | # Location caching settings for magic block detection 161 | location-cache: 162 | # Enable location caching to improve magic block detection performance (recommended: true) 163 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 164 | enabled: true 165 | # Cache duration in milliseconds (default: 5000 = 5 seconds) 166 | duration: 5000 167 | # Cache cleanup interval in seconds (default: 30) 168 | cleanup-interval: 30 169 | 170 | # Block physics optimization 171 | physics-optimization: 172 | # Enable smart physics event filtering to reduce unnecessary checks (recommended: true) 173 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 174 | enabled: true 175 | # Skip physics checks for blocks that are unlikely to be affected (recommended: true) 176 | skip-unaffected-blocks: true 177 | 178 | # Statistics saving settings 179 | statistics: 180 | # Batch save threshold - save after this many operations (default: 50) 181 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 182 | batch-threshold: 50 183 | # Auto-save interval in milliseconds (default: 30000 = 30 seconds) 184 | save-interval: 30000 185 | 186 | # Database optimization 187 | database-optimization: 188 | # Use async database operations to prevent main thread blocking (recommended: true) 189 | # ✅ Hot-reloadable: Changes take effect for new operations with /mb reload 190 | async-operations: true 191 | # Batch database updates to reduce I/O operations (recommended: true) 192 | batch-updates: true 193 | 194 | # Database Settings 195 | # ------------------------------------------------------------- 196 | # Settings for MySQL database connection for cross-server data storage 197 | # ⚠️ Requires restart: Database connections are established during plugin startup 198 | database: 199 | # Whether to use MySQL for data storage (if false, will use file-based storage) 200 | enabled: false 201 | # MySQL connection settings 202 | host: localhost 203 | port: 3306 204 | database: magicblock 205 | username: root 206 | password: password 207 | # Table prefix for all plugin tables 208 | table-prefix: mb_ 209 | 210 | # Allowed Materials 211 | # ------------------------------------------------------------- 212 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 213 | # CAUTION: Please be cautious when adding the following types of blocks to the ALLOWED blocks list! 214 | # 215 | # Due to Minecraft's game mechanics, some blocks may unexpectedly drop items under certain circumstances (e.g., water flow, destruction of the block they are attached to), even if they are on the allowed list. 216 | # 217 | # Therefore, it is strongly recommended that you EXERCISE CAUTION when adding the following types of blocks: 218 | # - Attached Blocks: Blocks that require attachment to another block to exist. 219 | # - Environmentally Sensitive Blocks: Blocks that are easily dropped due to environmental factors such as water flow, etc. 220 | # 221 | # Typical examples include, but are not limited to: 222 | # - Redstone Dust (redstone_wire) 223 | # - Redstone Torch (redstone_torch) 224 | # - Torch (torch) 225 | # - Various Flowers (e.g., poppy, dandelion, blue_orchid, etc.) 226 | # - Various Saplings (e.g., oak_sapling, spruce_sapling, birch_sapling, etc.) 227 | # - Various Buttons (e.g., oak_button, stone_button, etc.) 228 | # - Lever (lever) 229 | # - Pressure Plates (e.g., oak_pressure_plate, stone_pressure_plate, etc.) 230 | # - Other similar susceptible blocks 231 | 232 | 233 | # Materials that will appear in the GUI selection menu for all players. 234 | allowed-materials: 235 | # Natural Blocks 236 | - STONE 237 | - GRASS_BLOCK 238 | - DIRT 239 | - COBBLESTONE 240 | - SAND 241 | - GRAVEL 242 | - GOLD_ORE 243 | - IRON_ORE 244 | - COAL_ORE 245 | 246 | # Wood Types 247 | - OAK_WOOD 248 | - SPRUCE_WOOD 249 | - BIRCH_WOOD 250 | - JUNGLE_WOOD 251 | - ACACIA_WOOD 252 | - DARK_OAK_WOOD 253 | - OAK_LOG 254 | - SPRUCE_LOG 255 | - BIRCH_LOG 256 | - JUNGLE_LOG 257 | - ACACIA_LOG 258 | - DARK_OAK_LOG 259 | 260 | # Mineral Blocks 261 | - GOLD_BLOCK 262 | - IRON_BLOCK 263 | - DIAMOND_BLOCK 264 | - EMERALD_BLOCK 265 | - LAPIS_BLOCK 266 | - REDSTONE_BLOCK 267 | 268 | # Stone Types 269 | - GRANITE 270 | - DIORITE 271 | - ANDESITE 272 | - STONE_BRICKS 273 | - MOSSY_STONE_BRICKS 274 | - CRACKED_STONE_BRICKS 275 | 276 | # Decorative Blocks 277 | - GLASS 278 | - BOOKSHELF 279 | - OBSIDIAN 280 | - GLOWSTONE 281 | - SEA_LANTERN 282 | - CLAY 283 | - TERRACOTTA 284 | 285 | # Nether Blocks 286 | - NETHERRACK 287 | - SOUL_SAND 288 | - NETHER_BRICKS 289 | - MAGMA_BLOCK 290 | 291 | # End Blocks 292 | - END_STONE 293 | - PURPUR_BLOCK 294 | - END_STONE_BRICKS 295 | 296 | # Concrete Colors 297 | - WHITE_CONCRETE 298 | - ORANGE_CONCRETE 299 | - MAGENTA_CONCRETE 300 | - LIGHT_BLUE_CONCRETE 301 | - YELLOW_CONCRETE 302 | - LIME_CONCRETE 303 | - PINK_CONCRETE 304 | - GRAY_CONCRETE 305 | - LIGHT_GRAY_CONCRETE 306 | - CYAN_CONCRETE 307 | - PURPLE_CONCRETE 308 | - BLUE_CONCRETE 309 | - BROWN_CONCRETE 310 | - GREEN_CONCRETE 311 | - RED_CONCRETE 312 | - BLACK_CONCRETE 313 | 314 | # Group Permissions for Additional Materials 315 | # ------------------------------------------------------------- 316 | 317 | # Group-specific materials that will appear in the GUI for players with the corresponding permission. 318 | # Define permission nodes as follows: magicblock.group. (e.g., magicblock.group.vip-material) 319 | # ✅ Hot-reloadable: Changes take effect immediately with /mb reload 320 | group: 321 | # VIP group materials 322 | vip-material: 323 | # Example: Adds Diamond, Emerald, and Gold Blocks for players with the 'magicblock.group.vip-material' permission. 324 | - DIAMOND_BLOCK 325 | - EMERALD_BLOCK 326 | - GOLD_BLOCK 327 | 328 | # MVP group materials 329 | mvp-material: 330 | # Example: Adds Beacon, Dragon Egg, and Netherite Block for players with the 'magicblock.group.mvp-material' permission. 331 | - BEACON 332 | - DRAGON_EGG 333 | - NETHERITE_BLOCK 334 | -------------------------------------------------------------------------------- /src/main/resources/foodconf.yml: -------------------------------------------------------------------------------- 1 | # MagicBlock Food Configuration 2 | # This file is used to configure magic food settings 3 | # 4 | # IMPORTANT NOTICE: 5 | # Changes to decorative lore settings in this file will only take effect after a server restart. 6 | # Using /mb reload will not update the lore of existing or newly created food items. 7 | # This is a known limitation of the current implementation. 8 | 9 | # Special identifier for magic food items 10 | special-lore: "§7MagicFood" 11 | 12 | # Display Settings 13 | # ------------------------------------------------------------- 14 | display: 15 | # Format for displaying the name of magic food in item form 16 | # %s will be replaced with the food's name 17 | food-name-format: "&b✦ %s &b✦" 18 | 19 | # Control which information lines are shown in the lore 20 | show-info: 21 | usage-count: true # Show "Uses: X/Y" 22 | progress-bar: true # Show usage progress bar 23 | 24 | # Decorative lore lines will be displayed between magic-lore and usage information 25 | # Supports color codes (&) and PlaceholderAPI variables if installed 26 | # 27 | # Available MagicFood Variables: 28 | # %magicfood_food_level% - Food's hunger restoration 29 | # %magicfood_saturation% - Food's saturation value 30 | # %magicfood_heal% - Food's healing amount 31 | # %magicfood_food_uses% - Total food uses by player 32 | # %magicfood_remaining_uses% - Current food's remaining uses 33 | # %magicfood_max_uses% - Current food's maximum uses 34 | # %magicfood_uses_progress% - Usage progress (percentage) 35 | decorative-lore: 36 | enabled: true 37 | lines: 38 | - "&7This is a magical food item" 39 | - "&7Imbued with mystical properties" 40 | - "&7Food Level: &b%magicfood_food_level%" 41 | - "&7Saturation: &b%magicfood_saturation%" 42 | - "&7Healing: &b%magicfood_heal%" 43 | 44 | # Default number of uses for magic food 45 | default-food-times: 64 46 | 47 | # Whether to allow players to use magic food when their hunger bar is full 48 | allow-use-when-full: true 49 | 50 | # Sound effects when using magic food 51 | sound: 52 | enabled: true 53 | eat: ENTITY_PLAYER_BURP 54 | volume: 1.0 55 | pitch: 1.0 56 | 57 | # Particle effects when using magic food 58 | particles: 59 | enabled: true 60 | type: HEART 61 | count: 5 62 | spread: 63 | x: 0.5 64 | y: 0.5 65 | z: 0.5 66 | 67 | # Food items and their effects 68 | foods: 69 | GOLDEN_APPLE: 70 | food-level: 4 71 | saturation: 9.6 72 | heal: 4.0 73 | effects: 74 | REGENERATION: 75 | duration: 100 # in ticks (20 ticks = 1 second) 76 | amplifier: 1 # effect level - 1 77 | ABSORPTION: 78 | duration: 2400 79 | amplifier: 0 80 | 81 | ENCHANTED_GOLDEN_APPLE: 82 | food-level: 4 83 | saturation: 9.6 84 | heal: 4.0 85 | effects: 86 | REGENERATION: 87 | duration: 400 88 | amplifier: 1 89 | ABSORPTION: 90 | duration: 2400 91 | amplifier: 3 92 | RESISTANCE: 93 | duration: 6000 94 | amplifier: 0 95 | FIRE_RESISTANCE: 96 | duration: 6000 97 | amplifier: 0 98 | 99 | COOKED_BEEF: 100 | food-level: 8 101 | saturation: 12.8 102 | heal: 2.0 103 | 104 | GOLDEN_CARROT: 105 | food-level: 6 106 | saturation: 14.4 107 | heal: 3.0 108 | 109 | BREAD: 110 | food-level: 5 111 | saturation: 6.0 112 | heal: 2.5 113 | 114 | COOKED_CHICKEN: 115 | food-level: 6 116 | saturation: 7.2 117 | heal: 2.0 118 | 119 | COOKED_PORKCHOP: 120 | food-level: 8 121 | saturation: 12.8 122 | heal: 2.0 123 | 124 | COOKED_MUTTON: 125 | food-level: 6 126 | saturation: 9.6 127 | heal: 2.0 128 | 129 | COOKED_RABBIT: 130 | food-level: 5 131 | saturation: 6.0 132 | heal: 2.0 133 | 134 | COOKED_COD: 135 | food-level: 5 136 | saturation: 6.0 137 | heal: 1.0 138 | 139 | COOKED_SALMON: 140 | food-level: 6 141 | saturation: 9.6 142 | heal: 1.0 143 | -------------------------------------------------------------------------------- /src/main/resources/lang_en.yml: -------------------------------------------------------------------------------- 1 | general: 2 | prefix: "&8[&bMagicBlock&8] " 3 | reload: "&a✔ &7Configuration reloaded successfully!" 4 | no-permission: "&c✖ &7You don't have permission to do this!" 5 | plugin-enabled: "&a✔ &7Plugin enabled successfully!" 6 | plugin-disabled: "&c✖ &7Plugin disabled." 7 | placeholder-registered: "&a✔ &7PlaceholderAPI hook registered successfully!" 8 | food-config-reloaded: "&a✔ &7Food configuration reloaded successfully!" 9 | food-config-not-found: "&c✖ &7Food configuration file not found!" 10 | materials-updated: "&a✔ &7Allowed materials updated successfully!" 11 | config-load-error: "&c✖ &7Error loading configuration!" 12 | config-created: "&a✔ &7Created new configuration file: {0}" 13 | config-updated: "&a✔ &7Configuration file {0} has been updated" 14 | database-connected: "&a✔ &7Successfully connected to database!" 15 | database-error: "&c✖ &7Database error: {0}" 16 | database-tables-created: "&a✔ &7Database tables created successfully!" 17 | database-migration-start: "&e⚠ &7Starting data migration to database..." 18 | database-migration-complete: "&a✔ &7Data migration completed successfully!" 19 | config-key-added: "&7Added missing configuration key in {0}: {1}" 20 | update-found: "&eNew version available: &b{0}" 21 | current-version: "&eCurrent version: &b{0}" 22 | download-link: "&eDownload: &bhttps://www.spigotmc.org/resources/{0}" 23 | up-to-date: "&aPlugin is up to date!" 24 | dev-version: "&eDevelopment version detected: &b{0} &7(Latest stable: &b{1}&7)" 25 | update-check-failed: "&cUpdate check failed: {0}" 26 | 27 | gui: 28 | title: "&8⚡ &bMagicBlock Selection" 29 | search-button: "&e⚡ Search Block" 30 | search-lore: "&7» Click to search" 31 | previous-page: "&a« Previous Page" 32 | next-page: "&aNext Page »" 33 | select-block: "&7» Click to select this block" 34 | bound-blocks-title: "&8⚡ &bBound Blocks" 35 | retrieve-block: "&a▸ &7Left-click to retrieve this block" 36 | remove-block: "&c▸ &7Right-click to hide from list" 37 | remove-block-note: "&8• &7(Only hides from list, binding remains)" 38 | remaining-uses: "Remaining uses: " 39 | page-info: "&ePage {0}/{1}" 40 | close: "&cClose" 41 | 42 | messages: 43 | block-placed: "&a✔ &7Successfully placed MagicBlock!" 44 | block-broken: "&c✖ &7MagicBlock has been broken!" 45 | block-removed: "&c✖ &7Your MagicBlock has run out of uses!" 46 | block-removed-by-owner: "&c✖ &7A bound block in your inventory has been retrieved by its original owner!" 47 | food-removed: "&c✖ &7Your magic food has run out of uses!" 48 | blacklisted-world: "&c✖ &7MagicBlock is not allowed in this world!" 49 | item-changed: "&c✖ &7Operation cancelled due to item change!" 50 | success-replace: "&a✔ &7Successfully replaced with &b{0}&7!" 51 | invalid-material: "&c✖ &7Invalid block ID or material not allowed, please try again!" 52 | input-block-id: "&e⚡ &7Please enter the block ID to replace. Type 'cancel' to cancel." 53 | must-hold-magic-block: "&c✖ &7You must hold a MagicBlock to open the selection menu!" 54 | search-prompt: "&e⚡ &7Please enter the block name to search in chat &8(&7Type cancel to cancel search&8)" 55 | wait-cooldown: "&c✖ &7Please wait a moment before searching again..." 56 | no-results: "&c✖ &7No matching blocks found." 57 | already-bound: "&c✖ &7This block is already bound!" 58 | bind-success: "&a✔ &7Block bound successfully!" 59 | binding-disabled: "&c✖ &7Block binding is disabled on this server!" 60 | no-bound-blocks: "&c✖ &7You don't have any bound blocks!" 61 | not-bound-to-you: "&c✖ &7This block doesn't belong to you!" 62 | no-permission-use: "&c✖ &7You don't have permission to use magic blocks." 63 | no-permission-break: "&c✖ &7You don't have permission to break magic blocks." 64 | already-have-block: "&c✖ &7You already have this bound block!" 65 | block-retrieved: "&a✔ &7Block retrieved successfully!" 66 | cannot-break-others-block: "&c✖ &7You cannot break blocks bound to other players!" 67 | block-bind-removed: "&a✔ &7Block hidden from list!" 68 | bound-to: "Bound to:" 69 | click-again-to-remove: "&e⚡ &7Click again to hide this block from list" 70 | cannot-craft-with-magic-block: "&c✖ &7Magic blocks cannot be used in crafting!" 71 | food-full: "&c✖ &7Your hunger is full, cannot use magic food!" 72 | food-effect-applied: "&a✔ &7Successfully used magic food!" 73 | food-invalid: "&c✖ &7Invalid magic food!" 74 | food-not-edible: "&c✖ &7This item cannot be eaten!" 75 | 76 | commands: 77 | help: 78 | title: "&e⚡ &7MagicBlock Help" 79 | help: "&7/mb help &8- &7Show help information" 80 | get: "&7/mb get [uses] &8- &7Get a MagicBlock" 81 | give: "&7/mb give [uses] &8- &7Give a MagicBlock to a player" 82 | getfood: "&7/mb getfood [uses] &8- &7Get magic food" 83 | settimes: "&7/mb settimes &8- &7Set uses for held MagicBlock" 84 | addtimes: "&7/mb addtimes &8- &7Add uses to held MagicBlock" 85 | list: "&7/mb list &8- &7View bound blocks" 86 | reload: "&7/mb reload &8- &7Reload plugin configuration" 87 | performance: "&7/mb performance &8- &7View plugin performance report" 88 | tip: "&7Sneak + Right Click &8- &7Bind block" 89 | gui-tip: "&7Sneak + Left Click &8- &7Open block selection menu" 90 | get: 91 | no-permission: "&c✖ &7You don't have permission to use this command!" 92 | success: "&a✔ &7Successfully got a MagicBlock with &b{0} &7uses!" 93 | success-infinite: "&a✔ &7Successfully got a MagicBlock with &binfinite &7uses!" 94 | invalid-number: "&c✖ &7Invalid number of uses." 95 | give: 96 | no-permission: "&c✖ &7You don't have permission to use this command!" 97 | success: 98 | console: "&a✔ &7Successfully gave &b{0} &7a MagicBlock with &b{1} &7uses!" 99 | console-infinite: "&a✔ &7Successfully gave &b{0} &7a MagicBlock with &binfinite &7uses!" 100 | player: "&a✔ &7Successfully gave &b{0} &7a MagicBlock with &b{1} &7uses!" 101 | player-infinite: "&a✔ &7Successfully gave &b{0} &7a MagicBlock with &binfinite &7uses!" 102 | settimes: 103 | no-permission: "&c✖ &7You don't have permission to use this command!" 104 | must-hold: "&c✖ &7You must hold a MagicBlock to set uses!" 105 | invalid-number: "&c✖ &7Invalid number of uses." 106 | success: "&a✔ &7Successfully set MagicBlock uses to &b{0}&7!" 107 | success-infinite: "&a✔ &7Successfully set MagicBlock uses to &binfinite&7!" 108 | addtimes: 109 | no-permission: "&c✖ &7You don't have permission to use this command!" 110 | must-hold: "&c✖ &7You must hold a MagicBlock to add uses!" 111 | invalid-number: "&c✖ &7Invalid number of uses." 112 | unlimited: "&c✖ &7This MagicBlock already has infinite uses!" 113 | success: "&a✔ &7Successfully added &b{0} &7uses! Current total: &b{1}" 114 | usage: "&c✖ &7Usage: /mb addtimes " 115 | getfood: 116 | no-permission: "&c✖ &7You don't have permission to use this command!" 117 | usage: "&c✖ &7Usage: /mb getfood [uses]" 118 | invalid-food: "&c✖ &7Invalid food type!" 119 | invalid-number: "&c✖ &7Invalid number of uses." 120 | success: "&a✔ &7Successfully got magic food with &b{0} &7uses!" 121 | success-infinite: "&a✔ &7Successfully got magic food with &binfinite &7uses!" 122 | list: 123 | no-permission: "&c✖ &7You don't have permission to use this command!" 124 | reload: 125 | no-permission: "&c✖ &7You don't have permission to use this command!" 126 | success: "&a✔ &7Configuration reloaded successfully!" 127 | error: "&c✖ &7An error occurred while reloading configuration! Check console for details." 128 | performance: 129 | no-permission: "&c✖ &7You don't have permission to use this command!" 130 | -------------------------------------------------------------------------------- /src/main/resources/lang_zh_CN.yml: -------------------------------------------------------------------------------- 1 | general: 2 | prefix: "&8[&bMagicBlock&8] " 3 | reload: "&a✔ &7配置重载成功!" 4 | no-permission: "&c✖ &7你没有权限执行此操作!" 5 | plugin-enabled: "&a✔ &7插件启动成功!" 6 | plugin-disabled: "&c✖ &7插件已禁用。" 7 | placeholder-registered: "&a✔ &7PlaceholderAPI扩展注册成功!" 8 | food-config-reloaded: "&a✔ &7食物配置重载成功!" 9 | food-config-not-found: "&c✖ &7找不到食物配置文件!" 10 | materials-updated: "&a✔ &7允许的材料列表已更新!" 11 | config-load-error: "&c✖ &7加载配置文件时出错!" 12 | config-created: "&a✔ &7创建新的配置文件: {0}" 13 | config-updated: "&a✔ &7配置文件 {0} 已更新" 14 | database-connected: "&a✔ &7成功连接到数据库!" 15 | database-error: "&c✖ &7数据库错误: {0}" 16 | database-tables-created: "&a✔ &7数据库表创建成功!" 17 | database-migration-start: "&e⚠ &7开始迁移数据到数据库..." 18 | database-migration-complete: "&a✔ &7数据迁移完成!" 19 | config-key-added: "&7在 {0} 中添加了缺失的配置项: {1}" 20 | update-found: "&e发现新版本: &b{0}" 21 | current-version: "&e当前版本: &b{0}" 22 | download-link: "&e下载地址: &bhttps://www.spigotmc.org/resources/{0}" 23 | up-to-date: "&a插件已是最新版本!" 24 | dev-version: "&e检测到开发版本: &b{0} &7(最新稳定版: &b{1}&7)" 25 | update-check-failed: "&c检查更新失败: {0}" 26 | 27 | gui: 28 | title: "&8⚡ &bMagicBlock选择" 29 | search-button: "&e⚡ 搜索方块" 30 | search-lore: "&7» 点击进行搜索" 31 | previous-page: "&a« 上一页" 32 | next-page: "&a下一页 »" 33 | select-block: "&7» 点击选择此方块" 34 | bound-blocks-title: "&8⚡ &b已绑定的方块" 35 | retrieve-block: "&a▸ &7左键点击找回此方块" 36 | remove-block: "&c▸ &7右键点击从列表中隐藏" 37 | remove-block-note: "&8• &7(仅从列表中隐藏,不会解除绑定)" 38 | remaining-uses: "剩余使用次数: " 39 | page-info: "&e第 {0}/{1} 页" 40 | close: "&c关闭" 41 | 42 | messages: 43 | block-placed: "&a✔ &7成功放置MagicBlock!" 44 | block-broken: "&c✖ &7MagicBlock已被破坏!" 45 | block-removed: "&c✖ &7你的MagicBlock已耗尽使用次数!" 46 | block-removed-by-owner: "&c✖ &7你背包中的一个绑定方块已被原主人找回!" 47 | food-removed: "&c✖ &7你的魔法食物已耗尽使用次数!" 48 | blacklisted-world: "&c✖ &7当前世界不允许使用MagicBlock!" 49 | item-changed: "&c✖ &7由于物品改变,操作已取消!" 50 | success-replace: "&a✔ &7已成功替换为 &b{0}&7!" 51 | invalid-material: "&c✖ &7无效的方块ID或不允许使用该材料,请重新输入!" 52 | input-block-id: "&e⚡ &7请输入要替换的方块ID。输入'cancel'取消操作。" 53 | must-hold-magic-block: "&c✖ &7你必须手持一个MagicBlock才能打开选择界面!" 54 | search-prompt: "&e⚡ &7请在聊天栏中输入要搜索的方块名称 &8(&7输入 cancel 取消搜索&8)" 55 | wait-cooldown: "&c✖ &7请稍等片刻再进行搜索..." 56 | no-results: "&c✖ &7未找到匹配的方块。" 57 | already-bound: "&c✖ &7此方块已经被绑定!" 58 | bind-success: "&a✔ &7成功绑定方块!" 59 | binding-disabled: "&c✖ &7服务器已禁用方块绑定功能!" 60 | no-bound-blocks: "&c✖ &7你没有任何绑定的方块!" 61 | not-bound-to-you: "&c✖ &7这个方块不属于你!" 62 | no-permission-use: "&c✖ &7你没有权限使用魔术方块。" 63 | no-permission-break: "&c✖ &7你没有权限破坏魔术方块。" 64 | already-have-block: "&c✖ &7你已经拥有这个绑定的方块了!" 65 | block-retrieved: "&a✔ &7成功找回方块!" 66 | cannot-break-others-block: "&c✖ &7你不能破坏其他玩家绑定的方块!" 67 | block-bind-removed: "&a✔ &7方块已从列表中隐藏!" 68 | bound-to: "绑定玩家:" 69 | click-again-to-remove: "&e⚡ &7再次点击以从列表中隐藏此方块" 70 | cannot-craft-with-magic-block: "&c✖ &7魔法方块不能用于合成!" 71 | food-full: "&c✖ &7你的饥饿值已满,无法使用魔法食物!" 72 | food-effect-applied: "&a✔ &7成功使用魔法食物!" 73 | food-invalid: "&c✖ &7无效的魔法食物!" 74 | food-not-edible: "&c✖ &7这个物品不能食用!" 75 | 76 | commands: 77 | help: 78 | title: "&e⚡ &7MagicBlock 帮助" 79 | help: "&7/mb help &8- &7显示帮助信息" 80 | get: "&7/mb get [次数] &8- &7获取一个MagicBlock" 81 | give: "&7/mb give <玩家> [次数] &8- &7给予玩家MagicBlock" 82 | getfood: "&7/mb getfood <食物> [次数] &8- &7获取魔法食物" 83 | settimes: "&7/mb settimes <次数> &8- &7设置手持MagicBlock的使用次数" 84 | addtimes: "&7/mb addtimes <次数> &8- &7增加手持MagicBlock的使用次数" 85 | list: "&7/mb list &8- &7查看已绑定的方块" 86 | reload: "&7/mb reload &8- &7重载插件配置" 87 | performance: "&7/mb performance &8- &7查看插件性能报告" 88 | tip: "&7潜行 + 右键 &8- &7绑定方块" 89 | gui-tip: "&7潜行 + 左键 &8- &7打开方块选择界面" 90 | get: 91 | no-permission: "&c✖ &7你没有权限使用此命令!" 92 | success: "&a✔ &7成功获得一个拥有 &b{0} &7次使用次数的MagicBlock!" 93 | success-infinite: "&a✔ &7成功获得一个拥有 &b无限 &7次使用次数的MagicBlock!" 94 | invalid-number: "&c✖ &7无效的次数。" 95 | give: 96 | no-permission: "&c✖ &7你没有权限使用此命令!" 97 | success: 98 | console: "&a✔ &7成功给予 &b{0} &7个拥有 &b{1} &7次使用次数的MagicBlock!" 99 | console-infinite: "&a✔ &7成功给予 &b{0} &7一个拥有 &b无限 &7次使用次数的MagicBlock!" 100 | player: "&a✔ &7成功给予 &b{0} &7一个拥有 &b{1} &7次使用次数的MagicBlock!" 101 | player-infinite: "&a✔ &7成功给予 &b{0} &7一个拥有 &b无限 &7次使用次数的MagicBlock!" 102 | settimes: 103 | no-permission: "&c✖ &7你没有权限使用此命令!" 104 | must-hold: "&c✖ &7你必须手持一个MagicBlock来设置使用次数!" 105 | invalid-number: "&c✖ &7无效的次数。" 106 | success: "&a✔ &7成功将MagicBlock的使用次数设置为 &b{0} &7次!" 107 | success-infinite: "&a✔ &7成功将MagicBlock的使用次数设置为 &b无限 &7次!" 108 | addtimes: 109 | usage: "&c✖ &7用法: /mb addtimes <次数>" 110 | no-permission: "&c✖ &7你没有权限使用此命令!" 111 | must-hold: "&c✖ &7你必须手持一个MagicBlock来添加使用次数!" 112 | invalid-number: "&c✖ &7无效的次数。" 113 | unlimited: "&c✖ &7这个MagicBlock已经是无限使用次数了!" 114 | success: "&a✔ &7成功增加 &b{0} &7次使用次数!当前总次数: &b{1}" 115 | getfood: 116 | no-permission: "&c✖ &7你没有权限使用此命令!" 117 | usage: "&c✖ &7用法: /mb getfood <食物> [次数]" 118 | invalid-food: "&c✖ &7无效的食物类型!" 119 | invalid-number: "&c✖ &7无效的次数。" 120 | success: "&a✔ &7成功获得一个拥有 &b{0} &7次使用次数的魔法食物!" 121 | success-infinite: "&a✔ &7成功获得一个拥有 &b无限 &7次使用次数的魔法食物!" 122 | list: 123 | no-permission: "&c✖ &7你没有权限使用此命令!" 124 | reload: 125 | no-permission: "&c✖ &7你没有权限使用此命令!" 126 | success: "&a✔ &7配置重载成功!" 127 | error: "&c✖ &7配置重载时发生错误,请查看控制台获取详细信息!" 128 | performance: 129 | no-permission: "&c✖ &7你没有权限使用此命令!" 130 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: MagicBlock 2 | version: '3.1.6' 3 | main: io.github.syferie.magicblock.MagicBlockPlugin 4 | api-version: '1.18' 5 | authors: [Syferie] 6 | description: A plugin that allows players to use magic blocks with limited uses. 7 | softdepend: [PlaceholderAPI] 8 | folia-supported: true 9 | 10 | commands: 11 | magicblock: 12 | description: MagicBlock main command 13 | aliases: [mb] 14 | usage: / [args] 15 | 16 | permissions: 17 | magicblock.admin: 18 | description: Allows access to all MagicBlock commands and features 19 | default: op 20 | children: 21 | magicblock.use: true 22 | magicblock.break: true 23 | magicblock.get: true 24 | magicblock.give: true 25 | magicblock.reload: true 26 | magicblock.settimes: true 27 | magicblock.addtimes: true 28 | magicblock.getfood: true 29 | magicblock.list: true 30 | magicblock.performance: true 31 | magicblock.use: 32 | description: Allows placing and interacting with MagicBlocks (does not include breaking) 33 | default: true 34 | magicblock.break: 35 | description: Allows breaking MagicBlocks 36 | default: true 37 | magicblock.get: 38 | description: Allows getting MagicBlocks 39 | default: op 40 | magicblock.give: 41 | description: Allows giving MagicBlocks to other players 42 | default: op 43 | magicblock.reload: 44 | description: Allows reloading the plugin configuration 45 | default: op 46 | magicblock.settimes: 47 | description: Allows setting use times for MagicBlocks 48 | default: op 49 | magicblock.addtimes: 50 | description: Allows adding use times to MagicBlocks 51 | default: op 52 | magicblock.getfood: 53 | description: Allows getting magic food items 54 | default: op 55 | magicblock.list: 56 | description: Allows viewing bound blocks list 57 | default: true 58 | magicblock.performance: 59 | description: Allows viewing plugin performance reports 60 | default: op --------------------------------------------------------------------------------