Files
PlayerBlockLife/src/main/java/com/playerblocklife/PlayerBlockManager.java
xiaobai 032ef02ec8 2.2.2-1.20.4
可以通过配置文件修改方块生成的范围
2026-02-16 19:00:54 +08:00

810 lines
27 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 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;
/**
* 玩家方块管理器 - 负责管理玩家生命方块的核心组件
*
* <p>主要职责:
* <ul>
* <li>生成和放置玩家生命方块</li>
* <li>管理方块位置和所有者映射关系</li>
* <li>处理方块破坏和恢复逻辑</li>
* <li>提供方块数据持久化存储</li>
* <li>支持方块位置查询和验证</li>
* <li>与SkinManager协同工作确保方块正确显示玩家皮肤</li>
* </ul>
*
* <p><b>SkinsRestorer集成特性</b>
* <ul>
* <li>通过SkinManager获取SkinsRestorer提供的玩家皮肤纹理</li>
* <li>确保离线服务器上的方块显示正确的自定义皮肤</li>
* <li>支持异步皮肤加载,避免方块放置阻塞</li>
* <li>提供皮肤加载状态检查,确保皮肤就绪后再放置方块</li>
* </ul>
*
* <p>使用并发安全的数据结构确保多线程环境下的数据一致性。</p>
*
* @author xiaobai
* @version 2.2.2
* @since 1.0.0
*/
public class PlayerBlockManager {
private final PlayerBlockLife plugin;
private final SkinManager skinManager;
private final Map<UUID, List<Location>> playerBlocks = new ConcurrentHashMap<>();
private final Map<Location, UUID> blockOwners = new ConcurrentHashMap<>();
private final Map<UUID, Material> 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; // 使用默认值,因为配置已移除
int spreadRange = config.getSpreadRange(); // 从配置获取范围
int minDistance = config.getMinDistance(); // 从配置获取最小距离
return generateLifeBlocksForPlayer(player, 5, spreadRange, minDistance, requireOpenSky, maxAttempts);
}
/**
* 为玩家生成指定数量的生命方块
*
* <p>此方法负责生成玩家的生命方块,包括以下步骤:
* <ol>
* <li>检查玩家是否已有生命方块</li>
* <li>验证玩家皮肤是否已从SkinsRestorer或其他来源加载完成</li>
* <li>在指定范围内寻找合适的放置位置</li>
* <li>放置带有玩家皮肤纹理的玩家头颅方块</li>
* <li>记录方块位置和所有者关系</li>
* <li>保存数据并返回生成结果</li>
* </ol>
* </p>
*
* <p><b>皮肤加载检查:</b>
* <ul>
* <li>调用skinManager.isSkinLoaded()检查皮肤是否就绪</li>
* <li>如果皮肤未加载,方块生成将失败</li>
* <li>确保离线服务器通过SkinsRestorer获取的皮肤能正确应用</li>
* <li>避免放置默认Steve皮肤的方块</li>
* </ul>
* </p>
*
* @param player 目标玩家
* @param blockAmount 要生成的方块数量
* @param spreadRange 生成范围(以玩家为中心的半径)
* @param minDistance 生成方块距离玩家的最小距离
* @param requireOpenSky 是否需要开阔天空(上方无方块覆盖)
* @param maxAttempts 寻找合适位置的最大尝试次数
* @return 生成成功返回true失败返回false
* @see SkinManager#isSkinLoaded(UUID)
* @see SkinManager#getSkinFromSkinsRestorer(Player)
*/
public boolean generateLifeBlocksForPlayer(Player player, int blockAmount, int spreadRange, int minDistance, boolean requireOpenSky, int maxAttempts) {
UUID playerId = player.getUniqueId();
String playerName = player.getName();
// 检查是否已有生命方块
if (hasLifeBlocks(playerId)) {
return false;
}
// 检查玩家皮肤是否已加载
if (!skinManager.isSkinLoaded(playerId)) {
return false;
}
List<Location> blocks = new ArrayList<>();
int blocksPlaced = 0;
int attempts = 0;
// 尝试生成指定数量的方块
while (blocksPlaced < blockAmount && attempts < maxAttempts) {
Location blockLoc = findSurfaceLocation(player.getLocation(), spreadRange, minDistance, 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, int minDistance, 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;
// 检查与玩家的距离是否满足最小距离要求
double distance = Math.sqrt(x * x + z * z);
if (distance < minDistance) {
// 如果距离太近,重新生成坐标
continue;
}
// 以玩家坐标为中心,但高度从世界最高点开始寻找
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;
}
/**
* 放置玩家头颅方块
*
* <p>使用SkinManager创建带有正确皮肤的玩家头颅方块支持离线服务器皮肤显示。</p>
*
* @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<Location> 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<Location> blocks = playerBlocks.get(playerId);
return blocks != null ? blocks.size() : 0;
}
/**
* 检查玩家是否有生命方块
*/
public boolean hasLifeBlocks(UUID playerId) {
List<Location> blocks = playerBlocks.get(playerId);
return blocks != null && !blocks.isEmpty();
}
/**
* 获取玩家的所有生命方块位置
*/
public List<Location> getPlayerBlocks(UUID playerId) {
return playerBlocks.getOrDefault(playerId, new ArrayList<>());
}
/**
* 清除玩家的所有生命方块
*/
public void clearPlayerBlocks(UUID playerId) {
List<Location> 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<Location> blocks : playerBlocks.values()) {
total += blocks.size();
}
return total;
}
/**
* 加载数据
*/
@SuppressWarnings("unchecked")
public void loadData() {
if (!dataFile.exists()) {
return;
}
try {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(new FileReader(dataFile));
if (data == null) {
return;
}
// 加载方块数据
if (data.containsKey("blocks")) {
Map<String, List<Map<String, Object>>> blocksData =
(Map<String, List<Map<String, Object>>>) data.get("blocks");
for (Map.Entry<String, List<Map<String, Object>>> entry : blocksData.entrySet()) {
UUID playerId = UUID.fromString(entry.getKey());
List<Location> locations = new ArrayList<>();
for (Map<String, Object> 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<String, Object> data = new HashMap<>();
Map<String, List<Map<String, Object>>> blocksData = new HashMap<>();
// 保存方块数据
for (Map.Entry<UUID, List<Location>> entry : playerBlocks.entrySet()) {
String playerId = entry.getKey().toString();
List<Map<String, Object>> locations = new ArrayList<>();
for (Location loc : entry.getValue()) {
Map<String, Object> locData = new HashMap<>();
locData.put("world", loc.getWorld().getName());
locData.put("x", loc.getX());
locData.put("y", loc.getY());
locData.put("z", loc.getZ());
locations.add(locData);
}
blocksData.put(playerId, locations);
}
data.put("blocks", blocksData);
// 写入文件
Yaml yaml = new Yaml();
yaml.dump(data, new FileWriter(dataFile));
} catch (Exception e) {
plugin.logError("保存方块数据失败", e);
}
}
/**
* 获取所有玩家的方块数据
*/
public Map<UUID, List<Location>> getAllPlayerBlocks() {
return new HashMap<>(playerBlocks);
}
/**
* 获取所有方块的位置和所有者
*/
public Map<Location, UUID> getAllBlockOwners() {
return new HashMap<>(blockOwners);
}
/**
* 检查位置是否包含生命方块
*/
public boolean isLifeBlock(Location location) {
return blockOwners.containsKey(location);
}
/**
* 获取玩家的生命方块位置列表(用于显示)
*/
public List<String> getPlayerBlockLocations(UUID playerId) {
List<Location> blocks = playerBlocks.get(playerId);
List<String> locations = new ArrayList<>();
if (blocks != null) {
for (Location loc : blocks) {
String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "未知世界";
locations.add(String.format("世界: %s, 坐标: %d, %d, %d",
worldName,
loc.getBlockX(),
loc.getBlockY(),
loc.getBlockZ()));
}
}
return locations;
}
/**
* 获取距离玩家最近的方块
*/
public Location getNearestBlock(Player player) {
UUID playerId = player.getUniqueId();
List<Location> blocks = playerBlocks.get(playerId);
if (blocks == null || blocks.isEmpty()) {
return null;
}
Location nearest = null;
double nearestDistance = Double.MAX_VALUE;
for (Location block : blocks) {
double distance = player.getLocation().distance(block);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = block;
}
}
return nearest;
}
/**
* 重置游戏时删除所有未被破坏的生命方块
*/
public void resetGameBlocks() {
// 遍历所有生命方块位置并将其设置为空气
for (Location location : blockOwners.keySet()) {
if (location.getWorld() != null) {
Block block = location.getBlock();
// 只删除玩家头颅方块
if (block.getType() == Material.PLAYER_HEAD || block.getType() == Material.PLAYER_WALL_HEAD) {
block.setType(Material.AIR);
}
}
}
// 清空内部数据结构
playerBlocks.clear();
blockOwners.clear();
playerBlockTypes.clear();
// 保存数据更新
saveData();
}
/**
* 获取所有生命方块的统计信息
*/
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalPlayers", playerBlocks.size());
stats.put("totalBlocks", blockOwners.size());
// 按世界统计
Map<String, Integer> blocksPerWorld = new HashMap<>();
for (Location loc : blockOwners.keySet()) {
String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "unknown";
blocksPerWorld.put(worldName, blocksPerWorld.getOrDefault(worldName, 0) + 1);
}
stats.put("blocksPerWorld", blocksPerWorld);
return stats;
}
}