Compare commits
2 Commits
v2.1.0
...
8c68028924
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c68028924 | ||
|
|
b268a74eeb |
@@ -4,7 +4,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = 'com.playerblocklife'
|
||||
version = '2.0.0-1.20.4'
|
||||
version = '3.0.0-experimental-1.20.4'
|
||||
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
|
||||
@@ -5,5 +5,5 @@ org.gradle.caching=true
|
||||
org.gradle.daemon=true
|
||||
|
||||
# ????
|
||||
pluginVersion=2.0.0-1.20.4
|
||||
pluginVersion=2.1.0-1.20.4
|
||||
mcVersion=1.20.4
|
||||
@@ -9,18 +9,58 @@ 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 {
|
||||
private final PlayerBlockLife plugin;
|
||||
private FileConfiguration config;
|
||||
private File configFile;
|
||||
|
||||
/**
|
||||
* 构造一个新的配置管理器
|
||||
*
|
||||
* @param plugin 插件主类实例,用于访问插件相关功能
|
||||
*/
|
||||
public ConfigManager(PlayerBlockLife plugin) {
|
||||
this.plugin = plugin;
|
||||
this.configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
* 加载插件配置
|
||||
*
|
||||
* <p>执行以下操作:
|
||||
* <ol>
|
||||
* <li>确保插件数据文件夹存在</li>
|
||||
* <li>如果配置文件不存在,从JAR中复制默认配置</li>
|
||||
* <li>调用reloadConfig()重新加载配置</li>
|
||||
* </ol>
|
||||
*
|
||||
* @see #reloadConfig()
|
||||
*/
|
||||
public void loadConfig() {
|
||||
// 确保配置文件夹存在
|
||||
@@ -39,7 +79,16 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载配置
|
||||
* 重新加载配置文件
|
||||
*
|
||||
* <p>执行以下操作:
|
||||
* <ol>
|
||||
* <li>从磁盘重新加载配置文件</li>
|
||||
* <li>加载JAR中的默认配置作为后备</li>
|
||||
* <li>检查配置版本并进行必要的更新</li>
|
||||
* </ol>
|
||||
*
|
||||
* @see #checkConfigVersion()
|
||||
*/
|
||||
public void reloadConfig() {
|
||||
// 重新从磁盘加载配置
|
||||
@@ -127,7 +176,12 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
* 保存当前配置到文件
|
||||
*
|
||||
* <p>将内存中的配置数据写入到config.yml文件中。
|
||||
* 如果保存失败,会记录错误日志。</p>
|
||||
*
|
||||
* @throws IOException 如果文件写入失败
|
||||
*/
|
||||
public void saveConfig() {
|
||||
try {
|
||||
@@ -139,6 +193,11 @@ public class ConfigManager {
|
||||
|
||||
/**
|
||||
* 获取配置对象
|
||||
*
|
||||
* <p>如果配置对象为null,会自动调用reloadConfig()加载配置。</p>
|
||||
*
|
||||
* @return 当前的FileConfiguration配置对象
|
||||
* @see #reloadConfig()
|
||||
*/
|
||||
public FileConfiguration getConfig() {
|
||||
if (config == null) {
|
||||
@@ -157,6 +216,10 @@ public class ConfigManager {
|
||||
return getConfig().getInt("blocks.spread", 5);
|
||||
}
|
||||
|
||||
public int getMinDistance() {
|
||||
return getConfig().getInt("blocks.min-distance", 10);
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return getConfig().getInt("blocks.depth", -1);
|
||||
}
|
||||
@@ -181,8 +244,65 @@ public class ConfigManager {
|
||||
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() {
|
||||
return getConfig().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() {
|
||||
|
||||
@@ -10,6 +10,32 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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 {
|
||||
private final PlayerBlockLife plugin;
|
||||
private final Map<UUID, Integer> playerHealth = new HashMap<>();
|
||||
|
||||
@@ -5,6 +5,25 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
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 {
|
||||
private static PlayerBlockLife instance;
|
||||
private PlayerBlockManager blockManager;
|
||||
@@ -13,6 +32,22 @@ public class PlayerBlockLife extends JavaPlugin {
|
||||
private ConfigManager configManager;
|
||||
private MessageManager messageManager;
|
||||
|
||||
/**
|
||||
* 插件启用时调用,执行初始化操作
|
||||
*
|
||||
* <p>初始化流程:
|
||||
* <ol>
|
||||
* <li>保存默认配置文件</li>
|
||||
* <li>初始化所有管理器(注意依赖顺序)</li>
|
||||
* <li>加载配置和消息数据</li>
|
||||
* <li>注册事件监听器</li>
|
||||
* <li>注册命令执行器</li>
|
||||
* <li>加载玩家数据和皮肤缓存</li>
|
||||
* <li>启动定时任务</li>
|
||||
* </ol>
|
||||
*
|
||||
* @see #onDisable()
|
||||
*/
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
@@ -57,6 +92,18 @@ public class PlayerBlockLife extends JavaPlugin {
|
||||
getLogger().info("§a========================================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用时调用,执行清理操作
|
||||
*
|
||||
* <p>执行以下清理操作:
|
||||
* <ul>
|
||||
* <li>保存玩家方块数据到文件</li>
|
||||
* <li>保存皮肤缓存数据</li>
|
||||
* <li>记录插件禁用日志</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see #onEnable()
|
||||
*/
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// 保存数据
|
||||
@@ -81,6 +128,16 @@ public class PlayerBlockLife extends JavaPlugin {
|
||||
|
||||
/**
|
||||
* 插件的完整重载方法(用于命令)
|
||||
*
|
||||
* <p>重新加载所有插件配置和数据,包括:
|
||||
* <ul>
|
||||
* <li>主配置文件 (config.yml)</li>
|
||||
* <li>消息配置文件 (messages.yml)</li>
|
||||
* <li>玩家方块数据</li>
|
||||
* <li>皮肤缓存数据</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>这个方法通常由管理员通过/pblreload命令调用。</p>
|
||||
*/
|
||||
public void reloadPluginConfig() {
|
||||
if (configManager != null) {
|
||||
@@ -125,6 +182,14 @@ public class PlayerBlockLife extends JavaPlugin {
|
||||
}, 1200L, 1200L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件单例实例
|
||||
*
|
||||
* <p>提供全局访问点,允许其他类访问插件主实例。</p>
|
||||
*
|
||||
* @return PlayerBlockLife插件实例
|
||||
* @throws IllegalStateException 如果插件尚未启用(实例为null)
|
||||
*/
|
||||
public static PlayerBlockLife getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,34 @@ import java.io.FileWriter;
|
||||
import java.util.*;
|
||||
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 {
|
||||
private final PlayerBlockLife plugin;
|
||||
private final SkinManager skinManager;
|
||||
@@ -47,7 +75,36 @@ public class PlayerBlockManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 为玩家生成生命方块(新方法,支持自动生成)
|
||||
* 为玩家生成指定数量的生命方块
|
||||
*
|
||||
* <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();
|
||||
@@ -92,24 +149,28 @@ public class PlayerBlockManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找地表位置(上方无方块覆盖)
|
||||
* 寻找地表位置(放宽条件,只要是露天地面就可以)
|
||||
*/
|
||||
private Location findSurfaceLocation(Location center, int spreadRange, boolean requireOpenSky) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
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, 10, z);
|
||||
// 以玩家坐标为中心,但高度从世界最高点开始寻找
|
||||
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 = 10; y > world.getMinHeight(); y--) {
|
||||
for (int y = maxHeight; y > world.getMinHeight(); y--) {
|
||||
testLoc.setY(y);
|
||||
Block block = testLoc.getBlock();
|
||||
if (!block.getType().isAir()) {
|
||||
Material type = block.getType();
|
||||
|
||||
// 检查是否是固体方块(可以作为支撑)
|
||||
if (type.isSolid() && type.isBlock() && !type.isTransparent()) {
|
||||
groundBlock = block;
|
||||
break;
|
||||
}
|
||||
@@ -117,24 +178,23 @@ public class PlayerBlockManager {
|
||||
|
||||
if (groundBlock == null) continue;
|
||||
|
||||
// 检查地表方块上方位置
|
||||
// 地表位置 = 固体方块上方一格
|
||||
Location surfaceLoc = groundBlock.getLocation().add(0, 1, 0);
|
||||
Block surfaceBlock = surfaceLoc.getBlock();
|
||||
|
||||
// 检查是否已有方块
|
||||
if (blockOwners.containsKey(surfaceLoc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查地表方块是否合适
|
||||
if (!isSuitableLocation(surfaceLoc)) {
|
||||
// 检查位置是否合适(放宽条件)
|
||||
if (!isSuitableLocationRelaxed(surfaceLoc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果需要上方无方块覆盖,检查上方
|
||||
// 如果需要上方无方块覆盖,检查上方3格
|
||||
if (requireOpenSky) {
|
||||
boolean hasCover = false;
|
||||
for (int y = 1; y <= 5; y++) {
|
||||
for (int y = 1; y <= 3; y++) {
|
||||
Block aboveBlock = surfaceLoc.clone().add(0, y, 0).getBlock();
|
||||
if (!aboveBlock.getType().isAir()) {
|
||||
hasCover = true;
|
||||
@@ -234,8 +294,52 @@ public class PlayerBlockManager {
|
||||
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) {
|
||||
try {
|
||||
@@ -246,15 +350,37 @@ public class PlayerBlockManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查玩家皮肤是否已加载
|
||||
if (!skinManager.isSkinLoaded(playerId)) {
|
||||
plugin.logWarning("玩家 " + playerName + " 的皮肤未加载,无法放置头颅方块");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置方块为玩家头颅
|
||||
block.setType(Material.PLAYER_HEAD);
|
||||
|
||||
// 获取并设置头颅数据
|
||||
Skull skullState = (Skull) block.getState();
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
|
||||
|
||||
// 设置头颅所有者
|
||||
skullState.setOwningPlayer(offlinePlayer);
|
||||
|
||||
// 使用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);
|
||||
}
|
||||
} else {
|
||||
// 如果物品元数据为空,使用离线玩家
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
|
||||
skullState.setOwningPlayer(offlinePlayer);
|
||||
}
|
||||
|
||||
// 设置朝向(随机方向)
|
||||
BlockData blockData = block.getBlockData();
|
||||
@@ -268,6 +394,7 @@ public class PlayerBlockManager {
|
||||
// 更新方块
|
||||
skullState.update(true, false);
|
||||
|
||||
plugin.logInfo("成功放置玩家头颅方块: " + playerName + " 在 " + location);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.logError("放置玩家头颅失败: " + location, e);
|
||||
|
||||
@@ -137,8 +137,9 @@ public class PlayerJoinListener implements Listener {
|
||||
String onFailure = config.getOnFailureAction();
|
||||
if (onFailure.equals("notify")) {
|
||||
String notifyMsg = plugin.getMessageManager().getMessage("game.errors.cannot_generate_blocks",
|
||||
"&c无法生成生命方块:找不到合适的位置");
|
||||
notifyMsg = notifyMsg.replace("&", "§");
|
||||
"&c无法生成生命方块:{reason}");
|
||||
notifyMsg = notifyMsg.replace("{reason}", "找不到合适的位置(尝试了20次)")
|
||||
.replace("&", "§");
|
||||
player.sendMessage(notifyMsg);
|
||||
player.sendMessage("§7请手动使用 §e/setlifeblocks §7命令生成方块");
|
||||
} else if (onFailure.equals("teleport_to_spawn")) {
|
||||
|
||||
@@ -19,6 +19,42 @@ import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
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 {
|
||||
private final PlayerBlockLife plugin;
|
||||
private final Map<UUID, String> playerSkinData = new ConcurrentHashMap<>();
|
||||
@@ -60,11 +96,41 @@ public class SkinManager {
|
||||
try {
|
||||
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) {
|
||||
skinBase64 = getDefaultSteveSkin();
|
||||
plugin.logWarning("使用默认皮肤: " + player.getName());
|
||||
plugin.logWarning("所有皮肤来源都失败,使用默认Steve皮肤: " + player.getName());
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
PlayerProfile profile = player.getPlayerProfile();
|
||||
@@ -94,30 +258,27 @@ public class SkinManager {
|
||||
URL skinUrl = textures.getSkin();
|
||||
|
||||
if (skinUrl != null) {
|
||||
BufferedImage skinImage = ImageIO.read(skinUrl);
|
||||
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 timestampJson = new JsonObject();
|
||||
|
||||
return player.getUniqueId().toString();
|
||||
}
|
||||
// 创建纹理JSON对象
|
||||
JsonObject textureJson = new JsonObject();
|
||||
JsonObject texturesJson = new JsonObject();
|
||||
JsonObject skinJson = new JsonObject();
|
||||
|
||||
// 直接使用皮肤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) {
|
||||
plugin.logWarning("从PlayerProfile获取皮肤失败: " + e.getMessage());
|
||||
@@ -163,23 +324,92 @@ public class SkinManager {
|
||||
return modelData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认Steve皮肤的Base64纹理
|
||||
*
|
||||
* <p>当无法从任何来源获取玩家皮肤时,使用默认的Steve皮肤作为备用。</p>
|
||||
*
|
||||
* @return 默认Steve皮肤的Base64纹理值
|
||||
*/
|
||||
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) {
|
||||
ItemStack head = new ItemStack(org.bukkit.Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) head.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
|
||||
meta.setOwningPlayer(offlinePlayer);
|
||||
// 获取玩家的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);
|
||||
meta.setOwningPlayer(offlinePlayer);
|
||||
}
|
||||
} else {
|
||||
// 没有皮肤数据,使用离线玩家
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
|
||||
meta.setOwningPlayer(offlinePlayer);
|
||||
}
|
||||
|
||||
// 设置自定义模型数据(如果有)
|
||||
Integer customModelData = playerCustomModelData.get(playerId);
|
||||
if (customModelData != null) {
|
||||
meta.setCustomModelData(customModelData);
|
||||
}
|
||||
|
||||
// 设置显示名称和描述
|
||||
meta.setDisplayName("§e" + playerName + "的生命方块");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
|
||||
@@ -7,6 +7,8 @@ blocks:
|
||||
amount: 5
|
||||
# 方块生成范围(以玩家为中心的正方形边长的一半)
|
||||
spread: 5
|
||||
# 方块生成最小范围(方块之间最小距离,单位:格)
|
||||
min-distance: 10
|
||||
# 方块埋藏深度(0为地面,负数为地下)
|
||||
depth: -1
|
||||
# 方块材质类型
|
||||
@@ -41,8 +43,16 @@ game:
|
||||
skin:
|
||||
# 是否启用皮肤系统
|
||||
enabled: true
|
||||
# 皮肤来源 (player_profile, local_cache)
|
||||
source: player_profile
|
||||
# 皮肤来源 (player_profile, local_cache, skinsrestorer)
|
||||
# player_profile: 使用Bukkit的PlayerProfile API(需要在线验证)
|
||||
# local_cache: 使用本地缓存的皮肤数据
|
||||
# skinsrestorer: 使用SkinsRestorer插件的皮肤数据(推荐用于离线服务器)
|
||||
source: skinsrestorer
|
||||
|
||||
# 是否使用SkinsRestorer插件的皮肤(如果服务器有此插件)
|
||||
# 设置为true时,插件会优先从SkinsRestorer获取皮肤数据
|
||||
# 这对于离线服务器特别有用,可以避免默认Steve皮肤的问题
|
||||
use-skinsrestorer: true
|
||||
|
||||
# 缓存设置
|
||||
cache:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: PlayerBlockLife
|
||||
version: 2.0.0-1.20.4
|
||||
version: 3.0.0-experimental-1.20.4
|
||||
main: com.playerblocklife.PlayerBlockLife
|
||||
api-version: 1.20
|
||||
author: xiaobai
|
||||
|
||||
Reference in New Issue
Block a user