package com.playerblocklife; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.profile.PlayerProfile; import org.bukkit.profile.PlayerTextures; import org.yaml.snakeyaml.Yaml; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; import java.nio.file.Files; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * 皮肤管理器 - 负责玩家皮肤的获取、缓存和应用 * *

主要功能: *

* *

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

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

SkinsRestorer集成特性: *

* *

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

* * @author xiaobai * @version 2.2.0 * @since 1.0.0 */ public class SkinManager { private final PlayerBlockLife plugin; private final Map playerSkinData = new ConcurrentHashMap<>(); private final Map playerCustomModelData = new ConcurrentHashMap<>(); private final Map skinLoadTime = new ConcurrentHashMap<>(); private final File skinDataFile; private final File skinCacheDir; private int nextCustomModelData = 1000; private static final long SKIN_CACHE_TIME = 7 * 24 * 60 * 60 * 1000L; public SkinManager(PlayerBlockLife plugin) { this.plugin = plugin; this.skinDataFile = new File(plugin.getDataFolder(), "skindata.yml"); this.skinCacheDir = new File(plugin.getDataFolder(), "skincache"); if (!skinCacheDir.exists()) { skinCacheDir.mkdirs(); } } public void loadAllSkins() { loadSkinDataFromFile(); Bukkit.getOnlinePlayers().forEach(this::loadPlayerSkinAsync); } public void loadPlayerSkinAsync(Player player) { UUID playerId = player.getUniqueId(); if (playerSkinData.containsKey(playerId)) { return; } if (loadSkinFromCache(playerId)) { plugin.logInfo("从缓存加载皮肤: " + player.getName()); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { plugin.logInfo("开始加载皮肤: " + player.getName()); String skinBase64 = null; String skinSource = "skinsrestorer"; // 使用默认值,因为配置已移除 // 根据配置的皮肤来源优先级获取皮肤 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 && true) { 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 && true) { skinBase64 = getSkinFromSkinsRestorer(player); } } // 如果所有来源都失败,使用默认Steve皮肤 if (skinBase64 == null) { skinBase64 = getDefaultSteveSkin(); plugin.logWarning("所有皮肤来源都失败,使用默认Steve皮肤: " + player.getName()); } if (skinBase64 != null) { playerSkinData.put(playerId, skinBase64); int modelData = allocateCustomModelData(playerId); saveSkinToCache(playerId, skinBase64); skinLoadTime.put(playerId, System.currentTimeMillis()); plugin.logInfo("皮肤加载完成: " + player.getName() + " (模型数据: " + modelData + ")"); Bukkit.getScheduler().runTask(plugin, () -> { if (player.isOnline()) { player.sendMessage("§a你的皮肤方块已准备就绪!"); } }); } } catch (Exception e) { plugin.logError("加载皮肤失败: " + player.getName(), e); } }); } /** * 从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插件是否存在 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获取玩家皮肤数据 * *

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

* * @param player 要获取皮肤的玩家 * @return 皮肤的Base64纹理值,如果获取失败返回null */ private String getSkinFromPlayerProfile(Player player) { try { PlayerProfile profile = player.getPlayerProfile(); PlayerTextures textures = profile.getTextures(); URL skinUrl = textures.getSkin(); if (skinUrl != null) { // 创建纹理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()); } return null; } private boolean loadSkinFromCache(UUID playerId) { try { File cacheFile = new File(skinCacheDir, playerId.toString() + ".cache"); if (cacheFile.exists()) { long lastModified = cacheFile.lastModified(); if (System.currentTimeMillis() - lastModified < SKIN_CACHE_TIME) { String skinData = new String(Files.readAllBytes(cacheFile.toPath())); playerSkinData.put(playerId, skinData); allocateCustomModelData(playerId); return true; } } } catch (Exception e) { plugin.logWarning("读取皮肤缓存失败: " + e.getMessage()); } return false; } private void saveSkinToCache(UUID playerId, String skinData) { try { File cacheFile = new File(skinCacheDir, playerId.toString() + ".cache"); Files.write(cacheFile.toPath(), skinData.getBytes()); } catch (Exception e) { plugin.logWarning("保存皮肤缓存失败: " + e.getMessage()); } } private synchronized int allocateCustomModelData(UUID playerId) { if (playerCustomModelData.containsKey(playerId)) { return playerCustomModelData.get(playerId); } int modelData = nextCustomModelData++; playerCustomModelData.put(playerId, modelData); return modelData; } /** * 获取默认Steve皮肤的Base64纹理 * *

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

* * @return 默认Steve皮肤的Base64纹理值 */ private String getDefaultSteveSkin() { // 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) { // 获取玩家的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<>(); lore.add("§7所有者: §e" + playerName); lore.add("§c⚠ 警告: 挖掘此方块将减少玩家生命值"); lore.add("§7剩余生命: §a" + plugin.getBlockManager().getRemainingBlocks(playerId) + "/5"); meta.setLore(lore); head.setItemMeta(meta); } return head; } public boolean isSkinLoaded(UUID playerId) { return playerSkinData.containsKey(playerId); } public Integer getCustomModelData(UUID playerId) { return playerCustomModelData.get(playerId); } public Map getAllSkinData() { return new HashMap<>(playerSkinData); } public Map getAllCustomModelData() { return new HashMap<>(playerCustomModelData); } private void loadSkinDataFromFile() { if (!skinDataFile.exists()) { return; } try { Yaml yaml = new Yaml(); Map data = yaml.load(new FileReader(skinDataFile)); if (data != null && data.containsKey("skins")) { Map skins = (Map) data.get("skins"); for (Map.Entry entry : skins.entrySet()) { UUID playerId = UUID.fromString(entry.getKey()); String skinData = (String) entry.getValue(); playerSkinData.put(playerId, skinData); } } if (data != null && data.containsKey("modelData")) { Map modelData = (Map) data.get("modelData"); for (Map.Entry entry : modelData.entrySet()) { UUID playerId = UUID.fromString(entry.getKey()); playerCustomModelData.put(playerId, entry.getValue()); nextCustomModelData = Math.max(nextCustomModelData, entry.getValue() + 1); } } plugin.logInfo("已加载 " + playerSkinData.size() + " 个玩家的皮肤数据"); } catch (Exception e) { plugin.logError("加载皮肤数据文件失败", e); } } public void saveSkinData() { try { Map data = new HashMap<>(); Map skins = new HashMap<>(); for (Map.Entry entry : playerSkinData.entrySet()) { skins.put(entry.getKey().toString(), entry.getValue()); } data.put("skins", skins); Map modelData = new HashMap<>(); for (Map.Entry entry : playerCustomModelData.entrySet()) { modelData.put(entry.getKey().toString(), entry.getValue()); } data.put("modelData", modelData); Yaml yaml = new Yaml(); yaml.dump(data, new FileWriter(skinDataFile)); plugin.logInfo("皮肤数据已保存"); } catch (Exception e) { plugin.logError("保存皮肤数据失败", e); } } public void cleanupOldCache() { File[] cacheFiles = skinCacheDir.listFiles(); if (cacheFiles != null) { long now = System.currentTimeMillis(); int cleaned = 0; for (File file : cacheFiles) { if (now - file.lastModified() > SKIN_CACHE_TIME) { if (file.delete()) { cleaned++; } } } if (cleaned > 0) { plugin.logInfo("清理了 " + cleaned + " 个过期皮肤缓存"); } } } }