Files
PlayerBlockLife/src/main/java/com/playerblocklife/SkinManager.java
xiaobai ad5cdf1c64 2.2.0-1.20.4
将生命方块由玩家头换为其他原版多颜色方块
2026-02-16 17:44:38 +08:00

520 lines
22 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/**
* 皮肤管理器 - 负责玩家皮肤的获取、缓存和应用
*
* <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.2.0
* @since 1.0.0
*/
public class SkinManager {
private final PlayerBlockLife plugin;
private final Map<UUID, String> playerSkinData = new ConcurrentHashMap<>();
private final Map<UUID, Integer> playerCustomModelData = new ConcurrentHashMap<>();
private final Map<UUID, Long> 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插件获取玩家皮肤纹理数据
*
* <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();
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纹理
*
* <p>当无法从任何来源获取玩家皮肤时使用默认的Steve皮肤作为备用。</p>
*
* @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());
}
/**
* 创建带有玩家皮肤的头颅物品
*
* <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) {
// 获取玩家的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<>();
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<UUID, String> getAllSkinData() {
return new HashMap<>(playerSkinData);
}
public Map<UUID, Integer> getAllCustomModelData() {
return new HashMap<>(playerCustomModelData);
}
private void loadSkinDataFromFile() {
if (!skinDataFile.exists()) {
return;
}
try {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(new FileReader(skinDataFile));
if (data != null && data.containsKey("skins")) {
Map<String, Object> skins = (Map<String, Object>) data.get("skins");
for (Map.Entry<String, Object> 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<String, Integer> modelData = (Map<String, Integer>) data.get("modelData");
for (Map.Entry<String, Integer> 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<String, Object> data = new HashMap<>();
Map<String, String> skins = new HashMap<>();
for (Map.Entry<UUID, String> entry : playerSkinData.entrySet()) {
skins.put(entry.getKey().toString(), entry.getValue());
}
data.put("skins", skins);
Map<String, Integer> modelData = new HashMap<>();
for (Map.Entry<UUID, Integer> 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 + " 个过期皮肤缓存");
}
}
}
}