From 8c68028924a5218c2fc4fd48939961a0463cde84 Mon Sep 17 00:00:00 2001 From: xiaobai Date: Sat, 14 Feb 2026 19:56:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B3=A8=E9=87=8A=EF=BC=88?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0skinsrestorer=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=94=AF=E6=8C=81=E5=8A=9F=E8=83=BD=EF=BC=88?= =?UTF-8?q?=E6=9C=AA=E7=BB=8F=E5=AE=8C=E6=95=B4=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- gradle.properties | 2 +- .../com/playerblocklife/ConfigManager.java | 122 +++++++- .../java/com/playerblocklife/LifeSystem.java | 26 ++ .../com/playerblocklife/PlayerBlockLife.java | 65 +++++ .../playerblocklife/PlayerBlockManager.java | 97 ++++++- .../java/com/playerblocklife/SkinManager.java | 271 +++++++++++++++--- src/main/resources/config.yml | 12 +- src/main/resources/plugin.yml | 2 +- 9 files changed, 540 insertions(+), 59 deletions(-) diff --git a/build.gradle b/build.gradle index bcdb0cd..0934e8f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { } group = 'com.playerblocklife' -version = '2.0.1-1.20.4' +version = '3.0.0-experimental-1.20.4' sourceCompatibility = 17 targetCompatibility = 17 diff --git a/gradle.properties b/gradle.properties index 9b6fbcd..2f5e064 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ org.gradle.caching=true org.gradle.daemon=true # ???? -pluginVersion=2.0.1-1.20.4 +pluginVersion=2.1.0-1.20.4 mcVersion=1.20.4 \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/ConfigManager.java b/src/main/java/com/playerblocklife/ConfigManager.java index 1fe2810..02ecead 100644 --- a/src/main/java/com/playerblocklife/ConfigManager.java +++ b/src/main/java/com/playerblocklife/ConfigManager.java @@ -9,18 +9,58 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +/** + * 配置管理器 - 负责加载、保存和管理插件的配置文件 + * + *

主要功能包括: + *

+ * + *

SkinsRestorer配置支持: + *

+ *

+ * + * @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"); } /** - * 加载配置 + * 加载插件配置 + * + *

执行以下操作: + *

    + *
  1. 确保插件数据文件夹存在
  2. + *
  3. 如果配置文件不存在,从JAR中复制默认配置
  4. + *
  5. 调用reloadConfig()重新加载配置
  6. + *
+ * + * @see #reloadConfig() */ public void loadConfig() { // 确保配置文件夹存在 @@ -39,7 +79,16 @@ public class ConfigManager { } /** - * 重新加载配置 + * 重新加载配置文件 + * + *

执行以下操作: + *

    + *
  1. 从磁盘重新加载配置文件
  2. + *
  3. 加载JAR中的默认配置作为后备
  4. + *
  5. 检查配置版本并进行必要的更新
  6. + *
+ * + * @see #checkConfigVersion() */ public void reloadConfig() { // 重新从磁盘加载配置 @@ -127,7 +176,12 @@ public class ConfigManager { } /** - * 保存配置 + * 保存当前配置到文件 + * + *

将内存中的配置数据写入到config.yml文件中。 + * 如果保存失败,会记录错误日志。

+ * + * @throws IOException 如果文件写入失败 */ public void saveConfig() { try { @@ -139,6 +193,11 @@ public class ConfigManager { /** * 获取配置对象 + * + *

如果配置对象为null,会自动调用reloadConfig()加载配置。

+ * + * @return 当前的FileConfiguration配置对象 + * @see #reloadConfig() */ public FileConfiguration getConfig() { if (config == null) { @@ -185,12 +244,65 @@ public class ConfigManager { return getConfig().getBoolean("skin.enabled", true); } + /** + * 获取皮肤来源配置 + * + *

支持的皮肤来源: + *

+ *

+ * + *

默认配置已将此值设为"skinsrestorer",以优化离线服务器体验。

+ * + * @return 皮肤来源配置值 + * @see #useSkinsRestorer() + * @see SkinManager#loadPlayerSkinAsync() + */ public String getSkinSource() { - return getConfig().getString("skin.source", "player_profile"); + return getConfig().getString("skin.source", "skinsrestorer"); } + /** + * 检查是否启用SkinsRestorer插件支持 + * + *

当此方法返回true时,插件将: + *

+ *

+ * + *

默认配置已将此值设为true,以优化离线服务器体验。

+ * + * @return 如果启用SkinsRestorer支持返回true,否则返回false + * @see #getSkinSource() + * @see SkinManager#getSkinFromSkinsRestorer() + */ public boolean useSkinsRestorer() { - return getConfig().getBoolean("skin.use-skinsrestorer", false); + return getConfig().getBoolean("skin.use-skinsrestorer", true); } public int getCacheExpireDays() { diff --git a/src/main/java/com/playerblocklife/LifeSystem.java b/src/main/java/com/playerblocklife/LifeSystem.java index 35d8939..425f5ed 100644 --- a/src/main/java/com/playerblocklife/LifeSystem.java +++ b/src/main/java/com/playerblocklife/LifeSystem.java @@ -10,6 +10,32 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +/** + * 生命值系统 - 负责管理玩家生命值和状态效果 + * + *

根据玩家剩余生命方块数量计算生命值,并应用相应的状态效果: + *

    + *
  • 监控玩家剩余方块数量变化
  • + *
  • 计算对应的生命值比例
  • + *
  • 应用虚弱、缓慢、失明等负面效果
  • + *
  • 处理玩家淘汰和复活逻辑
  • + *
  • 提供生命值相关的音效和视觉反馈
  • + *
+ * + *

生命值计算公式: + *

+ * 5个方块 → 20点生命值(满血)
+ * 4个方块 → 16点生命值
+ * 3个方块 → 12点生命值
+ * 2个方块 → 8点生命值
+ * 1个方块 → 4点生命值
+ * 0个方块 → 0点生命值(淘汰)
+ * 
+ * + * @author xiaobai + * @version 2.1.0 + * @since 1.0.0 + */ public class LifeSystem { private final PlayerBlockLife plugin; private final Map playerHealth = new HashMap<>(); diff --git a/src/main/java/com/playerblocklife/PlayerBlockLife.java b/src/main/java/com/playerblocklife/PlayerBlockLife.java index ad5b236..b72827d 100644 --- a/src/main/java/com/playerblocklife/PlayerBlockLife.java +++ b/src/main/java/com/playerblocklife/PlayerBlockLife.java @@ -5,6 +5,25 @@ import org.bukkit.plugin.java.JavaPlugin; import java.util.logging.Level; +/** + * PlayerBlockLife插件主类 - 玩家生命方块系统的核心控制器 + * + *

这个插件为Minecraft服务器添加了一个独特的游戏机制:每个玩家拥有一定数量的生命方块, + * 这些方块使用玩家的皮肤作为材质。当其他玩家挖光某个玩家的所有生命方块时,该玩家会被淘汰。

+ * + *

主要功能: + *

    + *
  • 管理玩家生命方块的生成和销毁
  • + *
  • 处理玩家皮肤的获取和应用
  • + *
  • 监控玩家生命值状态
  • + *
  • 提供完整的命令和权限系统
  • + *
  • 支持配置热重载和数据持久化
  • + *
+ * + * @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; + /** + * 插件启用时调用,执行初始化操作 + * + *

初始化流程: + *

    + *
  1. 保存默认配置文件
  2. + *
  3. 初始化所有管理器(注意依赖顺序)
  4. + *
  5. 加载配置和消息数据
  6. + *
  7. 注册事件监听器
  8. + *
  9. 注册命令执行器
  10. + *
  11. 加载玩家数据和皮肤缓存
  12. + *
  13. 启动定时任务
  14. + *
+ * + * @see #onDisable() + */ @Override public void onEnable() { instance = this; @@ -57,6 +92,18 @@ public class PlayerBlockLife extends JavaPlugin { getLogger().info("§a========================================"); } + /** + * 插件禁用时调用,执行清理操作 + * + *

执行以下清理操作: + *

    + *
  • 保存玩家方块数据到文件
  • + *
  • 保存皮肤缓存数据
  • + *
  • 记录插件禁用日志
  • + *
+ * + * @see #onEnable() + */ @Override public void onDisable() { // 保存数据 @@ -81,6 +128,16 @@ public class PlayerBlockLife extends JavaPlugin { /** * 插件的完整重载方法(用于命令) + * + *

重新加载所有插件配置和数据,包括: + *

    + *
  • 主配置文件 (config.yml)
  • + *
  • 消息配置文件 (messages.yml)
  • + *
  • 玩家方块数据
  • + *
  • 皮肤缓存数据
  • + *
+ * + *

这个方法通常由管理员通过/pblreload命令调用。

*/ public void reloadPluginConfig() { if (configManager != null) { @@ -125,6 +182,14 @@ public class PlayerBlockLife extends JavaPlugin { }, 1200L, 1200L); } + /** + * 获取插件单例实例 + * + *

提供全局访问点,允许其他类访问插件主实例。

+ * + * @return PlayerBlockLife插件实例 + * @throws IllegalStateException 如果插件尚未启用(实例为null) + */ public static PlayerBlockLife getInstance() { return instance; } diff --git a/src/main/java/com/playerblocklife/PlayerBlockManager.java b/src/main/java/com/playerblocklife/PlayerBlockManager.java index 1ccb104..01ac9be 100644 --- a/src/main/java/com/playerblocklife/PlayerBlockManager.java +++ b/src/main/java/com/playerblocklife/PlayerBlockManager.java @@ -18,6 +18,34 @@ import java.io.FileWriter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +/** + * 玩家方块管理器 - 负责管理玩家生命方块的核心组件 + * + *

主要职责: + *

    + *
  • 生成和放置玩家生命方块
  • + *
  • 管理方块位置和所有者映射关系
  • + *
  • 处理方块破坏和恢复逻辑
  • + *
  • 提供方块数据持久化存储
  • + *
  • 支持方块位置查询和验证
  • + *
  • 与SkinManager协同工作,确保方块正确显示玩家皮肤
  • + *
+ * + *

SkinsRestorer集成特性: + *

    + *
  • 通过SkinManager获取SkinsRestorer提供的玩家皮肤纹理
  • + *
  • 确保离线服务器上的方块显示正确的自定义皮肤
  • + *
  • 支持异步皮肤加载,避免方块放置阻塞
  • + *
  • 提供皮肤加载状态检查,确保皮肤就绪后再放置方块
  • + *
+ *

+ * + *

使用并发安全的数据结构确保多线程环境下的数据一致性。

+ * + * @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 { } /** - * 为玩家生成生命方块(新方法,支持自动生成) + * 为玩家生成指定数量的生命方块 + * + *

此方法负责生成玩家的生命方块,包括以下步骤: + *

    + *
  1. 检查玩家是否已有生命方块
  2. + *
  3. 验证玩家皮肤是否已从SkinsRestorer或其他来源加载完成
  4. + *
  5. 在指定范围内寻找合适的放置位置
  6. + *
  7. 放置带有玩家皮肤纹理的玩家头颅方块
  8. + *
  9. 记录方块位置和所有者关系
  10. + *
  11. 保存数据并返回生成结果
  12. + *
+ *

+ * + *

皮肤加载检查: + *

    + *
  • 调用skinManager.isSkinLoaded()检查皮肤是否就绪
  • + *
  • 如果皮肤未加载,方块生成将失败
  • + *
  • 确保离线服务器通过SkinsRestorer获取的皮肤能正确应用
  • + *
  • 避免放置默认Steve皮肤的方块
  • + *
+ *

+ * + * @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(); @@ -276,6 +333,13 @@ public class PlayerBlockManager { /** * 放置玩家头颅方块 + * + *

使用SkinManager创建带有正确皮肤的玩家头颅方块,支持离线服务器皮肤显示。

+ * + * @param location 放置位置 + * @param playerId 玩家UUID + * @param playerName 玩家名称 + * @return 放置成功返回true,失败返回false */ private boolean placePlayerHead(Location location, UUID playerId, String playerName) { try { @@ -286,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(); @@ -308,6 +394,7 @@ public class PlayerBlockManager { // 更新方块 skullState.update(true, false); + plugin.logInfo("成功放置玩家头颅方块: " + playerName + " 在 " + location); return true; } catch (Exception e) { plugin.logError("放置玩家头颅失败: " + location, e); diff --git a/src/main/java/com/playerblocklife/SkinManager.java b/src/main/java/com/playerblocklife/SkinManager.java index 4198cf0..c3ebd91 100644 --- a/src/main/java/com/playerblocklife/SkinManager.java +++ b/src/main/java/com/playerblocklife/SkinManager.java @@ -19,6 +19,42 @@ import java.nio.file.Files; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +/** + * 皮肤管理器 - 负责玩家皮肤的获取、缓存和应用 + * + *

主要功能: + *

    + *
  • 从多种来源获取玩家皮肤数据(SkinsRestorer插件、PlayerProfile、本地缓存)
  • + *
  • 皮肤数据Base64编码和缓存管理
  • + *
  • 自定义模型数据分配和管理
  • + *
  • 异步皮肤加载避免阻塞主线程
  • + *
  • 皮肤缓存过期清理
  • + *
  • 完整的SkinsRestorer插件集成支持
  • + *
+ * + *

皮肤获取优先级(根据配置的source字段): + *

    + *
  1. skinsrestorer:优先从SkinsRestorer插件获取皮肤纹理数据
  2. + *
  3. player_profile:优先使用Bukkit的PlayerProfile API
  4. + *
  5. local_cache:优先从本地缓存加载
  6. + *
  7. 默认Steve皮肤(所有来源都失败时的备用)
  8. + *
+ * + *

SkinsRestorer集成特性: + *

    + *
  • 自动检测SkinsRestorer插件是否安装
  • + *
  • 使用反射安全调用SkinsRestorer API,避免硬依赖
  • + *
  • 获取完整的皮肤纹理数据(value和signature)
  • + *
  • 支持离线服务器,避免默认Steve皮肤问题
  • + *
  • 优雅降级:SkinsRestorer失败时自动回退到其他来源
  • + *
+ * + *

皮肤缓存默认保留7天,过期后自动重新获取。

+ * + * @author xiaobai + * @version 2.1.0 + * @since 1.0.0 + */ public class SkinManager { private final PlayerBlockLife plugin; private final Map playerSkinData = new ConcurrentHashMap<>(); @@ -61,20 +97,40 @@ public class SkinManager { plugin.logInfo("开始加载皮肤: " + player.getName()); String skinBase64 = null; + String skinSource = plugin.getConfigManager().getSkinSource(); - // 检查是否使用SkinsRestorer插件 - if (plugin.getConfigManager().useSkinsRestorer()) { + // 根据配置的皮肤来源优先级获取皮肤 + 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); + } } - // 如果SkinsRestorer未启用或获取失败,使用PlayerProfile - if (skinBase64 == null) { - skinBase64 = getSkinFromPlayerProfile(player); - } - + // 如果所有来源都失败,使用默认Steve皮肤 if (skinBase64 == null) { skinBase64 = getDefaultSteveSkin(); - plugin.logWarning("使用默认皮肤: " + player.getName()); + plugin.logWarning("所有皮肤来源都失败,使用默认Steve皮肤: " + player.getName()); } if (skinBase64 != null) { @@ -97,6 +153,37 @@ public class SkinManager { }); } + /** + * 从SkinsRestorer插件获取玩家皮肤纹理数据 + * + *

SkinsRestorer是一个流行的皮肤管理插件,可以在离线服务器上提供皮肤支持。

+ * + *

此方法使用反射安全调用SkinsRestorer API,避免硬依赖。支持离线服务器获取玩家自定义皮肤。

+ * + *

获取流程: + *

    + *
  1. 检查SkinsRestorer插件是否安装
  2. + *
  3. 使用反射获取SkinsRestorer API实例
  4. + *
  5. 优先通过UUID获取皮肤数据(更可靠)
  6. + *
  7. 如果UUID获取失败,回退到使用玩家名获取
  8. + *
  9. 提取皮肤纹理的value和signature字段
  10. + *
  11. 构建完整的Base64编码纹理JSON
  12. + *
+ *

+ * + *

离线服务器优势: + *

    + *
  • 即使玩家离线也能获取其预设皮肤
  • + *
  • 避免总是显示默认Steve皮肤的问题
  • + *
  • 支持管理员设置的皮肤和玩家自定义皮肤
  • + *
+ *

+ * + * @param player 要获取皮肤的玩家对象 + * @return 完整的Base64编码皮肤纹理JSON,如果获取失败返回null + * @throws ClassNotFoundException 如果SkinsRestorer API类未找到(插件未安装) + * @throws Exception 反射调用过程中的其他异常 + */ private String getSkinFromSkinsRestorer(Player player) { try { // 检查SkinsRestorer插件是否存在 @@ -111,20 +198,44 @@ public class SkinManager { Class skinsRestorerClass = Class.forName("net.skinsrestorer.api.SkinsRestorerAPI"); Object skinsRestorerAPI = skinsRestorerClass.getMethod("getApi").invoke(null); - // 获取玩家皮肤数据 - Class skinDataClass = Class.forName("net.skinsrestorer.api.property.SkinProperty"); - Object skinProperty = skinsRestorerAPI.getClass().getMethod("getSkinData", String.class) - .invoke(skinsRestorerAPI, player.getName()); + // 获取玩家皮肤数据 - 使用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) skinProperty.getClass().getMethod("getValue").invoke(skinProperty); - String signature = (String) skinProperty.getClass().getMethod("getSignature").invoke(skinProperty); + 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 player.getUniqueId().toString(); + return base64Texture; + } else { + plugin.logInfo("SkinsRestorer中没有找到玩家 " + player.getName() + " 的皮肤数据"); } } catch (ClassNotFoundException e) { - plugin.logWarning("SkinsRestorer API类未找到,插件可能未安装: " + e.getMessage()); + plugin.logWarning("SkinsRestorer API类未找到,插件可能未安装或版本不兼容: " + e.getMessage()); + } catch (NoSuchMethodException e) { + plugin.logWarning("SkinsRestorer API方法未找到,可能是版本不兼容: " + e.getMessage()); } catch (Exception e) { plugin.logWarning("从SkinsRestorer获取皮肤失败: " + e.getMessage()); } @@ -132,6 +243,14 @@ public class SkinManager { return null; } + /** + * 从PlayerProfile获取玩家皮肤数据 + * + *

使用Bukkit的PlayerProfile API获取在线玩家的皮肤URL,然后转换为Base64纹理。

+ * + * @param player 要获取皮肤的玩家 + * @return 皮肤的Base64纹理值,如果获取失败返回null + */ private String getSkinFromPlayerProfile(Player player) { try { PlayerProfile profile = player.getPlayerProfile(); @@ -139,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()); @@ -208,23 +324,92 @@ public class SkinManager { return modelData; } + /** + * 获取默认Steve皮肤的Base64纹理 + * + *

当无法从任何来源获取玩家皮肤时,使用默认的Steve皮肤作为备用。

+ * + * @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()); } + /** + * 创建带有玩家皮肤的头颅物品 + * + *

使用Base64纹理数据创建自定义玩家头颅,支持离线服务器皮肤显示。

+ * + * @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 lore = new ArrayList<>(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3199f6e..b2bde41 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -44,9 +44,15 @@ skin: # 是否启用皮肤系统 enabled: true # 皮肤来源 (player_profile, local_cache, skinsrestorer) - source: player_profile - # 是否使用SkinsRestorer插件皮肤(如果服务器有此插件) - use-skinsrestorer: false + # player_profile: 使用Bukkit的PlayerProfile API(需要在线验证) + # local_cache: 使用本地缓存的皮肤数据 + # skinsrestorer: 使用SkinsRestorer插件的皮肤数据(推荐用于离线服务器) + source: skinsrestorer + + # 是否使用SkinsRestorer插件的皮肤(如果服务器有此插件) + # 设置为true时,插件会优先从SkinsRestorer获取皮肤数据 + # 这对于离线服务器特别有用,可以避免默认Steve皮肤的问题 + use-skinsrestorer: true # 缓存设置 cache: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f11d9b6..4d63579 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: PlayerBlockLife -version: 2.0.1-1.20.4 +version: 3.0.0-experimental-1.20.4 main: com.playerblocklife.PlayerBlockLife api-version: 1.20 author: xiaobai