package com.playerblocklife;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Skull;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Rotatable;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.util.Vector;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 玩家方块管理器 - 负责管理玩家生命方块的核心组件
*
*
主要职责:
*
* - 生成和放置玩家生命方块
* - 管理方块位置和所有者映射关系
* - 处理方块破坏和恢复逻辑
* - 提供方块数据持久化存储
* - 支持方块位置查询和验证
* - 与SkinManager协同工作,确保方块正确显示玩家皮肤
*
*
* SkinsRestorer集成特性:
*
* - 通过SkinManager获取SkinsRestorer提供的玩家皮肤纹理
* - 确保离线服务器上的方块显示正确的自定义皮肤
* - 支持异步皮肤加载,避免方块放置阻塞
* - 提供皮肤加载状态检查,确保皮肤就绪后再放置方块
*
*
* 使用并发安全的数据结构确保多线程环境下的数据一致性。
*
* @author xiaobai
* @version 2.2.0
* @since 1.0.0
*/
public class PlayerBlockManager {
private final PlayerBlockLife plugin;
private final SkinManager skinManager;
private final Map> playerBlocks = new ConcurrentHashMap<>();
private final Map blockOwners = new ConcurrentHashMap<>();
private final Map playerBlockTypes = new ConcurrentHashMap<>();
private final File dataFile;
private final Random random = new Random();
public PlayerBlockManager(PlayerBlockLife plugin, SkinManager skinManager) {
this.plugin = plugin;
this.skinManager = skinManager;
this.dataFile = new File(plugin.getDataFolder(), "blockdata.yml");
}
/**
* 为玩家设置生命方块(兼容旧方法)
*/
public boolean setLifeBlocks(Player player, Location center) {
ConfigManager config = plugin.getConfigManager();
boolean requireOpenSky = true; // 使用默认值,因为配置已移除
int maxAttempts = 50; // 使用默认值,因为配置已移除
return generateLifeBlocksForPlayer(player, 5, 5, requireOpenSky, maxAttempts);
}
/**
* 为玩家生成指定数量的生命方块
*
* 此方法负责生成玩家的生命方块,包括以下步骤:
*
* - 检查玩家是否已有生命方块
* - 验证玩家皮肤是否已从SkinsRestorer或其他来源加载完成
* - 在指定范围内寻找合适的放置位置
* - 放置带有玩家皮肤纹理的玩家头颅方块
* - 记录方块位置和所有者关系
* - 保存数据并返回生成结果
*
*
*
* 皮肤加载检查:
*
* - 调用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();
String playerName = player.getName();
// 检查是否已有生命方块
if (hasLifeBlocks(playerId)) {
return false;
}
// 检查玩家皮肤是否已加载
if (!skinManager.isSkinLoaded(playerId)) {
return false;
}
List blocks = new ArrayList<>();
int blocksPlaced = 0;
int attempts = 0;
// 尝试生成指定数量的方块
while (blocksPlaced < blockAmount && attempts < maxAttempts) {
Location blockLoc = findSurfaceLocation(player.getLocation(), spreadRange, requireOpenSky);
attempts++;
if (blockLoc != null && placePlayerHead(blockLoc, playerId, playerName)) {
blocks.add(blockLoc);
blockOwners.put(blockLoc, playerId);
blocksPlaced++;
// 添加放置效果
spawnPlaceEffects(blockLoc);
}
}
if (blocksPlaced > 0) {
playerBlocks.put(playerId, blocks);
saveData();
return true;
} else {
return false;
}
}
/**
* 寻找地表位置(放宽条件,只要是露天地面就可以)
*/
private Location findSurfaceLocation(Location center, int spreadRange, boolean requireOpenSky) {
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, 0, z);
World world = testLoc.getWorld();
if (world == null) continue;
// 从世界最高点向下寻找第一个固体方块
int maxHeight = world.getMaxHeight();
Block groundBlock = null;
for (int y = maxHeight; y > world.getMinHeight(); y--) {
testLoc.setY(y);
Block block = testLoc.getBlock();
Material type = block.getType();
// 检查是否是固体方块(可以作为支撑)
if (type.isSolid() && type.isBlock() && !type.isTransparent()) {
groundBlock = block;
break;
}
}
if (groundBlock == null) continue;
// 地表位置 = 固体方块上方一格
Location surfaceLoc = groundBlock.getLocation().add(0, 1, 0);
// 检查是否已有方块
if (blockOwners.containsKey(surfaceLoc)) {
continue;
}
// 检查位置是否合适(放宽条件)
if (!isSuitableLocationRelaxed(surfaceLoc)) {
continue;
}
// 如果需要上方无方块覆盖,检查上方3格
if (requireOpenSky) {
boolean hasCover = false;
for (int y = 1; y <= 3; y++) {
Block aboveBlock = surfaceLoc.clone().add(0, y, 0).getBlock();
if (!aboveBlock.getType().isAir()) {
hasCover = true;
break;
}
}
if (hasCover) continue;
}
return surfaceLoc;
}
return null;
}
/**
* 寻找合适的位置
*/
private Location findSuitableLocation(Location center) {
for (int i = 0; i < 10; i++) {
int x = random.nextInt(11) - 5; // -5 到 5
int z = random.nextInt(11) - 5;
Location testLoc = center.clone().add(x, -1, z);
Block block = testLoc.getBlock();
// 检查位置是否合适
if (isSuitableLocation(testLoc)) {
return testLoc;
}
}
return null;
}
/**
* 检查位置是否合适
* 修复:移除 isReplaceable() 方法调用
*/
private boolean isSuitableLocation(Location location) {
Block block = location.getBlock();
// 检查是否已有方块
if (blockOwners.containsKey(location)) {
return false;
}
// 检查方块是否可替换(修复了 isReplaceable() 方法不存在的问题)
Material type = block.getType();
if (!type.isAir()) {
// 检查是否是固体方块,固体方块不能替换
if (type.isSolid()) {
return false;
}
// 检查是否是液体
if (type == Material.WATER || type == Material.LAVA ||
type == Material.WATER_CAULDRON || type == Material.LAVA_CAULDRON) {
return false;
}
// 检查一些特定的不可替换方块
switch (type) {
case FIRE:
case SOUL_FIRE:
case COBWEB:
case BAMBOO:
case BAMBOO_SAPLING:
case SCAFFOLDING:
case LADDER:
case VINE:
case TWISTING_VINES:
case WEEPING_VINES:
case GLOW_LICHEN:
// 这些方块可以替换
break;
default:
// 如果不是固体,也不是特定的透明方块,也不是空气,则检查是否是透明方块
if (!type.isTransparent()) {
return false;
}
break;
}
}
// 检查下方是否有支撑方块
Block below = location.clone().add(0, -1, 0).getBlock();
Material belowType = below.getType();
if (belowType.isAir() || !belowType.isSolid()) {
return false;
}
// 检查是否在水或岩浆中
if (block.isLiquid() || below.isLiquid()) {
return false;
}
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;
}
/**
* 放置玩家头颅方块
*
* 使用SkinManager创建带有正确皮肤的玩家头颅方块,支持离线服务器皮肤显示。
*
* @param location 放置位置
* @param playerId 玩家UUID
* @param playerName 玩家名称
* @return 放置成功返回true,失败返回false
*/
private boolean placePlayerHead(Location location, UUID playerId, String playerName) {
try {
Block block = location.getBlock();
// 检查方块是否已被占用
if (blockOwners.containsKey(location)) {
return false;
}
// 检查玩家皮肤是否已加载
if (!skinManager.isSkinLoaded(playerId)) {
plugin.logWarning("玩家 " + playerName + " 的皮肤未加载,无法放置头颅方块");
return false;
}
// 设置方块为玩家头颅
block.setType(Material.PLAYER_HEAD);
// 获取并设置头颅数据
Skull skullState = (Skull) block.getState();
// 使用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();
if (blockData instanceof Rotatable) {
Rotatable rotatable = (Rotatable) blockData;
BlockFace[] faces = {BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST};
rotatable.setRotation(faces[random.nextInt(faces.length)]);
block.setBlockData(rotatable);
}
// 更新方块
skullState.update(true, false);
plugin.logInfo("成功放置玩家头颅方块: " + playerName + " 在 " + location);
return true;
} catch (Exception e) {
plugin.logError("放置玩家头颅失败: " + location, e);
return false;
}
}
/**
* 生成放置效果
*/
private void spawnPlaceEffects(Location location) {
World world = location.getWorld();
if (world == null) return;
// 粒子效果
world.spawnParticle(Particle.ENCHANTMENT_TABLE,
location.clone().add(0.5, 0.5, 0.5),
30, 0.3, 0.3, 0.3, 0.1);
// 音效
world.playSound(location, Sound.BLOCK_ANVIL_PLACE, 0.5f, 1.2f);
}
/**
* 检查方块是否属于某个玩家
*/
public UUID getBlockOwner(Location location) {
return blockOwners.get(location);
}
/**
* 移除方块(当被挖掘时)
*/
public boolean removeBlock(Location location, Player breaker) {
UUID ownerId = blockOwners.get(location);
if (ownerId == null) {
return false;
}
List blocks = playerBlocks.get(ownerId);
if (blocks == null || !blocks.contains(location)) {
return false;
}
// 移除方块
blocks.remove(location);
blockOwners.remove(location);
// 设置方块为空气
location.getBlock().setType(Material.AIR);
// 生成破坏效果
spawnBreakEffects(location, breaker);
// 检查玩家是否还有剩余方块
int remaining = blocks.size();
// 通知所有相关玩家
notifyBlockBreak(ownerId, breaker, remaining);
// 保存数据
saveData();
return true;
}
/**
* 生成破坏效果
*/
private void spawnBreakEffects(Location location, Player breaker) {
World world = location.getWorld();
if (world == null) return;
// 粒子效果
world.spawnParticle(Particle.BLOCK_CRACK,
location.clone().add(0.5, 0.5, 0.5),
50, 0.5, 0.5, 0.5, 0.5, Material.PLAYER_HEAD.createBlockData());
world.spawnParticle(Particle.SMOKE_LARGE,
location.clone().add(0.5, 0.5, 0.5),
20, 0.3, 0.3, 0.3, 0.05);
// 音效
world.playSound(location, Sound.ENTITY_ITEM_BREAK, 1.0f, 0.8f);
world.playSound(location, Sound.BLOCK_GLASS_BREAK, 0.8f, 1.0f);
// 对挖掘者造成轻微击退
if (breaker != null) {
Location breakerLoc = breaker.getLocation();
Vector direction = location.toVector().subtract(breakerLoc.toVector()).normalize();
breaker.setVelocity(direction.multiply(-0.5).setY(0.3));
}
}
/**
* 通知方块被破坏
*/
private void notifyBlockBreak(UUID ownerId, Player breaker, int remaining) {
Player owner = Bukkit.getPlayer(ownerId);
String ownerName = Bukkit.getOfflinePlayer(ownerId).getName();
// 通知方块所有者
if (owner != null && owner.isOnline()) {
owner.sendMessage("§c⚠ 警告!你的生命方块被破坏了!");
owner.sendMessage("§7破坏者: §e" + (breaker != null ? breaker.getName() : "未知"));
owner.sendMessage("§7剩余生命方块: §a" + remaining + " §7/ §c5");
if (remaining <= 2) {
owner.sendMessage("§4⚠ 警告!生命方块即将耗尽!");
}
// 播放警告音效
owner.playSound(owner.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 0.8f, 0.5f);
}
// 通知破坏者
if (breaker != null && !breaker.getUniqueId().equals(ownerId)) {
breaker.sendMessage("§6你破坏了一个生命方块!");
breaker.sendMessage("§7所有者: §e" + (ownerName != null ? ownerName : "未知玩家"));
breaker.sendMessage("§7对方剩余生命方块: §a" + remaining);
// 给予挖掘者经验奖励
breaker.giveExp(5);
breaker.playSound(breaker.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f);
}
// 广播给附近玩家
World world = owner != null ? owner.getWorld() : (breaker != null ? breaker.getWorld() : null);
if (world != null) {
for (Player nearby : world.getPlayers()) {
if (nearby != owner && nearby != breaker &&
nearby.getLocation().distance(owner != null ? owner.getLocation() : breaker.getLocation()) < 30) {
nearby.sendMessage("§7[附近] §e一个生命方块被破坏了!");
}
}
}
}
/**
* 获取玩家剩余方块数量
*/
public int getRemainingBlocks(UUID playerId) {
List blocks = playerBlocks.get(playerId);
return blocks != null ? blocks.size() : 0;
}
/**
* 检查玩家是否有生命方块
*/
public boolean hasLifeBlocks(UUID playerId) {
List blocks = playerBlocks.get(playerId);
return blocks != null && !blocks.isEmpty();
}
/**
* 获取玩家的所有生命方块位置
*/
public List getPlayerBlocks(UUID playerId) {
return playerBlocks.getOrDefault(playerId, new ArrayList<>());
}
/**
* 清除玩家的所有生命方块
*/
public void clearPlayerBlocks(UUID playerId) {
List blocks = playerBlocks.remove(playerId);
if (blocks != null) {
for (Location loc : blocks) {
blockOwners.remove(loc);
loc.getBlock().setType(Material.AIR);
}
}
saveData();
}
/**
* 重新生成玩家的生命方块
*/
public boolean regeneratePlayerBlocks(Player player) {
UUID playerId = player.getUniqueId();
// 清除旧方块
clearPlayerBlocks(playerId);
// 生成新方块
return setLifeBlocks(player, player.getLocation());
}
/**
* 获取已注册玩家的数量
*/
public int getPlayerBlocksCount() {
return playerBlocks.size();
}
/**
* 获取总方块数量
*/
public int getTotalBlocksCount() {
int total = 0;
for (List blocks : playerBlocks.values()) {
total += blocks.size();
}
return total;
}
/**
* 加载数据
*/
@SuppressWarnings("unchecked")
public void loadData() {
if (!dataFile.exists()) {
return;
}
try {
Yaml yaml = new Yaml();
Map data = yaml.load(new FileReader(dataFile));
if (data == null) {
return;
}
// 加载方块数据
if (data.containsKey("blocks")) {
Map>> blocksData =
(Map>>) data.get("blocks");
for (Map.Entry>> entry : blocksData.entrySet()) {
UUID playerId = UUID.fromString(entry.getKey());
List locations = new ArrayList<>();
for (Map locData : entry.getValue()) {
String worldName = (String) locData.get("world");
double x = (double) locData.get("x");
double y = (double) locData.get("y");
double z = (double) locData.get("z");
World world = Bukkit.getWorld(worldName);
if (world != null) {
Location location = new Location(world, x, y, z);
locations.add(location);
blockOwners.put(location, playerId);
}
}
if (!locations.isEmpty()) {
playerBlocks.put(playerId, locations);
}
}
}
plugin.logInfo("已加载 " + playerBlocks.size() + " 个玩家的方块数据");
plugin.logInfo("总共 " + blockOwners.size() + " 个生命方块");
} catch (Exception e) {
plugin.logError("加载方块数据失败", e);
}
}
/**
* 保存数据
*/
public void saveData() {
try {
Map data = new HashMap<>();
Map>> blocksData = new HashMap<>();
// 保存方块数据
for (Map.Entry> entry : playerBlocks.entrySet()) {
String playerId = entry.getKey().toString();
List