21 Commits

Author SHA1 Message Date
xiaobai
8c68028924 完善注释(添加文档注释),添加skinsrestorer皮肤加载支持功能(未经完整测试) 2026-02-14 19:56:16 +08:00
xiaobai
b268a74eeb 修改方块生成范围集中的问题 2026-02-13 22:45:17 +08:00
xiaobai
2fbf5cfd7d 修复bug:破坏方块无反应 2026-02-13 22:27:03 +08:00
xiaobai
8b502459b0 1.0.1:Fix bug:[21:01:10 INFO]: [PBL] §a========================================
[21:01:10 INFO]: [PBL] §ePlayerBlockLife v1.0.1-1.20.4 已启用
              [21:01:10 INFO]: [PBL] §e作者: [YourName]
              [21:01:10 INFO]: [PBL] §a========================================
2026-02-13 22:03:17 +08:00
xiaobai
f899540449 1.0.1:Fix bug:[21:01:10 INFO]: [PBL] §a========================================
[21:01:10 INFO]: [PBL] §ePlayerBlockLife v1.0.1-1.20.4 已启用
              [21:01:10 INFO]: [PBL] §e作者: [YourName]
              [21:01:10 INFO]: [PBL] §a========================================
2026-02-13 21:02:28 +08:00
xiaobai
1fbd92ec72 1.0.1:Fix error:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:59:33 +08:00
xiaobai
c15b30f666 1.0.1:Fix error:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:58:03 +08:00
xiaobai
8d8cdcb244 1.0.1:Fix error:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:56:03 +08:00
xiaobai
c7d09c3039 1.0.1:Fix error:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:34:20 +08:00
xiaobai
a3cb82c1b7 1.0.1:Fix error:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:33:56 +08:00
xiaobai
c9035d488c 1.0.0 2026-02-13 20:32:43 +08:00
xiaobai
e3b46b6946 1.0.0 2026-02-13 20:32:18 +08:00
xiaobai
f92fdb40c6 1.0.0 2026-02-13 20:31:46 +08:00
xiaobai
dd6c570fa7 修复报错:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:30:45 +08:00
xiaobai
68fadf4e1a Merge remote-tracking branch 'origin/master' 2026-02-13 20:29:50 +08:00
xiaobai
1e6b0cb19e 修复报错:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:29:10 +08:00
xiaobai
e588ca9866 修复报错:[DirectoryProviderSource] Error loading plugin: name is not defined 2026-02-13 20:28:17 +08:00
9e113fc37d 更新 LICENSE 2026-02-13 19:41:02 +08:00
xiaobai
e40fcb344f Merge remote-tracking branch 'origin/master' 2026-02-13 19:39:16 +08:00
xiaobai
231e1acdf5 MIT License 2026-02-13 19:37:46 +08:00
c44219ad7b 更新 README.md 2026-02-13 19:07:33 +08:00
19 changed files with 1864 additions and 527 deletions

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2026 xiaobai
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -19,7 +19,7 @@ PlayerBlockLife is a unique Minecraft plugin that adds a novel gameplay mechanic
### 🚀 Quick Start ### 🚀 Quick Start
#### Installation #### Installation
1. Download the plugin from the ./Releases folder 1. Download the plugin from the ./Releases folder or https://gt.funmc.cn/xiaobai/PlayerBlockLife/releases
2. Place the JAR file in your server's `plugins` folder 2. Place the JAR file in your server's `plugins` folder
3. Restart your server 3. Restart your server
@@ -79,7 +79,7 @@ If you want to build the plugin yourself:
5. The compiled JAR will be in `build/libs/` 5. The compiled JAR will be in `build/libs/`
### 📦 Releases ### 📦 Releases
Pre-compiled versions are available in the ./Releases folder. We recommend downloading from there for stable builds. Pre-compiled versions are available in the ./Releases folder or https://gt.funmc.cn/xiaobai/PlayerBlockLife/releases. We recommend downloading from there for stable builds.
### 🎯 Gameplay ### 🎯 Gameplay
@@ -147,7 +147,7 @@ PlayerBlockLife 是一个独特的Minecraft插件为服务器增加了一种
### 🚀 快速开始 ### 🚀 快速开始
#### 安装方法 #### 安装方法
1. 从./Releases文件夹下载插件 1. 从./Releases文件夹或者https://gt.funmc.cn/xiaobai/PlayerBlockLife/releases下载插件
2. 将JAR文件放入服务器的`plugins`文件夹 2. 将JAR文件放入服务器的`plugins`文件夹
3. 重启服务器 3. 重启服务器
@@ -207,7 +207,7 @@ PlayerBlockLife 是一个独特的Minecraft插件为服务器增加了一种
5. 编译后的JAR文件位于 `build/libs/` 5. 编译后的JAR文件位于 `build/libs/`
### 📦 发布版本 ### 📦 发布版本
预编译的版本可以在 ./Releases 文件夹中找到。我们建议从这里下载以获得稳定版本。 预编译的版本可以在 ./Releases 文件夹或https://gt.funmc.cn/xiaobai/PlayerBlockLife/releases中找到。我们建议从这里下载以获得稳定版本。
### 🎯 游戏玩法 ### 🎯 游戏玩法

View File

@@ -4,7 +4,7 @@ plugins {
} }
group = 'com.playerblocklife' group = 'com.playerblocklife'
version = '1.0.0' version = '3.0.0-experimental-1.20.4'
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17

View File

@@ -5,5 +5,5 @@ org.gradle.caching=true
org.gradle.daemon=true org.gradle.daemon=true
# ???? # ????
pluginVersion=1.0.0 pluginVersion=2.1.0-1.20.4
mcVersion=1.20.4 mcVersion=1.20.4

View File

@@ -17,22 +17,41 @@ public class AdminCommands implements CommandExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("playerblocklife.admin")) { MessageManager msgManager = plugin.getMessageManager();
sender.sendMessage("§c你没有权限使用此命令"); ConfigManager config = plugin.getConfigManager();
String commandName = command.getName().toLowerCase();
// 检查命令是否启用
if (!config.isCommandEnabled(commandName)) {
String message = msgManager.getMessage("game.errors.command_disabled",
"&c此命令已被禁用");
sender.sendMessage(message);
return true; return true;
} }
if (command.getName().equalsIgnoreCase("pblreload")) { // 检查管理员权限和配置
plugin.reloadConfig(); if (!sender.hasPermission("playerblocklife.admin") || config.isAdminOnly(commandName)) {
plugin.getBlockManager().loadData(); String message = msgManager.getMessage("game.errors.no_permission",
plugin.getSkinManager().loadAllSkins(); "&c你没有权限使用此命令");
sender.sendMessage("§a插件配置已重载"); sender.sendMessage(message);
return true; return true;
} }
if (command.getName().equalsIgnoreCase("pbldelete")) { if (commandName.equals("pblreload")) {
// 调用插件的完整重载方法
plugin.reloadPluginConfig();
String message = msgManager.getCommandMessage("pblreload", "success",
"&a配置已重载");
sender.sendMessage(message);
return true;
}
if (commandName.equals("pbldelete")) {
if (args.length < 1) { if (args.length < 1) {
sender.sendMessage("§c用法: /pbldelete <玩家名>"); String usage = msgManager.getCommandMessage("pbldelete", "usage",
"&c用法: /pbldelete <玩家>");
sender.sendMessage(usage);
return true; return true;
} }
@@ -44,59 +63,111 @@ public class AdminCommands implements CommandExecutor {
targetId = target.getUniqueId(); targetId = target.getUniqueId();
} else { } else {
// 尝试从离线玩家获取UUID // 尝试从离线玩家获取UUID
try {
targetId = Bukkit.getOfflinePlayer(targetName).getUniqueId(); targetId = Bukkit.getOfflinePlayer(targetName).getUniqueId();
} catch (Exception e) {
String message = msgManager.getMessage("game.errors.player_not_found",
"&c找不到玩家: {player}");
message = message.replace("{player}", targetName);
sender.sendMessage(message);
return true;
}
} }
plugin.getBlockManager().clearPlayerBlocks(targetId); plugin.getBlockManager().clearPlayerBlocks(targetId);
sender.sendMessage("§a已删除玩家 " + targetName + " 的生命方块"); String message = msgManager.getCommandMessage("pbldelete", "success",
"&a已删除玩家 {player} 的生命方块!");
message = message.replace("{player}", targetName);
sender.sendMessage(message);
return true; return true;
} }
if (command.getName().equalsIgnoreCase("pblrevive")) { if (commandName.equals("pblrevive")) {
Player target; Player target;
if (args.length < 1) { if (args.length < 1) {
if (!(sender instanceof Player)) { if (!(sender instanceof Player)) {
sender.sendMessage("§c控制台使用时必须指定玩家名: /pblrevive <玩家名>"); String usage = msgManager.getCommandMessage("pblrevive", "usage",
"&c用法: /pblrevive [玩家]");
sender.sendMessage(usage);
return true; return true;
} }
target = (Player) sender; target = (Player) sender;
} else { } else {
target = Bukkit.getPlayer(args[0]); target = Bukkit.getPlayer(args[0]);
if (target == null) { if (target == null) {
sender.sendMessage("§c玩家不存在或不在线"); String message = msgManager.getMessage("game.errors.player_offline",
"&c玩家 {player} 不在线!");
message = message.replace("{player}", args[0]);
sender.sendMessage(message);
return true; return true;
} }
} }
if (plugin.getLifeSystem() != null) {
plugin.getLifeSystem().revivePlayer(target); plugin.getLifeSystem().revivePlayer(target);
sender.sendMessage("§a玩家 " + target.getName() + " 已复活!"); String message = msgManager.getCommandMessage("pblrevive", "success",
"&a玩家 {player} 已复活!");
message = message.replace("{player}", target.getName());
sender.sendMessage(message);
} else {
sender.sendMessage("§c复活失败生命系统未初始化");
}
return true;
}
if (commandName.equals("pblstats")) {
if (plugin.getBlockManager() == null) {
sender.sendMessage("§c方块管理器未初始化");
return true; return true;
} }
if (command.getName().equalsIgnoreCase("pblstats")) {
int totalPlayers = plugin.getBlockManager().getPlayerBlocksCount(); int totalPlayers = plugin.getBlockManager().getPlayerBlocksCount();
int totalBlocks = plugin.getBlockManager().getTotalBlocksCount(); int totalBlocks = plugin.getBlockManager().getTotalBlocksCount();
sender.sendMessage("§a===== PlayerBlockLife 统计 ====="); // 获取统计标题
sender.sendMessage("§7注册玩家数: §e" + totalPlayers); String title = msgManager.getCommandMessage("pblstats", "title",
sender.sendMessage("§7总生命方块数: §e" + totalBlocks); "&6=== PlayerBlockLife 统计 ===");
sender.sendMessage("§7在线玩家生命方块:"); sender.sendMessage(title);
// 在线玩家统计
String onlineMsg = msgManager.getCommandMessage("pblstats", "online_players",
"&e在线玩家: {count}");
onlineMsg = onlineMsg.replace("{count}", String.valueOf(Bukkit.getOnlinePlayers().size()));
sender.sendMessage(onlineMsg);
// 总方块统计
String blocksMsg = msgManager.getCommandMessage("pblstats", "total_blocks",
"&e总生命方块: {count}");
blocksMsg = blocksMsg.replace("{count}", String.valueOf(totalBlocks));
sender.sendMessage(blocksMsg);
// 淘汰玩家统计
int eliminatedCount = 0;
for (Player player : Bukkit.getOnlinePlayers()) {
if (plugin.getBlockManager().getRemainingBlocks(player.getUniqueId()) == 0) {
eliminatedCount++;
}
}
String eliminatedMsg = msgManager.getCommandMessage("pblstats", "eliminated_players",
"&e已淘汰玩家: {count}");
eliminatedMsg = eliminatedMsg.replace("{count}", String.valueOf(eliminatedCount));
sender.sendMessage(eliminatedMsg);
sender.sendMessage("§7在线玩家详情:");
for (Player player : Bukkit.getOnlinePlayers()) { for (Player player : Bukkit.getOnlinePlayers()) {
int blocks = plugin.getBlockManager().getRemainingBlocks(player.getUniqueId()); int blocks = plugin.getBlockManager().getRemainingBlocks(player.getUniqueId());
sender.sendMessage("§7- " + player.getName() + ": §e" + blocks + " §7/ §a5"); String status = blocks > 0 ? "§a存活" : "§c已淘汰";
sender.sendMessage("§7- " + player.getName() + ": §e" + blocks + " §7/ §a5 §7(" + status + "§7)");
} }
sender.sendMessage("§a=================================");
return true; return true;
} }
sender.sendMessage("§c未知的管理员命令"); String unknownMsg = msgManager.getMessage("game.errors.invalid_arguments",
sender.sendMessage("§e可用命令:"); "&c未知的管理员命令");
sender.sendMessage("§7/pblreload §8- §f重载插件配置"); sender.sendMessage(unknownMsg);
sender.sendMessage("§7/pbldelete <玩家> §8- §f删除玩家的生命方块");
sender.sendMessage("§7/pblrevive [玩家] §8- §f复活被淘汰的玩家");
sender.sendMessage("§7/pblstats §8- §f查看插件统计");
return true; return true;
} }
} }

View File

@@ -12,6 +12,8 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class BlockBreakListener implements Listener { public class BlockBreakListener implements Listener {
@@ -39,7 +41,9 @@ public class BlockBreakListener implements Listener {
if (ownerId.equals(breaker.getUniqueId())) { if (ownerId.equals(breaker.getUniqueId())) {
if (breaker.getGameMode() != GameMode.CREATIVE) { if (breaker.getGameMode() != GameMode.CREATIVE) {
breaker.sendMessage("§c你不能挖掘自己的生命方块"); String message = plugin.getMessageManager().getMessage("game.errors.cannot_break_own_block",
"&c你不能挖掘自己的生命方块");
breaker.sendMessage(message);
event.setCancelled(true); event.setCancelled(true);
} }
return; return;
@@ -47,19 +51,61 @@ public class BlockBreakListener implements Listener {
if (plugin.getBlockManager().removeBlock(location, breaker)) { if (plugin.getBlockManager().removeBlock(location, breaker)) {
int remaining = plugin.getBlockManager().getRemainingBlocks(ownerId); int remaining = plugin.getBlockManager().getRemainingBlocks(ownerId);
Player owner = Bukkit.getPlayer(ownerId);
String ownerName = owner != null ? owner.getName() : Bukkit.getOfflinePlayer(ownerId).getName();
if (remaining <= 0) { if (remaining <= 0) {
plugin.getLifeSystem().handlePlayerDeath(ownerId); plugin.getLifeSystem().handlePlayerDeath(ownerId);
} }
breaker.sendMessage("§a✓ 成功破坏一个生命方块!"); // 通知破坏者
breaker.sendMessage("§7剩余方块: §e" + remaining); Map<String, String> variables = new HashMap<>();
variables.put("owner", ownerName != null ? ownerName : "未知玩家");
variables.put("remaining", String.valueOf(remaining));
variables.put("total", String.valueOf(plugin.getConfigManager().getBlocksPerPlayer()));
String breakerMsg = plugin.getMessageManager().getFormattedMessage("game.block.destroyed.breaker",
"&a你破坏了 {owner} 的生命方块!", variables);
breaker.sendMessage(breakerMsg);
String remainingMsg = plugin.getMessageManager().getFormattedMessage("game.block.destroyed.remaining",
"&7剩余方块: {remaining}/{total}", variables);
breaker.sendMessage(remainingMsg);
if (remaining == 1) { if (remaining == 1) {
breaker.sendMessage("§6⚡ 对方只剩最后1个生命方块了"); String lastBlockMsg = plugin.getMessageManager().getMessage("game.block.last_block_warning",
"&6⚡ 对方只剩最后1个生命方块了");
breaker.sendMessage(lastBlockMsg);
breaker.playSound(breaker.getLocation(), breaker.playSound(breaker.getLocation(),
org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f); org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f);
} }
// 通知方块所有者
if (owner != null && owner.isOnline()) {
variables.put("breaker", breaker.getName());
String ownerMsg = plugin.getMessageManager().getFormattedMessage("game.block.destroyed.owner",
"&c⚠ 警告!你的生命方块被 {breaker} 破坏了!剩余: {remaining}/{total}", variables);
owner.sendMessage(ownerMsg);
if (remaining == 0) {
String allDestroyedMsg = plugin.getMessageManager().getMessage("game.block.all_destroyed",
"&c☠ 你的所有生命方块已被破坏!你已被淘汰!");
owner.sendMessage(allDestroyedMsg);
}
}
// 广播(如果启用)
if (plugin.getConfigManager().isBroadcastOnBlockBreak()) {
String broadcastMsg = plugin.getMessageManager().getFormattedMessage("broadcast.block_destroyed",
"&6{breaker} &7破坏了 &c{owner} &7的生命方块", variables);
int range = plugin.getConfigManager().getBroadcastRange();
for (Player nearby : breaker.getWorld().getPlayers()) {
if (nearby.getLocation().distance(breaker.getLocation()) <= range &&
nearby != breaker && (owner == null || nearby != owner)) {
nearby.sendMessage(broadcastMsg);
}
}
}
} }
} }
@@ -73,7 +119,9 @@ public class BlockBreakListener implements Listener {
UUID ownerId = plugin.getBlockManager().getBlockOwner(block.getLocation()); UUID ownerId = plugin.getBlockManager().getBlockOwner(block.getLocation());
if (ownerId != null && !ownerId.equals(player.getUniqueId())) { if (ownerId != null && !ownerId.equals(player.getUniqueId())) {
player.sendMessage("§c你不能在这里放置方块这是别人的生命方块区域"); String message = plugin.getMessageManager().getMessage("game.errors.cannot_place_in_block_area",
"&c你不能在这里放置方块这是别人的生命方块区域");
player.sendMessage(message);
event.setCancelled(true); event.setCancelled(true);
} }
} }

View File

@@ -19,49 +19,81 @@ public class CheckLifeBlocksCommand implements CommandExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
// 检查命令是否启用
if (!plugin.getConfigManager().isCommandEnabled("checklifeblocks")) {
String message = plugin.getMessageManager().getMessage("game.errors.command_disabled",
"&c此命令已被禁用");
sender.sendMessage(message);
return true;
}
if (!(sender instanceof Player)) { if (!(sender instanceof Player)) {
sender.sendMessage("§c只有玩家可以使用此命令"); String message = plugin.getMessageManager().getMessage("game.errors.no_permission",
"&c只有玩家可以使用此命令");
sender.sendMessage(message);
return true; return true;
} }
Player player = (Player) sender; Player player = (Player) sender;
UUID playerId = player.getUniqueId(); UUID playerId = player.getUniqueId();
List<Location> blocks = plugin.getBlockManager().getPlayerBlocks(playerId); // 检查是否允许玩家自己使用
int remaining = blocks.size(); if (!plugin.getConfigManager().isSelfUseAllowed("checklifeblocks")) {
String message = plugin.getMessageManager().getMessage("game.errors.no_permission",
if (remaining == 0) { "&c你没有权限使用此命令");
player.sendMessage("§c你还没有设置生命方块"); player.sendMessage(message);
player.sendMessage("§7使用 §e/setlifeblocks §7来设置你的生命方块");
return true; return true;
} }
player.sendMessage("§a========== 你的生命方块信息 =========="); List<Location> blocks = plugin.getBlockManager().getPlayerBlocks(playerId);
int remaining = blocks.size();
MessageManager msgManager = plugin.getMessageManager();
if (remaining == 0) {
String noBlocksMsg = msgManager.getCommandMessage("checklifeblocks", "no_blocks",
"&c你还没有生命方块");
player.sendMessage(noBlocksMsg);
return true;
}
// 获取消息
String successMsg = msgManager.getCommandMessage("checklifeblocks", "success",
"&e你的生命方块位置");
player.sendMessage(successMsg);
player.sendMessage("§7剩余方块数量: §e" + remaining + " §7/ §a5"); player.sendMessage("§7剩余方块数量: §e" + remaining + " §7/ §a5");
player.sendMessage("§7当前生命值: §c" +
(plugin.getLifeSystem().getPlayerHealth(playerId) != null ? // 显示生命值(如果启用)
plugin.getLifeSystem().getPlayerHealth(playerId) : "20") + ""); if (plugin.getConfigManager().isHealthSystemEnabled()) {
Integer health = plugin.getLifeSystem().getPlayerHealth(playerId);
player.sendMessage("§7当前生命值: §c" + (health != null ? health : "20") + "");
}
if (remaining <= 2) { if (remaining <= 2) {
player.sendMessage("§c⚠ 警告!生命方块即将耗尽!"); String warningMsg = msgManager.getMessage("game.block.warning_low_blocks",
"&c⚠ 警告!生命方块即将耗尽!");
player.sendMessage(warningMsg);
} }
player.sendMessage("§7方块位置:"); player.sendMessage("§7方块位置:");
for (int i = 0; i < blocks.size(); i++) { for (int i = 0; i < blocks.size(); i++) {
Location loc = blocks.get(i); Location loc = blocks.get(i);
String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "未知世界"; String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "未知世界";
player.sendMessage("§7" + (i + 1) + ". §e世界: " + worldName + String locationMsg = msgManager.getMessage("game.block.location_item",
" §7坐标: §a" + loc.getBlockX() + " §7, §a" + "&7- {world} ({x}, {y}, {z})");
loc.getBlockY() + " §7, §a" + loc.getBlockZ()); locationMsg = locationMsg.replace("{world}", worldName)
.replace("{x}", String.valueOf(loc.getBlockX()))
.replace("{y}", String.valueOf(loc.getBlockY()))
.replace("{z}", String.valueOf(loc.getBlockZ()));
player.sendMessage(locationMsg);
} }
player.sendMessage("§a======================================");
if (remaining > 0) { if (remaining > 0) {
player.sendMessage("§e提示"); Location nearestBlock = plugin.getBlockManager().getNearestBlock(player);
player.sendMessage("§7- 方块距离你: §a" + if (nearestBlock != null) {
(int) player.getLocation().distance(blocks.get(0)) + " §7格"); double distance = player.getLocation().distance(nearestBlock);
player.sendMessage("§7- 使用指南针可以追踪方块位置"); player.sendMessage("§7最近方块距离: §a" + (int)distance + " §7格");
}
} }
return true; return true;

View File

@@ -1,60 +1,411 @@
package com.playerblocklife; package com.playerblocklife;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 配置管理器 - 负责加载、保存和管理插件的配置文件
*
* <p>主要功能包括:
* <ul>
* <li>加载和验证配置文件</li>
* <li>配置版本检查和自动更新</li>
* <li>提供类型安全的配置项访问方法</li>
* <li>处理配置文件的保存和重载</li>
* <li>管理SkinsRestorer插件集成配置</li>
* <li>支持多种皮肤来源的优先级配置</li>
* </ul>
*
* <p><b>SkinsRestorer配置支持</b>
* <ul>
* <li><code>skin.source</code>皮肤来源优先级skinsrestorer/player_profile/local_cache</li>
* <li><code>skin.use-skinsrestorer</code>是否启用SkinsRestorer支持</li>
* <li><code>skin.cache.expire_days</code>:皮肤缓存过期时间</li>
* <li>默认配置已优化优先使用SkinsRestorer以支持离线服务器</li>
* </ul>
* </p>
*
* @author xiaobai
* @version 2.1.0
* @since 1.0.0
*/
public class ConfigManager { public class ConfigManager {
private final PlayerBlockLife plugin; private final PlayerBlockLife plugin;
private FileConfiguration config; private FileConfiguration config;
private File configFile;
/**
* 构造一个新的配置管理器
*
* @param plugin 插件主类实例,用于访问插件相关功能
*/
public ConfigManager(PlayerBlockLife plugin) { public ConfigManager(PlayerBlockLife plugin) {
this.plugin = plugin; this.plugin = plugin;
loadConfig(); this.configFile = new File(plugin.getDataFolder(), "config.yml");
} }
/**
* 加载插件配置
*
* <p>执行以下操作:
* <ol>
* <li>确保插件数据文件夹存在</li>
* <li>如果配置文件不存在从JAR中复制默认配置</li>
* <li>调用reloadConfig()重新加载配置</li>
* </ol>
*
* @see #reloadConfig()
*/
public void loadConfig() { public void loadConfig() {
plugin.saveDefaultConfig(); // 确保配置文件夹存在
config = plugin.getConfig(); if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
} }
public void reloadConfig() { // 如果配置文件不存在从JAR中复制默认配置
plugin.reloadConfig(); if (!configFile.exists()) {
config = plugin.getConfig(); plugin.saveDefaultConfig();
plugin.logInfo("创建默认配置文件");
} }
// 重新加载配置
reloadConfig();
}
/**
* 重新加载配置文件
*
* <p>执行以下操作:
* <ol>
* <li>从磁盘重新加载配置文件</li>
* <li>加载JAR中的默认配置作为后备</li>
* <li>检查配置版本并进行必要的更新</li>
* </ol>
*
* @see #checkConfigVersion()
*/
public void reloadConfig() {
// 重新从磁盘加载配置
config = YamlConfiguration.loadConfiguration(configFile);
// 加载默认配置作为后备
InputStream defaultConfigStream = plugin.getResource("config.yml");
if (defaultConfigStream != null) {
YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(
new InputStreamReader(defaultConfigStream, StandardCharsets.UTF_8));
config.setDefaults(defaultConfig);
}
// 检查配置版本,如果需要则更新
checkConfigVersion();
plugin.logInfo("配置已加载");
}
/**
* 检查配置版本并更新
*/
private void checkConfigVersion() {
int currentVersion = config.getInt("config-version", 1);
int latestVersion = 1; // 最新配置版本
if (currentVersion < latestVersion) {
plugin.logWarning("检测到旧版配置文件,正在更新...");
updateConfig(currentVersion, latestVersion);
}
}
/**
* 更新配置文件
*/
private void updateConfig(int fromVersion, int toVersion) {
if (fromVersion == 1 && toVersion == 2) {
// 添加自动生成配置
if (!config.contains("auto-generation.enabled")) {
config.set("auto-generation.enabled", true);
config.set("auto-generation.require_open_sky", true);
config.set("auto-generation.max_attempts", 50);
config.set("auto-generation.on_failure", "notify");
}
// 添加命令启用配置
if (!config.contains("commands.setlifeblocks.enabled")) {
config.set("commands.setlifeblocks.enabled", true);
config.set("commands.setlifeblocks.allow_self_use", true);
config.set("commands.setlifeblocks.allow_admin_use", true);
config.set("commands.checklifeblocks.enabled", true);
config.set("commands.checklifeblocks.allow_self_use", true);
config.set("commands.checklifeblocks.allow_admin_use", true);
config.set("commands.pblreload.enabled", true);
config.set("commands.pblreload.admin_only", true);
config.set("commands.pbldelete.enabled", true);
config.set("commands.pbldelete.admin_only", true);
config.set("commands.pblrevive.enabled", true);
config.set("commands.pblrevive.admin_only", true);
config.set("commands.pblstats.enabled", true);
config.set("commands.pblstats.admin_only", true);
}
// 更新消息配置
if (!config.contains("messages.use_external_file")) {
config.set("messages.use_external_file", true);
config.set("messages.external_file", "messages.yml");
}
// 更新版本号
config.set("config-version", toVersion);
try {
config.save(configFile);
plugin.logInfo("配置文件已更新到版本 " + toVersion);
} catch (IOException e) {
plugin.logError("保存更新后的配置文件失败", e);
}
}
}
/**
* 保存当前配置到文件
*
* <p>将内存中的配置数据写入到config.yml文件中。
* 如果保存失败,会记录错误日志。</p>
*
* @throws IOException 如果文件写入失败
*/
public void saveConfig() {
try {
config.save(configFile);
} catch (IOException e) {
plugin.logError("保存配置文件失败", e);
}
}
/**
* 获取配置对象
*
* <p>如果配置对象为null会自动调用reloadConfig()加载配置。</p>
*
* @return 当前的FileConfiguration配置对象
* @see #reloadConfig()
*/
public FileConfiguration getConfig() {
if (config == null) {
reloadConfig();
}
return config;
}
// 以下为配置项的获取方法
public int getBlocksPerPlayer() { public int getBlocksPerPlayer() {
return config.getInt("blocks.amount", 5); return getConfig().getInt("blocks.amount", 5);
} }
public int getSpreadRange() { public int getSpreadRange() {
return config.getInt("blocks.spread", 5); return getConfig().getInt("blocks.spread", 5);
}
public int getMinDistance() {
return getConfig().getInt("blocks.min-distance", 10);
} }
public int getDepth() { public int getDepth() {
return config.getInt("blocks.depth", -1); return getConfig().getInt("blocks.depth", -1);
}
public String getBlockMaterial() {
return getConfig().getString("blocks.material", "player_head");
} }
public boolean isDieWhenBlocksGone() { public boolean isDieWhenBlocksGone() {
return config.getBoolean("game.die_when_blocks_gone", true); return getConfig().getBoolean("game.die_when_blocks_gone", true);
} }
public boolean isBecomeSpectator() { public boolean isBecomeSpectator() {
return config.getBoolean("game.become_spectator", true); return getConfig().getBoolean("game.become_spectator", true);
} }
public boolean isHealthSystemEnabled() { public boolean isHealthSystemEnabled() {
return config.getBoolean("game.health_system", true); return getConfig().getBoolean("game.health_system", true);
} }
public boolean isSkinSystemEnabled() { public boolean isSkinSystemEnabled() {
return config.getBoolean("skin.enabled", true); return getConfig().getBoolean("skin.enabled", true);
} }
/**
* 获取皮肤来源配置
*
* <p>支持的皮肤来源:
* <ul>
* <li><b>skinsrestorer</b>优先从SkinsRestorer插件获取皮肤纹理数据
* <ul>
* <li>推荐用于离线服务器</li>
* <li>支持玩家自定义皮肤</li>
* <li>避免默认Steve皮肤问题</li>
* </ul>
* </li>
* <li><b>player_profile</b>优先使用Bukkit的PlayerProfile API
* <ul>
* <li>需要玩家在线验证</li>
* <li>适合在线服务器</li>
* <li>支持Mojang官方皮肤</li>
* </ul>
* </li>
* <li><b>local_cache</b>:优先从本地缓存加载皮肤数据
* <ul>
* <li>减少网络请求</li>
* <li>提高加载速度</li>
* <li>支持离线使用</li>
* </ul>
* </li>
* </ul>
* </p>
*
* <p>默认配置已将此值设为"skinsrestorer",以优化离线服务器体验。</p>
*
* @return 皮肤来源配置值
* @see #useSkinsRestorer()
* @see SkinManager#loadPlayerSkinAsync(<Player>)
*/
public String getSkinSource() { public String getSkinSource() {
return config.getString("skin.source", "player_profile"); return getConfig().getString("skin.source", "skinsrestorer");
}
/**
* 检查是否启用SkinsRestorer插件支持
*
* <p>当此方法返回true时插件将
* <ul>
* <li>优先从SkinsRestorer插件获取玩家皮肤纹理</li>
* <li>支持离线服务器获取玩家自定义皮肤</li>
* <li>避免方块总是显示默认Steve皮肤的问题</li>
* <li>使用反射安全调用SkinsRestorer API无需硬依赖</li>
* </ul>
* </p>
*
* <p>默认配置已将此值设为true以优化离线服务器体验。</p>
*
* @return 如果启用SkinsRestorer支持返回true否则返回false
* @see #getSkinSource()
* @see SkinManager#getSkinFromSkinsRestorer(<Player>)
*/
public boolean useSkinsRestorer() {
return getConfig().getBoolean("skin.use-skinsrestorer", true);
}
public int getCacheExpireDays() {
return getConfig().getInt("skin.cache.expire_days", 7);
}
public boolean isAutoSaveEnabled() {
return getConfig().getBoolean("storage.auto_save.enabled", true);
}
public int getAutoSaveInterval() {
return getConfig().getInt("storage.auto_save.interval", 300);
}
public String getStorageType() {
return getConfig().getString("storage.type", "yaml");
}
public boolean isBroadcastOnBlockBreak() {
return getConfig().getBoolean("game.broadcast.on_block_break", true);
}
public boolean isBroadcastOnPlayerDeath() {
return getConfig().getBoolean("game.broadcast.on_player_death", true);
}
public int getBroadcastRange() {
return getConfig().getInt("game.broadcast.range", 30);
}
public boolean isGiveExpReward() {
return getConfig().getBoolean("game.break_rewards.give_exp", true);
}
public int getExpRewardAmount() {
return getConfig().getInt("game.break_rewards.exp_amount", 5);
}
public boolean isProtectFromExplosions() {
return getConfig().getBoolean("protection.protect_from_explosions", true);
}
public boolean isProtectFromFire() {
return getConfig().getBoolean("protection.protect_from_fire", true);
}
public boolean isProtectFromPistons() {
return getConfig().getBoolean("protection.protect_from_pistons", true);
}
// 自动生成配置获取方法
public boolean isAutoGenerationEnabled() {
return getConfig().getBoolean("auto-generation.enabled", true);
}
public boolean isRequireOpenSky() {
return getConfig().getBoolean("auto-generation.require_open_sky", true);
}
public int getMaxAttempts() {
return getConfig().getInt("auto-generation.max_attempts", 50);
}
public String getOnFailureAction() {
return getConfig().getString("auto-generation.on_failure", "notify");
}
// 命令启用配置获取方法
public boolean isCommandEnabled(String commandName) {
return getConfig().getBoolean("commands." + commandName + ".enabled", true);
}
public boolean isSelfUseAllowed(String commandName) {
return getConfig().getBoolean("commands." + commandName + ".allow_self_use", true);
}
public boolean isAdminUseAllowed(String commandName) {
return getConfig().getBoolean("commands." + commandName + ".allow_admin_use", true);
}
public boolean isAdminOnly(String commandName) {
return getConfig().getBoolean("commands." + commandName + ".admin_only", false);
}
// 消息文件配置获取方法
public boolean useExternalMessageFile() {
return getConfig().getBoolean("messages.use_external_file", true);
}
public String getExternalMessageFileName() {
return getConfig().getString("messages.external_file", "messages.yml");
} }
public String getMessage(String path, String defaultValue) { public String getMessage(String path, String defaultValue) {
String message = config.getString("messages." + path, defaultValue); // 优先从外部消息文件获取
if (useExternalMessageFile()) {
// 这里应该调用MessageManager来获取消息
// 暂时返回默认值MessageManager会处理具体逻辑
return defaultValue;
}
// 从config.yml获取
String message = getConfig().getString("messages." + path, defaultValue);
if (message != null) { if (message != null) {
message = message.replace("&", "§"); message = message.replace("&", "§");
} }

View File

@@ -10,6 +10,32 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
/**
* 生命值系统 - 负责管理玩家生命值和状态效果
*
* <p>根据玩家剩余生命方块数量计算生命值,并应用相应的状态效果:
* <ul>
* <li>监控玩家剩余方块数量变化</li>
* <li>计算对应的生命值比例</li>
* <li>应用虚弱、缓慢、失明等负面效果</li>
* <li>处理玩家淘汰和复活逻辑</li>
* <li>提供生命值相关的音效和视觉反馈</li>
* </ul>
*
* <p>生命值计算公式:
* <pre>
* 5个方块 → 20点生命值满血
* 4个方块 → 16点生命值
* 3个方块 → 12点生命值
* 2个方块 → 8点生命值
* 1个方块 → 4点生命值
* 0个方块 → 0点生命值淘汰
* </pre>
*
* @author xiaobai
* @version 2.1.0
* @since 1.0.0
*/
public class LifeSystem { public class LifeSystem {
private final PlayerBlockLife plugin; private final PlayerBlockLife plugin;
private final Map<UUID, Integer> playerHealth = new HashMap<>(); private final Map<UUID, Integer> playerHealth = new HashMap<>();

View File

@@ -0,0 +1,183 @@
package com.playerblocklife;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class MessageManager {
private final PlayerBlockLife plugin;
private FileConfiguration messageConfig;
private File messageFile;
private final Map<String, String> messageCache = new HashMap<>();
public MessageManager(PlayerBlockLife plugin) {
this.plugin = plugin;
this.messageFile = new File(plugin.getDataFolder(), "messages.yml");
}
/**
* 加载消息配置
*/
public void loadMessages() {
ConfigManager config = plugin.getConfigManager();
// 检查是否使用外部消息文件
if (config.useExternalMessageFile()) {
String fileName = config.getExternalMessageFileName();
this.messageFile = new File(plugin.getDataFolder(), fileName);
// 确保配置文件夹存在
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
}
// 如果消息文件不存在从JAR中复制默认配置
if (!messageFile.exists()) {
plugin.saveResource(fileName, false);
plugin.logInfo("创建默认消息配置文件: " + fileName);
}
// 加载消息配置
reloadMessages();
} else {
// 使用config.yml中的消息
plugin.logInfo("使用config.yml中的消息配置");
messageConfig = null;
messageCache.clear();
}
}
/**
* 重新加载消息配置
*/
public void reloadMessages() {
ConfigManager config = plugin.getConfigManager();
if (!config.useExternalMessageFile()) {
return;
}
try {
messageConfig = YamlConfiguration.loadConfiguration(messageFile);
// 加载默认消息作为后备
InputStream defaultStream = plugin.getResource(messageFile.getName());
if (defaultStream != null) {
YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(
new InputStreamReader(defaultStream, StandardCharsets.UTF_8));
messageConfig.setDefaults(defaultConfig);
}
// 清空缓存
messageCache.clear();
plugin.logInfo("消息配置已加载: " + messageFile.getName());
} catch (Exception e) {
plugin.logError("加载消息配置文件失败", e);
}
}
/**
* 获取消息
*/
public String getMessage(String path, String defaultValue) {
ConfigManager config = plugin.getConfigManager();
// 检查缓存
String cacheKey = path + "|" + defaultValue;
if (messageCache.containsKey(cacheKey)) {
return messageCache.get(cacheKey);
}
String message;
if (config.useExternalMessageFile() && messageConfig != null) {
// 从外部消息文件获取
message = messageConfig.getString(path, defaultValue);
if (message == null) {
message = defaultValue;
}
} else {
// 从config.yml获取或使用默认值
message = config.getMessage(path, defaultValue);
}
// 替换颜色代码
if (message != null) {
message = message.replace("&", "§");
}
// 缓存结果
messageCache.put(cacheKey, message);
return message;
}
/**
* 获取格式化消息(替换变量)
*/
public String getFormattedMessage(String path, String defaultValue, Map<String, String> variables) {
String message = getMessage(path, defaultValue);
if (message != null && variables != null) {
for (Map.Entry<String, String> entry : variables.entrySet()) {
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
}
}
return message;
}
/**
* 获取控制台消息
*/
public String getConsoleMessage(String path, String defaultValue) {
String message = getMessage("console." + path, defaultValue);
// 移除颜色代码(控制台不需要)
if (message != null) {
message = message.replace("§", "&");
}
return message;
}
/**
* 获取游戏内消息
*/
public String getGameMessage(String path, String defaultValue) {
return getMessage("game." + path, defaultValue);
}
/**
* 获取命令消息
*/
public String getCommandMessage(String command, String path, String defaultValue) {
return getMessage("commands." + command + "." + path, defaultValue);
}
/**
* 获取广播消息
*/
public String getBroadcastMessage(String path, String defaultValue) {
return getMessage("broadcast." + path, defaultValue);
}
/**
* 检查消息文件是否存在
*/
public boolean hasExternalMessageFile() {
return messageFile.exists();
}
/**
* 获取消息文件路径
*/
public String getMessageFilePath() {
return messageFile.getAbsolutePath();
}
}

View File

@@ -1,45 +1,89 @@
package com.playerblocklife; package com.playerblocklife;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.util.logging.Level; import java.util.logging.Level;
/**
* PlayerBlockLife插件主类 - 玩家生命方块系统的核心控制器
*
* <p>这个插件为Minecraft服务器添加了一个独特的游戏机制每个玩家拥有一定数量的生命方块
* 这些方块使用玩家的皮肤作为材质。当其他玩家挖光某个玩家的所有生命方块时,该玩家会被淘汰。</p>
*
* <p>主要功能:
* <ul>
* <li>管理玩家生命方块的生成和销毁</li>
* <li>处理玩家皮肤的获取和应用</li>
* <li>监控玩家生命值状态</li>
* <li>提供完整的命令和权限系统</li>
* <li>支持配置热重载和数据持久化</li>
* </ul>
*
* @author xiaobai
* @version 3.0.0-experimental-1.20.4
* @since 1.0.0
*/
public class PlayerBlockLife extends JavaPlugin { public class PlayerBlockLife extends JavaPlugin {
private static PlayerBlockLife instance; private static PlayerBlockLife instance;
private PlayerBlockManager blockManager; private PlayerBlockManager blockManager;
private SkinManager skinManager; private SkinManager skinManager;
private LifeSystem lifeSystem; private LifeSystem lifeSystem;
private ConfigManager configManager; private ConfigManager configManager;
private MessageManager messageManager;
// 在 PlayerBlockLife.java 中添加: /**
* 插件启用时调用,执行初始化操作
@Override *
public void reloadConfig() { * <p>初始化流程:
super.reloadConfig(); * <ol>
configManager.reloadConfig(); * <li>保存默认配置文件</li>
getLogger().info("配置已重新加载"); * <li>初始化所有管理器(注意依赖顺序)</li>
} * <li>加载配置和消息数据</li>
* <li>注册事件监听器</li>
* <li>注册命令执行器</li>
* <li>加载玩家数据和皮肤缓存</li>
* <li>启动定时任务</li>
* </ol>
*
* @see #onDisable()
*/
@Override @Override
public void onEnable() { public void onEnable() {
instance = this; instance = this;
// 第一步:保存默认配置
saveDefaultConfig(); saveDefaultConfig();
// 第二步:初始化管理器(注意顺序!)
this.configManager = new ConfigManager(this); this.configManager = new ConfigManager(this);
this.messageManager = new MessageManager(this);
this.skinManager = new SkinManager(this); this.skinManager = new SkinManager(this);
this.blockManager = new PlayerBlockManager(this, skinManager); this.blockManager = new PlayerBlockManager(this, skinManager);
this.lifeSystem = new LifeSystem(this); this.lifeSystem = new LifeSystem(this);
// 第三步:加载数据(必须在管理器初始化之后)
this.configManager.loadConfig();
this.messageManager.loadMessages();
// 第四步:注册事件监听器
getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerQuitListener(this), this); getServer().getPluginManager().registerEvents(new PlayerQuitListener(this), this);
// 第五步:注册命令
getCommand("setlifeblocks").setExecutor(new SetLifeBlocksCommand(this)); getCommand("setlifeblocks").setExecutor(new SetLifeBlocksCommand(this));
getCommand("checklifeblocks").setExecutor(new CheckLifeBlocksCommand(this)); getCommand("checklifeblocks").setExecutor(new CheckLifeBlocksCommand(this));
getCommand("pblreload").setExecutor(new AdminCommands(this)); getCommand("pblreload").setExecutor(new AdminCommands(this));
getCommand("pbldelete").setExecutor(new AdminCommands(this)); getCommand("pbldelete").setExecutor(new AdminCommands(this));
getCommand("pblrevive").setExecutor(new AdminCommands(this));
getCommand("pblstats").setExecutor(new AdminCommands(this));
// 第六步:加载其他数据
blockManager.loadData(); blockManager.loadData();
skinManager.loadAllSkins(); skinManager.loadAllSkins();
// 第七步:启动定时任务
startScheduler(); startScheduler();
getLogger().info("§a========================================"); getLogger().info("§a========================================");
@@ -48,8 +92,21 @@ public class PlayerBlockLife extends JavaPlugin {
getLogger().info("§a========================================"); getLogger().info("§a========================================");
} }
/**
* 插件禁用时调用,执行清理操作
*
* <p>执行以下清理操作:
* <ul>
* <li>保存玩家方块数据到文件</li>
* <li>保存皮肤缓存数据</li>
* <li>记录插件禁用日志</li>
* </ul>
*
* @see #onEnable()
*/
@Override @Override
public void onDisable() { public void onDisable() {
// 保存数据
if (blockManager != null) { if (blockManager != null) {
blockManager.saveData(); blockManager.saveData();
} }
@@ -59,17 +116,80 @@ public class PlayerBlockLife extends JavaPlugin {
getLogger().info("§cPlayerBlockLife 插件已禁用"); getLogger().info("§cPlayerBlockLife 插件已禁用");
} }
private void startScheduler() { /**
getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { * 重写 reloadConfig 方法,避免循环依赖
blockManager.saveData(); */
skinManager.saveSkinData(); @Override
}, 6000L, 6000L); public void reloadConfig() {
// 只调用父类的reloadConfig不调用configManager的方法
getServer().getScheduler().runTaskTimer(this, () -> { super.reloadConfig();
lifeSystem.checkAllPlayers(); getLogger().info("基础配置文件已重新加载");
}, 200L, 200L);
} }
/**
* 插件的完整重载方法(用于命令)
*
* <p>重新加载所有插件配置和数据,包括:
* <ul>
* <li>主配置文件 (config.yml)</li>
* <li>消息配置文件 (messages.yml)</li>
* <li>玩家方块数据</li>
* <li>皮肤缓存数据</li>
* </ul>
*
* <p>这个方法通常由管理员通过/pblreload命令调用。</p>
*/
public void reloadPluginConfig() {
if (configManager != null) {
configManager.reloadConfig();
}
if (messageManager != null) {
messageManager.reloadMessages();
}
if (blockManager != null) {
blockManager.loadData();
}
if (skinManager != null) {
skinManager.loadAllSkins();
}
getLogger().info("插件配置已完全重载");
}
private void startScheduler() {
// 每5分钟自动保存数据
getServer().getScheduler().runTaskTimerAsynchronously(this, () -> {
if (blockManager != null) {
blockManager.saveData();
}
if (skinManager != null) {
skinManager.saveSkinData();
}
getLogger().info("数据已自动保存");
}, 6000L, 6000L);
// 每10秒检查玩家生命方块
getServer().getScheduler().runTaskTimer(this, () -> {
if (lifeSystem != null) {
lifeSystem.checkAllPlayers();
}
}, 200L, 200L);
// 每分钟清理一次过期缓存
getServer().getScheduler().runTaskTimerAsynchronously(this, () -> {
if (skinManager != null) {
skinManager.cleanupOldCache();
}
}, 1200L, 1200L);
}
/**
* 获取插件单例实例
*
* <p>提供全局访问点,允许其他类访问插件主实例。</p>
*
* @return PlayerBlockLife插件实例
* @throws IllegalStateException 如果插件尚未启用实例为null
*/
public static PlayerBlockLife getInstance() { public static PlayerBlockLife getInstance() {
return instance; return instance;
} }
@@ -90,6 +210,10 @@ public class PlayerBlockLife extends JavaPlugin {
return configManager; return configManager;
} }
public MessageManager getMessageManager() {
return messageManager;
}
public void logInfo(String message) { public void logInfo(String message) {
getLogger().info(message); getLogger().info(message);
} }

View File

@@ -18,6 +18,34 @@ import java.io.FileWriter;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* 玩家方块管理器 - 负责管理玩家生命方块的核心组件
*
* <p>主要职责:
* <ul>
* <li>生成和放置玩家生命方块</li>
* <li>管理方块位置和所有者映射关系</li>
* <li>处理方块破坏和恢复逻辑</li>
* <li>提供方块数据持久化存储</li>
* <li>支持方块位置查询和验证</li>
* <li>与SkinManager协同工作确保方块正确显示玩家皮肤</li>
* </ul>
*
* <p><b>SkinsRestorer集成特性</b>
* <ul>
* <li>通过SkinManager获取SkinsRestorer提供的玩家皮肤纹理</li>
* <li>确保离线服务器上的方块显示正确的自定义皮肤</li>
* <li>支持异步皮肤加载,避免方块放置阻塞</li>
* <li>提供皮肤加载状态检查,确保皮肤就绪后再放置方块</li>
* </ul>
* </p>
*
* <p>使用并发安全的数据结构确保多线程环境下的数据一致性。</p>
*
* @author xiaobai
* @version 2.1.0
* @since 1.0.0
*/
public class PlayerBlockManager { public class PlayerBlockManager {
private final PlayerBlockLife plugin; private final PlayerBlockLife plugin;
private final SkinManager skinManager; private final SkinManager skinManager;
@@ -34,32 +62,72 @@ public class PlayerBlockManager {
} }
/** /**
* 为玩家设置生命方块 * 为玩家设置生命方块(兼容旧方法)
*/ */
public boolean setLifeBlocks(Player player, Location center) { public boolean setLifeBlocks(Player player, Location center) {
ConfigManager config = plugin.getConfigManager();
int blockAmount = config.getBlocksPerPlayer();
int spreadRange = config.getSpreadRange();
boolean requireOpenSky = config.isRequireOpenSky();
int maxAttempts = config.getMaxAttempts();
return generateLifeBlocksForPlayer(player, blockAmount, spreadRange, requireOpenSky, maxAttempts);
}
/**
* 为玩家生成指定数量的生命方块
*
* <p>此方法负责生成玩家的生命方块,包括以下步骤:
* <ol>
* <li>检查玩家是否已有生命方块</li>
* <li>验证玩家皮肤是否已从SkinsRestorer或其他来源加载完成</li>
* <li>在指定范围内寻找合适的放置位置</li>
* <li>放置带有玩家皮肤纹理的玩家头颅方块</li>
* <li>记录方块位置和所有者关系</li>
* <li>保存数据并返回生成结果</li>
* </ol>
* </p>
*
* <p><b>皮肤加载检查:</b>
* <ul>
* <li>调用skinManager.isSkinLoaded()检查皮肤是否就绪</li>
* <li>如果皮肤未加载,方块生成将失败</li>
* <li>确保离线服务器通过SkinsRestorer获取的皮肤能正确应用</li>
* <li>避免放置默认Steve皮肤的方块</li>
* </ul>
* </p>
*
* @param player 目标玩家
* @param blockAmount 要生成的方块数量
* @param spreadRange 生成范围(以玩家为中心的正方形边长的一半)
* @param requireOpenSky 是否需要开阔天空(上方无方块覆盖)
* @param maxAttempts 寻找合适位置的最大尝试次数
* @return 生成成功返回true失败返回false
* @see SkinManager#isSkinLoaded(UUID)
* @see SkinManager#getSkinFromSkinsRestorer(Player)
*/
public boolean generateLifeBlocksForPlayer(Player player, int blockAmount, int spreadRange, boolean requireOpenSky, int maxAttempts) {
UUID playerId = player.getUniqueId(); UUID playerId = player.getUniqueId();
String playerName = player.getName(); String playerName = player.getName();
// 检查是否已有生命方块 // 检查是否已有生命方块
if (hasLifeBlocks(playerId)) { if (hasLifeBlocks(playerId)) {
player.sendMessage("§c你已经有生命方块了");
player.sendMessage("§e使用 /checklifeblocks 查看位置");
return false; return false;
} }
// 检查玩家皮肤是否已加载 // 检查玩家皮肤是否已加载
if (!skinManager.isSkinLoaded(playerId)) { if (!skinManager.isSkinLoaded(playerId)) {
player.sendMessage("§e你的皮肤正在加载中请稍候...");
player.sendMessage("§7(如果长时间未加载完成,请重新加入服务器)");
return false; return false;
} }
List<Location> blocks = new ArrayList<>(); List<Location> blocks = new ArrayList<>();
int blocksPlaced = 0; int blocksPlaced = 0;
int attempts = 0;
// 尝试在中心周围生成5个方块 // 尝试生成指定数量的方块
for (int attempt = 0; attempt < 20 && blocksPlaced < 5; attempt++) { while (blocksPlaced < blockAmount && attempts < maxAttempts) {
Location blockLoc = findSuitableLocation(center); Location blockLoc = findSurfaceLocation(player.getLocation(), spreadRange, requireOpenSky);
attempts++;
if (blockLoc != null && placePlayerHead(blockLoc, playerId, playerName)) { if (blockLoc != null && placePlayerHead(blockLoc, playerId, playerName)) {
blocks.add(blockLoc); blocks.add(blockLoc);
@@ -74,22 +142,73 @@ public class PlayerBlockManager {
if (blocksPlaced > 0) { if (blocksPlaced > 0) {
playerBlocks.put(playerId, blocks); playerBlocks.put(playerId, blocks);
saveData(); saveData();
player.sendMessage("§a========================================");
player.sendMessage("§a成功生成 §e" + blocksPlaced + " §a个生命方块");
player.sendMessage("§6方块使用了你的皮肤头像");
player.sendMessage("§c⚠ 警告: 方块被挖光时,你将死亡!");
player.sendMessage("§7使用 /checklifeblocks 查看方块位置");
player.sendMessage("§a========================================");
return true; return true;
} else { } else {
player.sendMessage("§c无法生成生命方块");
player.sendMessage("§7请确保周围有足够的空间至少5个可放置位置");
return false; return false;
} }
} }
/**
* 寻找地表位置(放宽条件,只要是露天地面就可以)
*/
private Location findSurfaceLocation(Location center, int spreadRange, boolean requireOpenSky) {
for (int i = 0; i < 20; i++) { // 增加尝试次数
int x = random.nextInt(spreadRange * 2 + 1) - spreadRange;
int z = random.nextInt(spreadRange * 2 + 1) - spreadRange;
// 以玩家坐标为中心,但高度从世界最高点开始寻找
Location testLoc = center.clone().add(x, 0, z);
World world = testLoc.getWorld();
if (world == null) continue;
// 从世界最高点向下寻找第一个固体方块
int maxHeight = world.getMaxHeight();
Block groundBlock = null;
for (int y = maxHeight; y > world.getMinHeight(); y--) {
testLoc.setY(y);
Block block = testLoc.getBlock();
Material type = block.getType();
// 检查是否是固体方块(可以作为支撑)
if (type.isSolid() && type.isBlock() && !type.isTransparent()) {
groundBlock = block;
break;
}
}
if (groundBlock == null) continue;
// 地表位置 = 固体方块上方一格
Location surfaceLoc = groundBlock.getLocation().add(0, 1, 0);
// 检查是否已有方块
if (blockOwners.containsKey(surfaceLoc)) {
continue;
}
// 检查位置是否合适(放宽条件)
if (!isSuitableLocationRelaxed(surfaceLoc)) {
continue;
}
// 如果需要上方无方块覆盖检查上方3格
if (requireOpenSky) {
boolean hasCover = false;
for (int y = 1; y <= 3; y++) {
Block aboveBlock = surfaceLoc.clone().add(0, y, 0).getBlock();
if (!aboveBlock.getType().isAir()) {
hasCover = true;
break;
}
}
if (hasCover) continue;
}
return surfaceLoc;
}
return null;
}
/** /**
* 寻找合适的位置 * 寻找合适的位置
*/ */
@@ -175,8 +294,52 @@ public class PlayerBlockManager {
return true; return true;
} }
/**
* 放宽条件的检查方法(用于新的生成逻辑)
*/
private boolean isSuitableLocationRelaxed(Location location) {
Block block = location.getBlock();
// 检查是否已有方块
if (blockOwners.containsKey(location)) {
return false;
}
// 检查方块是否可替换(放宽条件)
Material type = block.getType();
if (!type.isAir()) {
// 固体方块不能替换
if (type.isSolid()) {
return false;
}
// 液体方块不能替换
if (type == Material.WATER || type == Material.LAVA) {
return false;
}
}
// 检查下方是否有支撑方块(放宽条件)
Block below = location.clone().add(0, -1, 0).getBlock();
Material belowType = below.getType();
// 只要下方不是空气或液体就可以
if (belowType.isAir() || belowType == Material.WATER || belowType == Material.LAVA) {
return false;
}
return true;
}
/** /**
* 放置玩家头颅方块 * 放置玩家头颅方块
*
* <p>使用SkinManager创建带有正确皮肤的玩家头颅方块支持离线服务器皮肤显示。</p>
*
* @param location 放置位置
* @param playerId 玩家UUID
* @param playerName 玩家名称
* @return 放置成功返回true失败返回false
*/ */
private boolean placePlayerHead(Location location, UUID playerId, String playerName) { private boolean placePlayerHead(Location location, UUID playerId, String playerName) {
try { try {
@@ -187,15 +350,37 @@ public class PlayerBlockManager {
return false; return false;
} }
// 检查玩家皮肤是否已加载
if (!skinManager.isSkinLoaded(playerId)) {
plugin.logWarning("玩家 " + playerName + " 的皮肤未加载,无法放置头颅方块");
return false;
}
// 设置方块为玩家头颅 // 设置方块为玩家头颅
block.setType(Material.PLAYER_HEAD); block.setType(Material.PLAYER_HEAD);
// 获取并设置头颅数据 // 获取并设置头颅数据
Skull skullState = (Skull) block.getState(); Skull skullState = (Skull) block.getState();
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
// 设置头颅所有者 // 使用SkinManager创建玩家头颅物品然后应用到方块上
ItemStack headItem = skinManager.createPlayerHead(playerId, playerName);
SkullMeta itemMeta = (SkullMeta) headItem.getItemMeta();
if (itemMeta != null) {
// 获取物品的玩家档案并应用到方块上
org.bukkit.profile.PlayerProfile profile = itemMeta.getPlayerProfile();
if (profile != null) {
skullState.setOwnerProfile(profile);
} else {
// 如果无法获取档案,回退到使用离线玩家
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
skullState.setOwningPlayer(offlinePlayer); skullState.setOwningPlayer(offlinePlayer);
}
} else {
// 如果物品元数据为空,使用离线玩家
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
skullState.setOwningPlayer(offlinePlayer);
}
// 设置朝向(随机方向) // 设置朝向(随机方向)
BlockData blockData = block.getBlockData(); BlockData blockData = block.getBlockData();
@@ -209,6 +394,7 @@ public class PlayerBlockManager {
// 更新方块 // 更新方块
skullState.update(true, false); skullState.update(true, false);
plugin.logInfo("成功放置玩家头颅方块: " + playerName + "" + location);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.logError("放置玩家头颅失败: " + location, e); plugin.logError("放置玩家头颅失败: " + location, e);

View File

@@ -9,6 +9,8 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class PlayerJoinListener implements Listener { public class PlayerJoinListener implements Listener {
@@ -26,34 +28,141 @@ public class PlayerJoinListener implements Listener {
// 异步加载玩家皮肤 // 异步加载玩家皮肤
plugin.getSkinManager().loadPlayerSkinAsync(player); plugin.getSkinManager().loadPlayerSkinAsync(player);
// 延迟发送消息,确保皮肤加载完成 // 延迟执行,确保皮肤加载完成
Bukkit.getScheduler().runTaskLater(plugin, () -> { Bukkit.getScheduler().runTaskLater(plugin, () -> {
int remainingBlocks = plugin.getBlockManager().getRemainingBlocks(playerId); int remainingBlocks = plugin.getBlockManager().getRemainingBlocks(playerId);
if (remainingBlocks > 0) { // 检查是否启用自动生成
if (plugin.getConfigManager().isAutoGenerationEnabled() && remainingBlocks == 0) {
// 自动生成生命方块
generateLifeBlocksForPlayer(player);
} else if (remainingBlocks > 0) {
// 已有方块,显示欢迎信息
Map<String, String> variables = new HashMap<>();
variables.put("player", player.getName());
variables.put("remaining", String.valueOf(remainingBlocks));
variables.put("total", String.valueOf(plugin.getConfigManager().getBlocksPerPlayer()));
String welcomeMsg = plugin.getMessageManager().getFormattedMessage("game.player.welcome_back",
"&e欢迎回来&6{player}&e", variables);
String blocksMsg = plugin.getMessageManager().getFormattedMessage("game.player.remaining_blocks",
"&7你还有 &e{remaining} &7个生命方块", variables);
String checkMsg = plugin.getMessageManager().getMessage("game.player.check_blocks_hint",
"&7使用 &e/checklifeblocks &7查看方块位置");
player.sendMessage("§a========================================"); player.sendMessage("§a========================================");
player.sendMessage("§e欢迎回来§6" + player.getName() + "§e"); player.sendMessage(welcomeMsg);
player.sendMessage("§7你还有 §e" + remainingBlocks + " §7个生命方块"); player.sendMessage(blocksMsg);
player.sendMessage("§7使用 §e/checklifeblocks §7查看方块位置"); player.sendMessage(checkMsg);
player.sendMessage("§a========================================"); player.sendMessage("§a========================================");
if (remainingBlocks <= 2) { if (remainingBlocks <= 2) {
player.sendMessage("§c⚠ 警告!你的生命方块即将耗尽!"); String warningMsg = plugin.getMessageManager().getMessage("game.block.warning_low_blocks",
"&c⚠ 警告!你的生命方块即将耗尽!");
player.sendMessage(warningMsg);
player.playSound(player.getLocation(), player.playSound(player.getLocation(),
org.bukkit.Sound.ENTITY_WITHER_SPAWN, 0.5f, 1.0f); org.bukkit.Sound.ENTITY_WITHER_SPAWN, 0.5f, 1.0f);
} }
} else { } else {
player.sendMessage("§e欢迎加入游戏"); // 没有方块且自动生成未启用
player.sendMessage("§7使用 §e/setlifeblocks §7来设置你的生命方块"); String welcomeMsg = plugin.getMessageManager().getMessage("game.player.welcome_new",
player.sendMessage("§6游戏规则"); "&e欢迎加入游戏");
player.sendMessage("§7- 每个玩家有5个生命方块"); String setBlocksMsg = plugin.getMessageManager().getMessage("game.player.set_blocks_hint",
player.sendMessage("§7- 方块被其他玩家挖光时,你将死亡"); "&7使用 &e/setlifeblocks &7来设置你的生命方块");
player.sendMessage("§7- 方块使用你的皮肤作为材质"); String rulesTitle = plugin.getMessageManager().getMessage("game.player.rules_title",
player.sendMessage("§7- 你可以自由移动,但方块固定位置"); "&6游戏规则");
String rule1 = plugin.getMessageManager().getMessage("game.player.rule1",
"&7- 每个玩家有5个生命方块");
String rule2 = plugin.getMessageManager().getMessage("game.player.rule2",
"&7- 方块被其他玩家挖光时,你将死亡");
String rule3 = plugin.getMessageManager().getMessage("game.player.rule3",
"&7- 方块使用你的皮肤作为材质");
String rule4 = plugin.getMessageManager().getMessage("game.player.rule4",
"&7- 你可以自由移动,但方块固定位置");
player.sendMessage(welcomeMsg);
player.sendMessage(setBlocksMsg);
player.sendMessage(rulesTitle);
player.sendMessage(rule1);
player.sendMessage(rule2);
player.sendMessage(rule3);
player.sendMessage(rule4);
} }
}, 40L); }, 40L);
} }
/**
* 为玩家自动生成生命方块
*/
private void generateLifeBlocksForPlayer(Player player) {
try {
// 获取配置
ConfigManager config = plugin.getConfigManager();
int blockAmount = config.getBlocksPerPlayer();
int spreadRange = config.getSpreadRange();
boolean requireOpenSky = config.isRequireOpenSky();
int maxAttempts = config.getMaxAttempts();
// 调用方块管理器生成方块
boolean success = plugin.getBlockManager().generateLifeBlocksForPlayer(
player, blockAmount, spreadRange, requireOpenSky, maxAttempts
);
if (success) {
// 发送成功消息
String message = plugin.getMessageManager().getMessage("console.blocks_generated",
"&a[PlayerBlockLife] 已为玩家 {player} 生成 {amount} 个生命方块");
message = message.replace("{player}", player.getName())
.replace("{amount}", String.valueOf(blockAmount));
// 移除颜色代码用于日志
String logMessage = message.replace("&", "");
plugin.getLogger().info(logMessage);
// 发送给玩家
String playerMsg = plugin.getMessageManager().getMessage("game.block.placed",
"&a已为你生成 {amount} 个生命方块!");
playerMsg = playerMsg.replace("{amount}", String.valueOf(blockAmount))
.replace("&", "§");
player.sendMessage(playerMsg);
} else {
// 生成失败
String failureMsg = plugin.getMessageManager().getMessage("console.error_generating_blocks",
"&c[PlayerBlockLife] 为玩家 {player} 生成生命方块时失败");
failureMsg = failureMsg.replace("{player}", player.getName());
// 移除颜色代码用于日志
String logFailureMsg = failureMsg.replace("&", "");
plugin.getLogger().warning(logFailureMsg);
// 根据配置处理失败
String onFailure = config.getOnFailureAction();
if (onFailure.equals("notify")) {
String notifyMsg = plugin.getMessageManager().getMessage("game.errors.cannot_generate_blocks",
"&c无法生成生命方块{reason}");
notifyMsg = notifyMsg.replace("{reason}", "找不到合适的位置尝试了20次")
.replace("&", "§");
player.sendMessage(notifyMsg);
player.sendMessage("§7请手动使用 §e/setlifeblocks §7命令生成方块");
} else if (onFailure.equals("teleport_to_spawn")) {
player.teleport(player.getWorld().getSpawnLocation());
player.sendMessage("§e已将你传送至出生点请手动生成方块");
}
}
} catch (Exception e) {
plugin.getLogger().severe("为玩家 " + player.getName() + " 生成生命方块时发生错误: " + e.getMessage());
e.printStackTrace();
String errorMsg = plugin.getMessageManager().getMessage("console.error_generating_blocks",
"&c[PlayerBlockLife] 为玩家 {player} 生成生命方块时出错: {error}");
if (errorMsg != null) {
String errorDetail = e.getMessage() != null ? e.getMessage() : "未知错误";
errorMsg = errorMsg.replace("{player}", player.getName())
.replace("{error}", errorDetail)
.replace("&", "§");
plugin.getLogger().severe(errorMsg);
}
}
}
@EventHandler @EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) { public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();

View File

@@ -18,8 +18,18 @@ public class SetLifeBlocksCommand implements CommandExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
// 检查命令是否启用
if (!plugin.getConfigManager().isCommandEnabled("setlifeblocks")) {
String message = plugin.getMessageManager().getMessage("game.errors.command_disabled",
"&c此命令已被禁用");
sender.sendMessage(message);
return true;
}
if (!(sender instanceof Player)) { if (!(sender instanceof Player)) {
sender.sendMessage("§c只有玩家可以使用此命令"); String message = plugin.getMessageManager().getMessage("game.errors.no_permission",
"&c只有玩家可以使用此命令");
sender.sendMessage(message);
return true; return true;
} }
@@ -31,22 +41,42 @@ public class SetLifeBlocksCommand implements CommandExecutor {
return true; return true;
} }
if (args.length > 0 && player.hasPermission("playerblocklife.admin")) { // 检查管理员权限和配置
if (args[0].equalsIgnoreCase("other")) { if (args.length > 0 && args[0].equalsIgnoreCase("other")) {
// 检查是否允许管理员使用
if (!plugin.getConfigManager().isAdminUseAllowed("setlifeblocks") || !player.hasPermission("playerblocklife.admin")) {
String message = plugin.getMessageManager().getMessage("game.errors.no_permission",
"&c你没有权限使用此命令");
player.sendMessage(message);
return true;
}
if (args.length < 2) { if (args.length < 2) {
player.sendMessage("§c用法: /setlifeblocks other <玩家名>"); String usage = plugin.getMessageManager().getCommandMessage("setlifeblocks", "usage",
"&c用法: /setlifeblocks [reset|other|help]");
player.sendMessage(usage);
return true; return true;
} }
Player target = Bukkit.getPlayer(args[1]); Player target = Bukkit.getPlayer(args[1]);
if (target == null) { if (target == null) {
player.sendMessage("§c找不到玩家: " + args[1]); String message = plugin.getMessageManager().getMessage("game.errors.player_not_found",
"&c找不到玩家: {player}");
message = message.replace("{player}", args[1]);
player.sendMessage(message);
return true; return true;
} }
setBlocksForPlayer(target, player); setBlocksForPlayer(target, player);
return true; return true;
} }
// 检查是否允许玩家自己使用
if (!plugin.getConfigManager().isSelfUseAllowed("setlifeblocks")) {
String message = plugin.getMessageManager().getMessage("game.errors.no_permission",
"&c你没有权限使用此命令");
player.sendMessage(message);
return true;
} }
setBlocksForPlayer(player, null); setBlocksForPlayer(player, null);
@@ -55,21 +85,28 @@ public class SetLifeBlocksCommand implements CommandExecutor {
private void setBlocksForPlayer(Player target, Player executor) { private void setBlocksForPlayer(Player target, Player executor) {
UUID targetId = target.getUniqueId(); UUID targetId = target.getUniqueId();
MessageManager msgManager = plugin.getMessageManager();
if (plugin.getBlockManager().hasLifeBlocks(targetId)) { if (plugin.getBlockManager().hasLifeBlocks(targetId)) {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§c玩家 " + target.getName() + " 已经有生命方块了!"); String message = msgManager.getMessage("game.errors.player_already_has_blocks",
"&c玩家 {player} 已经有生命方块了!");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else { } else {
target.sendMessage("§c你已经有生命方块了"); String alreadyHas = msgManager.getCommandMessage("setlifeblocks", "already_has",
target.sendMessage("§e使用 /checklifeblocks 查看位置"); "&c你已经有生命方块了使用 /checklifeblocks 查看位置");
target.sendMessage("§e使用 /setlifeblocks reset 重置方块位置"); target.sendMessage(alreadyHas);
} }
return; return;
} }
if (!plugin.getSkinManager().isSkinLoaded(targetId)) { if (!plugin.getSkinManager().isSkinLoaded(targetId)) {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§e玩家 " + target.getName() + " 的皮肤正在加载中,请稍候..."); String message = msgManager.getMessage("game.skin_loading",
"&e玩家 {player} 的皮肤正在加载中,请稍候...");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else { } else {
target.sendMessage("§e你的皮肤正在加载中请稍候..."); target.sendMessage("§e你的皮肤正在加载中请稍候...");
target.sendMessage("§7(如果长时间未加载完成,请重新加入服务器)"); target.sendMessage("§7(如果长时间未加载完成,请重新加入服务器)");
@@ -81,11 +118,23 @@ public class SetLifeBlocksCommand implements CommandExecutor {
boolean success = plugin.getBlockManager().setLifeBlocks(target, target.getLocation()); boolean success = plugin.getBlockManager().setLifeBlocks(target, target.getLocation());
if (success) { if (success) {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§a已为玩家 " + target.getName() + " 生成生命方块!"); String message = msgManager.getMessage("game.blocks_generated_for_other",
"&a已为玩家 {player} 生成生命方块!");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else {
String successMsg = msgManager.getCommandMessage("setlifeblocks", "success",
"&a已为你生成 {blocks} 个生命方块!");
ConfigManager config = plugin.getConfigManager();
successMsg = successMsg.replace("{blocks}", String.valueOf(config.getBlocksPerPlayer()));
target.sendMessage(successMsg);
} }
} else { } else {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§c为玩家 " + target.getName() + " 生成生命方块失败!"); String message = msgManager.getMessage("game.errors.failed_to_generate_blocks",
"&c为玩家 {player} 生成生命方块失败!");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else { } else {
target.sendMessage("§c生成失败请稍后再试或联系管理员"); target.sendMessage("§c生成失败请稍后再试或联系管理员");
} }
@@ -99,11 +148,23 @@ public class SetLifeBlocksCommand implements CommandExecutor {
if (success) { if (success) {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§a已为玩家 " + target.getName() + " 生成生命方块!"); String message = msgManager.getMessage("game.blocks_generated_for_other",
"&a已为玩家 {player} 生成生命方块!");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else {
String successMsg = msgManager.getCommandMessage("setlifeblocks", "success",
"&a已为你生成 {blocks} 个生命方块!");
ConfigManager config = plugin.getConfigManager();
successMsg = successMsg.replace("{blocks}", String.valueOf(config.getBlocksPerPlayer()));
target.sendMessage(successMsg);
} }
} else { } else {
if (executor != null && !targetId.equals(executor.getUniqueId())) { if (executor != null && !targetId.equals(executor.getUniqueId())) {
executor.sendMessage("§c为玩家 " + target.getName() + " 生成生命方块失败!"); String message = msgManager.getMessage("game.errors.failed_to_generate_blocks",
"&c为玩家 {player} 生成生命方块失败!");
message = message.replace("{player}", target.getName());
executor.sendMessage(message);
} else { } else {
target.sendMessage("§c生成失败请确保周围有足够空间"); target.sendMessage("§c生成失败请确保周围有足够空间");
} }
@@ -111,22 +172,13 @@ public class SetLifeBlocksCommand implements CommandExecutor {
} }
private void showHelp(Player player) { private void showHelp(Player player) {
player.sendMessage("§a========== PlayerBlockLife 帮助 =========="); String helpMessage = plugin.getMessageManager().getCommandMessage("setlifeblocks", "help",
player.sendMessage("§e/setlifeblocks §7- 设置你的生命方块"); "&6=== PlayerBlockLife 帮助 ===\n" +
player.sendMessage("§e/checklifeblocks §7- 查看你的生命方块位置"); "&e/setlifeblocks &7- 设置你的生命方块\n" +
player.sendMessage("§e/setlifeblocks reset §7- 重置生命方块位置"); "&e/setlifeblocks reset &7- 重置生命方块位置\n" +
"&e/setlifeblocks other <玩家> &7- 为其他玩家设置(管理员)\n" +
"&e/setlifeblocks help &7- 显示此帮助");
if (player.hasPermission("playerblocklife.admin")) { player.sendMessage(helpMessage);
player.sendMessage("§6管理员命令:");
player.sendMessage("§e/setlifeblocks other <玩家> §7- 为其他玩家设置生命方块");
player.sendMessage("§e/pblreload §7- 重载插件配置");
player.sendMessage("§e/pbldelete <玩家> §7- 删除玩家的生命方块");
}
player.sendMessage("§7游戏规则:");
player.sendMessage("§7- 每个玩家有5个生命方块");
player.sendMessage("§7- 方块被挖光时,玩家死亡");
player.sendMessage("§7- 方块使用玩家的皮肤作为材质");
player.sendMessage("§a======================================");
} }
} }

View File

@@ -19,6 +19,42 @@ import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* 皮肤管理器 - 负责玩家皮肤的获取、缓存和应用
*
* <p>主要功能:
* <ul>
* <li>从多种来源获取玩家皮肤数据SkinsRestorer插件、PlayerProfile、本地缓存</li>
* <li>皮肤数据Base64编码和缓存管理</li>
* <li>自定义模型数据分配和管理</li>
* <li>异步皮肤加载避免阻塞主线程</li>
* <li>皮肤缓存过期清理</li>
* <li>完整的SkinsRestorer插件集成支持</li>
* </ul>
*
* <p>皮肤获取优先级根据配置的source字段
* <ol>
* <li><b>skinsrestorer</b>优先从SkinsRestorer插件获取皮肤纹理数据</li>
* <li><b>player_profile</b>优先使用Bukkit的PlayerProfile API</li>
* <li><b>local_cache</b>:优先从本地缓存加载</li>
* <li>默认Steve皮肤所有来源都失败时的备用</li>
* </ol>
*
* <p><b>SkinsRestorer集成特性</b>
* <ul>
* <li>自动检测SkinsRestorer插件是否安装</li>
* <li>使用反射安全调用SkinsRestorer API避免硬依赖</li>
* <li>获取完整的皮肤纹理数据value和signature</li>
* <li>支持离线服务器避免默认Steve皮肤问题</li>
* <li>优雅降级SkinsRestorer失败时自动回退到其他来源</li>
* </ul>
*
* <p>皮肤缓存默认保留7天过期后自动重新获取。</p>
*
* @author xiaobai
* @version 2.1.0
* @since 1.0.0
*/
public class SkinManager { public class SkinManager {
private final PlayerBlockLife plugin; private final PlayerBlockLife plugin;
private final Map<UUID, String> playerSkinData = new ConcurrentHashMap<>(); private final Map<UUID, String> playerSkinData = new ConcurrentHashMap<>();
@@ -60,11 +96,41 @@ public class SkinManager {
try { try {
plugin.logInfo("开始加载皮肤: " + player.getName()); plugin.logInfo("开始加载皮肤: " + player.getName());
String skinBase64 = getSkinFromPlayerProfile(player); String skinBase64 = null;
String skinSource = plugin.getConfigManager().getSkinSource();
// 根据配置的皮肤来源优先级获取皮肤
if ("skinsrestorer".equalsIgnoreCase(skinSource)) {
// 优先尝试SkinsRestorer
skinBase64 = getSkinFromSkinsRestorer(player);
if (skinBase64 == null) {
plugin.logInfo("SkinsRestorer获取失败尝试PlayerProfile: " + player.getName());
skinBase64 = getSkinFromPlayerProfile(player);
}
} else if ("player_profile".equalsIgnoreCase(skinSource)) {
// 优先尝试PlayerProfile
skinBase64 = getSkinFromPlayerProfile(player);
if (skinBase64 == null && plugin.getConfigManager().useSkinsRestorer()) {
plugin.logInfo("PlayerProfile获取失败尝试SkinsRestorer: " + player.getName());
skinBase64 = getSkinFromSkinsRestorer(player);
}
} else if ("local_cache".equalsIgnoreCase(skinSource)) {
// 优先从本地缓存加载
if (loadSkinFromCache(playerId)) {
plugin.logInfo("从缓存加载皮肤: " + player.getName());
return;
}
// 缓存不存在,尝试其他来源
skinBase64 = getSkinFromPlayerProfile(player);
if (skinBase64 == null && plugin.getConfigManager().useSkinsRestorer()) {
skinBase64 = getSkinFromSkinsRestorer(player);
}
}
// 如果所有来源都失败使用默认Steve皮肤
if (skinBase64 == null) { if (skinBase64 == null) {
skinBase64 = getDefaultSteveSkin(); skinBase64 = getDefaultSteveSkin();
plugin.logWarning("使用默认皮肤: " + player.getName()); plugin.logWarning("所有皮肤来源都失败使用默认Steve皮肤: " + player.getName());
} }
if (skinBase64 != null) { if (skinBase64 != null) {
@@ -87,6 +153,104 @@ public class SkinManager {
}); });
} }
/**
* 从SkinsRestorer插件获取玩家皮肤纹理数据
*
* <p>SkinsRestorer是一个流行的皮肤管理插件可以在离线服务器上提供皮肤支持。</p>
*
* <p>此方法使用反射安全调用SkinsRestorer API避免硬依赖。支持离线服务器获取玩家自定义皮肤。</p>
*
* <p>获取流程:
* <ol>
* <li>检查SkinsRestorer插件是否安装</li>
* <li>使用反射获取SkinsRestorer API实例</li>
* <li>优先通过UUID获取皮肤数据更可靠</li>
* <li>如果UUID获取失败回退到使用玩家名获取</li>
* <li>提取皮肤纹理的value和signature字段</li>
* <li>构建完整的Base64编码纹理JSON</li>
* </ol>
* </p>
*
* <p><b>离线服务器优势:</b>
* <ul>
* <li>即使玩家离线也能获取其预设皮肤</li>
* <li>避免总是显示默认Steve皮肤的问题</li>
* <li>支持管理员设置的皮肤和玩家自定义皮肤</li>
* </ul>
* </p>
*
* @param player 要获取皮肤的玩家对象
* @return 完整的Base64编码皮肤纹理JSON如果获取失败返回null
* @throws ClassNotFoundException 如果SkinsRestorer API类未找到插件未安装
* @throws Exception 反射调用过程中的其他异常
*/
private String getSkinFromSkinsRestorer(Player player) {
try {
// 检查SkinsRestorer插件是否存在
if (Bukkit.getPluginManager().getPlugin("SkinsRestorer") == null) {
plugin.logInfo("SkinsRestorer插件未安装跳过从SkinsRestorer获取皮肤");
return null;
}
plugin.logInfo("尝试从SkinsRestorer获取皮肤: " + player.getName());
// 使用反射调用SkinsRestorer API
Class<?> skinsRestorerClass = Class.forName("net.skinsrestorer.api.SkinsRestorerAPI");
Object skinsRestorerAPI = skinsRestorerClass.getMethod("getApi").invoke(null);
// 获取玩家皮肤数据 - 使用UUID而不是玩家名更可靠
Class<?> skinPropertyClass = Class.forName("net.skinsrestorer.api.property.SkinProperty");
Object skinProperty = skinsRestorerAPI.getClass().getMethod("getSkinData", UUID.class)
.invoke(skinsRestorerAPI, player.getUniqueId());
// 如果通过UUID获取失败尝试使用玩家名
if (skinProperty == null) {
skinProperty = skinsRestorerAPI.getClass().getMethod("getSkinData", String.class)
.invoke(skinsRestorerAPI, player.getName());
}
if (skinProperty != null) {
String value = (String) skinPropertyClass.getMethod("getValue").invoke(skinProperty);
String signature = (String) skinPropertyClass.getMethod("getSignature").invoke(skinProperty);
// 创建完整的纹理JSON对象
JsonObject textureJson = new JsonObject();
JsonObject texturesJson = new JsonObject();
JsonObject skinJson = new JsonObject();
skinJson.addProperty("url", "http://textures.minecraft.net/texture/" + value);
texturesJson.add("SKIN", skinJson);
textureJson.add("textures", texturesJson);
// 添加时间戳确保唯一性
textureJson.addProperty("timestamp", System.currentTimeMillis());
String base64Texture = java.util.Base64.getEncoder().encodeToString(textureJson.toString().getBytes());
plugin.logInfo("成功从SkinsRestorer获取皮肤: " + player.getName());
return base64Texture;
} else {
plugin.logInfo("SkinsRestorer中没有找到玩家 " + player.getName() + " 的皮肤数据");
}
} catch (ClassNotFoundException e) {
plugin.logWarning("SkinsRestorer API类未找到插件可能未安装或版本不兼容: " + e.getMessage());
} catch (NoSuchMethodException e) {
plugin.logWarning("SkinsRestorer API方法未找到可能是版本不兼容: " + e.getMessage());
} catch (Exception e) {
plugin.logWarning("从SkinsRestorer获取皮肤失败: " + e.getMessage());
}
return null;
}
/**
* 从PlayerProfile获取玩家皮肤数据
*
* <p>使用Bukkit的PlayerProfile API获取在线玩家的皮肤URL然后转换为Base64纹理。</p>
*
* @param player 要获取皮肤的玩家
* @return 皮肤的Base64纹理值如果获取失败返回null
*/
private String getSkinFromPlayerProfile(Player player) { private String getSkinFromPlayerProfile(Player player) {
try { try {
PlayerProfile profile = player.getPlayerProfile(); PlayerProfile profile = player.getPlayerProfile();
@@ -94,30 +258,27 @@ public class SkinManager {
URL skinUrl = textures.getSkin(); URL skinUrl = textures.getSkin();
if (skinUrl != null) { if (skinUrl != null) {
BufferedImage skinImage = ImageIO.read(skinUrl); // 创建纹理JSON对象
if (skinImage == null) {
return null;
}
int headX = 8;
int headY = 8;
int headWidth = 8;
int headHeight = 8;
if (skinImage.getWidth() >= headX + headWidth &&
skinImage.getHeight() >= headY + headHeight) {
BufferedImage headImage = skinImage.getSubimage(headX, headY, headWidth, headHeight);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(headImage, "PNG", baos);
byte[] imageBytes = baos.toByteArray();
JsonObject textureJson = new JsonObject(); JsonObject textureJson = new JsonObject();
JsonObject timestampJson = new JsonObject(); JsonObject texturesJson = new JsonObject();
JsonObject skinJson = new JsonObject();
return player.getUniqueId().toString(); // 直接使用皮肤URL不需要处理图像
} skinJson.addProperty("url", skinUrl.toString());
texturesJson.add("SKIN", skinJson);
textureJson.add("textures", texturesJson);
// 添加时间戳确保唯一性
textureJson.addProperty("timestamp", System.currentTimeMillis());
textureJson.addProperty("profileId", player.getUniqueId().toString());
textureJson.addProperty("profileName", player.getName());
String base64Texture = java.util.Base64.getEncoder().encodeToString(textureJson.toString().getBytes());
plugin.logInfo("成功从PlayerProfile获取皮肤: " + player.getName());
return base64Texture;
} else {
plugin.logInfo("PlayerProfile中没有找到玩家 " + player.getName() + " 的皮肤URL");
} }
} catch (Exception e) { } catch (Exception e) {
plugin.logWarning("从PlayerProfile获取皮肤失败: " + e.getMessage()); plugin.logWarning("从PlayerProfile获取皮肤失败: " + e.getMessage());
@@ -163,23 +324,92 @@ public class SkinManager {
return modelData; return modelData;
} }
/**
* 获取默认Steve皮肤的Base64纹理
*
* <p>当无法从任何来源获取玩家皮肤时使用默认的Steve皮肤作为备用。</p>
*
* @return 默认Steve皮肤的Base64纹理值
*/
private String getDefaultSteveSkin() { private String getDefaultSteveSkin() {
return "8667ba71-b85a-4004-af54-457a9734eed7"; // Steve皮肤的纹理哈希值
String steveTextureHash = "8667ba71b85a4004af54457a9734eed7";
JsonObject textureJson = new JsonObject();
JsonObject texturesJson = new JsonObject();
JsonObject skinJson = new JsonObject();
// 使用Minecraft官方纹理服务器URL
skinJson.addProperty("url", "http://textures.minecraft.net/texture/" + steveTextureHash);
texturesJson.add("SKIN", skinJson);
textureJson.add("textures", texturesJson);
// 添加时间戳
textureJson.addProperty("timestamp", System.currentTimeMillis());
textureJson.addProperty("profileId", "c06f8906-4c8a-4911-9c29-ea1dbd1aab82"); // Steve的UUID
textureJson.addProperty("profileName", "Steve");
return java.util.Base64.getEncoder().encodeToString(textureJson.toString().getBytes());
} }
/**
* 创建带有玩家皮肤的头颅物品
*
* <p>使用Base64纹理数据创建自定义玩家头颅支持离线服务器皮肤显示。</p>
*
* @param playerId 玩家UUID
* @param playerName 玩家名称
* @return 带有玩家皮肤的玩家头颅物品
*/
public ItemStack createPlayerHead(UUID playerId, String playerName) { public ItemStack createPlayerHead(UUID playerId, String playerName) {
ItemStack head = new ItemStack(org.bukkit.Material.PLAYER_HEAD); ItemStack head = new ItemStack(org.bukkit.Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) head.getItemMeta(); SkullMeta meta = (SkullMeta) head.getItemMeta();
if (meta != null) { if (meta != null) {
// 获取玩家的Base64皮肤数据
String skinBase64 = playerSkinData.get(playerId);
if (skinBase64 != null) {
try {
// 对于Paper API我们需要使用Paper特定的方法
// 首先尝试使用Paper的ProfileProperty API
Class<?> propertyClass = Class.forName("com.destroystokyo.paper.profile.ProfileProperty");
// 创建ProfileProperty对象
Object property = propertyClass.getConstructor(String.class, String.class)
.newInstance("textures", skinBase64);
// 获取Paper的PlayerProfile
Object profile = Bukkit.class.getMethod("createProfile", UUID.class, String.class)
.invoke(null, playerId, playerName);
// 设置属性到档案
Class<?> profileClass = profile.getClass();
profileClass.getMethod("setProperty", propertyClass).invoke(profile, property);
// 设置头颅的所有者档案 - 使用正确的类型转换
// Paper的SkullMeta.setPlayerProfile期望com.destroystokyo.paper.profile.PlayerProfile
meta.setPlayerProfile((com.destroystokyo.paper.profile.PlayerProfile) profile);
} catch (Exception e) {
// 如果反射失败,回退到使用离线玩家(可能显示默认皮肤)
plugin.logWarning("无法设置Base64皮肤使用离线玩家档案: " + e.getMessage());
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId); OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
meta.setOwningPlayer(offlinePlayer); meta.setOwningPlayer(offlinePlayer);
}
} else {
// 没有皮肤数据,使用离线玩家
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
meta.setOwningPlayer(offlinePlayer);
}
// 设置自定义模型数据(如果有)
Integer customModelData = playerCustomModelData.get(playerId); Integer customModelData = playerCustomModelData.get(playerId);
if (customModelData != null) { if (customModelData != null) {
meta.setCustomModelData(customModelData); meta.setCustomModelData(customModelData);
} }
// 设置显示名称和描述
meta.setDisplayName("§e" + playerName + "的生命方块"); meta.setDisplayName("§e" + playerName + "的生命方块");
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();

View File

@@ -1,5 +1,5 @@
# PlayerBlockLife 配置文件 # PlayerBlockLife 配置文件
# 版本: 1.0.0 config-version: 1
# 方块设置 # 方块设置
blocks: blocks:
@@ -7,31 +7,12 @@ blocks:
amount: 5 amount: 5
# 方块生成范围(以玩家为中心的正方形边长的一半) # 方块生成范围(以玩家为中心的正方形边长的一半)
spread: 5 spread: 5
# 方块生成最小范围(方块之间最小距离,单位:格)
min-distance: 10
# 方块埋藏深度0为地面负数为地下 # 方块埋藏深度0为地面负数为地下
depth: -1 depth: -1
# 方块材质类型 # 方块材质类型
# 可选值: player_head, custom_block, default
material: player_head material: player_head
# 是否在生成方块时自动填充周围的方块
fill_around: true
# 当使用玩家头颅时的设置
player_head:
# 是否随机旋转头颅方向
random_rotation: true
# 是否显示玩家名字
show_player_name: true
# 是否显示特殊效果
show_effects: true
# 当使用自定义方块时的设置
custom_block:
# 自定义方块材质
material: DIAMOND_BLOCK
# 是否发光
glowing: true
# 发光等级 (0-15)
light_level: 3
# 游戏规则 # 游戏规则
game: game:
@@ -39,59 +20,8 @@ game:
die_when_blocks_gone: true die_when_blocks_gone: true
# 死亡后是否变成观察者 # 死亡后是否变成观察者
become_spectator: true become_spectator: true
# 观察者模式
spectator_mode:
# 是否可以飞行
can_fly: true
# 是否可以看到其他玩家
can_see_players: true
# 是否可以穿墙
can_clip: true
# 是否启用生命值系统 # 是否启用生命值系统
health_system: true health_system: true
# 生命值计算公式
health_formula:
# 方块数量与生命值的关系
# 格式: 方块数量:生命值
5: 20
4: 16
3: 12
2: 8
1: 4
0: 0
# 状态效果
status_effects:
# 当剩余2个方块时
two_blocks_left:
- type: SLOW
amplifier: 1
duration: 100
# 当剩余1个方块时
one_block_left:
- type: SLOW
amplifier: 2
duration: 100
- type: WEAKNESS
amplifier: 0
duration: 100
- type: BLINDNESS
amplifier: 0
duration: 100
# 挖掘奖励
break_rewards:
# 是否给予经验
give_exp: true
# 经验数量
exp_amount: 5
# 是否给予物品
give_items: false
# 物品列表
items:
- DIAMOND:1
- GOLD_INGOT:3
# 广播设置 # 广播设置
broadcast: broadcast:
@@ -101,58 +31,37 @@ game:
range: 30 range: 30
# 玩家死亡时是否全服广播 # 玩家死亡时是否全服广播
on_player_death: true on_player_death: true
# 新玩家加入时是否广播
on_player_join: false # 挖掘奖励
break_rewards:
# 是否给予经验
give_exp: true
# 经验数量
exp_amount: 5
# 皮肤系统 # 皮肤系统
skin: skin:
# 是否启用皮肤系统 # 是否启用皮肤系统
enabled: true enabled: true
# 皮肤来源 # 皮肤来源 (player_profile, local_cache, skinsrestorer)
source: player_profile # player_profile: 使用Bukkit的PlayerProfile API需要在线验证
# 可选值: player_profile, mojang_api, local_cache # local_cache: 使用本地缓存的皮肤数据
# player_profile: 从玩家本地缓存获取推荐不调用外部API # skinsrestorer: 使用SkinsRestorer插件的皮肤数据推荐用于离线服务器
# mojang_api: 从Mojang API获取需要网络 source: skinsrestorer
# local_cache: 从本地缓存获取
# 是否使用SkinsRestorer插件的皮肤如果服务器有此插件
# 设置为true时插件会优先从SkinsRestorer获取皮肤数据
# 这对于离线服务器特别有用可以避免默认Steve皮肤的问题
use-skinsrestorer: true
# 缓存设置 # 缓存设置
cache: cache:
# 是否启用缓存
enabled: true
# 缓存过期时间(天) # 缓存过期时间(天)
expire_days: 7 expire_days: 7
# 缓存最大大小MB
max_size_mb: 100
# 默认皮肤(当无法获取玩家皮肤时)
default_skin:
# 使用哪个玩家的皮肤作为默认
player_name: Steve
# 或使用自定义UUID
uuid: 8667ba71-b85a-4004-af54-457a9734eed7
# 是否随机分配默认皮肤
random_default: false
# 可选的默认皮肤列表
available_skins:
- Steve
- Alex
- Enderman
# 皮肤处理
processing:
# 是否异步处理皮肤
async: true
# 处理线程数
threads: 2
# 超时时间(秒)
timeout: 10
# 重试次数
retry_times: 3
# 数据存储 # 数据存储
storage: storage:
# 存储类型 # 存储类型 (yaml, json, sqlite)
# 可选值: yaml, json, sqlite, mysql
type: yaml type: yaml
# 自动保存 # 自动保存
@@ -162,221 +71,71 @@ storage:
# 保存间隔(秒) # 保存间隔(秒)
interval: 300 interval: 300
# YAML存储设置
yaml:
# 数据文件编码
encoding: UTF-8
# 是否压缩
compress: false
# SQLite设置
sqlite:
# 数据库文件路径
file: plugins/PlayerBlockLife/data.db
# 连接池大小
pool_size: 5
# MySQL设置
mysql:
# 数据库主机
host: localhost
# 数据库端口
port: 3306
# 数据库名
database: minecraft
# 用户名
username: root
# 密码
password: password
# 表前缀
table_prefix: pbl_
# 连接池设置
pool:
max_connections: 10
min_connections: 2
connection_timeout: 30000
idle_timeout: 600000
# 消息配置
messages:
# 消息前缀
prefix: "&6[&ePlayerBlockLife&6]&r "
# 消息颜色
colors:
success: "&a"
error: "&c"
warning: "&e"
info: "&7"
highlight: "&6"
# 命令消息
commands:
setlifeblocks:
success: "&a已为你生成 {blocks} 个生命方块!"
already_has: "&c你已经有生命方块了使用 /checklifeblocks 查看位置"
no_skin: "&e你的皮肤正在加载中请稍候..."
failed: "&c无法生成生命方块请确保周围有足够空间"
checklifeblocks:
no_blocks: "&c你还没有设置生命方块使用 /setlifeblocks 来设置"
info: "&a你的生命方块信息"
location: "&7{index}. &e世界: {world} &7坐标: &a{x}&7, &a{y}&7, &a{z}"
remaining: "&7剩余方块: &e{remaining}&7/&a5"
health: "&7当前生命值: &c{health} ❤"
admin:
reload: "&a插件配置已重载"
delete_success: "&a已删除玩家 {player} 的生命方块"
delete_failed: "&c删除失败玩家不存在或没有生命方块"
revive_success: "&a玩家 {player} 已复活!"
revive_failed: "&c复活失败玩家不存在或未死亡"
# 游戏消息
game:
block_destroyed:
owner: "&c⚠ 警告!你的生命方块被 {breaker} 破坏了!"
breaker: "&a你破坏了 {owner} 的生命方块!"
remaining: "&7对方剩余生命方块: &a{remaining}"
broadcast: "&7[附近] &e一个生命方块被破坏了"
death:
title: "&4☠ 你死了!"
subtitle: "&c所有生命方块已被挖光"
broadcast: "&4☠ 玩家 {player} 的生命方块已被全部挖光,惨遭淘汰!"
spectator: "&e你已被淘汰可以观察其他玩家。等待下一轮游戏开始..."
warning:
low_blocks: "&c⚠ 警告!生命方块即将耗尽!"
last_block: "&4⚠ 警告!这是最后一个生命方块!"
health_low: "&4⚠ 警告!生命值过低!"
# 加入消息
join:
welcome: "&e欢迎加入游戏"
welcome_back: "&e欢迎回来{player}"
rules: |
&6游戏规则
&7- 每个玩家有5个生命方块
&7- 方块被其他玩家挖光时,你将死亡
&7- 方块使用你的皮肤作为材质
&7- 你可以自由移动,但方块固定位置
remaining_blocks: "&7你还有 {blocks} 个生命方块"
commands: "&7使用 &e/setlifeblocks &7来设置你的生命方块"
# 效果设置
effects:
# 方块放置效果
place:
particles:
- type: ENCHANTMENT_TABLE
count: 30
offset_x: 0.3
offset_y: 0.3
offset_z: 0.3
speed: 0.1
sounds:
- type: BLOCK_ANVIL_PLACE
volume: 0.5
pitch: 1.2
# 方块破坏效果
break:
particles:
- type: BLOCK_CRACK
count: 50
offset_x: 0.5
offset_y: 0.5
offset_z: 0.5
speed: 0.5
data: PLAYER_HEAD
- type: SMOKE_LARGE
count: 20
offset_x: 0.3
offset_y: 0.3
offset_z: 0.3
speed: 0.05
sounds:
- type: ENTITY_ITEM_BREAK
volume: 1.0
pitch: 0.8
- type: BLOCK_GLASS_BREAK
volume: 0.8
pitch: 1.0
knockback:
enabled: true
power: 0.5
vertical: 0.3
# 玩家受伤效果
damage:
particles:
- type: DAMAGE_INDICATOR
count: 10
sounds:
- type: ENTITY_PLAYER_HURT
volume: 1.0
pitch: 1.0
# 玩家死亡效果
player_death:
particles:
- type: EXPLOSION_HUGE
count: 5
sounds:
- type: ENTITY_WITHER_DEATH
volume: 0.7
pitch: 0.8
- type: ENTITY_LIGHTNING_BOLT_THUNDER
volume: 1.0
pitch: 0.5
# 保护设置 # 保护设置
protection: protection:
# 是否保护生命方块不被非玩家破坏
protect_from_non_players: true
# 是否保护生命方块不被爆炸破坏 # 是否保护生命方块不被爆炸破坏
protect_from_explosions: true protect_from_explosions: true
# 是否保护生命方块不被火焰烧毁 # 是否保护生命方块不被火焰烧毁
protect_from_fire: true protect_from_fire: true
# 是否保护生命方块不被活塞推动 # 是否保护生命方块不被活塞推动
protect_from_pistons: true protect_from_pistons: true
# 是否允许TNT破坏生命方块
allow_tnt_damage: false
# 是否允许苦力怕爆炸破坏生命方块
allow_creeper_explosions: false
# 是否允许末影龙破坏生命方块
allow_ender_dragon_damage: false
# 是否允许其他插件修改生命方块
allow_plugin_modification: false
# 世界保护 # 自动生成设置
world_guard: auto-generation:
# 是否与WorldGuard集成 # 玩家加入时是否自动生成生命方块
enabled: false enabled: true
# 需要保护的地区标志 # 生成位置要求:上方无方块覆盖的地表
region_flags: require_open_sky: true
- block-break # 最大尝试次数(如果找不到合适位置)
- block-place max_attempts: 50
- pvp # 生成失败时的处理方式 (none, notify, teleport_to_spawn)
# 白名单地区(允许破坏生命方块的地区) on_failure: notify
whitelist_regions: []
# 黑名单地区(不允许破坏生命方块的地区)
blacklist_regions: []
# 调试设 # 命令启用配
debug: commands:
# 是否启用调试模式 # setlifeblocks 命令
enabled: false setlifeblocks:
# 调试级别 enabled: true
# 可选值: INFO, WARNING, ERROR, DEBUG # 是否允许玩家自己使用
level: INFO allow_self_use: true
# 是否记录到文件 # 是否允许管理员为其他玩家设置
log_to_file: true allow_admin_use: true
# 日志文件路径
log_file: plugins/PlayerBlockLife/debug.log # checklifeblocks 命令
# 是否显示详细事件日志 checklifeblocks:
verbose_events: false enabled: true
# 是否记录性能数据 # 是否允许玩家自己查看
log_performance: false allow_self_use: true
# 性能日志间隔(秒) # 是否允许管理员查看其他玩家
performance_log_interval: 60 allow_admin_use: true
# pblreload 命令
pblreload:
enabled: true
# 仅限管理员使用
admin_only: true
# pbldelete 命令
pbldelete:
enabled: true
# 仅限管理员使用
admin_only: true
# pblrevive 命令
pblrevive:
enabled: true
# 仅限管理员使用
admin_only: true
# pblstats 命令
pblstats:
enabled: true
# 仅限管理员使用
admin_only: true
# 消息配置(现在使用独立的 messages.yml 文件)
messages:
# 是否启用独立的消息文件
use_external_file: true
# 外部消息文件名称
external_file: "messages.yml"

View File

@@ -0,0 +1,108 @@
# PlayerBlockLife 消息配置文件
# 所有插件输出消息都可以在这里自定义
# 控制台消息
console:
plugin_enabled: "&a[PlayerBlockLife] 插件已启用!版本: {version}"
plugin_disabled: "&c[PlayerBlockLife] 插件已禁用!"
config_reloaded: "&a[PlayerBlockLife] 配置已重载!"
player_joined: "&a[PlayerBlockLife] 玩家 {player} 已加入,正在生成生命方块..."
blocks_generated: "&a[PlayerBlockLife] 已为玩家 {player} 生成 {amount} 个生命方块"
player_eliminated: "&c[PlayerBlockLife] 玩家 {player} 的生命方块已被挖光,已被淘汰!"
error_generating_blocks: "&c[PlayerBlockLife] 为玩家 {player} 生成生命方块时出错: {error}"
# 游戏内消息
game:
# 方块相关
block:
destroyed:
owner: "&c⚠ 警告!你的生命方块被 {breaker} 破坏了!剩余: {remaining}/{total}"
breaker: "&a你破坏了 {owner} 的生命方块!"
all_destroyed: "&c☠ 你的所有生命方块已被破坏!你已被淘汰!"
placed: "&a已为你生成 {amount} 个生命方块!"
check_location: "&e你的生命方块位置"
location_item: "&7- {world} ({x}, {y}, {z})"
no_blocks: "&c你还没有生命方块"
# 玩家状态
player:
eliminated: "&c玩家 {player} 已被淘汰!"
revived: "&a玩家 {player} 已被复活!"
already_eliminated: "&c玩家 {player} 已被淘汰,无法执行此操作!"
not_eliminated: "&c玩家 {player} 未被淘汰!"
# 错误消息
errors:
no_permission: "&c你没有权限使用此命令"
player_not_found: "&c玩家 {player} 未找到!"
player_offline: "&c玩家 {player} 不在线!"
invalid_arguments: "&c参数无效用法: {usage}"
command_disabled: "&c此命令已被禁用"
world_not_found: "&c世界 {world} 未找到!"
cannot_generate_blocks: "&c无法生成生命方块{reason}"
internal_error: "&c发生内部错误请联系管理员"
# 成功消息
success:
blocks_reset: "&a已重置你的生命方块"
blocks_deleted: "&a已删除玩家 {player} 的生命方块!"
config_reloaded: "&a配置已重载"
player_revived: "&a玩家 {player} 已复活!"
# 命令消息
commands:
setlifeblocks:
success: "&a已为你生成 {blocks} 个生命方块!"
already_has: "&c你已经有生命方块了使用 /checklifeblocks 查看位置"
help: |
&6=== PlayerBlockLife 帮助 ===
&e/setlifeblocks &7- 设置你的生命方块
&e/setlifeblocks reset &7- 重置生命方块位置
&e/setlifeblocks other <玩家> &7- 为其他玩家设置(管理员)
&e/setlifeblocks help &7- 显示此帮助
usage: "&c用法: /setlifeblocks [reset|other|help]"
checklifeblocks:
success: "&e你的生命方块位置"
no_blocks: "&c你还没有生命方块"
usage: "&c用法: /checklifeblocks"
pblreload:
success: "&a配置已重载"
usage: "&c用法: /pblreload"
pbldelete:
success: "&a已删除玩家 {player} 的生命方块!"
usage: "&c用法: /pbldelete <玩家>"
pblrevive:
success: "&a玩家 {player} 已复活!"
usage: "&c用法: /pblrevive [玩家]"
pblstats:
title: "&6=== PlayerBlockLife 统计 ==="
online_players: "&e在线玩家: {count}"
total_blocks: "&e总生命方块: {count}"
eliminated_players: "&e已淘汰玩家: {count}"
usage: "&c用法: /pblstats"
# 广播消息
broadcast:
block_destroyed: "&6{breaker} &7破坏了 &c{owner} &7的生命方块"
player_eliminated: "&c☠ {player} &7的生命方块已被挖光已被淘汰"
player_revived: "&a✨ {player} &7已被复活"
# 变量说明
# {player} - 玩家名称
# {breaker} - 破坏者名称
# {owner} - 方块所有者名称
# {amount} - 方块数量
# {remaining} - 剩余方块数量
# {total} - 总方块数量
# {x}, {y}, {z} - 坐标
# {world} - 世界名称
# {version} - 插件版本
# {error} - 错误信息
# {reason} - 原因
# {count} - 计数
# {usage} - 命令用法

View File

@@ -1,35 +1,75 @@
name: PlayerBlockLife
version: 3.0.0-experimental-1.20.4
main: com.playerblocklife.PlayerBlockLife
api-version: 1.20
author: xiaobai
description: 玩家生命方块系统 - 方块被挖光则死亡
website: https://github.com/yourname/PlayerBlockLife
prefix: PBL
commands: commands:
setlifeblocks: setlifeblocks:
description: 设置你的生命方块(使用你的皮肤) description: 设置你的生命方块(使用你的皮肤)
usage: /<command> usage: |
/<command> - 设置你的生命方块
/<command> help - 显示帮助
/<command> reset - 重置生命方块位置
/<command> other <玩家> - 为其他玩家设置(管理员)
aliases: [sbl, lifeblocks, setblocks] aliases: [sbl, lifeblocks, setblocks]
permission: playerblocklife.set permission: playerblocklife.set
permission-message: "§c你没有权限使用此命令"
checklifeblocks: checklifeblocks:
description: 查看你的生命方块位置 description: 查看你的生命方块位置
usage: /<command> usage: /<command>
aliases: [cbl, checklife, myblocks] aliases: [cbl, checklife, myblocks]
permission: playerblocklife.check permission: playerblocklife.check
permission-message: "§c你没有权限使用此命令"
pblreload: pblreload:
description: 重载插件配置 description: 重载插件配置
usage: /<command> usage: /<command>
aliases: [pblr, pblreload] aliases: [pblr]
permission: playerblocklife.admin permission: playerblocklife.admin
permission-message: "§c你没有权限使用此命令"
pbldelete: pbldelete:
description: 删除指定玩家的生命方块 description: 删除指定玩家的生命方块
usage: /<command> <玩家> usage: /<command> <玩家>
aliases: [pbldel, deleteblocks] aliases: [pbldel, deleteblocks]
permission: playerblocklife.admin permission: playerblocklife.admin
permission-message: "§c你没有权限使用此命令"
pblrevive: pblrevive:
description: 复活被淘汰的玩家 description: 复活被淘汰的玩家
usage: /<command> [玩家] usage: /<command> [玩家]
aliases: [revive] aliases: [revive]
permission: playerblocklife.admin permission: playerblocklife.admin
permission-message: "§c你没有权限使用此命令"
pblstats: pblstats:
description: 查看插件统计信息 description: 查看插件统计信息
usage: /<command> usage: /<command>
permission: playerblocklife.admin permission: playerblocklife.admin
permission-message: "§c你没有权限使用此命令"
permissions:
playerblocklife.*:
description: 所有 PlayerBlockLife 权限
children:
playerblocklife.set: true
playerblocklife.check: true
playerblocklife.admin: true
default: op
playerblocklife.set:
description: 允许设置生命方块
default: true
playerblocklife.check:
description: 允许查看生命方块
default: true
playerblocklife.admin:
description: 管理员权限
default: op