520 lines
22 KiB
Java
520 lines
22 KiB
Java
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 + " 个过期皮肤缓存");
|
||
}
|
||
}
|
||
}
|
||
} |